【Python/Pillow(PIL)】transformメソッドでアフィン変換

「Python アフィン変換」と検索すると、OpenCVを使った説明が多いような気がしますが、画像を表示するだけなら、Pillowにtransform()メソッドというのがあり、これもなかなか高機能な処理が可能になります。

Pillowならtkinterとの相性もいいので、GUIで画像をウィンドウに表示したいのなら、おススメです。

最終的には、以下のようにウィンドウに画像を表示するのが目標です。

transformメソッドの構文

Image.transform(size, method, data=None, resample=0, fill=1, fillcolor=None)
パラメータ 説明
size (幅, 高さ)のタプル
method 変換方法
  • PIL.Image.EXTENT:元画像の矩形領域を指定して出力先に合わせて変換
  • PIL.Image.AFFINE:アフィン変換
  • PIL.Image.PERSPECTIVE:射影変換(ホモグラフィ変換)
  • PIL.Image.QUAD:元画像の4点で示した領域を出力先に合わせて変換(ホモグラフィ変換の簡易版)
  • PIL.Image.MESH:任意グリッドの変換??
data methodにより内容が異なります。
  • PIL.Image.EXTENT
  • PIL.Image.AFFINE
    アフィン変換行列の2行3列の要素をタプルで指定します。
  • PIL.Image.PERSPECTIVE
    ホモグラフィ変換行列の3行3列の要素をタプルで指定します。
  • PIL.Image.QUAD
  • PIL.Image.MESH
resample 画素補間方法
  • PIL.Image.NEAREST:ニアレストネイバー
  • PIL.Image.BILINEAR:バイリニア
  • PIL.Image.BICUBIC:バイキュービック

(参考)https://imagingsolution.net/imaging/interpolation/

fill ???
fillcolor 出力先の画像の外側の背景色

(参考)
https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.transform

 

アフィン変換のサンプルプログラム

画像ファイルを読み込み、30°回転 → 横2倍、縦0.5倍 → 横+50、縦+70の平行移動 を行うサンプルを示します。

import numpy as np              # アフィン変換行列演算用
import tkinter as tk

from PIL import Image, ImageTk  # 画像データ用

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

        self.master.title("画像の表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)
        
        # Canvasの作成
        back_color = "#008B8B" # 背景色
        canvas = tk.Canvas(self.master, bg = back_color)
        # Canvasを配置
        canvas.pack(expand = True, fill = tk.BOTH)

        # アフィン変換行列(numpy ndarray)
        # 30°回転→横2倍、縦0.5倍→横+50、縦+70の平行移動の例
        # 回転行列(30°回転)
        rad = np.radians(30) # 30°をラジアン単位へ変換
        r = np.array([
            [np.cos(rad), -np.sin(rad), 0],
            [np.sin(rad),  np.cos(rad), 0],
            [0,            0,           1]
            ])
        # 拡大縮小(横2倍、縦0.5倍)
        s = np.array([
            [2,   0, 0],
            [0,   0.5, 0],
            [0,   0, 1]
            ])
        # 平行移動(横+50、縦+70)
        t = np.array([
            [1, 0,  50],
            [0, 1,  70],
            [0, 0,   1]
            ])
        # アフィン変換行列の計算(回転→拡大縮小→平行移動の順)
        affine = np.matmul(t, np.matmul(s, r))
        # 等倍のとき、単位行列
        #affine = np.identity(3)

        # アフィン変換を使った画像の表示
        self.disp_image(canvas, "./Mandrill.bmp", affine, fillcolor = back_color)

    def disp_image(self, canvas, filename, affine, method = Image.AFFINE, resample = Image.NEAREST, fillcolor = None):
        '''画像をCanvasに表示する
        canvas      :表示先のCanvasオブジェクト
        filename    :表示する画像ファイルパス
        affine      :画像データ→表示先へのアフィン変換行列
        method      :アフィン変換(Image.AFFINE)、射影変換(Image.PERSPECTIVE)
        resample    :補間方法:ニアレストネイバー(Image.NEAREST)、バイリニア(Image.BILINEAR)、バイキュービック(Image.BICUBIC)
        fillcolor   :画像の外側の色
        '''

        # キャンバスのサイズを取得(Canvasのサイズに合わせています)
        self.master.update()
        size = (canvas.winfo_width(), canvas.winfo_height())

        # PIL.Imageで開く
        pil_image = Image.open(filename)

        # アフィン変換行列の逆行列の計算
        affine_inv = np.linalg.inv(affine)
        # アフィン変換行列を一次元のタプルへ変換
        affine_tuple = tuple(affine_inv.flatten())

        # アフィン変換で画像を変換
        pil_image = pil_image.transform(
                    size,           # 出力サイズ(幅, 高さ)
                    method,         # 変換方法
                    affine_tuple,   # アフィン変換行列(出力→入力への変換行列)
                    resample,       # 補間方法
                    1,
                    fillcolor       # 画像の外側の背景色
                    )

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

        # 画像の描画
        canvas.create_image(
                0, 0,                   # 画像表示位置
                anchor = tk.NW,         # 画像表示位置の基準(左上)
                image=self.photo_image  # 表示画像データ
                )

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

(実行結果)

アフィン変換(method = PIL.Image.AFFINE)で使う時のポイント

transform()メソッドの第3引数(data)に渡す引数にアフィン変換行列のデータを渡すのですが、transform()メソッドのアフィン変換行列は出力先から元画像へ変換するアフィン変換行列となります。

 

ただし、一般的には元画像から出力先へのアフィン変換で考える場合が多いので、元画像から出力先へのアフィン変換行列(affine)を計算して、最後にアフィン変換行列の逆行列(affine-1)をtransform()メソッドの data へ渡します。

つまり、変換前の座標を(x, y)、変換後の座標を(x’, y’)とすると、アフィン変換の行列は

$$\left(\begin{array}{c}x^{‘}\\ y^{‘}\\ 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)
\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

となりますが、この逆の変換を計算し、

$$\left(\begin{array}{c}x\\ y\\ 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)^{-1}
\left(\begin{array}{c}x^{‘}\\ y^{‘} \\ 1\end{array}\right)$$

から、逆行列の部分を

$$\left(\begin{array}{c}a^{‘} & b^{‘} & c^{‘}\\ d^{‘} & e^{‘} & f^{‘} \\ 0 & 0 & 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)^{-1}$$

と置くと、transform()メソッドの第3引数(data)へは

$$data = (a^{‘}, b^{‘}, c^{‘}, d^{‘}, e^{‘}, f^{‘})$$

となるタプルを渡せばOKです。(サンプルプログラムではタプルの要素数を9個指定してますが、使わない部分を渡しても大丈夫なようです。)

アフィン変換の行列の計算部分については、行列の積、逆行列が計算できるnumpyを使うと便利だと思います。

(参考)
https://imagingsolution.net/program/python/numpy/matrix_operation/

また、アフィン変換の座標系ですが、画像の左上原点(左上の画素のさらに左上が原点)で、右方向が+X方向、下方向が+Y方向、時計回りが+θ方向となります。

ちなみに、C#では左上の画素の中心が原点(0, 0)でした。

(参考)
https://imagingsolution.net/program/csharp/image_coordinate/

参考ページ

【Python/Pillow(PIL)】transformメソッドでアフィン変換」への2件のフィードバック

    • コメントありがとうございます!
      ブログのモチベーションにつながります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください