【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/NumPy】カラー画像データをRGBからBGRへ変換

カラー画像データは各データが8bit(0~255の256諧調)のR, G, Bの要素からなる24bitカラー画像や、さらに透過率(A)を追加しR, G, B, Aの要素からなる32bitカラー画像があります。

Pythonでは、このカラー画像のデータの並びが使用するモジュールによって異なり、PillowではRGBRGBA の並びとなり、NumPyのndarrayで画像データを管理しているOpenCVでは BGRBGRA の並びとなっています。

つまり、カラー画像をPillowとOpenCV間で変換する場合、画像データの並びも RGB⇔BGRやRGBA⇔BGRA と変換する必要があります。

これを間違うと、RとBが入れ替わった状態となるため、下図のように変な画像になってしまいます。

(正しい画像データの並び)

(間違った画像データの並び)

カラー画像データの事前準備

Pillowでカラー画像を開き、24bitのカラー画像と32bitのカラー画像を用意し、これをNumPyのndarrayへ変換します。

import numpy as np
from PIL import Image
import cv2

# Pillowでカラー画像(RGB)を開く
pillow_rgb24 = Image.open("Mandrill.bmp")
# 24bitカラー(RGB)から32bitカラー(RGBA)へ変換
pillow_rgb32 = pillow_rgb24.convert("RGBA")

###############################
# PillowからNumPyのndarrayへ変換
numpy_rgb24 = np.array(pillow_rgb24) # 24bitカラー(RGB)
numpy_rgb32 = np.array(pillow_rgb32) # 32bitカラー(RGBA)

上記のようにPillowで開いたカラー画像をNumPyへ変換しただけの状態の画像をOpenCVのimshowで表示すると、RとBが入れ替わった画像が表示されます。

cv2.imshow("Image", numpy_rgb24)
cv2.waitKey()

(表示結果)

NumPyでRGB→BGR, RGBA→BGRAへ変換

Pillowのカラー画像をNumPyへ変換した直後のデータは
24bitカラーのとき
[[[R, G, B], [R, G, B], [R, G, B]],
[[R, G, B], [R, G, B], [R, G, B]],
[[R, G, B], [R, G, B], [R, G, B]]]

32bitカラーのとき
[[[R, G, B, A], [R, G, B, A], [R, G, B, A]],
[[R, G, B, A], [R, G, B, A], [R, G, B, A]],
[[R, G, B, A], [R, G, B, A], [R, G, B, A]]]

のように、RGBやRGBAの順で並んでいます。

これをOpenCVで使うときは、データの並びをBGRやBGRAの順へ変換する必要があります。

具体的には

24bitカラーのとき
[[[B, G, R], [B, G, R], [B, G, R]],
[[B, G, R], [B, G, R], [B, G, R]],
[[B, G, R], [B, G, R], [B, G, R]]]

32bitカラーのとき
[[[B, G, R, A], [B, G, R, A], [B, G, R, A]],
[[B, G, R, A], [B, G, R, A], [B, G, R, A]],
[[B, G, R, A], [B, G, R, A], [B, G, R, A]]]

のようにR, G, Bのデータを並び変える必要があります。

このRGB→BGRRGBA→BGRAの変換は以下のように行います。

# NumPyでRGBからBGRへ変換(24bitの場合) その1
numpy_bgr24 = numpy_rgb24[:, :, ::-1]
# NumPyでRGBからBGRへ変換(24bitの場合) その2
numpy_bgr24 = numpy_rgb24[:, :, [2, 1, 0]]

# RGBAからBGRAへ変換(32bitの場合)
numpy_bgr32 = numpy_rgb32[:, :, [2, 1, 0, 3]]

OpenCVでRGB→BGR, RGBA→BGRAへ変換

OpenCVの画像データはNumPyのndarrayなので、PillowからNumPyへ変換した画像データは、そのままOpenCVの関数で処理することができます。

# cvtColorで24bitカラー(RGB)から24bitカラー(BGR)へ変換
numpy_bgr24 = cv2.cvtColor(numpy_rgb24, cv2.COLOR_RGB2BGR)

# cvtColorで32bitカラー(RGBA)から32bitカラー(BGRA)へ変換
numpy_bgr32 = cv2.cvtColor(numpy_rgb32, cv2.COLOR_RGBA2BGRA)

