OpenCV – warpAffine で画像を回転する方法

目次

概要

OpenCV のアフィン変換を適用する関数 cv2.warpAffine() を使用して、画像を回転する方法について解説します。

関連記事

画像に適用するアフィン変換については、OpenCV – 画像に適用するアフィン変換について で解説しています。

cv2.warpAffine

dst = cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])

公式リファレンス: cv2.warpAffine()

引数
名前 デフォルト値
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} $$

回転を表すアフィン変換行列を作成するには cv2.getRotationMatrix2D() を使用します。

dsize には出力画像の大きさを指定します。今回は入力画像と同じ大きさを指定しました。

sample.jpg

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 rotate_img(img, angle):
    # 画像の中心周りに angle 度回転するアフィン変換行列を求める。
    img_h, img_w = img.shape[:2]
    center = img_w / 2, img_h / 2
    M = cv2.getRotationMatrix2D(center=center, angle=angle, scale=1)

    # アフィン変換を適用する。
    dst = cv2.warpAffine(img, M, dsize=(img_w, img_h))

    return dst


img = cv2.imread("sample.jpg")

dst = rotate_img(img, 30)
imshow(dst)

回転後の画像が収まるように、画像サイズを調整する。

最初のコードだと、回転後の画像は、はみ出た部分が切れてしまいます。そのため、回転後の画素がすべて収まるように、画像の大きさ及び位置を調整するコードを紹介します。

In [3]:
import cv2
import numpy as np


def rotate_img(img, angle, bg_color=(0, 0, 0)):
    # 画像を中心周りに angle 回転するアフィン変換行列を作成する。
    img_h, img_w = img.shape[:2]
    center = img_w / 2, img_h / 2
    M = cv2.getRotationMatrix2D(center=center, angle=angle, scale=1)

    # 回転後の画像の四隅の座標を計算する。
    corners_rotated = cv2.boxPoints((center, (img_w, img_h), -angle))

    # 回転後の画像の四隅の最小値、最大値を計算する。
    min_x, min_y = corners_rotated.min(axis=0)
    max_x, max_y = corners_rotated.max(axis=0)

    # 回転後の画像の四隅が収まる大きさを計算する。
    dst_w, dst_h = int(max_x - min_x), int(max_y - min_y)

    # はみ出している分だけ平行移動する。
    M[0, 2] += -min_x
    M[1, 2] += -min_y

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

    return dst


img = cv2.imread("sample.jpg")

dst = rotate_img(img, angle=30)
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 [4]:
img = cv2.imread("sample.jpg")

# 画像の中央を中心に 30 度回転するアフィン変換行列を作成する。
img_h, img_w = img.shape[:2]
center = img_w / 2, img_h / 2
M = cv2.getRotationMatrix2D(center=center, angle=30, scale=1)

# flags=cv2.WARP_INVERSE_MAP を指定しない場合、M がそのまま適用される。
dst = cv2.warpAffine(img, M, dsize=(img_w, img_h))
imshow(dst)

# flags=cv2.WARP_INVERSE_MAP を指定した場合、M の逆行列が適用される。
dst = cv2.warpAffine(img, M, dsize=(img_w, img_h), flags=cv2.WARP_INVERSE_MAP)
imshow(dst)

borderMode – 外挿方法

変換後に値がない画素を外挿する方法を BorderTypes から指定できます。 デフォルトでは、borderMode=cv2.BORDER_CONSTANTborderValue=0 となっており、値がない画素は黒になります。

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

# 画像の中央を中心に 30 度回転するアフィン変換行列を作成する。
img_h, img_w = img.shape[:2]
center = img_w / 2, img_h / 2
M = cv2.getRotationMatrix2D(center=center, angle=30, scale=1)

# 青色で外挿する。
dst = cv2.warpAffine(img, M, dsize=(img_w, img_h), borderValue=(255, 0, 0))
imshow(dst)

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

background.jpg

In [6]:
# 前景画像を読み込む。
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)

cv2.getRotationMatrix2D

cv2.getRotationMatrix2D は、指定した点を中心に回転するアフィン変換行列を作成する関数です。 この関数は、回転の中心を $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} $$
retval = cv2.getRotationMatrix2D(center, angle, scale)
引数
名前 デフォルト値
center tuple of 2-floats
回転する中心
angle float
回転角度 (degree)
scale float
拡大縮小率
返り値
名前 説明
retval アフィン変換行列
In [7]:
import cv2
import numpy as np

cx, cy = (100, 100)
angle = 120
scale = 3

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

# cv2.getRotationMatrix2D() が作成する関数が上の式と一致することを確認する。
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]]

画像を 90° 単位で回転する

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

cv2.rotate

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

時計回りに 90° (反時計回りに 270°) 回転する

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

# 反時計回りに270° (時計回りに90°) 回転する。
dst = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
imshow(dst)

時計回りに 180° (反時計回りに 180°) 回転する

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

# 反時計回りに270° (時計回りに90°) 回転する。
dst = cv2.rotate(img, cv2.ROTATE_180)
imshow(dst)

反時計回りに 90° (時計回りに 270°) 回転する

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

# 反時計回りに90° (時計回りに270°) 回転する。
dst = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
imshow(dst)

コメント

コメントする

目次