OpenCV – warpAffine でアフィン変換を行う方法について

OpenCV – warpAffine でアフィン変換を行う方法について

概要

OpenCV – 画像に適用するアフィン変換について では、画像に適用するアフィン変換について紹介しました。今回は OpenCV で画像にアフィン変換を適用する関数 cv2.warpAffine() について解説します。

cv2.warpAffine

アフィン変換行列を画像に適用するには、cv2.warpAffine() を使用します。

dst = cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])
引数
名前 デフォルト値
src ndarray
入力画像
M ndarray
(2, 3) のアフィン変換行列
dsize tuple of 2-ints
出力画像の大きさ
dst ndarray None
出力画像を作成する際に使用する画像
flags int cv2.INTER_LINEAR
補間方法
borderMode int cv2.BORDER_CONSTANT
外挿方法
borderValue int or tuple of 3-ints 0
外挿する値
返り値
名前 説明
dst リサイズ後の画像

使い方

M には、(3, 3) のアフィン変換行列のうち、3行目を除いた (2, 3) の行列を指定します。

$$ \begin{pmatrix} a_{11} & a_{12} & t_1\\ a_{21} & a_{22} & t_2 \end{pmatrix} $$

dsize には出力画像の大きさを指定します。

In [1]:
import cv2
import numpy as np
from IPython.display import Image, display


def imshow(img):
    """ndarray 配列をインラインで Notebook 上に表示する。
    """
    ret, encoded = cv2.imencode(".jpg", img)
    display(Image(encoded))
In [2]:
def scale_matrix(sx, sy):
    M = np.array([[sx, 0, 0],
                  [0, sy, 0]], dtype=float)
    return M


# 画像を読み込む。
img = cv2.imread("sample.jpg")
h, w = img.shape[:2]

# x 方法に2倍、y 方法に2倍だけ拡大するアフィン変換行列
M = scale_matrix(sx=2, sy=2)

# 画像にアフィン変換行列を適用する。
dst = cv2.warpAffine(img, M, dsize=(w * 2, h * 2))
imshow(dst)

flags – 補間方法の設定など

InterpolationFlags 及び cv2.WARP_INVERSE_MAP の組み合わせを指定できます。

補間方法は次の種類があります。

  • cv2.INTER_NEAREST: 最近傍補間
  • cv2.INTER_LINEAR: バイリニア補間
  • cv2.INTER_CUBIC: バイキュービック補間
  • cv2.INTER_AREA: ピクセル領域の関係を利用したリサンプリング
  • cv2.INTER_LANCZOS4: Lanczos 補間

また、cv2.WARP_INVERSE_MAP を指定した場合、アフィン変換行列 M は入力画像から出力画像の変換でなく、出力画像から入力画像への変換として解釈されます。

In [3]:
center = w / 2, h / 2
M = cv2.getRotationMatrix2D(center=center, angle=30, scale=1)

dst = cv2.warpAffine(img, M, dsize=(w, h))
imshow(dst)

# flags=cv2.WARP_INVERSE_MAP の場合、M の逆行列が適用される。
dst = cv2.warpAffine(img, M, dsize=(w, h), flags=cv2.WARP_INVERSE_MAP)
imshow(dst)

borderMode – 外挿方法

変換後に値がないピクセルを外挿する方法を BorderTypes から指定できます。 デフォルトでは、borderMode=cv2.BORDER_CONSTANTborderValue=0 となっており、黒で塗りつぶされるようになっています。

In [4]:
img = cv2.imread("sample.jpg")

h, w, c = img.shape
center = w / 2, h / 2
M = cv2.getRotationMatrix2D(center=center, angle=30, scale=1)

# 画像にアフィン変換行列を適用する。
rotated = cv2.warpAffine(img, M, dsize=(w, h), borderValue=(0, 255, 0))
imshow(rotated)

外挿方法で cv2.BORDER_TRANSPARENT を指定すると、dst 引数に指定した画像の画素値が外挿に使用されます。これを利用することである画像を別の画像に埋め込むことができます。

background.jpg

In [5]:
# 前景画像、背景画像を読み込む。
fg_img = cv2.imread("sample.jpg")  # アフィン変換を適用する画像
bg_img = cv2.imread("background.jpg")  # 背景画像

# 画像の中心回りに120度回転し、0.5倍に縮小するアフィン変換行列を作成する。
fg_h, fg_w = fg_img.shape[:2]
M = cv2.getRotationMatrix2D(center=(fg_w / 2, fg_h / 2), angle=120, scale=0.5)

# 画像にアフィン変換行列を適用する。
bg_h, bg_w = bg_img.shape[:2]
dst = cv2.warpAffine(
    fg_img, M, dsize=(bg_w, bg_h), dst=bg_img, borderMode=cv2.BORDER_TRANSPARENT
)
imshow(dst)

アフィン変換の種類

拡大縮小

$x$ 軸方向に $s_x$、$y$ 軸方向に $s_y$ だけ拡大縮小 (scaling) します。

In [6]:
def scale(sx, sy):
    M = np.array([[sx, 0, 0],
                  [0, sy, 0]], dtype=float)

    return M


M = scale(2, 2)
dst = cv2.warpAffine(img, M, dsize=(w * 2, h * 2))
imshow(dst)

cv2.getRotationMatrix2D – 回転

回転を表すアフィン変換行列を作成する関数 cv2.getRotationMatrix2D() が用意されています。

retval = cv2.getRotationMatrix2D(center, angle, scale)
引数
名前 デフォルト値
center tuple of 2-floats
回転する中心
angle float
回転角度 (degree)
scale float
拡大縮小率
返り値
名前 説明
retval アフィン変換行列
In [7]:
# 画像の中心回りに30度回転するアフィン変換行列を作成する。
M = cv2.getRotationMatrix2D(center=(w / 2, h / 2), angle=30, scale=1)

