【Python/Pillow(PIL)】JPEG画像の品質を指定して保存する

JPEG画像の保存では、品質を指定することで、画像のファイルサイズが変わります。

品質を良くすると、画像はキレイになりますが、ファイルサイズが大きくなります。

逆に品質を下げると、画像にノイズが乗りますが、ファイルサイズが小さくなります。

JPEG画像の品質を下げたときに乗る代表的なノイズとして、ブロックノイズモスキートノイズというものがあります。

品質を指定してJPEGファイルを保存するときの構文は以下の通りです。

Image.save(jpegfilename, quality = 75)

となります。

品質の値(quality)は初期値が75で指定可能なのは、0(低品質)~95(高品質)です。

サンプル

from PIL import Image

# 画像を開く
img = Image.open("Mandrill.bmp")

# JPEGで品質を指定して画像を保存する
img.save("Mandrill_q10.jpg", quality = 10)
img.save("Mandrill_q50.jpg", quality = 50)
img.save("Mandrill_q75.jpg", quality = 75)
img.save("Mandrill_q95.jpg", quality = 95)

# 画像を開く
img = Image.open("ImagingSolution.bmp")

# JPEGで品質を指定して画像を保存する
img.save("ImagingSolution_q10.jpg", quality = 10)
img.save("ImagingSolution_q50.jpg", quality = 50)
img.save("ImagingSolution_q75.jpg", quality = 75)
img.save("ImagingSolution_q95.jpg", quality = 95)

入力画像

評価用に読み込む画像は非圧縮のビットマップファイル(*.bmp)にしました。

一つは一般的な画像としてマンドリルの画像と、もう一つはモノクロでエッジの強い文字の画像にしました。

Mandrill.bmp
256×256画素24bitカラー
196,662バイト(非圧縮)
ImagingSolution.bmp
256×256画素8bitグレースケール
66,614バイト(非圧縮)

ファイルサイズの比較

quality 画像 サイズ(バイト)
10 4,465
50 13,189
75 19,908
95 47,073
10 3,594
50 6,629
75 8,487
95 15,452

等倍の画像で見ると qualityが50以上であれば、見た目に差はさほど感じられないでしょうか?

qualityが75と95とで比較すると、特に文字の画像を見ると、ほんの少しだけエッジがくっきり見えますが、ファイルサイズが倍近く異なります。

ノイズの比較

画像の中心付近を拡大して、ノイズを確認してみます。

quality = 10

quality = 50

quality = 75

quality = 95

quality = 10

quality = 50

quality = 75

quality = 95

マンドリルの画像ではquality = 50ぐらいまでが、四角いマス状のノイズ(ブロックノイズ)が確認できます。

文字の画像は、明暗の差が大きく、エッジが強い画像の代表例として用いたのですが、黒い周りにゴミのように見えるモスキートノイズが確認できます。

 

ノイズの出方やファイルサイズの両方を考えると、qualityの初期値である 75 ぐらいがバランスが良さそうです。

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save

https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html

【Python/Pillow(PIL)】画像の上下左右反転、90°ごとの回転

Pillowで画像(画像データ)を画像の上下左右反転、90°ごとの回転をさせるには、Imageクラスのtransposeメソッドを用います。

これとは別にImageOpsモジュールmirror関数(左右反転)flip関数(上下反転)があるので、これは後半で説明します。

構文

Image.transpose(method)

パラメータ

引数名 説明
method 上下左右反転、90°ごとの回転の種類を以下の中から指定します。
Image.FLIP_LEFT_RIGHT
Image.FLIP_FLIP_TOP_BOTTOM
Image.FLIP_ROTATE_90
Image.FLIP_ROTATE_180
Image.FLIP_ROTATE_270
Image.FLIP_TRANSPOSE
Image.FLIP_TRANSVERSE
戻り値 上下左右反転、回転した画像(PIL.Image)

サンプル

from PIL import Image

# 画像を開く
img = Image.open("image.jpg")

# 左右上下反転、90度ごとの回転
img_transpose = img.transpose(Image.FLIP_LEFT_RIGHT)

# 画像の保存
img_transpose.save("image_transpose_FLIP_LEFT_RIGHT.jpg")

(実行結果)

元画像 左右反転(Image.FLIP_LEFT_RIGHT)

上下左右、回転の種類

methodに指定する種類は以下の通りです。

Image.FLIP_LEFT_RIGHT Image.FLIP_TOP_BOTTOM
Image.ROTATE_180 Image.ROTATE_90
Image.ROTATE_270 元画像
Image.TRANSPOSE Image.TRANSVERSE

ImageOpsモジュールのmirror(左右反転)とflip(上下反転)

ImageOpsモジュールにも、左右反転(mirror)と上下反転(flip)の関数があります。

こちらの方が関数名的には覚えやすいでしょうか?

 

(サンプル)

from PIL import Image
from PIL import ImageOps

# 画像を開く
img = Image.open("image.jpg")

# 左右反転
img_mirror = ImageOps.mirror(img)
# 画像の保存
img_mirror.save("image_mirror.jpg")

# 上下反転
img_flip = ImageOps.flip(img)
# 画像の保存
img_flip.save("image_flip.jpg")

(実行結果)

ImageOps.mirror() ImageOps.flip()

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.transpose

https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.flip

【Python/Pillow(PIL)】画像の回転

【Python/Pillow(PIL)】画像の回転

Pillowで画像(画像データ)を拡大/縮小するには、Imageクラスのrotateメソッドを用います。

構文

Image.rotate(angle, resample=0, expand=0, center=None, translate=None, fillcolor=None)

パラメータ

引数名 説明
angle 回転角度を度数(°)で指定します。
resample 回転時の補間方法を指定します。
Image.NEAREST
Image.BILINEAR
Image.BICUBIC
(参考)画像のリサイズ、補間指定
expand Trueのとき、画像が回転しても画像全体が表示されるよう画像サイズを拡張します。
False(初期値)のとき、画像が回転するしたときにはみ出した部分が切り取られます。
ただし、画像回転の中心は画像の中心で、平行移動が無い事を想定しています。
center 回転の中心座標を(x, y)のタプルで指定します。
初期値:画像の中心
translate 回転後の移動量を(Tx, Ty)のタプルで指定します。
fillcolor 画像の外側の色を指定します。
初期値:黒
戻り値 回転した画像(PIL.Image)

サンプル

from PIL import Image

# 画像を開く
img = Image.open("image.bmp")

# 画像を回転する
img_rotate = img.rotate(30)

# 回転した画像の保存
img_rotate.save("image_rotate.bmp")

(実行結果)

元画像 回転画像

画像がはみ出さないようにする

expand = True に設定することで、画像がはみ出さないよう、画像サイズを自動で拡張し調整します。

(例)

img_rotate = img.rotate(30, expand = True)

(結果)

画像の外側の色の指定

画像の外側の色をfillcolorに指定します。

(例)

img_rotate = img.rotate(30, fillcolor = "Blue")

(結果)

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.rotate

https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-filters

 

【Python/Pillow(PIL)】画像のリサイズ、補間指定

Pillowで画像(画像データ)を拡大/縮小するには、Imageクラスのresizeメソッドを用います。

構文

Image.resize(size, resample=None, box=None, reducing_gap=None)

パラメータ

引数名 説明
size リサイズ後の大きさを (幅, 高さ)のタプルで指定します。
resample リサイズ時の補間方法を指定します。
Image.NEAREST
Image.BOX
Image.BILINEAR
Image.HAMMING
Image.BICUBIC
Image.LANCZOS
画像の mode が 1, P や I;16などのビット指定モードのときは、Image.NEAREST が初期値
その他の場合、Image.BICUBICが初期値
box 画像を拡大/縮小する領域を(左, 上, 右, 下)の座標のタプルで指定します。
初期値は(0, 0, 画像の幅, 画像の高さ)の画像全体
cropメソッドの処理と同じ
reducing_gap リサイズ時の最適化(詳細わからず。。)
初期値: None (最適化なし)
戻り値 リサイズされた画像(PIL.Image)

サンプル

from PIL import Image

# 画像を開く
img = Image.open("Parrots.bmp")

# 画像を拡大/縮小する
img_resize = img.resize((64, 64))

# 切り抜いた画像の保存
img_resize.save("Parrots_resize.bmp")

(実行結果)

元画像 縮小画像

resample(補間方法)について

resampleの設定は、主に画像を拡大するときに画像の画素と画素の間の輝度値を求める事になるのですが、この時の求め方の方法を指定します。

この画素間の輝度値を求める方法は一般に補間と言いますが、補間については、下記のページを参照ください。

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

Pillowでは、補間方法にImage.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING,  Image.BICUBIC, Image.LANCZOSの方法を指定する事ができますが、

NEAREST→BOX→BILINEAR→HAMMING→BICUBIC→LANCZOS

の順で、キレイにリサイズすることができますが、処理時間は逆に遅くなるため、用途に応じてresampleの設定を行ってください。

個人的には、画像処理後の1画素1画素の輝度値を見せたいときはNEAREST、とりあえずのBILINEARという使い分けが多いです。

下図のように部分的に画像を拡大したときに、resampleの設定を変えると、どのようになるか?見てみたいと思います。

 

NEAREST BOX BILINEAR
HAMMING BICUBIC LANCZOS

拡大の時のキレイさは、NEAREST, BOX → HAMMING → BILEAR → BICUBIC → LANCZOS の順ですかね?

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.resize

https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-filters

【Python/Pillow(PIL)】画像の一部を切り抜く

Pillowで画像の一部を切り抜くには、Imageクラスのcropメソッドを用います。

書式は

Image.crop(box=None)
引数 説明
box 切り抜く領域を(左, 上, 右, 下)の座標のタプルで指定します。

 

(サンプルプログラム)

from PIL import Image

# 画像を開く
img = Image.open("Parrots.bmp")

# 画像を切り抜く
img_roi = img.crop((146, 81, 253, 183)) # (left, upper, right, lower)

# 切り抜いた画像の保存
img_roi.save("Parrots_roi.bmp")

(実行結果)

元画像 切り抜き画像

(補足)指定座標について

画像を切り抜く座標の定義ですが、画像の幅がWidth、画像の高さがHeightとすると、
画像の左上が(0, 0)、画像の右下が(Width-1, Height-1)となります。

実際に切り抜かれる画像の領域は、左上の座標(left, upper) を含み、右下の座標(right, lower) の1画素内側の領域が切り抜かれます。

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.crop

【Python/Pillow】線や円などの図形の描画

Pythonで線や円などの図形を書く方法としては、OpenCVやtkinterなどを用いても出来ますが、今回はPillowを用いて描画する方法です。tkinterの場合はこちらを参照ください。

Pillowで図形を描画するには、描画先のImageオブジェクトからImageDrawオブジェクトを作成し、このImageDrawオブジェクトに対して線などを描画を行います。

ImageDrawオブジェクトに線などを描画すると、書いたデータはImageオブジェクトに反映されます。

簡単なサンプル

from PIL import Image, ImageDraw

# カラーの画像データ(Imageオブジェクト)の作成
img = Image.new("RGB", (300, 100), "White")
# ImageDrawオブジェクトの作成
draw = ImageDraw.Draw(img)

# 直線の描画
draw.line([(10, 90), (290, 10)], fill = "Blue", width = 10)

# 画像の表示
img.show()

図形の種類

● 直線、折れ線

ImageDraw.line(xy, fill=None, width=0, joint=None)
xy 始点と終点、もしくは折れ線を構成する交点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 線色を指定します。
width 線幅を指定します。
joint 折れ線の場合の、つなぎ目の状態を指定します。

何も指定しない(None)と線の継ぎ目に切れ目ができます。