24bitか?、32bitか?を調べる

カラー画像データの並びを入れ替える時は、24bitカラーのときと、32bitカラーのときとで、処理を変える必要があるため、NumPy配列(ndarray)が24bitと32bitのどちらなのか?を調べる必要があります。

それには、NumPyのshapeを取得しshape[2]の値が3であれば24bit、4であれば32bitとなります。

# 24bitか?32bit?かを調べる
print(numpy_rgb24.shape)
print("チャンネル数 = ", numpy_rgb24.shape[2])
print(numpy_rgb32.shape)
print("チャンネル数 = ", numpy_rgb32.shape[2])

(実行結果)

参考

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

【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/NumPy】座標からホモグラフィ変換行列を求める方法

アフィン変換では長方形を平行四辺形には変換できるものの、台形には変換できないと説明しましたが、任意四角形から任意四角形へ変換できるのがホモグラフィ変換となります。

実際には書類や名刺のような長方形の被写体を斜めから撮影した時に、上から撮影したような感じに長方形に見えるように変換するときにホモグラフィ変換が用いられることが多いです。

 

 

 

 

 

 

 

このホモグラフィ変換は、変換前の座標を(x, y)、変換後の座標を (x’, y’) とすると、

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

となります。

アフィン変換の行列と比較すると、変換後の座標の頭に s が付いているのと、アフィン変換行列の3行目の要素が 0 だった部分に gh が登場しています。

このホモグラフィ変換の行列の計算は以下のようにします。

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

を展開し、

$$\begin{cases}sx^{‘}=ax+by+c\\sy^{‘}=dx+ey+f\\s=gx+hy+1\end{cases}$$

より

$$\begin{cases}x^{‘}=\frac{ax+by+c}{gx+hy+1}\\y^{‘}=\frac{dx+ey+f}{gx+hy+1}\end{cases}$$

となり、ホモグラフィ変換後の座標を求める事ができます。

式を見ると分かりますが、 g = h = 0 のときは s = 1 となり、アフィン変換そのものとなります。
つまり、アフィン変換で出来る変換はホモグラフィ変換でも可能です。

このホモグラフィ変換行列の未知数(a ~ h)を求めるには、座標からアフィン変換行列を求める方法でも似たような説明をしていますが、ホモグラフィ変換行列では未知数が8個なので8本の連立方程式を立てれば未知数を解く事ができます。

$$\begin{cases}x^{‘}=\frac{ax+by+c}{gx+hy+1}\\y^{‘}=\frac{dx+ey+f}{gx+hy+1}\end{cases}$$

の式を変形して、

$$\begin{cases}x^{‘}(gx+hy+1)=ax+by+c\\y^{‘}(gx+hy+1)=dx+ey+f\end{cases}$$

$$\begin{cases}ax+by+c-gxx^{‘}-hyx^{‘}=x^{‘}\\
dx+ey+f-gxy^{‘}-hyy^{‘}= y^{‘}\end{cases}$$

となり、この x, y, x’, y’ に変換前の座標 (x0, y0), (x1, y1), (x2, y2), (x3, y3)と変換後の座標
(x’0, y’0), (x’1, y’1), (x’2, y’2), (x’3, y’3)を代入すると、式が8本立つので、8個の未知数を求める事ができます。

実際に変換前、変換後の座標を代入して8本の式を書くと

$$\begin{cases}
ax_{0}+by_{0}+c-gx_{0}x_0^{‘}-hy_{0}x_0^{‘}=x_0^{‘}
\\
dx_{0}+ey_{0}+f-gx_{0}y_0^{‘}-hy_{0}y_0^{‘}= y_0^{‘}
\\
ax_{1}+by_{1}+c-gx_{1}x_1^{‘}-hy_{1}x_1^{‘}=x_1^{‘}
\\
dx_{1}+ey_{1}+f-gx_{1}y_1^{‘}-hy_{1}y_1^{‘}= y_1^{‘}
\\
ax_{2}+by_{2}+c-gx_{2}x_2^{‘}-hy_{2}x_2^{‘}=x_2^{‘}
\\
dx_{2}+ey_{2}+f-gx_{2}y_2^{‘}-hy_{2}y_2^{‘}= y_2^{‘}
\\
ax_{3}+by_{3}+c-gx_{3}x_3^{‘}-hy_{3}x_3^{‘}=x_3^{‘}
\\
dx_{3}+ey_{3}+f-gx_{3}y_3^{‘}-hy_{3}y_3^{‘}= y_3^{‘}
\end{cases}$$