# 画像にアフィン変換行列を適用する。
dst = cv2.warpAffine(img, M, dsize=(w, h))
imshow(dst)

この関数は、回転の中心を $center = (c_x, c_y)^T$、回転角度を $\theta$、スケールを $s$ としたとき、次の4つの変換をまとめたアフィン変換行列が作成されます。

  1. 回転の中心が原点となるように、$(-c_x, -c_y)$ だけ平行移動する $$ A_1 = \begin{pmatrix} 1 & 0 & -c_x \\ 0 & 1 & -c_y \\ 0 & 0 & 1 \\ \end{pmatrix} $$
  2. 原点を中心に反時計回りに $\theta$ だけ回転する $$ A_2 = \begin{pmatrix} \cos \theta & \sin \theta & 0 \\ -\sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} $$
  3. $s$ 倍だけスケールする $$ A_3 = \begin{pmatrix} s & 1 & 0 \\ 1 & s & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} $$
  4. 1で平行移動した分を戻すため、$(c_x, c_y)$ だけ平行移動する $$ A_4 = \begin{pmatrix} 1 & 0 & c_x \\ 0 & 1 & c_y \\ 0 & 0 & 1 \\ \end{pmatrix} $$

以上4つのアフィン変換を1つにまとめると、

$$ \begin{aligned} A_4 A_3 A_2 A_1 &= \begin{pmatrix} 1 & 0 & c_x \\ 0 & 1 & c_y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} s & 1 & 0 \\ 1 & s & 0 \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} \cos \theta & \sin \theta & 0 \\ -\sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} 1 & 0 & -c_x \\ 0 & 1 & -c_y \\ 0 & 0 & 1 \end{pmatrix} \\ &= \begin{pmatrix} s \cos \theta & s \sin \theta & -s c_x \cos \theta -s c_y \sin \theta + c_x \\ – s \sin \theta & s \cos \theta & s c_x \sin \theta -s c_y \cos \theta + c_y \\ 0 & 0 & 1 \end{pmatrix} \end{aligned} $$

ここで、$\alpha = s \cos \theta, \beta = s \sin \theta$ とおくと、

$$ \begin{pmatrix} \alpha & \beta & (1 – \alpha) c_x – \beta c_y \\ -\beta & \alpha & \beta c_x + (1 – \alpha) c_y \\ 0 & 0 & 1 \\ \end{pmatrix} $$
In [8]:
cx, cy = (100, 100)
angle = 120
scale = 3

M = cv2.getRotationMatrix2D((cx, cy), angle, scale)
print(M)

# 上の式と一致することを確認する。
a = scale * np.cos(np.deg2rad(angle))
b = scale * np.sin(np.deg2rad(angle))
M2 = np.array([[a, b, (1 - a) * cx - b * cy], [-b, a, b * cx + (1 - a) * cy]])
print(M2)
assert np.allclose(M, M2)
[[ -1.5 2.59807621 -9.80762114] [ -2.59807621 -1.5 509.80762114]] [[ -1.5 2.59807621 -9.80762114] [ -2.59807621 -1.5 509.80762114]]

cv2.rotate – 画像を90°単位で回転

90°単位で回転させる場合は、cv2.warpAffine() ではなく、cv2.rotate() を使います。

dst = cv2.rotate(src, rotateCode[, dst])
引数
名前 デフォルト値
rotateCode int
  • cv2.ROTATE_90_CLOCKWISE:
    反時計回りに270° (時計回りに90°) 回転する
  • cv2.ROTATE_180:
    反時計回りに180° (時計回りに180°) 回転する
  • cv2.ROTATE_90_COUNTERCLOCKWISE:
    反時計回りに90° (時計回りに270°) 回転する
返り値
名前 説明
dst 出力画像
In [9]:
# 反時計回りに90° (時計回りに270°) 回転させる。
dst = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
imshow(dst)
In [10]:
# 反時計回りに180° (時計回りに180°) 回転させる。
dst = cv2.rotate(img, cv2.ROTATE_180)
imshow(dst)
In [11]:
# 反時計回りに270° (時計回りに90°) 回転する。
dst = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
imshow(dst)

平行移動

$x$ 軸方向に $t_x$、$y$ 軸方向に $t_y$ だけ平行移動 (translation) します。

In [12]:
def translation(tx, ty):
    M = np.array([[1, 0, tx],
                  [0, 1, ty]], dtype=float)

    return M


M = translation(20, 30)
dst = cv2.warpAffine(img, M, dsize=(w, h))
imshow(dst)

cv2.flip – 反転

画像を上下左右に反転させる場合は cv2.flip() を使います。

dst = cv2.flip(src, flipCode[, dst])
引数
名前 デフォルト値
flipCode int
  • 0: 上下反転する
  • 1: 左右反転する
  • -1: 上下左右反転する
返り値
名前 説明
dst 出力画像
In [13]:
# 上下反転する。
dst = cv2.flip(img, 0)
imshow(dst)
In [14]:
# 左右反転する。
dst = cv2.flip(img, 1)
imshow(dst)

せん断

水平せん断 (sheer) します。

In [15]:
def sheer_x(l):
    M = np.array([[1, -l, 0],
                  [0, 1, 0]], dtype=float)

    return M


M = sheer_x(0.3)
dst = cv2.warpAffine(img, M, dsize=(w, h))
imshow(dst)

鉛直せん断 (sheer) します。

In [16]:
def sheer_y(l):
    M = np.array([[1, 0, 0],
                  [-l, 1, 0]], dtype=float)

    return M


M = sheer_y(0.3)
dst = cv2.warpAffine(img, M, dsize=(w, h))
imshow(dst)