“curve”を指定すると、丸く繋いでくれます。

● 矩形(四角形)

ImageDraw.rectangle(xy, fill=None, outline=None, width=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 角の丸い矩形

ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
radius 角の半径を指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 楕円、円

ImageDraw.ellipse(xyfill=Noneoutline=Nonewidth=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● ポリゴン(折れ線の始点と終点を結んだ図形)

ImageDraw.polygon(xy, fill=None, outline=None)
xy 折れ線を構成する交点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。

● 正n角形

ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
bounding_circle 多角形に外接する円の中心(x, y)および半径r を指定します。
指定方法
(x, y, r) および ((x, y), r)
n_sides  何角形かを指定します。 3以上
rotation  回転角度を度数で指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。

● 円弧

ImageDraw.arc(xy, start, end, fill=None, width=0)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 線色を指定します。
width 線幅を指定します。

● 円弧(始点と終点が直線で結ばれた状態)

ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 円弧(始点と終点がそれぞれ原点と直線で結ばれた状態)

ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 点

ImageDraw.point(xy, fill=None)
xy 点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 点の色を指定します。

 

備考

色の指定は”Red”, ”Green”などの色の名前の文字列を使うか、R,G,Bの順で16進数で指定(#FF0000だと赤)するか、(R,G,B)のタプルで指定します。

(参考)

https://www.w3schools.com/colors/colors_names.asp

楕円や円弧の座標指定位置は、下図のようになります。

サンプルプログラム

from PIL import Image, ImageDraw
import random # 点のランダム表示用

# カラーの画像データ(Imageオブジェクト)の作成
img = Image.new("RGB", (600, 700), "White")
# ImageDrawオブジェクトの作成
draw = ImageDraw.Draw(img)

# 直線
draw.line([(10, 60), (290, 10)], fill = "Blue", width = 3)

# 折れ線
draw.line([(10, 100), (150, 180), (290, 100)], fill = "Green", width = 20)
draw.line([(10, 200), (150, 280), (290, 200)], fill = "Red", width = 20, joint="curve")

# 四角形
draw.rectangle([(10, 300), (290, 380)], fill = "#FF8C00", outline="#DC143C", width = 5)

# 角の丸い四角形
draw.rounded_rectangle([(10, 400), (290, 480)], radius = 15, fill = (0, 0, 255), outline="Red", width = 5)

# 楕円
draw.ellipse([(10, 500), (290, 580)], fill = "Magenta", outline="Cyan", width = 5)
# 円
draw.ellipse([(110, 600), (190, 680)], fill = "Green", outline="Yellow", width = 5)

# ポリゴン(始点と終点を結んだ多角形)
draw.polygon([(310, 0), (450, 30), (590, 0), (590, 80), (450, 50), (310, 80)], fill = "Red", outline="FireBrick")
draw.polygon([(310, 100), (410, 180), (490, 100), (590, 180)], fill = "blue", outline="red")

# 正n角形
draw.regular_polygon((450, 240, 40), 5, 0, fill = "blue", outline="red")

# 円弧
draw.arc([(310, 300), (590, 380)], 0, 240, fill = "Blue", width = 5)

# 円弧(始点と終点を結ぶ)
draw.chord([(310, 400), (590, 480)], 0, 240, fill = "Blue", outline="Red", width = 5)

# 円弧(始点・終点と原点を結ぶ)
draw.pieslice([(310, 500), (590, 580)], 0, 240, fill = "Blue", outline="Red", width = 5)

# 点
for i in range(1000):
    x = random.randrange(310, 590)
    y = random.randrange(600, 680)
    # 点の描画
    draw.point((x,y), fill = "Blue")

# 画像の保存
img.save("image.png")

# 画像の表示
img.show()

(実行結果)

参照ページ

https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html

https://www.w3schools.com/colors/colors_names.asp

関連記事

【Python/tkinter】線や円などの図形の描画

【Python/Pillow(PIL)】画像データの新規作成

画像データ(PIL.Image)を画像ファイルなどからではなく、新規に作成するには、Imageモジュールのnew関数を使います。

new関数の書式は以下の通り

PIL.Image.new(mode, size, color=0)
mode 画像のモードを設定します。
主なものとして、
“L”      8bitグレースケール
“RGB”  3x8bit カラー画像
詳細はこちらを参照ください。
size 画像のサイズを(幅, 高さ)のタプルで指定します。
color 画像全体のデータの色の値を指定します。
初期値は黒となります。
カラーの場合は、(R, G, B)のように各チャンネルごとの値のタプルで指定します。

 

グレースケール画像を作成するには、以下のようにします。

from PIL import Image

# グレースケールの画像データを作成
img = Image.new("L", (320, 240))
# 画像の表示
img.show()

(実行結果)

初期値を指定すると

# 輝度値を指定して画像データを作成
img = Image.new("L", (320, 240), 128)
# 画像の表示
img.show()

(実行結果)

カラー画像の場合は以下のようにします。

# カラー画像データを作成
img = Image.new("RGB", (320, 240), (0, 128, 255))
# 画像の表示
img.show()

(実行結果)

【Python/Pillow(PIL)】画像ファイルを開く,保存する

jpegやbmpなどの画像ファイルをPillowで開くには、Imageモジュールのopen関数を使います。

同様に画像をファイルに保存するにはsave関数を用います。

以下に、bmp形式の画像ファイルを開き、画像をカラーからモノクロのグレースケールに変換し、pngファイルに保存する例を示します。

from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("Parrots.bmp")

# OS標準の画像ビューアで表示
img.show()

# グレースケールへ変換
img_gray = img.convert("L")
img_gray.show()

# 画像のファイル保存
img_gray.save("image_gray.png")

(実行結果)

 

ファイル名に日本語も指定できる(OpenCVのimread関数は日本語が使えない)ので、使い勝手がいいと思います。

読込、保存のできる画像ファイルのフォーマット(bmp,jegなど)は別途こちら↓にまとめました。

【Python/Pillow(PIL)】対応画像ファイルフォーマット

 

画像ファイルの保存では、jpegファイルでは品質(quality)など、ファイルフォーマットごとに指定できるオプションがあるので、詳細はこちらのページ↓を参照ください。

【Python/Pillow(PIL)】JPEG画像の品質を指定して保存する

【Python】画像データがNumPyかPillowか調べる方法

Pythonで画像処理をしていると、画像データの型(クラス)は、OpenCVを使っているとNumPyだし、Tkinterで画像を表示しようとすると、Pillowを使ったりもするので、どうしても画像データがNumPyとPillowが混在しがちです。

そこで、画像データがNumPyなのか?Pillowなのか?を調べる方法の紹介です。

画像データに限らず、インスタンスしたクラスのオブジェクトが、どのクラスなのかを調べるにはisinstance関数を用います。

ininstance関数の書式は以下の通りです。

ininstance(クラスオブジェクト, クラス)

クラスオブジェクトが指定したクラスと一致している場合はTrueが、異なる場合はFalseが返ります。

このininstance関数を使って、画像データがNumPyなのか?Pillowなのか?を調べる関数の例を以下に示します。

def check_image_data(image):
    '''画像データがNumPyか、Pillowかを調べる'''
    if isinstance(image, np.ndarray):
        print("NumPy Image")
    elif isinstance(image, Image.Image):
        print("Pillow Image")

この関数を使って、実際に画像データがNumPyかPillowかを調べるサンプルは以下の通りです。

from PIL import Image
import numpy as np

def check_image_data(image):
    '''画像データがNumPyか、Pillowかを調べる'''
    if isinstance(image, np.ndarray):
        print("NumPy Image")
    elif isinstance(image, Image.Image):
        print("Pillow Image")

# Pillowの画像データ
pillow_image = Image.open("Mandrill.bmp")
check_image_data(pillow_image)

# NumPyの画像データ
numpy_image = np.asarray(pillow_image)
check_image_data(numpy_image)

実行結果

 

型を調べるだけならtype関数を使うこともできます。

(例)

print(type(numpy_image))
# <class 'numpy.ndarray'>
print(type(pillow_image))
# <class 'PIL.BmpImagePlugin.BmpImageFile'>

上記のコメント部分がtype関数を使って型を表示した結果になりますが、NumPyの型は‘numpy.ndarray’と表示されているので、まだ分かり易いのですが、Pillowの型は、’PIL.Image.Image’と表示されるのを期待しているのですが、‘PIL.BmpImagePlugin.BmpImageFile’と表示されてしまいます。

これは、bmpファイルからPillowの画像データを開いたためで、別のjpegファイルから開くと別の型が表示されます。

そのため、型を調べる、表示するだけなら type関数、型を判断するならisinstance関数という使い分けが良さそうです。

関連記事

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【Python/Pillow(PIL)】カラー,モノクロ,HSVなどの変換

PythonのPillowでカラー画像からモノクロ画像などへの変換は convert関数を用います。

 

カラー画像からモノクロ画像への変換は以下のように行います。

from PIL import Image

img_rgb = Image.open("Parrots.bmp")
img_rgb.show()

# グレースケール("L")へ変換
img_gray = img_rgb.convert("L")
img_gray.show()

# モノクロ("1")へ変換
img_mono = img_rgb.convert("1")
img_mono.show()

実行結果

Pillowでは、”モノクロ”と言っても、0~255までの256諧調の輝度値を持ったグレースケール(“L”)と、黒(0)と白(1)の2諧調のモノクロ(“1”)とがあります。
上図の左側がグレースケール(“L”)で、右側がモノクロ(“1”)です。

モノクロ(“1”)の画像は一般にディザ(dither)と呼ばれる処理で、新聞の写真のように1色のインクしか無い印刷で使われます。このディザの処理にもいくつか手法があり、Pillowでは誤差拡散法という処理になっています。(参考書籍:ディジタル画像処理

ただし、このモノクロ(“1”)は、画像処理的には、あまり出番が無いので、私は特に断りの無い限り、グレースケール画像の事を「モノクロ画像」と呼んでいます。

今回は、カラー画像(“RGB”)からグレースケール画像(“L”)へ変換しましたが、convert関数の引数の部分を変えることで、他の色空間への変換も可能です。

どのような色空間へ変換できるか?は以下のページを参照ください。

【Python/Pillow(PIL)】画像データフォーマット(mode)

 

このconvert関数はOpenCVでいうところの cvtColor関数に相当しますが、個人的に興味があったのが、色相のように0~360°の値を持つ色空間は、どのように変換されるのか?ということ。

OpenCVでは、0~360°を8bitのデータに収まるように、0~180へ変換する COLOR_BGR2HSV と0~255へ変換する COLOR_BGR2HSV_FULL の2種類があります。

そこで、Pillowではどのように変換されるのか?確認してみました。

 

評価プログラム

from PIL import Image

img_rgb = Image.new("RGB", (3, 1))

# R, G, Bの値を設定
img_rgb.putpixel((0,0), (255, 0, 0))
img_rgb.putpixel((1,0), (0, 255, 0))
img_rgb.putpixel((2,0), (0, 0, 255))

# RGB -> HSV
img_hsv = img_rgb.convert("HSV")

# 各画素の値を取得
print("(0, 0)", img_hsv.getpixel((0, 0)))
print("(1, 0)", img_hsv.getpixel((1, 0)))
print("(2, 0)", img_hsv.getpixel((2, 0)))

実行結果

この結果から、0°は0へ、120°は85へ、240°は170へ変換されていることから、0~360°の角度は0~255へ変換されていることが分かります。

Pillowで行われる色相の角度の変換は、OpenCVの COLOR_BGR2HSV_FULL 相当で変換されている!という事でした。

【Python/Pillow(PIL)】画像の輝度値をCSVファイルに保存/読込

画像処理をしていると、画像の輝度値をCSVファイル保存して、輝度値そのものや、輝度値の分布などを見たくなります。

Pythonにはcsvモジュールがあり、比較的簡単に画像の輝度値をCSVファイルに保存することができます。

輝度値をCSVファイルに保存するサンプルを示します。

ただし、モノクロとカラーの画像が混在すると難しいので、モノクロ限定とします。

 

(参考)csvモジュール

https://docs.python.org/ja/3/library/csv.html

輝度値の取得はPillowのgetdata()メソッドを使用します。

【Python/Pillow(PIL)】画像の輝度値の取得/設定

 

輝度値のCSVファイル保存

import csv
from PIL import Image

# 画像読込
img = Image.open("Mandrill.bmp")

# モノクロ画像へ変換
img = img.convert("L")
width, height = img.size

########################################################
# 輝度値の取得、CSVファイルに保存

# 画像の輝度値をlistで取得
data = list(img.getdata())

# 輝度値をCSVファイルで保存
with open('image_data.csv', 'w', newline='') as csvfile:
    spamwriter  = csv.writer(csvfile)

    # 画像データを一行ごと書き込み
    x = 0
    for y in range(height):
        # 一行分のデータ
        line_data = data[x:x+width]
        # 一行分のデータを書き込み
        spamwriter.writerow(line_data)
        x += width

CSVファイルをエクセルで開くと以下のようになります。

 

CSVファイルを開き画像へ変換

CSVファイルを開くのも保存と同様にcsvモジュールを用います。

ただし、CSVファイルは前項で保存したCSVファイルのように二次元でモノクロの輝度値が配置されたファイルとします。

csvモジュールでCSVファイルを開いたとき、CSVファイルの各値は文字列のリストに格納されるので、各要素をint型に変換している部分がポイントとなります。

import csv
from PIL import Image

########################################################
# CSVファイルを開く、Pillowの画像データに変換
load_data = []
# CSVファイルを開く
with open('image_data.csv', newline='') as csvfile:
    # ファイルの読込
    spamreader = csv.reader(csvfile)

    height = 0
    # データを一行ごとにリストに追加
    for line_data in spamreader:
        # 各要素の文字列をintに変換
        row = [int(val) for val in line_data]
        # リストに行データを追加
        load_data += row
        # 行数(画像の高さ)カウント
        height += 1

# 画像の幅を計算
width = len(load_data) / height

# 画像を作成
csv_image = Image.new("L", (int(width), height))
# データを読込(輝度値が格納されたリストのデータをPillowの画像データに設定)
csv_image.putdata(load_data)

# 画像の表示
csv_image.show()

処理結果は以下のようにCSVファイルを開くと、画像が表示されます。

CSVファイルをエクセルで見やすくする

CSVファイルをエクセルで開くと、こんな感じ↓で味気ないものとなります。

これを画像らしく、少し見やすくします。

まず、セルのサイズを正方形に近くなるように列の幅を調整します。

輝度値が記載されている列を全て選択し、列の部分を右ボタンでクリックし、列の幅を選択します。

表示された設定画面で、列の幅に2.7を入力します。

するとセルのサイズがだいたい正方形になります。

さらにセルに色を付けて画像らしくします。

輝度値が記載されているセルを全て選択し、ホーム→条件付き書式→カラースケール→その他のルールと選択します。

表示されたウィンドウで、最小値、最大値の部分を以下のように設定します。

最小値 最大値
種類 数値 数値
0 255

すると、セルの背景色が画像らしくなります。

この表示を縮小すると、まさに画像になってます。

エクセルで画像の輝度値を編集

試しに保存されたCSVファイルをエクセルで開き、画像の輝度値をじかに編集してみます。

これをCSVファイルに保存するのですが、エクセルのCSVファイル形式には CSV UTF-8 と CSV があるので、何も付いていない CSV(コンマ区切り)(*.csv)の方を選択して、CSVファイルに保存します。

このCSVファイルを、先ほどのCSVファイルを CSVファイルを開き画像へ変換 のプログラムで開くと以下のようになります。

エクセルで画像を直接編集できるのは、ちょと楽しいのですが、エクセルで画像処理を本気でやろうとするのは大変なので、画像をCSVファイルに保存するときは、画像の輝度値を解析的に見る程度に留めておく事をお勧めします。

【Python/Pillow(PIL)】画像の輝度値の取得/設定

画像を開き輝度値を取得/設定するのは、画像処理を行う、はじめの一歩的な処理ですよね。

まずは、最も基本的なgetpixel/putpixelを使った方法を紹介します。

getpixel()を使った輝度値の取得

getpixel()の構文は以下の通りです。

value = Image.getpixel(xy)

xyは画像の左上を原点とするxy座標で、(x, y)のようにタプルで指定します。

戻り値が指定した画像の輝度値となり、モノクロ画像の場合は、指定した座標の輝度値が戻り、カラーの場合は(r, g, b)のように3つの要素のタプルが戻ってきます。カラー画像でも ‘RGBA’ のように透過付きの画像データの場合は(r, g, b, a)のように4つの要素のタプルが戻ります。

実行例

putpixel()を使った輝度値の設定

putpixel()の構文は以下の通りです。

Image.putpixel(xy, value)

引数はgetpixel()と同じ用に、xyには(x, y)のように座標をタプルを指定します。valueの部分には、モノクロの場合は、指定座標の輝度値を、カラーの場合は(r, g, b)もしくは(r, g, b, a)のように輝度値をタプルで指定します。

さらに輝度値の値に0~255の範囲を超えて指定した場合、値が負の場合は0、値が256以上の場合は255に修正されます。

実行例

処理の高速化の検討

C#でも似た関数は処理時間が遅いで有名でしたが、getpixel()、putpixel()も処理時間が遅いらしい。

そこでいくつかの輝度値の取得/設定方法を試して処理時間の比較を行ってみたいと思います。

 

まずは処理時間の基準となるgetpixel(), putpixel() を使った処理時間を計測します。

輝度値を取得し、コントラストを調整し、輝度値を画像に設定するサンプルです。

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

上記プログラムを実行すると、以下のようになります。

 

処理前画像

処理後画像

 

この他に以下の方法を試してみます。

  • getdata(), putdata() を使った方法
    getdata()は画像全体の輝度値を画像の左上から順に各画素の輝度値(R, G, B)の値がタプルの一次元のリストで取得します。
    putdata()は輝度値(R, G, B)のタプルの一次元のリストを指定し、画像全体の輝度値を設定します。
  • numpy を使った方法
    Pillowの画像データからNumPyの画像データへ変換し、NumPyデータを処理し、Pillowの画像データに戻しています。
  • Pillow <-> numpy の相互変換だけの時間
    PillowとNumPyの画像データの変換時間を参考に計測します。
  • point() を使った方法
    輝度値の取得/設定の処理時間の評価の趣旨から外れますが、画像処理に周辺画素の輝度値を用いない場合、LUT(Look Up Table)を用いると高速に処理が行えるため、Pillowのpoint()メソッドでLUT変換を行った処理時間を参考に計測しています。

 

使用した全プログラム

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# getdata(), putdata() を使った方法
img = img_original.copy()
start = time.perf_counter()

data = img.getdata()
# 処理後のデータをlistで確保
data_dst = [None] * len(data)

for y in range(height):
    for x in range(width):
        r, g, b = data[x + y * width]
        data_dst[x + y * width] = (r * 5 - 500, g * 5 - 500, b * 5 - 500)

img.putdata(data_dst)

print("getdata(), putdata() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpy を使った方法
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

for y in range(height):
    for x in range(width):
        r = numpy_iamge[y, x, 0]
        g = numpy_iamge[y, x, 1]
        b = numpy_iamge[y, x, 2]
        r = r * 5 - 500
        g = g * 5 - 500
        b = b * 5 - 500
        if r < 0:
            r = 0
        if g < 0:
            g = 0
        if b < 0: b = 0 if r > 255:
            r = 255
        if g > 255:
            g = 255
        if b > 255:
            b = 255
        numpy_iamge[y, x, 0] = r
        numpy_iamge[y, x, 1] = g
        numpy_iamge[y, x, 2] = b

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpy を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その1
# numpyの配列(ndarray)をそのまま演算する
# 0~255に制限するのにclipを用いる
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換(計算後、負になるのでint32型へ変換)
numpy_iamge = np.array(img, dtype = np.int32)

dst_img = numpy_iamge * 5 - 500

# 0~255のuint8型へ変換
dst_img = dst_img.clip(0, 255).astype(np.uint8)

# numpy → pillowへ変換
img = Image.fromarray(dst_img)

print("numpyらしい処理 その1\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その2
# LUTを使った変換
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

# LUT(Look Up Table)の作成
lut = np.empty(256, dtype = np.uint8)
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0
    if val > 255:
        val = 255
    lut[i] = val

# LUTを介して変換
numpy_iamge = lut[numpy_iamge]

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpyらしい処理 その2\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# Pillow ⇔ numpy の相互変換だけの時間
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)
# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("Pillow ⇔ numpy の相互変換だけの時間\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# point() を使った方法
img = img_original.copy()
start = time.perf_counter()

# LUT(Look Up Table)の作成
lut = []
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0 
    if val > 255:
        val = 255
    lut.append(val)
# R, G, Bに同じLUTを使用
lut = lut * 3

# pointメソッドでLUT変換を行う
img = img.point(lut)

print("point() を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

処理時間の比較(使用した画像は256×256の24bitカラー画像)

方法 処理時間
getpixel(), putpixel() を使った方法 99.223 msec
getdata(), putdata() を使った方法 37.833 msec
numpy を使った方法 440.229 msec
(参考)numpyらしい処理 その1 2.081 msec
(参考)numpyらしい処理 その2 1.041 msec
(参考)Pillow <-> numpy の相互変換だけの時間 0.254 msec
(参考)point() を使った方法 0.156 msec

まとめ

輝度値の取得/設定を行う処理については、getdata(), putdata() を使った方法が一番速い結果となりました。

numpyを使うと、もう少し速いかと思っていたのですが、あまりに遅かったので、Pillow <-> numpy の相互変換の処理時間を計測してみましたが、やはり画像処理している部分が遅い事が分かりました。
numpyの処理だけ、0~255に輝度値が入るようにif文で処理をしていますが、これは、numpyのデータがuint8(8bitの符号なし整数)になるため、この処理を入れないと、下図のように変な画像になってしまいます。

逆に、getpixel(), putpixel() も getdata(), putdata() も、輝度値に0~255の範囲外の値を指定しても0~255の範囲に調整してくれるので、これは便利です。

ただし、numpyに画像データを変換すると、OpenCVも使えるので、numpyで画像処理するなら、使える処理があれば極力OpenCVを使うようにするとよいでしょうね。
numpyのデータはfor文で値を参照すると、どうしても遅いようです。

また、参考にpoint()メソッドによりLUTを使った処理時間を計測してみましたが、こちらは爆速でした!

今回はpoint()メソッドを使いましたが、他にもPillowでできる画像処理のメソッドが用意されているので、おいおい紹介したいと思います。

結局、Pythonでベタな画像処理をしてはいけないということですね。
OpenCVなどに無いオリジナルの画像処理をしたい場合は、やっぱりC言語のライブラリで処理を行う必要があるんでしょうね。

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

Pythonで画像処理をしていると、画像データの扱いは各ライブラリによって、NumPyのndarrayかPillowのPIL.Imageのどちらかになる場合が多いかと思います。

そこで NumPyとPillowの画像データの相互変換をまとめておきます。

 

NumPy -> Pillowへの変換

NumPy からPillowへの変換は Pillowの fromarray関数を用います。

from PIL import Image

pil_image = Image.fromarray(numpy_image)

Pillow -> NumPyへの変換

PillowからNumPyへの変換は NumPyの array関数を用います。

import numpy as np

numpy_image = np.array(pil_image)

array関数と似たものにasarray関数がありますが、このasarrayで変換されたNumPyの配列(ndarray)は読み取り専用となり、値の参照はできますが、値を設定することはできません。

import numpy as np

numpy_image = np.asarray(pil_image) # numpy_imageは読み取り専用となる

変換サンプル

NumPyとPillowの画像データを相互変換したサンプルを示します。

import numpy as np
from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_mono = Image.open("image_mono.bmp")
print(type(pil_image_mono))     # <class 'PIL.BmpImagePlugin.BmpImageFile'>
print(pil_image_mono.mode)      # L
print(pil_image_mono.size)      # (400, 300)

# Pillow でカラー画像を読み込む
pil_image_color = Image.open("image_color.bmp")
print(type(pil_image_color))    # <class 'PIL.BmpImagePlugin.BmpImageFile'>
print(pil_image_color.mode)     # RGB
print(pil_image_color.size)     # (400, 300)

# Pillow -> NumPyへ変換(モノクロ画像)
ndarray_mono = np.array(pil_image_mono)
print(type(ndarray_mono))       # <class 'numpy.ndarray'>
print(ndarray_mono.dtype)       # uint8
print(ndarray_mono.shape)       # (300, 400)

# Pillow -> NumPyへ変換(カラー画像)
ndarray_color = np.array(pil_image_color)
print(type(ndarray_color))      # <class 'numpy.ndarray'>
print(ndarray_color.dtype)      # uint8
print(ndarray_color.shape)      # (300, 400, 3)

# NumPy -> Pillowへ変換(モノクロ画像)
pil_image_mono = Image.fromarray(ndarray_mono)
print(type(pil_image_mono))     # <class 'PIL.Image.Image'>
print(pil_image_mono.mode)      # L
print(pil_image_mono.size)      # (400, 300)

# NumPy -> Pillowへ変換(カラー画像)
pil_image_color = Image.fromarray(ndarray_color)
print(type(pil_image_color))    # <class 'PIL.Image.Image'>
print(pil_image_color.mode)     # RGB
print(pil_image_color.size)     # (400, 300)

 

ここで注意しておきたいのが、

Pillowのモノクロ画像をNumPyへ変換したときは
[画像の高さ, 画像の幅]
の順の二次元配列となります。

Pillowのカラー画像をNumPyへ変換したときは
[画像の高さ, 画像の幅, 色(R, B, Gの順)]
の順の三次元配列となります。

NumPyのカラー画像をPillowへ変換する場合は、カラーデータの並びが R,G,B である必要があります。
OpenCVの画像データもNumPyのndarrayで扱われますが、OpenCVの場合、カラーデータの並びが
B,G,Rとなるため、OpenCVからPillowの画像データへ変換する場合は、cvtColor関数を使って、R,G,Bに変換しておく必要があります。

コード例

image_color = cv2.cvtColor(image_color, cv2.COLOR_BGR2RGB)

(参考)

matplotlibで画像データ(OpenCV,pillow,list)を表示する

【Python/NumPy】カラー画像データをRGBからBGRへ変換

【Python】画像データがNumPyかPillowか調べる方法

【Python/Pillow(PIL)】画像のビット数、チャンネル数を調べる

画像のビット数(8や24など)やチャンネル数(色の数、Lの場合は1、RGBの場合は3など)は画像処理をするときに、画像データを直接参照する場合などに必要になってきます。

jpegファイル(*.jpg)を開いたときには bits という値が拾え、1画素、1色あたり8bitであることがわかります。

しかしながら、他の形式のファイル(少なくとも bmp, pmg, gif)では、この bits の値がありません。

 

そこで、どのファイルでも画像のビット数やチャンネル数を調べられるようにするには、mode の値を調べるようにします。
modeの取得は、以下のように行います。

from PIL import Image

# Pillow で画像を読み込む
pil_image = Image.open("image_color.bmp")
# modeの表示
print("モード:", pil_image.mode)

モードの種類は以下の通りです。

mode 説明
1 1-bit pixels, black and white, stored with one pixel per byte
L 8-bit pixels, black and white
P 8-bit pixels, mapped to any other mode using a color palette
RGB 3×8-bit pixels, true color
RGBA 4×8-bit pixels, true color with transparency mask
CMYK 4×8-bit pixels, color separation
YCbCr 3×8-bit pixels, color video format
LAB 3×8-bit pixels, the L*a*b color space
HSV 3×8-bit pixels, Hue, Saturation, Value color space
I 32-bit signed integer pixels
F 32-bit floating point pixels

(参考)

https://pillow.readthedocs.io/en/stable/handbook/concepts.html

 

画像のビット数やチャンネル数を調べるのに、この mode を取得して、条件分岐でビット数やチャンネル数を上記の表から取得すると正確に求まります。

ただ、実際に使われるのは L, RGB, RGBA の3つぐらいなので、1画素8bit限定として考えると、チャンネル数が求まればビット数も求まります。

チャンネル数を直接取得できる方法は無さそうなので、 getbands() というメソッドを使って行います。

この getbands() は、例えば mode = ‘RGB’ のとき、各チャンネルの色の名前の(‘R’, ‘G’, ‘B’)というタプルを返すメソッドになります。そのため、このタプルの長さを取得すればチャンネル数も求まります。

 

サンプルプログラム

from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_mono = Image.open("image_mono.bmp")
print("■■ モノクロ画像 ■■")
print("モード:\t\t", pil_image_mono.mode)
print("バンド:\t\t", pil_image_mono.getbands())
print("チャンネル数:\t", len(pil_image_mono.getbands()))

# Pillow でカラー画像を読み込む
pil_image_color = Image.open("image_color.bmp")
print("■■ カラー画像 ■■")
print("モード:\t\t", pil_image_color.mode)
print("バンド:\t\t", pil_image_color.getbands())
print("チャンネル数:\t", len(pil_image_color.getbands()))

実行結果

 

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=getbands#PIL.Image.Image.getbands

【Python】画像ビューア(ズーム(拡大/縮小)、移動表示)

Canvasに画像を表示する のページではtkinterでGUIを作り画像ビューアを作りましたが、これに アフィン変換 を追加し、画像の拡大/縮小、移動の出来る画像ビューアを作成しました。

機能は、Fileメニューから画像ファイルを開き、マウスホイールの上下で画像の拡大/縮小を行い、マウスの左ボタンのドラッグで画像を移動します。
左ボタンのダブルクリックで画像全体を表示します。
また、ウィンドウ下にはCanvas上のマウスポインタの座標と、マウスポインタ位置の画像の座標および、その輝度値を表示します。
ウィンドウの右下には画像ファイルの種類、画像サイズ、画像の種類を表示します。

全ソースコード

参考

【Python/tkinter】Canvasに画像を表示する

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

【Python/NumPy】行列の演算(積、逆行列、転置行列、擬似逆行列など)

【Python/Pillow(PIL)】カラーパレットの設定(インデックスカラー)

PythonのPillowでモノクロ画像ファイルを開くと、Imageクラスの mode は “L” となりますが、これはカラーパレットを持たない画像データとなります。

C言語やC#ではモノクロ画像データを表示するときは、カラーパレットを参照して表示してモニタ上に画像を表示するインデックスカラーという仕組みがありました。

このインデックスカラーですが、モノクロ画像データの場合は 0~255 の輝度値を持ちますが、実際にモニタ上に表示する場合は、 0~255 のインデックスを持つカラーパレットのR,G,Bの値を参照して、モニタに表示されています。

index R G B
0 0 0 0
1 1 1 1
2 2 2 2
3 3 3 3
: : : :
254 254 254 254
255 255 255 255

モノクロ画像の場合、通常、indexの値とR,G,Bの値を同じ値にして表示するのですが、このR,G,Bの値を変更する事で、モノクロ画像に擬似的に色を付けて表示する事が可能になります。

Pillowでモノクロ画像にカラーパレットを使うには putpalette関数を用います。

構文

Image.putpalette(data, rawmode='RGB')

(参考)

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.putpalette

dataの部分にカラーパレットを指定しますが、これはリストで、

[R0, G0, B0, R1, G1, B1, R2, G2, B2, R3, G3, B3, … R255, G255, B255]

の順で、256×3個(768個)の一次元の配列で指定します。

サンプルプログラム

from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("./Mandrill.bmp")

palette = []

for i in range(0, 64):
    palette.append(i) # R
    palette.append(i) # G
    palette.append(i) # B

for i in range(64, 128):
    palette.append(0) # R
    palette.append(0) # G
    palette.append(255) # B

for i in range(128, 192):
    palette.append(0) # R
    palette.append(255) # G
    palette.append(0) # B

for i in range(192, 256):
    palette.append(255) # R
    palette.append(0) # G
    palette.append(0) # B

img.putpalette(palette)

# 画像の表示
img.show()

(実行結果)

実際にはカラーパレットは、モノクロカメラに擬似的に色を付けて輝度分布を見やすくする疑似カラー表示を行う場合だったり、二値化を行う際のプレビュー用として私は使っています。

二値化のプレビュー用のプログラムを以下に示します。

from PIL import Image, ImageTk, ImageOps
import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("カラーパレット")     # ウィンドウタイトル

        self.src_img = Image.open("./Mandrill.bmp")

        #---------------------------------------------------------------
        # Canvasの作成
        self.canvas = tk.Canvas(self.master, bg = "#008B8B")
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)   
        
        #---------------------------------------------------------------
        # Scaleの作成
        self.scale_var = tk.IntVar()
        scaleH = tk.Scale( self.master, 
                    variable = self.scale_var, 
                    command = self.slider_scroll,
                    orient=tk.HORIZONTAL,   # 配置の向き、水平(HORIZONTAL)、垂直(VERTICAL)
                    from_ = 0,            # 最小値(開始の値)
                    to = 256,               # 最大値(終了の値)
                    tickinterval=64         # 目盛りの分解能(初期値0で表示なし)
                    )
        scaleH.pack(fill = tk.X)
        #---------------------------------------------------------------

    def slider_scroll(self, event=None):
        '''スライダーを移動したとき'''
        self.disp_image(self.src_img, self.scale_var.get())

    def disp_image(self, image, threshold):
        '''画像をCanvasに表示する'''

        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        #---------------------------------------------------------------
        # カラーパレットの設定
        palette = []

        for i in range(0, threshold):
            palette.append(i) # R
            palette.append(i) # G
            palette.append(i) # B

        for i in range(threshold, 256):
            palette.append(255) # R
            palette.append(0)   # G
            palette.append(0)   # B
        
        # カラーパレットの設定
        image.putpalette(palette)

        #---------------------------------------------------------------
        #PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=image)

        # 画像の描画
        self.canvas.create_image(
                canvas_width / 2,       # 画像表示位置(Canvasの中心)
                canvas_height / 2,                   
                image=self.photo_image  # 表示画像データ
                )

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

(実行結果)

まとめ

PythonのPillowを使っていると、モノクロ画像のカラーパレットを意識する事は少ない気がしますが、カラーパレットを使わずにモノクロ画像に擬似的に色を付けようとすると、モノクロ8bitのデータをカラーの24bitに変換して、輝度値に合わせてR,G,Bの値を画像の全画素に対して変換を行う必要がありますが、カラーパレットを使うと、たかだか768個のデータを指定するだけなので、簡単に色を付ける事が可能になります。

ただし、このカラーパレットを指定できるのは、8bitのモノクロ画像(インデックスカラーも含む)のみとなります。

参考記事

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.putpalette

疑似カラー(Pseudo-color)

【C#】Bitmapのカラーパレットの設定