Python, OpenCVで幾何変換(アフィン変換・射影変換など)
Python, OpenCVで画像の幾何変換(線形変換・アフィン変換・射影変換)を行うには関数cv2.warpAffine()
およびcv2.warpPerspective()
を使う。
ここでは以下の内容について説明する。
- 幾何変換(幾何学的変換)の種類
- 線形変換
- 同次座標で表す変換
- アフィン変換
- 射影変換
- OpenCVのアフィン変換:
cv2.warpAffine()
cv2.warpAffine()
の基本的な使い方- 補間処理の指定
- 領域外の処理
- OpenCVの射影変換:
cv2.warpPerspective()
cv2.warpPerspective()
の基本的な使い方
- 任意の変換行列を定義
- 回転の変換行列を生成:
cv2.getRotationMatrix2D()
- アフィン変換の変換行列を生成:
cv2.getAffineTransform()
- 射影変換の変換行列を生成:
cv2.getPerspectiveTransform()
OpenCVの公式ドキュメントの幾何変換についてのチュートリアルおよびAPIリファレンスは以下。
サンプルコードのOpenCVのバージョンは4.0.1
。OpenCV3系と4系はあまり変わらないはずだが、OpenCV2系は異なっている可能性があるので注意。
幾何変換(幾何学的変換)の種類
画像の幾何変換(幾何学的変換)は行列を用いて表される。これを理解したほうがOpenCVにおける処理も理解しやすいので先に紹介する。
以下のページおよびその出典が参考になった。
書籍では以下のものがオススメ。本項の説明も以下の書籍を参考にしている。
線形変換
座標(x, y)
の点が座標(x', y')
に移動するとき、以下の式で表される変換を線形変換と呼ぶ。
$$ \left( \begin{matrix} x' \\ y' \end{matrix} \right) = \left( \begin{matrix} a & b \\ c & d \end{matrix} \right) \left( \begin{matrix} x \\ y \end{matrix} \right) $$
線形変換には拡大・縮小、回転、スキュー(せん断)などが含まれる。
拡大・縮小。
$$ \left( \begin{matrix} x' \\ y' \end{matrix} \right) = \left( \begin{matrix} s_x & 0 \\ 0 & s_y \end{matrix} \right) \left( \begin{matrix} x \\ y \end{matrix} \right) $$
回転。
$$ \left( \begin{matrix} x' \\ y' \end{matrix} \right) = \left( \begin{matrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{matrix} \right) \left( \begin{matrix} x \\ y \end{matrix} \right) $$
x軸方向へのスキュー。スキューは画像を傾けるような処理。傾ける角度を$\theta$とすると、$b = \tan\theta$という関係になる。
$$ \left( \begin{matrix} x' \\ y' \end{matrix} \right) = \left( \begin{matrix} 1 & b \\ 0 & 1 \end{matrix} \right) \left( \begin{matrix} x \\ y \end{matrix} \right) $$
$\theta$ = 15度の結果は以下の通り。OpenCVでは左上が原点となるので、上辺右向きがx軸、左辺下向きがy軸となる。y軸からx軸方向へ15度傾いている。
y軸方向へのスキュー。$c = \tan\theta$という関係。
$$ \left( \begin{matrix} x' \\ y' \end{matrix} \right) = \left( \begin{matrix} 1 & 0 \\ c & 1 \end{matrix} \right) \left( \begin{matrix} x \\ y \end{matrix} \right) $$
$\theta$ = 15度の結果は以下の通り。x軸からy軸方向へ15度傾いている。
変換行列の積によりこれらを組み合わせることもできる。
同次座標で表す変換
平行移動を表すと以下のようになる。
$$ x' = x + t_x \\ y' = y + t_y $$
これは上述の線形変換では表現できない。
平行移動を表現するためには座標(x, y)
に対して要素の数を一つ増やした同次座標(斉次座標)を導入する必要がある。
平行移動は以下の式で表せる。なお、同次座標においては定数倍しても表す点が変わらないため、定数倍の違いを許容して等しいこと(同値)を~
で表す場合があるが、ここでは=
を用いる。
$$ \left( \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right) = \left( \begin{matrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{matrix} \right) \left( \begin{matrix} x \\ y \\ 1 \end{matrix} \right) $$
同次座標で線形変換を表すと以下のようになる。
$$ \left( \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right) = \left( \begin{matrix} a & b & 0 \\ c & d & 0 \\ 0 & 0 & 1 \end{matrix} \right) \left( \begin{matrix} x \\ y \\ 1 \end{matrix} \right) $$
アフィン変換
任意の線形変換と平行移動の組み合わせは以下のように一般化できる。
$$ \left( \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right) = \left( \begin{matrix} a & b & t_x \\ c & d & t_y \\ 0 & 0 & 1 \end{matrix} \right) \left( \begin{matrix} x \\ y \\ 1 \end{matrix} \right) $$
このような、変換行列の2 x 3
部分を使った変換をアフィン変換(affine transformation)と呼ぶ。
アフィン変換の中でも、特に回転と平行移動の組み合わせをユークリッド変換(Euclidean transformation)と呼んだり、回転と平行移動にさらに拡大・縮小を加えた組み合わせを相似変換(similarity transformation)と呼んだりする。
アフィン変換は任意の平行四辺形から別の任意の平行四辺形への変換となる。
射影変換
アフィン変換では2 x 3
の部分しか使っていないが、3 x 3
すべてを使うとより一般的な変換を表現できる。
$$ \left( \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right) = \left( \begin{matrix} h_{11} & h_{12} & h_{13} \\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{matrix} \right) \left( \begin{matrix} x \\ y \\ 1 \end{matrix} \right) $$
このような変換を射影変換(projective transformation)と呼ぶ。透視変換(perspective transformation)やホモグラフィ変換(homography transformation: 平面射影変換)とも呼ばれる。
射影変換は任意の四角形から別の任意の四角形への変換となる。台形補正などはこちらを使う。
OpenCVのアフィン変換: cv2.warpAffine()
OpenCVではcv2.warpAffine()
関数でアフィン変換を実現できる。
cv2.warpAffine()の基本的な使い方
dst = cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
第一引数に元画像(NumPy配列ndarray
)、第二引数に2 x 3
の変換行列(NumPy配列ndarray
)、第三引数に出力画像のサイズ(タプル)を指定する。
以下の画像を例とする。あとの処理で使うため、画像の幅、高さを取得しておく。
import cv2
import numpy as np
import math
img = cv2.imread('data/src/lena.jpg')
h, w, c = img.shape
print(h, w, c)
# 225 400 3
ここではcv2.getRotationMatrix2D()
で変換行列を生成する。cv2.getRotationMatrix2D()
の詳細は後述。変換行列を生成する関数にはcv2.getAffineTransform()
もある。これも後述。関数を使わずに任意の変換行列を自分で定義してもOK。
mat = cv2.getRotationMatrix2D((w / 2, h / 2), 45, 0.5)
print(mat)
# [[ 0.35355339 0.35355339 89.51456544]
# [ -0.35355339 0.35355339 143.43592168]]
アフィン変換の実行。なお、ここでは画像をファイルとして保存しているが、別ウィンドウで表示したい場合はcv2.imshow()
を使えばよい(例: cv2.imshow('window_name', img)
)。以降のサンプルコードでも同じ。
affine_img = cv2.warpAffine(img, mat, (w, h))
cv2.imwrite('data/dst/opencv_affine.jpg', affine_img)
出力画像のサイズは自分で決定する必要がある。元画像全体が残るように自動的にサイズが決定されたりはしない。指定を誤ると途中で切れた画像になってしまう場合もあるので注意。
affine_img_half = cv2.warpAffine(img, mat, (w, h // 2))
cv2.imwrite('data/dst/opencv_affine_half.jpg', affine_img_half)
補間処理の指定
補完処理のアルゴリズムを引数flags
で指定できる。
主な補間アルゴリズムは以下の通り。バイキュービックやランチョスにするとデフォルトよりはきれいになるかもしれない。
cv2.INTER_NEAREST
: ニアレストネイバーcv2.INTER_LINEAR
: バイリニア(デフォルト)cv2.INTER_CUBIC
: バイキュービックcv2.INTER_LANCZOS4
: ランチョス
affine_img_flags = cv2.warpAffine(img, mat, (w, h), flags=cv2.INTER_CUBIC)
cv2.imwrite('data/dst/opencv_affine_flags.jpg', affine_img_flags)
領域外の処理
領域外に対する処理を引数borderMode
およびborderValue
, dst
で指定できる。
borderMode
には以下の公式ドキュメントに挙げられているモードを指定可能。
以下に引用する。画素値abcdefgh
に対してどのような値で埋められるかを示している。
cv2.BORDER_CONSTANT
:iiiiii|abcdefgh|iiiiiii
(デフォルト)cv2.BORDER_REPLICATE
:aaaaaa|abcdefgh|hhhhhhh
cv2.BORDER_REFLECT
:fedcba|abcdefgh|hgfedcb
cv2.BORDER_WRAP
:cdefgh|abcdefgh|abcdefg
cv2.BORDER_REFLECT_101
:gfedcb|abcdefgh|gfedcba
cv2.BORDER_TRANSPARENT
:uvwxyz|abcdefgh|ijklmno
cv2.BORDER_CONSTANT
では固定値で埋められる。埋める値は引数borderValue
で指定できる。デフォルトはborderMode
がcv2.BORDER_CONSTANT
でborderValue
が0
なので黒で埋められる。カラー画像の場合、引数borderValue
には(Blue, Red, Green)
で色を指定する。
affine_img_bv = cv2.warpAffine(img, mat, (w, h), borderValue=(0, 128, 255))
cv2.imwrite('data/dst/opencv_affine_border_value.jpg', affine_img_bv)
cv2.BORDER_TRANSPARENT
とすると別の画像が背景となる。背景画像は引数dst
で指定する。この場合、第三引数dsize
がdst
のサイズと一致していないと正しく処理されないので注意。
dst = img // 4
affine_img_bm_bt = cv2.warpAffine(img, mat, (w, h), borderMode=cv2.BORDER_TRANSPARENT, dst=dst)
cv2.imwrite('data/dst/opencv_affine_border_transparent.jpg', affine_img_bm_bt)
その他の例。
affine_img_bm_br = cv2.warpAffine(img, mat, (w, h), borderMode=cv2.BORDER_REPLICATE)
cv2.imwrite('data/dst/opencv_affine_border_replicate.jpg', affine_img_bm_br)
affine_img_bm_bw = cv2.warpAffine(img, mat, (w, h), borderMode=cv2.BORDER_WRAP)
cv2.imwrite('data/dst/opencv_affine_border_wrap.jpg', affine_img_bm_bw)
OpenCVの射影変換: cv2.warpPerspective()
OpenCVではcv2.warpPerspective()
関数で射影変換を実現できる。
cv2.warpPerspective()の基本的な使い方
dst = cv2.warpPerspective(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
第一引数に元画像(NumPy配列ndarray
)、第二引数に3 x 3
の変換行列(NumPy配列ndarray
)、第三引数に出力画像のサイズ(タプル)を指定する。
変換行列が3 x 3
である以外は上述のcv2.warpAffine()
とまったく同じ。
3 x 3
の変換行列はcv2.getPerspectiveTransform()
で生成できる。cv2.getPerspectiveTransform()
の詳細および具体例については後述。
また、補完処理のアルゴリズムは引数flags
、領域外に対する処理は引数borderMode
およびborderValue
, dst
で指定するのもcv2.warpAffine()
と同じ。例は省略する。
任意の変換行列を定義
ここからは具体的な幾何変換の処理の例を挙げる。
まずは変換行列を自分で定義する例として平行移動(並進)を行う。
cv2.warpAffine()
およびcv2.warpPerspective()
の第二引数に指定する変換行列はNumPy配列ndarray
で、そのデータ型dtype
が浮動小数点数float
である必要がある。整数int
だとエラーになるので注意。サンプルコードを実行した環境ではfloat32
でもfloat64
でもOKだった。
上述の通り、cv2.warpAffine()
の第三引数には適切なサイズを指定する必要がある。以下の例のように元画像のサイズそのままだと全体が入らない。
mat = np.array([[1, 0, 50], [0, 1, 20]], dtype=np.float32)
print(mat)
# [[ 1. 0. 50.]
# [ 0. 1. 20.]]
affine_img_translation = cv2.warpAffine(img, mat, (w, h))
cv2.imwrite('data/dst/opencv_affine_translation.jpg', affine_img_translation)
スキューの例は以下の通り。
x軸方向へのスキュー。15度傾けている。
a = math.tan(math.radians(15))
mat = np.array([[1, a, 0], [0, 1, 0]], dtype=np.float32)
print(mat)
# [[1. 0.2679492 0. ]
# [0. 1. 0. ]]
affine_img_skew_x = cv2.warpAffine(img, mat, (int(w + h * a), h))
cv2.imwrite('data/dst/opencv_affine_skew_x.jpg', affine_img_skew_x)
y軸方向へのスキュー。同じく15度傾けている。
mat = np.array([[1, 0, 0], [a, 1, 0]], dtype=np.float32)
print(mat)
# [[1. 0. 0. ]
# [0.2679492 1. 0. ]]
affine_img_skew_y = cv2.warpAffine(img, mat, (w, int(h + w * a)))
cv2.imwrite('data/dst/opencv_affine_skew_y.jpg', affine_img_skew_y)
なお、線形変換のところの説明にも書いたように、OpenCVでは画像左上が原点となるので、上辺右向きがx軸、左辺下向きがy軸となる。参考書などの図では左下が原点、上向きがy軸となっている図が多いため、傾き方向が逆に見える場合もある。
回転の変換行列を生成: cv2.getRotationMatrix2D()
回転のための変換行列を生成する関数cv2.getRotationMatrix2D()
が用意されている。sin
やcos
を使って自分で計算するより便利。
retval= cv2.getRotationMatrix2D(center, angle, scale)
第一引数が回転の原点となる座標、第二引数が回転の角度(ラジアンではなく度degree
)、第三引数が拡大・縮小倍率。
mat = cv2.getRotationMatrix2D((w / 2, h / 2), 45, 0.5)
print(mat)
# [[ 0.35355339 0.35355339 89.51456544]
なお、90度、180度、270度に回転する場合はcv2.rotate()
のほうが簡単。以下の記事を参照。
また、ほかのOpenCVの処理と組み合わせる必要がなく、単純に画像を任意の角度で回転したいだけであれば、Pillow(PIL)を使うほうが簡単。
アフィン変換の変換行列を生成: cv2.getAffineTransform()
上述の通り、アフィン変換は任意の平行四辺形から別の任意の平行四辺形への変換となる。
平行四辺形は3点が決定すれば残りの1点も決まるので、変換前の3点の座標と変換後の3点の座標が与えられればアフィン変換の変換行列は一意に決まる。
任意のアフィン変換のための変換行列はcv2.getAffineTransform()
で生成できる。
第一引数に変換前の3点の座標、第二引数に変換後の3点の座標をNumPy配列ndarray
で指定する。ndarray
のデータ型dtype
はfloat32
である必要がありfloat64
ではエラー(※環境によって違うかもしれない)。
src_pts = np.array([[30, 30], [50, 200], [350, 50]], dtype=np.float32)
dst_pts = np.array([[90, 20], [140, 170], [280, 80]], dtype=np.float32)
mat = cv2.getAffineTransform(src_pts, dst_pts)
print(mat)
# [[ 0.57962963 0.22592593 65.83333333]
# [ 0.13333333 0.86666667 -10. ]]
説明のため、元画像の第一引数に指定した座標にマークを描画する。
img_mark = img.copy()
for pt in src_pts:
cv2.drawMarker(img_mark, tuple(pt), (0, 255, 0), thickness=4)
cv2.imwrite('data/dst/opencv_affine_mark_src.jpg', img_mark)
cv2.warpAffine()
でアフィン変換を実行。
affine_img = cv2.warpAffine(img_mark, mat, (w, h))
cv2.imwrite('data/dst/opencv_affine_mark_dst.jpg', affine_img)
出力画像の第二引数に指定した座標にマークを描画すると、変換前のマークと一致していることが確認できる。
affine_img_mark = affine_img.copy()
for pt in dst_pts:
cv2.drawMarker(affine_img_mark, tuple(pt), (255, 0, 0), markerType=cv2.MARKER_SQUARE, thickness=4)
cv2.imwrite('data/dst/opencv_affine_mark_dst_mark.jpg', affine_img_mark)
第一引数と第二引数の1点目、2点目、3点目がそれぞれ対応しているので同じ組み合わせでも順番が変わると結果も変わる。
src_pts = np.array([[30, 30], [50, 200], [350, 50]], dtype=np.float32)
dst_pts = np.array([[140, 170], [280, 80], [90, 20]], dtype=np.float32)
mat = cv2.getAffineTransform(src_pts, dst_pts)
print(mat)
# [[ -0.20925926 0.84814815 120.83333333]
# [ -0.43888889 -0.47777778 197.5 ]]
affine_img = cv2.warpAffine(img_mark, mat, (w, h))
cv2.imwrite('data/dst/opencv_affine_mark_dst_another.jpg', affine_img)
変換自体は元画像全体に対して行われる。指定した座標の三角形部分のみを取り出して別の画像に貼り付けたい場合は以下の記事を参照。
射影変換の変換行列を生成: cv2.getPerspectiveTransform()
上述の通り、射影変換は任意の四角形から別の任意の四角形への変換となるため、変換前の4点の座標と変換後の4点の座標が与えられれば射影変換の変換行列は一意に決まる。
任意の射影変換のための変換行列はcv2.getAffineTransform()
で生成できる。
第一引数に変換前の4点の座標、第二引数に変換後の4点の座標をNumPy配列ndarray
で指定する。ndarray
のデータ型dtype
はfloat32
である必要があり、float64
ではエラー(※環境によって違うかもしれない)。
src_pts = np.array([[0, 0], [0, h], [w, h], [w, 0]], dtype=np.float32)
dst_pts = np.array([[20, 50], [50, 175], [300, 205], [380, 20]], dtype=np.float32)
mat = cv2.getPerspectiveTransform(src_pts, dst_pts)
print(mat)
# [[ 5.42651593e-01 2.04362225e-01 2.00000000e+01]
# [-9.38078109e-02 8.04156675e-01 5.00000000e+01]
# [-9.40390545e-04 1.42057782e-03 1.00000000e+00]]
perspective_img = cv2.warpPerspective(img, mat, (w, h))
cv2.imwrite('data/dst/opencv_perspective_dst.jpg', perspective_img)
アフィン変換と同様、第一引数と第二引数の1点目、2点目、3点目、4点目がそれぞれ対応しているので同じ組み合わせでも順番が変わると結果も変わる。
射影変換のよくある利用例として台形補正がある。
例えば(台形ではないが)上の結果画像のような絵が傾いている画像に対して傾きを補正したい場合、補正したい領域のコーナー4点の座標から矩形のコーナー4点の座標への変換行列をcv2.getAffineTransform()
で生成して射影変換を行う。
mat_i = cv2.getPerspectiveTransform(dst_pts, src_pts)
print(mat_i)
# [[ 1.60933274e+00 -3.86239857e-01 -1.28746619e+01]
# [ 1.02707766e-01 1.23249319e+00 -6.36788147e+01]
# [ 1.36749691e-03 -2.11406880e-03 1.00000000e+00]]
perspective_img_i = cv2.warpPerspective(perspective_img, mat_i, (w, h))
cv2.imwrite('data/dst/opencv_perspective_dst_inverse.jpg', perspective_img_i)
ここでは既知の座標だが、実際はコーナー検出やテンプレートマッチングなどで座標を取得する必要がある。
cv2.warpAffine()
と同様にcv2.warpPerspective()
でも変換自体は元画像全体に対して行われる。指定した座標の四角形部分のみを取り出して別の画像に貼り付けたい場合は以下の記事を参照。