となります。

この8本の式を行列を使って解くのですが、行列を使って連立方程式を解く時のポイントですが、未知数の部分を1列の行列になるようにして、行列で表現します。

今回の場合は、未知数が a~h までの8個あるので、

$$\left(\begin{array}{c}{}\\ {}\\ {}\\ {}&{}&{?}&{}&{}\\ {}\\ {}\\ {}\\ {}\end{array}\right)
\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}{}\\ {}\\ {}\\ {?}\\ {}\\ {}\\ {}\\ {}\end{array}\right)$$

という形になるように連立方程式を行列で表現します。

すると、

$$\left(\begin{array}{c}
x_{0}&y_{0}&1&0&0&0&-x_{0}x_0^{‘}&-y_{0}x_0^{‘}
\\
0&0&0&x_{0}&y_{0}&1&-x_{0}y_0^{‘}&-y_{0}y_0^{‘}
\\
x_{1}&y_{1}&1&0&0&0&-x_{1}x_1^{‘}&-y_{1}x_1^{‘}
\\
0&0&0&x_{1}&y_{1}&1&-x_{1}y_1^{‘}&-y_{1}y_1^{‘}
\\
x_{2}&y_{2}&1&0&0&0&-x_{2}x_2^{‘}&-y_{2}x_2^{‘}
\\
0&0&0&x_{2}&y_{2}&1&-x_{2}y_2^{‘}&-y_{2}y_2^{‘}
\\
x_{3}&y_{3}&1&0&0&0&-x_{3}x_3^{‘}&-y_{3}x_3^{‘}
\\
0&0&0&x_{3}&y_{3}&1&-x_{3}y_3^{‘}&-y_{3}y_3^{‘}
\end{array}\right)
\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\ x_1^{‘}\\ y_1^{‘}\\ x_2^{‘}\\ y_2^{‘}\\ x_3^{‘}\\ y_3^{‘}\end{array}\right)$$

となるので、あとは逆行列を使って

$$\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}
x_{0}&y_{0}&1&0&0&0&-x_{0}x_0^{‘}&-y_{0}x_0^{‘}
\\
0&0&0&x_{0}&y_{0}&1&-x_{0}y_0^{‘}&-y_{0}y_0^{‘}
\\
x_{1}&y_{1}&1&0&0&0&-x_{1}x_1^{‘}&-y_{1}x_1^{‘}
\\
0&0&0&x_{1}&y_{1}&1&-x_{1}y_1^{‘}&-y_{1}y_1^{‘}
\\
x_{2}&y_{2}&1&0&0&0&-x_{2}x_2^{‘}&-y_{2}x_2^{‘}
\\
0&0&0&x_{2}&y_{2}&1&-x_{2}y_2^{‘}&-y_{2}y_2^{‘}
\\
x_{3}&y_{3}&1&0&0&0&-x_{3}x_3^{‘}&-y_{3}x_3^{‘}
\\
0&0&0&x_{3}&y_{3}&1&-x_{3}y_3^{‘}&-y_{3}y_3^{‘}
\end{array}\right)^{-1}
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\ x_1^{‘}\\ y_1^{‘}\\ x_2^{‘}\\ y_2^{‘}\\ x_3^{‘}\\ y_3^{‘}\end{array}\right)$$

を求めると未知数が求まるので、ホモグラフィ変換行列も求まります。

それでは、具体的に変換前、変換後の座標を使ってPythonのNumPyでホモグラフィ変換行列を解いてみたいと思います。

変換前 → 変換後

(100, 50) → (50, 50)
(120, 350) → (50, 400)
(500, 500) → (500, 400)
(600, 200) → (500, 50)

import numpy as np

