基本的にDeepL翻訳です。
サンプルコードはだいぶ変更・追加してます。

原典:Tutorial — Pillow (PIL Fork) 9.1.0 documentation

Imageクラスの使用

Using the Image class

Python Imaging Libraryで最も重要なクラスはImageクラスで、同名のモジュールで定義されています。このクラスのインスタンスは、ファイルから画像を読み込んだり、他の画像を加工したり、ゼロから画像を作成したりと、いくつかの方法で作成することができます。

ファイルから画像を読み込むには、Imageモジュールのopen()関数を使用します。

import os import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np from PIL import Image

サンプル画像ada.pngのダウンロード

# 縮小表示 url = "https://arakaki.tokyo/content/images/upload/ada.png" with open("ada.png", "wb") as f: f.write(await (await pyodide.http.pyfetch(url)).bytes())

またはローカルの画像をpythonのCWDにロード(外部サーバへの送信は行いません)

# 上でローカルの画像をロードした場合は以下でファイルを確認できます。 # import os # print(os.listdir()) # 使用する画像ファイルのパスを指定してください。 image_path = "ada.png" im = Image.open(image_path)

成功すれば、Imageオブジェクトを返します。
インスタンス属性を使ってファイルの中身を調べることができます。

print(im.format, im.size, im.mode)

format属性は、画像のソースを識別します。(画像がファイルから読み込まれなかった場合はNone)
size属性は、幅と高さ(ピクセル単位)を含むタプルです。
mode属性は、画像のバンド数とバンド名、画素の種類と深さを定義します。一般的なモードは、グレイスケール画像では"L"(ルミナンス)、トゥルーカラー画像では"RGB"、プリプレス画像では"CMYK"です。

ファイルを開くことができない場合、OSError 例外が発生します。

Imageクラスのインスタンスを取得すると、このクラスで定義されたメソッドを使って画像を加工したり操作したりすることができるようになります。例えば、先ほど読み込んだ画像を表示してみましょう。

im.show()

Note
標準版のshow()は、画像を一時ファイルに保存し、ユーティリティを呼び出して表示するため、あまり効率的ではありません。適切なユーティリティがインストールされていない場合は、動作すらしないでしょう。しかし、これが動作すると、デバッグやテストに非常に便利です。

次のセクションでは、このライブラリで提供されるさまざまな機能の概要を説明します。

自由欄

画像の読み取りと書き込み

Reading and writing images

Python Imaging Libraryは、様々な画像ファイル形式をサポートしています。ディスクからファイルを読み込むには、Imageモジュールの open() 関数を使用します。ファイルを開くためにファイル形式を知る必要はありません。ライブラリは、ファイルの内容に基づいて自動的に形式を判断します。

ファイルを保存するには、Image クラスの save() メソッドを使用します。ファイルを保存するときは、名前が重要になります。フォーマットを指定しない限り、ライブラリはファイル名の拡張子を使って、どのファイル保存形式を使うかを判断します。

ファイルをJPEGに変換する

Convert files to 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サムネイルを作成する

Create JPEG thumbnails

def make_thmbnail(file_path, size=(128, 128), outfile="thumb.jpg", file_format=None): "画像ファイルのpathを受け取り、サムネイル化してCWDに保存する" infile = os.path.basename(file_path) if infile != outfile: with Image.open(file_path) as im: im.thumbnail(size) try: im.save(outfile, file_format) except (OSError): im.convert("RGB").save(outfile, file_format) return Image.open(outfile) thumb = make_thmbnail(image_path) # 保存したサムネイルの確認 print(f"{thumb.filename}, {thumb.format}, {thumb.size}") thumb.show()

重要なのは、ライブラリは本当に必要なとき以外は、ラスターデータをデコードしたり読み込んだりしないということだ。ファイルを開くと、ファイルヘッダを読んでファイル形式を判別し、モードやサイズなど、デコードに必要なプロパティを抽出しますが、それ以外の部分は後から処理されません。

つまり、画像ファイルを開くのは、ファイルサイズや圧縮の種類に依存しない高速な操作なのです。ここでは、一連の画像ファイルを素早く識別する簡単なスクリプトを紹介します。

自由欄

画像の切り取り、貼り付け、マージ

Cutting, pasting, and merging images

Imageクラスには、画像内の領域を操作するためのメソッドが用意されています。画像から矩形領域を切り出すには、crop()メソッドを使用します。

画像から矩形を切り取る

Copying a subrectangle from an image

# 大まかな座標の確認 fig, ax = plt.subplots() ax.imshow(im) ax.grid() plt.show() # (left, upper, right, lower) box = (118, 235, 400, 610) region = im.crop(box) region.show() # 元画像より広い範囲を指定 fig, ax = plt.subplots() pad = 100 ax.imshow(im.crop((-pad, -pad, *[p + pad for p in im.size]))) ax.grid() plt.show()

