note.nkmk.me

Python, OpenCVで幾何変換(アフィン変換・射影変換など)

Date: 2019-02-11 / tags: 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軸方向へのスキュー。

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

y軸方向へのスキュー。

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

変換行列の積によりこれらを組み合わせることもできる。

同次座標で表す変換

平行移動を表すと以下のようになる。

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

img = cv2.imread('data/src/lena.jpg')

h, w, c = img.shape
print(h, w, c)
# 225 400 3

lena

ここでは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)

OpenCV warpAffine()

出力画像のサイズは自分で決定する必要がある。元画像全体が残るように自動的にサイズが決定されたりはしない。指定を誤ると途中で切れた画像になってしまう場合もあるので注意。

affine_img_half = cv2.warpAffine(img, mat, (w, h // 2))
cv2.imwrite('data/dst/opencv_affine_half.jpg', affine_img_half)

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

OpenCV warpAffine() 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で指定できる。デフォルトはborderModecv2.BORDER_CONSTANTborderValue0なので黒で埋められる。カラー画像の場合、引数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)

OpenCV warpAffine() borderValue

cv2.BORDER_TRANSPARENTとすると別の画像が背景となる。背景画像は引数dstで指定する。この場合、第三引数dsizedstのサイズと一致していないと正しく処理されないので注意。

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)

OpenCV warpAffine() BORDER_TRANSPARENT

その他の例。

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)

OpenCV warpAffine() BORDER_REPLICATE

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

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配列ndattayで、そのデータ型dtypeが浮動小数点数floatである必要がある。整数intだとエラーになるので注意。サンプルコードを実行した環境ではfloat32でもfloat64でもOKだった。

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)

OpenCV warpAffine() translation

拡大・縮小、回転、せん断(スキュー)などの線形変換についても上述の数式の通り変換行列を定義すればOK。

回転の変換行列を生成: cv2.getRotationMatrix2D()

回転のための変換行列を生成する関数cv2.getRotationMatrix2D()が用意されている。sincosを使って自分で計算するより便利。

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]
#  [ -0.35355339   0.35355339 143.43592168]]

OpenCV warpAffine() getRotationMatrix2D

アフィン変換の変換行列を生成: cv2.getAffineTransform()

上述の通り、アフィン変換は任意の平行四辺形から別の任意の平行四辺形への変換となる。

平行四辺形は3点が決定すれば残りの1点も決まるので、変換前の3点の座標と変換後の3点の座標が与えられればアフィン変換の変換行列は一意に決まる。

任意のアフィン変換のための変換行列はcv2.getAffineTransform()で生成できる。

第一引数に変換前の3点の座標、第二引数に変換後の3点の座標をNumPy配列ndarrayで指定する。ndarrayのデータ型dtypefloat32である必要があり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)

OpenCV warpAffine() getAffineTransform() src with mark

cv2.warpAffine()でアフィン変換を実行。

affine_img = cv2.warpAffine(img_mark, mat, (w, h))
cv2.imwrite('data/dst/opencv_affine_mark_dst.jpg', affine_img)

OpenCV warpAffine() getAffineTransform() affine result

出力画像の第二引数に指定した座標にマークを描画すると、変換前のマークと一致していることが確認できる。

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)

OpenCV warpAffine() getAffineTransform() affine result with 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)

OpenCV warpAffine() getAffineTransform() affine another result

変換自体は元画像全体に対して行われる。指定した座標の三角形部分のみを取り出して別の画像に貼り付けたい場合は以下の記事を参照。

射影変換の変換行列を生成: cv2.getPerspectiveTransform()

上述の通り、射影変換は任意の四角形から別の任意の四角形への変換となるため、変換前の4点の座標と変換後の4点の座標が与えられれば射影変換の変換行列は一意に決まる。

任意の射影変換のための変換行列はcv2.getAffineTransform()で生成できる。

第一引数に変換前の4点の座標、第二引数に変換後の4点の座標をNumPy配列ndarrayで指定する。ndarrayのデータ型dtypefloat32である必要があり、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)

OpenCV warpPerspective() getPerspectiveTransform()

アフィン変換と同様、第一引数と第二引数の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)

OpenCV warpPerspective() getPerspectiveTransform() inverse

ここでは既知の座標だが、実際はコーナー検出やテンプレートマッチングなどで座標を取得する必要がある。

cv2.warpAffine()と同様にcv2.warpPerspective()でも変換自体は元画像全体に対して行われる。指定した座標の四角形部分のみを取り出して別の画像に貼り付けたい場合はは以下の記事を参照。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事