mat = np.array([[100, 50, 1, 0, 0, 0, -100*50, -50*50],
                [0, 0, 0, 100, 50, 1, -100*50, -50*50],
                [120, 350, 1, 0, 0, 0, -120*50, -350*50],
                [0, 0, 0, 120, 350, 1, -120*400, -350*400],
                [500, 500, 1, 0, 0, 0, -500*500, -500*500],
                [0, 0, 0, 500, 500, 1, -500*400, -500*400],
                [600, 200, 1, 0, 0, 0, -600*500, -200*500],
                [0, 0, 0, 600, 200, 1, -600*50, -200*50]])

dst = np.array([50, 50, 50, 400, 500, 400, 500, 50]).T

ans = np.matmul(np.linalg.inv(mat), dst)

homography = np.array([[ans[0], ans[1], ans[2]],
                   [ans[3], ans[4], ans[5]],
                   [ans[6], ans[7], 1]])

print(homography)

# 座標変換の確認
dst = np.matmul(homography, np.array([100, 50, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([120, 350, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([500, 500, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([600, 200, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])

(実行結果)

[[ 1.11407581e+00 -1.11269833e-01 -5.49716851e+01]
 [-2.55930820e-01  9.08099927e-01  3.10604901e+01]
 [ 5.63236570e-04 -7.77511351e-04  1.00000000e+00]]
x'= 50.000000000000064
y'= 50.000000000000014
x'= 50.000000000000135
y'= 400.00000000000006
x'= 500.0000000000006
y'= 400.0000000000002
x'= 500.00000000000057
y'= 50.000000000000064

出来た!!

参考

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

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

【Python/NumPy】座標からアフィン変換行列を求める方法

【Python/NumPy】座標からアフィン変換行列を求める方法

アフィン変換行列は、これまで移動量、スケール、回転角度からアフィン変換行列を求める方法を紹介してきました。

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

ただ、実際にはアフィン変換前の点とアフィン変換後の点の組み合わせからアフィン変換行列を求めたい場合もあるので、今回はその方法を紹介します。

アフィン変換行列は、こんな感じ↓です。

$$\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)$$

アフィン変換行列を求めるという事は、この未知数の a, b, c, d, e, f を求める事になります。

この行列を求めるには、アフィン変換前の3点、アフィン変換後の3点の3ペアの座標があれば求める事ができます。

求め方は比較的簡単で、式で書くと以下のようになります。

$$\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 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_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)$$

より、逆行列を用いて、

$$\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}
=
\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

$$\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
=
\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

として求める事ができます。

この計算は行列の積と逆行列を求める事ができれば、解くことができますが、最近勉強しているPythonのNumPyを使って例題を解いてみたいと思います。

変換前 → 変換後
(0, 0) → (200, 100)
(600, 0) → (719.6152, 400)
(0, 400) → (0, 446.4102)

このアフィン変換行列は

$$\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
=
\left(\begin{array}{c}200 & 719.6152 & 0\\ 100 & 400 & 446.4102\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}0 & 600 & 0\\ 0 & 0 & 400 \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

で求まります。

この計算をNumPyを使って計算すると、

import numpy as np

src = np.array([[0, 600, 0],
               [0, 0, 400],
               [1, 1, 1]])

dst = np.array([[200, 719.6152, 0],
               [100, 400, 446.4102],
               [1, 1, 1]])

affine = np.matmul(dst, np.linalg.inv(src))

print(affine)

(実行結果)

[[  0.86602533  -0.5        200.        ]
 [  0.5          0.8660255  100.        ]
 [  0.           0.           1.        ]]

として、アフィン変換行列を求める事ができます。

ただし、この計算方法だと、3行1列目、3行2列目の要素が計算誤差できっちりと 0 にならない場合もあり、気持ち悪いので、もう一つの計算方法を紹介します。

アフィン変換の行列の式は

$$\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)$$

でしたが、この式を展開すると

$$x^{‘}=ax + by + c$$

$$y^{‘}=dx + ey + f$$

この式にアフィン変換前の3点、アフィン変換後の3点の3ペアの座標を代入すると、

$$x_0^{‘}=ax_{0} + by_{0} + c$$

$$y_0^{‘}=dx_{0} + ey_{0} + f$$

$$x_1^{‘}=ax_{1} + by_{1} + c$$

$$y_1^{‘}=dx_{1} + ey_{1} + f$$

$$x_2^{‘}=ax_{2} + by_{2} + c$$

$$y_2^{‘}=dx_{2} + ey_{2} + f$$

となります。

式が6本で未知数が a ~f の6個なので、この連立方程式を求めれば、アフィン変換行列も求まります。

この連立方程式も行列を用いて解きたいと思います。

未知数の部分を6行1列の行列になるように、行列で表すと、

$$\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\x_1^{‘}\\ y_1^{‘}\\x_2^{‘}\\ y_2^{‘} \end{array}\right)
=
\left(\begin{array}{c}x_{0} & y_{0} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{0} & y_{0} & 1\\x_{1} & y_{1} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{1} & y_{1} & 1\\x_{2} & y_{2} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{2} & y_{2} & 1 \end{array}\right)
\left(\begin{array}{c}a\\ b\\c\\d\\e\\f\end{array}\right)$$

となり、6行6列の行列の部分に左側から逆行列を掛ければ、未知数が求まるので、

$$\left(\begin{array}{c}a\\ b\\c\\d\\e\\f\end{array}\right)=\left(\begin{array}{c}x_{0} & y_{0} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{0} & y_{0} & 1\\x_{1} & y_{1} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{1} & y_{1} & 1\\x_{2} & y_{2} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{2} & y_{2} & 1 \end{array}\right)^{-1}
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\x_1^{‘}\\ y_1^{‘}\\x_2^{‘}\\ y_2^{‘} \end{array}\right)$$

を計算すれば、アフィン変換行列が求まります。

この計算もNumPyを使って計算してみたいと思います。

import numpy as np

mat = np.array([[0, 0, 1, 0, 0, 0],
                [0, 0, 0, 0, 0, 1],
                [600, 0, 1, 0, 0, 0],
                [0, 0, 0, 600, 0, 1],
                [0, 400, 1, 0, 0, 0],
                [0, 0, 0, 0, 400, 1]])

dst = np.array([200, 100, 719.6152, 400, 0, 446.4102]).T

ans = np.matmul(np.linalg.inv(mat), dst)

affine = np.array([[ans[0], ans[1], ans[2]],
                   [ans[3], ans[4], ans[5]],
                   [0, 0, 1]])

print(affine)

(実行結果)

[[  0.86602533  -0.5        200.        ]
 [  0.5          0.8660255  100.        ]
 [  0.           0.           1.        ]]

となり、アフィン変換行列が求まります。

参考

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

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

【Python/NumPy】座標からホモグラフィ変換行列を求める方法

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

個人的には、行列は最小二乗法で近似式を求めるときや、アフィン変換を用いて画像の表示やリサイズを行う際に用いるのですが、この行列の演算は、PythonではNumPyを用いて行います。

NumPyのインポート

import numpy as np

行列の生成(array)

# 行列の生成
mat = np.array([[1, 2], [3, 4], [5, 6]])
print(mat)

(実行結果)

[[1 2]
 [3 4]
 [5 6]]

行列の積(matmul)

matA = np.array([[1, 2], [3, 4]])
matB = np.array([[5, 6], [7, 8]])
matAB = np.matmul(matA, matB)
print(matAB)

(実行結果)

[[19 22]
 [43 50]]

単位行列(identity)

matI = np.identity(3)
print(matI)

(実行結果)

[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

零行列(zeros)

mat = np.zeros((3, 4))
print(mat)

(実行結果)

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

逆行列(linalg.inv)

mat = np.array([[1, 2], [3, 4]])
matInv = np.linalg.inv(mat)
print(matInv)

(実行結果)

[[-2.   1. ]
 [ 1.5 -0.5]]

擬似逆行列(linalg.pinv)

mat = np.array([[1, 2], [3, 4], [5, 6]])
matInv = np.linalg.pinv(mat)
print(matInv)

(実行結果)

[[-1.33333333 -0.33333333  0.66666667]
 [ 1.08333333  0.33333333 -0.41666667]]

転置行列(.T)

mat = np.array([[1, 2], [3, 4]])
matT = mat.T
print(matT)

(実行結果)

[[1 3]
 [2 4]]

行列要素ごとの積(multiply)

matA = np.array([[1, 2], [3, 4]])
matB = np.array([[5, 6], [7, 8]])
matAB = np.multiply(matA, matB)
print(matAB)

(実行結果)

[[ 5 12]
 [21 32]]