Pillowチュートリアル(抄訳+@)
基本的にDeepL翻訳です。
サンプルコードはだいぶ変更・追加してます。
原典:Tutorial — Pillow (PIL Fork) 9.1.0 documentation
Imageクラスの使用
Python Imaging Libraryで最も重要なクラスはImage
クラスで、同名のモジュールで定義されています。このクラスのインスタンスは、ファイルから画像を読み込んだり、他の画像を加工したり、ゼロから画像を作成したりと、いくつかの方法で作成することができます。
ファイルから画像を読み込むには、Imageモジュールのopen()
関数を使用します。
サンプル画像ada.png
のダウンロード
またはローカルの画像をpythonのCWDにロード(外部サーバへの送信は行いません)
# 上でローカルの画像をロードした場合は以下でファイルを確認できます。 # import os # print(os.listdir()) # 使用する画像ファイルのパスを指定してください。 image_path = "ada.png" im = Image.open(image_path)成功すれば、Imageオブジェクトを返します。
インスタンス属性を使ってファイルの中身を調べることができます。
format属性は、画像のソースを識別します。(画像がファイルから読み込まれなかった場合はNone)
size属性は、幅と高さ(ピクセル単位)を含むタプルです。
mode属性は、画像のバンド数とバンド名、画素の種類と深さを定義します。一般的なモードは、グレイスケール画像では"L"(ルミナンス)、トゥルーカラー画像では"RGB"、プリプレス画像では"CMYK"です。
ファイルを開くことができない場合、OSError
例外が発生します。
Imageクラスのインスタンスを取得すると、このクラスで定義されたメソッドを使って画像を加工したり操作したりすることができるようになります。例えば、先ほど読み込んだ画像を表示してみましょう。
im.show()Note
標準版のshow()は、画像を一時ファイルに保存し、ユーティリティを呼び出して表示するため、あまり効率的ではありません。適切なユーティリティがインストールされていない場合は、動作すらしないでしょう。しかし、これが動作すると、デバッグやテストに非常に便利です。
次のセクションでは、このライブラリで提供されるさまざまな機能の概要を説明します。
自由欄
画像の読み取りと書き込み
Python Imaging Libraryは、様々な画像ファイル形式をサポートしています。ディスクからファイルを読み込むには、Imageモジュールの open()
関数を使用します。ファイルを開くためにファイル形式を知る必要はありません。ライブラリは、ファイルの内容に基づいて自動的に形式を判断します。
ファイルを保存するには、Image クラスの save()
メソッドを使用します。ファイルを保存するときは、名前が重要になります。フォーマットを指定しない限り、ライブラリはファイル名の拡張子を使って、どのファイル保存形式を使うかを判断します。
ファイルをJPEGに変換する
import os from PIL import Image def to_JPEG(file_path): "画像ファイルのpathを受け取り、JPEGに変換してCWDに保存する" infile = os.path.basename(file_path) f, e = os.path.splitext(infile) outfile = f + ".jpg" if infile != outfile: with Image.open(file_path) as im: # 元画像のmodeがRGBAの場合エラー:"OSError: cannot write mode RGBA as JPEG" if im.mode == "RGBA": im = im.convert("RGB") im.save(outfile) return Image.open(outfile) jpg = to_JPEG(image_path) # 保存したJPEG画像の確認 print(f"{jpg.filename}, {jpg.format}") jpg.show()save()メソッドには、ファイル形式を明示的に指定する第2引数を与えることができます。非標準の拡張子を使用する場合は、常にこの方法で形式を指定する必要があります。
JPEGサムネイルを作成する
- API
重要なのは、ライブラリは本当に必要なとき以外は、ラスターデータをデコードしたり読み込んだりしないということだ。ファイルを開くと、ファイルヘッダを読んでファイル形式を判別し、モードやサイズなど、デコードに必要なプロパティを抽出しますが、それ以外の部分は後から処理されません。
つまり、画像ファイルを開くのは、ファイルサイズや圧縮の種類に依存しない高速な操作なのです。ここでは、一連の画像ファイルを素早く識別する簡単なスクリプトを紹介します。
自由欄
画像の切り取り、貼り付け、マージ
Cutting, pasting, and merging images
Imageクラスには、画像内の領域を操作するためのメソッドが用意されています。画像から矩形領域を切り出すには、crop()
メソッドを使用します。
画像から矩形を切り取る
Copying a subrectangle from an image
- API
領域は4タプルで定義され、座標は(left, upper, right, lower)で指定します。Python Imaging Libraryは左上を(0, 0)とする座標系を使用します。また、座標はピクセル間の位置を参照しているので、領域は正確に(left-right)x(uper-lower)ピクセルであることに注意してください。
参考:座標系 Coordinate System
矩形を処理、貼り付け
Processing a subrectangle, and pasting it back
領域を貼り戻す場合、領域のサイズは指定された領域と完全に一致しなければなりません。また、領域は画像の外にはみ出してはいけません。ただし、元の画像と領域のモードは一致する必要はありません。一致しない場合、領域は貼り付ける前に自動的に変換されます。
tmp = im.copy() transposed = region.transpose(Image.Transpose.FLIP_LEFT_RIGHT) tmp.paste(transposed, box) tmp.show()元画像のサイズをはみ出す場合
と言いつつ、box
を2タプルで指定する場合は元画像上のその座標を左上として別画像を貼り付けるだけなのでサイズを気にする必要はありません。
また、元画像をはみ出す場合でも単に切り捨てられるだけなので挙動を理解していれば問題ないでしょう。
mask
の使用
mask
が指定されている場合、このメソッドはmask
によって示された領域のみを更新します。
mask
として使用できる画像のmodeは“1”, “L”, “LA”, “RGBA”, “RGBa”のいずれかです(存在する場合は、アルファバンドがマスクとして使用されます)。
mask
が255の場合、指定された画像はそのままコピーされます。0の場合、元画像の値が保持されます。中間値は、2つの画像を混合します。アルファチャネルがある場合はそれも含まれます。
ada.png
はアルファバンドを持つので、そのままmask
画像として使用できます。
画像のローリング
def roll(im, delta): """Roll an image sideways.""" xsize, ysize = im.size delta = delta % xsize if delta == 0: return im part1 = im.crop((0, 0, delta, ysize)) part2 = im.crop((delta, 0, xsize, ysize)) im.paste(part1, (xsize - delta, 0, xsize, ysize)) im.paste(part2, (0, 0, xsize - delta, ysize)) return imローリングした画像でGIFアニメーションの作成
images = list() base_im = im.copy() base_im.thumbnail(size=(128, 128)) delta = 2 for width in range(delta, im.size[0], delta): images.append(roll(base_im.copy(), width)) base_im.save( "out.gif", save_all=True, append_images=images, transparency=0, disposal=2, loop=0 )保存したGIFアニメーションの確認
# 縮小表示 # for jupyter # import IPython.display as d # d.HTML('画像のマージ
def merge(im1, im2): w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) im.paste(im1) im.paste(im2, (im1.size[0], 0)) return im merge(im, duplicate).show()バンドの分割とマージ
Python Imaging Libraryでは、RGB画像のようなマルチバンド画像の個々のバンドを操作することも可能です。分割メソッドは、元のマルチバンド画像からそれぞれ1つのバンドを含む一連の新しい画像を作成します。マージ関数は、モードと画像のタプルを受け取り、それらを新しい画像に結合します。次のサンプルは、RGB画像の3つのバンドを入れ替えるものです。
def common_for_ax(ax): # 枠非表示 ax.spines[:].set_visible(False) # 軸非表示 ax.xaxis.set_visible(False) ax.yaxis.set_visible(False) return ax import itertools perm = itertools.permutations r, g, b, *a = im.split() a = tuple(a) mode = "RGB" if len(a) == 0 else "RGBA" fig = plt.figure(figsize=(10, 10)) for i, bands in enumerate(zip(perm([r, g, b]), perm(["r", "g", "b"]))): ax = fig.add_subplot(2, 3, i + 1) ax.imshow(Image.merge(mode, bands[0] + a)) ax.set_title(f"{i+1}. RGB -> {bands[1]}") common_for_ax(ax) plt.show()なお、単一バンドの画像の場合、split()は画像そのものを返します。個々のカラーバンドを扱うには、まず画像を "RGB" に変換しておくとよいでしょう。
自由欄
幾何学的変換
PIL.Image.Image クラスには、画像のresize()
と rotate()
を行うメソッドがあります。前者は新しいサイズを与えるタプル、後者は反時計回りの角度を与えるタプルをとります。
単純なジオメトリ変換
im.resize((128, 128)).show() im.rotate(45).show() # degrees counter-clockwise画像のトランスポーズ
画像を90度単位で回転させるには、rotate()
メソッドかtranspose()
メソッドを使用することができます。後者は、画像を水平軸や垂直軸のまわりに反転させる場合にも使用できます。
transpose(ROTATE)
は、rotate(expand=True)
と同様に元画像のサイズに収まるように回転した画像サイズを変更します。
より一般的な画像変換は、transform()メソッドで行うことができます。
自由欄
色変換
PILでは、
convert()
メソッドを使用して、異なるピクセル表現間で画像を変換することができます。
- API
Pillowでサポートされている代表的なmodeは以下の通りです。(参照:Modes)
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
(3x8-bit pixels, true color)RGBA
(4x8-bit pixels, true color with transparency mask)CMYK
(4x8-bit pixels, color separation)YCbCr
(3x8-bit pixels, color video format)- Note that this refers to the JPEG, and not the ITU-R BT.2020, standard
LAB
(3x8-bit pixels, the Lab color space)HSV
(3x8-bit pixels, Hue, Saturation, Value color space)I
(32-bit signed integer pixels)F
(32-bit floating point pixels)
モード間の変換
print("original image's mode: ", im.mode) fig = plt.figure(figsize=(10, 10)) modes = ["1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] for i, mode in enumerate(modes): ax = fig.add_subplot(3, 4, i + 1) try: ax.set_title(mode) ax.imshow(im.convert(mode)) common_for_ax(ax) except Exception as e: print(f"{mode}: {e}") plt.show()なぜかmode: "L"
のときだけmatplotlibでデフォルトのカラーマップが使われてしまいました。(なぜ…?)
matplotlibで明示的にGrayscale表示するにはcmap="gray", vmin=0, vmax=255
を指定します。
ところで、ada.png
では絵の背景(額縁の外側)を透過していますが、アルファレイヤを除くと背景部分が黒になってしまいます。
アルファレイヤで透過させる部分をRGBの白にするには、例えば以下のようにします。
グレースケールでも背景が白ですっきりします。
im_rgb.convert("L").show()ちなみに上の操作は以下のようにnumpy
を使用することでも実現できます。
自由欄
画像補正
PILは、画像の補正に使用できるメソッドやモジュールを多数提供しています。
フィルタ
ImageFilter
モジュールには、filter()
メソッドで使用できる以下の定義済みの拡張フィルタが含まれています。
- BLUR
- CONTOUR
- DETAIL
- EDGE_ENHANCE
- EDGE_ENHANCE_MORE
- EMBOSS
- FIND_EDGES
- SHARPEN
- SMOOTH
- SMOOTH_MORE
ポイント操作
point()
メソッドは、画像のピクセル値を変換するために使用できます(例:画像のコントラスト操作)。ほとんどの場合、このメソッドには、1つの引数を期待する関数オブジェクトを渡すことができます。各ピクセルは、その関数にしたがって処理されます。
どんな簡単な式でもすぐに画像に適用することができます。また、point()とpaste()を組み合わせることで、画像を選択的に変更することができます。
# split the image into individual bands source = im.split() R, G, B = 0, 1, 2 # select regions where red is less than 100 mask = source[R].point(lambda i: i < 100 and 255) # process the green band # out = source[G].point(lambda i: i * 0.7) out = source[G].point(lambda i: int(i * 0.7)) # for pyodide # paste the processed band back, but only where red was < 100 source[G].paste(out, None, mask) # build a new multiband image new_im = Image.merge(im.mode, source) # ステップ毎の画像を表示 fig = plt.figure(figsize=(10, 10)) i = 1 ax = fig.add_subplot(2, 3, i,) ax.imshow(im) ax.set_title("ORIGINAL") common_for_ax(ax) i += 1 ax = fig.add_subplot(2, 3, i) ax.imshow(mask, cmap="gray", vmin=0, vmax=255) ax.set_title("MASK BASED ON RED BAND") common_for_ax(ax) i += 1 ax = fig.add_subplot(2, 3, i) ax.imshow(im.split()[1], cmap="gray", vmin=0, vmax=255) ax.set_title("ORIGINAL GREEN BAND") common_for_ax(ax) i += 1 ax = fig.add_subplot(2, 3, i) ax.imshow(out, cmap="gray", vmin=0, vmax=255) ax.set_title("PROCESSED GREEN BAND") common_for_ax(ax) i += 1 ax = fig.add_subplot(2, 3, i) ax.imshow(source[G], cmap="gray", vmin=0, vmax=255) ax.set_title("PASTED GREEN BAND") common_for_ax(ax) i += 1 ax = fig.add_subplot(2, 3, i) ax.imshow(new_im) ax.set_title("NEW IMAGE") common_for_ax(ax) plt.show()i < 100 and 255
について
Pythonは論理式のうち結果を決定するのに必要な部分のみを評価し、最後に評価した値を式の結果として返します。つまり、i < 100
が偽(0)の場合、Pythonは第2オペランドを見ずに0を返します。そうでなければ、255を返します。
補正
より高度な画像補正を行うには、ImageEnhance
モジュールのクラスを使用することができます。画像から作成されたエンハンスメントオブジェクトは、さまざまな設定をすばやく試すために使用することができます。
この方法で、コントラスト、明るさ、カラーバランス、シャープネスを調整することができます。
from PIL import ImageEnhance def enhance_all(im): enhances = [ ImageEnhance.Color, ImageEnhance.Contrast, ImageEnhance.Brightness, ImageEnhance.Sharpness, ] factor = np.linspace(0, 2, 11) ROW = len(enhances) COL = len(factor) fig = plt.figure(figsize=(15, 10)) for r, e in enumerate(enhances): enh = e(im) # Enhance名 ax = fig.add_subplot(4, 1, r + 1) ax.set_title(e.__name__, pad=15, weight="bold", size=16) common_for_ax(ax) # Enhanceにfactorを適用する for c, f in enumerate(factor): f = round(f, 2) ax = fig.add_subplot(ROW, COL, r * COL + c + 1) ax.imshow(enh.enhance(f)) ax.set_title(f) common_for_ax(ax) return plt.show() tmp = im.copy() tmp.thumbnail((200, 200)) enhance_all(tmp)自由欄
イメージシーケンス
- API
PILは、イメージシーケンス(アニメーション形式とも呼ばれます)の基本的なサポートを含んでいます。サポートされているシーケンス形式は、FLI/FLC、GIFおよびいくつかの実験的な形式です。TIFF ファイルは複数のフレームを含むことができます。
シーケンスファイルを開くと、PILは自動的にそのシーケンスの最初のフレームを読み込みます。異なるフレーム間を移動するには、シークとテルのメソッドを使用できます。
Reading sequences
GIFアニメーションの作成(ani.gif
)
表示(Runをクリックして再生)
# 縮小表示 # for jupyter # import IPython.display as d # d.HTML('シーケンスが終了すると EOFError
が発生します。
次のクラスでは、forステートメントを使って、シーケンスをループさせることができます。
Using the ImageSequence Iterator class
from PIL import ImageSequence ani = Image.open("ani.gif") fig = plt.figure() COL = 6 ROW = ani.n_frames // COL + 1 for i, f in enumerate(ImageSequence.Iterator(ani)): ax = fig.add_subplot(ROW, COL, i + 1) ax.imshow(f) common_for_ax(ax) plt.show()ところで、少し上で作成したout.gif
は、40程度のフレーム数のはずがn_frames
は256となっています。
実際にフレームを取り出してみても同様です。
len(list(ImageSequence.Iterator(ani)))恐らくGIFアニメーションがループする設定になっているからだと思われます。
from pprint import * pp(ani.info)ループするGIFアニメーションから重複せずにフレームを取り出すには、例えば以下のようにします。
from PIL import ImageChops ani_iter = ImageSequence.Iterator(Image.open("out.gif")) frames = [next(ani_iter).copy()] for f in ani_iter: if frames[0].mode != f.mode: frames[0] = frames[0].convert(f.mode) # 画像の同値性確認 if ImageChops.difference(frames[0], f).getbbox() is None: break frames.append(f.copy()) len(frames)取り出したフレームの描画
fig = plt.figure(tight_layout=True) COL = 8 ROW = len(frames) // COL + 1 for i, f in enumerate(frames): ax = fig.add_subplot(ROW, COL, i + 1) ax.imshow(f) ax.set_title(i + 1) common_for_ax(ax) plt.show()if frames[0].mode != f.mode: frames[0] = frames[0].convert(f.mode)
について
GIFファイルは、初期状態ではグレースケール(L)またはパレットモード(P)画像として読み込まれます。P画像で後のフレームを求めると、画像はRGB(最初のフレームが透明であればRGBA)に変更されます。(参照:Image file formats | GIF)
つまり最初のフレームだけmodeが違うので、そのままImageChops.difference
に掛けるとエラーとなってしまうのです。
最初のフレームから同じmodeで読み込むには以下のように設定します。
from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS tmp = Image.open("out.gif") first = tmp.copy() print(f"{tmp.tell()}: {tmp.mode}") tmp.seek(1) print(f"{tmp.tell()}: {tmp.mode}") ImageChops.difference(first, tmp).convert("RGBA").show()自由欄
以下略