領域は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タプルで指定する場合は元画像上のその座標を左上として別画像を貼り付けるだけなのでサイズを気にする必要はありません。
また、元画像をはみ出す場合でも単に切り捨てられるだけなので挙動を理解していれば問題ないでしょう。

tmp = im.copy() tmp.paste(im, (100, 200)) tmp.show()

maskの使用

maskが指定されている場合、このメソッドはmaskによって示された領域のみを更新します。
maskとして使用できる画像のmodeは“1”, “L”, “LA”, “RGBA”, “RGBa”のいずれかです(存在する場合は、アルファバンドがマスクとして使用されます)。
maskが255の場合、指定された画像はそのままコピーされます。0の場合、元画像の値が保持されます。中間値は、2つの画像を混合します。アルファチャネルがある場合はそれも含まれます。

ada.pngはアルファバンドを持つので、そのままmask画像として使用できます。

fig, ax = plt.subplots() ims = ax.imshow(np.array(im)[:, :, 3]) ax.set_title("アルファレイヤーの値") fig.colorbar(ims) plt.show() duplicate = im.copy() gap = 2 while gap < min(duplicate.size): duplicate.paste(im, (gap, gap), im) gap *= 2 duplicate.show()

画像のローリング

Rolling an image

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('') # このページの環境でのみ動作 with open("out.gif", "rb") as f: blob_opt = js.Object.fromEntries(pyodide.to_js([["type", "image/gif"]])) jsblob = js.Blob.new([pyodide.to_js(f.read()).buffer], blob_opt) url = js.URL.createObjectURL(jsblob) f''

画像のマージ

Merging images

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()

バンドの分割とマージ

Splitting and merging bands

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" に変換しておくとよいでしょう。

自由欄

幾何学的変換

Geometrical transforms

PIL.Image.Image クラスには、画像のresize()rotate() を行うメソッドがあります。前者は新しいサイズを与えるタプル、後者は反時計回りの角度を与えるタプルをとります。

単純なジオメトリ変換

Simple geometry transforms

im.resize((128, 128)).show() im.rotate(45).show() # degrees counter-clockwise

画像のトランスポーズ

Transposing an image

画像を90度単位で回転させるには、rotate() メソッドかtranspose()メソッドを使用することができます。後者は、画像を水平軸や垂直軸のまわりに反転させる場合にも使用できます。

fig = plt.figure(figsize=(10, 10)) trans_args = [ Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, Image.Transpose.ROTATE_270, ] for i, t in enumerate(trans_args): ax = fig.add_subplot(2, 3, i + 1) ax.imshow(im.transpose(t)) ax.set_title(t.name) common_for_ax(ax) plt.show()

transpose(ROTATE)は、rotate(expand=True)と同様に元画像のサイズに収まるように回転した画像サイズを変更します。
より一般的な画像変換は、transform()メソッドで行うことができます。

自由欄

色変換

Color transforms

PILでは、 convert()メソッドを使用して、異なるピクセル表現間で画像を変換することができます。

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)

モード間の変換

Converting between modes

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を指定します。

# ついでに mode: "1"と比較 fig = plt.figure(figsize=(13, 7)) ax = fig.add_subplot(1, 3, 1) ax.imshow(im.convert("1")) ax.set_title('mode: "1"') ax = fig.add_subplot(1, 3, 2) ax.imshow(im.convert("L")) ax.set_title('mode: "L"') ax = fig.add_subplot(1, 3, 3) ax.imshow(im.convert("L"), cmap="gray", vmin=0, vmax=255) ax.set_title('mode: "L", cmap="gray",\nvmin=0, vmax=255') plt.show()

ところで、ada.pngでは絵の背景(額縁の外側)を透過していますが、アルファレイヤを除くと背景部分が黒になってしまいます。
アルファレイヤで透過させる部分をRGBの白にするには、例えば以下のようにします。

def fill_alpha(im, by=(255, 255, 255)): new_im = Image.new("RGB", im.size, color=by) new_im.paste(im, (0, 0), im) return new_im im_rgb = fill_alpha(im) print(f"{im_rgb.mode=}") im_rgb.show()

グレースケールでも背景が白ですっきりします。

im_rgb.convert("L").show()

ちなみに上の操作は以下のようにnumpyを使用することでも実現できます。

im_array = np.array(im) to_white = im_array[:, :, 3] == 0 im_array[:, :, :3][to_white] = 255 im_rgb2 = Image.fromarray(im_array[:, :, :3]) im_rgb2.show()

自由欄

画像補正

Image enhancement

PILは、画像の補正に使用できるメソッドやモジュールを多数提供しています。

フィルタ

Filters

ImageFilterモジュールには、filter()メソッドで使用できる以下の定義済みの拡張フィルタが含まれています。

  • BLUR
  • CONTOUR
  • DETAIL
  • EDGE_ENHANCE
  • EDGE_ENHANCE_MORE
  • EMBOSS
  • FIND_EDGES
  • SHARPEN
  • SMOOTH
  • SMOOTH_MORE
from PIL import ImageFilter filters = [ "BLUR", "CONTOUR", "DETAIL", "EDGE_ENHANCE", "EDGE_ENHANCE_MORE", "EMBOSS", "FIND_EDGES", "SHARPEN", "SMOOTH", "SMOOTH_MORE", ] fig = plt.figure(figsize=(10, 10)) for i, f in enumerate(["ORIGINAL"] + filters): ax = fig.add_subplot(3, 4, i + 1) if i == 0: ax.set_title(f) ax.imshow(im) else: ax.set_title(f) ax.imshow(im.filter(eval(f"ImageFilter.{f}"))) common_for_ax(ax) plt.show()

ポイント操作

Point Operations

point()メソッドは、画像のピクセル値を変換するために使用できます(例:画像のコントラスト操作)。ほとんどの場合、このメソッドには、1つの引数を期待する関数オブジェクトを渡すことができます。各ピクセルは、その関数にしたがって処理されます。

Applying point transforms

fig = plt.figure(figsize=(10, 15)) for i in np.arange(20): ax = fig.add_subplot(4, 5, i + 1) factor = round(1.18 ** (1.2 ** i), 2) # ax.imshow(im.point(lambda v: v * factor)) ax.imshow(im.point(lambda v: int(v * factor))) # for pyodide # タイトル ax.set_title(f"pixel *= {factor}") common_for_ax(ax) plt.show()

どんな簡単な式でもすぐに画像に適用することができます。また、point()とpaste()を組み合わせることで、画像を選択的に変更することができます。

Processing individual bands

# 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を返します。

補正

Enhancement

より高度な画像補正を行うには、ImageEnhanceモジュールのクラスを使用することができます。画像から作成されたエンハンスメントオブジェクトは、さまざまな設定をすばやく試すために使用することができます。

この方法で、コントラスト、明るさ、カラーバランス、シャープネスを調整することができます。

Enhancing images

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)

自由欄

イメージシーケンス

Image sequences

PILは、イメージシーケンス(アニメーション形式とも呼ばれます)の基本的なサポートを含んでいます。サポートされているシーケンス形式は、FLI/FLC、GIFおよびいくつかの実験的な形式です。TIFF ファイルは複数のフレームを含むことができます。

シーケンスファイルを開くと、PILは自動的にそのシーケンスの最初のフレームを読み込みます。異なるフレーム間を移動するには、シークとテルのメソッドを使用できます。

Reading sequences

GIFアニメーションの作成(ani.gif)

base = im_rgb base.thumbnail((128, 128)) frames = list() for f in range(1, 32): frames.append( base.filter(ImageFilter.GaussianBlur(f)).point(lambda v: int(v * (1 + f / 30))) ) base.save("ani.gif", save_all=True, append_images=frames, disposal=2)

表示(Runをクリックして再生)

# 縮小表示 # for jupyter # import IPython.display as d # d.HTML('') # このページの環境でのみ動作 with open("ani.gif", "rb") as f: blob_opt = js.Object.fromEntries(pyodide.to_js([["type", "image/gif"]])) jsblob = js.Blob.new([pyodide.to_js(f.read()).buffer], blob_opt) ani_url = js.URL.createObjectURL(jsblob) import time f'' ani = Image.open("ani.gif") # 全フレームの抽出 frames = [ani.copy()] for i in range(ani.n_frames - 1): ani.seek(ani.tell() + 1) frames.append(ani.copy()) # 全フレームの描画 fig = plt.figure() COL = 6 ROW = len(frames) // COL + 1 for i, f in enumerate(frames): ax = fig.add_subplot(ROW, COL, i + 1) ax.imshow(f) common_for_ax(ax) plt.show()

シーケンスが終了すると EOFErrorが発生します。

print(f"{ani.n_frames=}, {ani.tell()=}") ani.seek(ani.tell() + 1)

次のクラスでは、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となっています。

ani = Image.open("out.gif") ani.n_frames

実際にフレームを取り出してみても同様です。

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に掛けるとエラーとなってしまうのです。

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).show()

最初のフレームから同じ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()

自由欄

以下略

PostScript printing

More on reading images

Controlling the decoder