【Python・OpenCV】画像を回転する - cv2.rotateとcv2.warpAffineの違いと使い分け

※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】画像を回転する - cv2.rotateとcv2.warpAffineの違いと使い分け

はじめに

画像処理の基本的な操作のひとつである画像の回転は、さまざまな効果を出すことができます。
例えば、風景写真を回転することで、視点を変えて新たな景色を楽しむことができます。
また、人物の写真を回転することで、正面を向いた顔を正しく表示することができます。

OpenCVでは、画像を回転する方法として、cv2.rotate関数とcv2.warpAffine関数の2つを使うことができます。
本記事では、cv2.rotate関数とcv2.warpAffine関数の違いや使い分けについて解説します。

(広告) OpenCV関連書籍をAmazonで探す

画像の回転

画像の回転は、以下の様々な場面で利用されます。例えば、以下のようなケースがあります。

  1. 補正と調整:画像が正しい向きでない場合、回転して正しい方向に修正することがあります。
  2. オブジェクト検出:物体検出アルゴリズムでは、画像内のオブジェクトを検出する前に、画像を適切な向きに回転させることがあります。
  3. データ拡張:機械学習モデルのトレーニングデータを増やすために、画像に対してランダムな回転を適用して汎化性能を向上させることがあります。

使用される場面が少なくない画像の回転ですが、画像のサイズが変化する場合があります。
そのため、画像のサイズが変化することを想定して、処理を行う必要があります。

cv2.rotate関数とcv2.warpAffine関数

OpenCVで画像の回転を行うcv2.rotate関数とcv2.warpAffine関数を紹介します。
cv2.rotate関数は画像を回転することが目的の関数ですが、cv2.warpAffine関数は画像の回転のみが目的ではなく、アフィン変換を行う関数です。

これらの使い分けは以下の様になると思います。

ポイント

  • cv2.rotate関数は90°単位回転に特化し、高速
  • cv2.warpAffine関数は任意のアフィン変換が可能だが低速

処理速度が重要な場合はcv2.rotate関数、任意の回転が必要な場合はcv2.warpAffine関数を使い分けるとよいでしょう。

では、以下に詳しく解説します。

cv2.rotate関数

cv2.rotate関数は、90°刻みの回転を行います。

cv2.rotate(入力画像, rotateCode[, dst])

引数

名称説明
入力画像入力画像。この画像を回転します。
rotateCode画像を回転する方法として、RotateFlagsを指定します。
dst(オプション)出力画像。指定された場合、結果がこの変数に格納されます。指定されない場合は、新しい配列が作成されて結果が格納され戻り値として取得できます。

rotateCodeに指定できるRotateFlagsは下の3つのいずれかとなります。

フラグの種類説明
cv2.ROTATE_90_CLOCKWISE時計回りに 90°回転
cv2.ROTATE_180時計回りに 180°回転
cv2.ROTATE_90_COUNTERCLOCKWISE時計回りに 270°回転(反時計回りに90°回転)
RotateFlags

戻り値

戻り値は回転された画像データです。

ポイント

回転角度はフラグによる指定となり、数値による指定ではありません。

サンプルコード

下に示すサンプルコードの処理の手順は次の通りです。

  1. 入力画像の読み込み
  2. BGRのチャンネル並びをRGBの並びに変更
    この処理は結果の表示にmatplotlibを使うために行なっています。
  3. 変換行列を求める
  4. 画像を回転
    時計回りに90°、180°、反時計回りに90°の順番で実行しています。
  5. matplotlibを使って、結果を表示

結果は以下の通りとなります。

90°刻みの回転角度に限られていますが、とても簡単に画像を回転することができました。

サンプルコードの全体は、下に示します。

import cv2
from matplotlib import pyplot as plt

import cv2

# 画像を読み込む
image = cv2.imread("image.jpg")

# BGRのチャンネル並びをRGBの並びに変更(matplotlibで結果を表示するため)
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 画像を時計回りに90°回転する
rotated_cw90 = cv2.rotate(rgb_image, cv2.ROTATE_90_CLOCKWISE)

# 画像を180°回転する
rotated_180 = cv2.rotate(rgb_image, cv2.ROTATE_180)

# 画像を反時計回りに90°回転する
rotated_ccw90 = cv2.rotate(rgb_image, cv2.ROTATE_90_COUNTERCLOCKWISE)

# 結果の可視化
plt.rcParams["figure.figsize"] = [12,6]                             # 表示領域のアスペクト比を設定
title = "cv2.rotate: codevace.com"
plt.figure(title)                                                   # ウィンドウタイトルを設定
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95)   # 余白を設定

plt.subplot(231)                                                    # 2行3列の1番目の領域にプロットを設定
plt.imshow(rgb_image)                                               # 入力画像を表示
plt.title("Original Image")                                         # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

plt.subplot(234)                                                    # 2行3列の4番目の領域にプロットを設定
plt.imshow(rotated_cw90)                                            # 時計回りに90°回転した画像を表示
plt.title("cv2.ROTATE_90_CLOCKWISE")                                # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

plt.subplot(235)                                                    # 2行3列の5番目の領域にプロットを設定
plt.imshow(rotated_180)                                             # 180°回転した画像を表示
plt.title("cv2.ROTATE_90_CLOCKWISE")                                # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

plt.subplot(236)                                                    # 2行3列の6番目の領域にプロットを設定
plt.imshow(rotated_ccw90)                                           # 反時計回りに90°回転した画像を表示
plt.title("cv2.ROTATE_90_COUNTERCLOCKWISE")                         # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

plt.show()                                                          # ウィンドウを表示する

入力画像として、この画像を使用しました。

引用元
黄色と赤のプラスチック玩具

アフィン変換による画像の回転

並行移動、拡大縮小、回転、反転など、行列を使って行う変換をアフィン変換と呼びます。
変換行列を適切に作成すれば、さまざまな画像の移動・変形が行えます。

OpenCVでアフィン変換を行う関数としてcv2.warpAffine関数が用意されています。

このページでは、アフィン変換の「回転」にフォーカスして解説します。

cv2.warpAffine関数

cv2.warpAffine関数の構文が下になります。

cv2.warpAffine(入力画像, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])

引数
名称説明
入力画像(必須)変換前の入力画像
M(必須)2x3の変換行列
dsize(必須)出力画像のサイズ。タプルで指定します。
dst(オプション)出力画像。指定された場合、結果がこの変数に格納されます。指定されない場合は、新しい配列が作成されて結果が格納され戻り値として取得できます。
flags(オプション)InterpolationFlagsで指定される、補間方法の指定(デフォルト:cv2.INTER_LINEAR)
borderMode(オプション)BorderTypesで指定される、境界ピクセルの指定(デフォルト:cv2.BORDER_CONSTANT)
borderValue(オプション)境界の値。タプルで指定します。(デフォルト:(0, 0, 0))

flagsに指定できるInterpolationFlagsは次の通りです。

補間方法のflag説明
cv2.INTER_NEAREST最近傍法(ニアレストネイバー法)
cv2.INTER_LINEARバイリニア補間法
cv2.INTER_CUBICバイキュービック補間法
cv2.INTER_AREAピクセル領域の関係を使用したリサンプリングを行います。
画像の縮小した場合にモアレのない結果となり、画像を拡大した場合には、cv2.INTER_NEARESTに似た結果となります。
cv2.INTER_LANCZOS48x8近傍に対するのLanczos補間法。
cv2.INTER_LINEAR_EXACTBit exact バイリニア補間法。
cv2.INTER_NEAREST_EXACTBit exact 最近傍法。この方法はPIL, scikit-imageまたはMatlabの最近傍法と同じ結果となります。
InterpolationFlags

補間方法の違いはcv2.resize関数の解説記事で説明しているのでチェックして下さい。

borderModeに指定できるBorderTypesは次の通りです。

borderType説明
cv2.BORDER_CONSTANT境界ピクセルを指定した定数値に設定します。定数値は引数のborderValueで指定します。
cv2.BORDER_REPLICATE画像の端のピクセルをそのままコピーします。
cv2.BORDER_REFLECT画像の端のピクセルを反射させます。
cv2.BORDER_WRAP画像の端のピクセルが反対側にWrappingされます。
具体的には以下のような動作になります:
 ・画像の左端のピクセルは右端からコピーされる
 ・画像の右端のピクセルは左端からコピーされる
 ・画像の上端のピクセルは下端からコピーされる
 ・画像の下端のピクセルは上端からコピーされる
つまり、画像がタイルのように繰り返されるイメージです。
cv2.BORDER_REFLECT_101cv2.BORDER_REFLECTと同じですが、端の1画素は反射せずにそのままコピーします。
cv2.BORDER_TRANSPARENT境界ピクセルを透明にします。アルファチャンネルの設定が必要です。
cv2.BORDER_REFLECT101cv2.BORDER_REFLECT_101と同じです。
cv2.BORDER_DEFAULTcv2.BORDER_REFLECT_101と同じです。
cv2.BORDER_ISOLATED境界ピクセルを0にします。
BorderTypes
戻り値

戻り値は回転された画像データです。

変換行列を作成するcv2.getRotationMatrix2D関数

cv2.warpAffine関数の引数Mである2x3の変換行列を事前に計算する必要があります。
2x3の変換行列はcv2.getRotationMatrix2D関数を使用して作成します。

cv2.getRotationMatrix2D(center, angle, scale)

引数
名称説明
center(必須)入力画像の回転の中心座標
angle(必須)回転角度 (単位は度)。正の値を反時計回りの回転になります 。
scale(必須)画像の拡大縮小率
戻り値

2x3のアフィン変換行列 M

cv2.warpAffine関数とcv2.getRotationMatrix2D関数の組み合わせではなく、cv2.warpPerspective関数と以下に示す3x3の変換行列を使う方法もあります。

変換行列の2x2の部分に回転行列を指定し、3行目を[0, 0, 1]とすることで、3x3の変換行列として回転変換を表現できます。
$$M=\begin{bmatrix} cos(angle) & sin(angle) & 0 \\ sin(angle) & cos(angle) & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

ただし、この方法は計算コストが高くなるなど効率的ではないため、回転変換を行う場合はcv2.warpAffine関数を利用することをおすすめします。cv2.warpPerspective関数は、透視変換に特化した関数であることを念頭において利用するのが望ましいでしょう。

サンプルコード

下に示すサンプルコードの処理の手順は次の通りです。

  1. 入力画像の読み込み
  2. BGRのチャンネル並びをRGBの並びに変更
    この処理は結果の表示にmatplotlibを使うために行なっています。
  3. 変換行列を求める
  4. 回転後の画像のサイズを計算
  5. 回転の中心のズレを修正
  6. 画像を回転
    30°、90°、125°、180°、345°(全て時計回り)の順番で回転を実行しています。
  7. matplotlibを使って、結果を表示

結果は下に示す通りです。

cv2.warpAffine関数を使用する場合、任意の角度で画像を回転することができますが、変換行列を求める必要があるためcv2.rotate関数ほど簡単ではありません。
また、回転後に画像サイズが変わります。90°刻みのであれば高さと幅が入れ替わる程度ですが、任意の角度で回転した場合は画像サイズを計算するプロセスが発生します。
画像サイズを変更しない場合は以下の様な結果となります。

アフィン変換における回転を行う場合の変換行列の各要素には以下の様になります。

$$M=\begin{bmatrix} a & b & c \\ d & e & f \end{bmatrix}$$

  1. ae:拡大縮小因子
    • aは x 軸方向の拡大縮小を表します。
    • eは y 軸方向の拡大縮小を表します。
    • 回転の場合はa=eとなり、(拡大率)*cos(回転角度)で表されます。
    • 拡大率は等しい1より大きい場合は拡大、1より小さい場合は縮小となります。
  2. bd:せん断因子または回転因子
    • bは x 軸方向のせん断または回転を表します。
    • dは y 軸方向のせん断または回転を表します。
    • 回転の場合はb=dとなり、(拡大率)*sin(回転角度) で表されます。
    • これらの値が0であれば回転なし、0以外であればせん断がかかります。
  3. cf:平行移動因子
    • cは x 軸方向の平行移動を表し、
      (1-a)*(回転中心のx座標) - b*(回転中心のy座標) で表されます。
    • fは y 軸方向の平行移動を表、
      b*(回転中心のx座標) - (1-a)*(回転中心のy座標) で表されます。

以上より、変換行列はこの様になります。

$$M=\begin{bmatrix} a & b & (1-a)*(center.x) - b*(center.y)  \\ b & a & b*(center.x) - (1-a)*(center.y) \end{bmatrix}$$

$$a = (scale)*cos(angle) $$

$$b = (scale)*sin(angle) $$

拡大・縮小はしないのでscale = 1です。aは回転角度のsin、bは回転角度のcosとなります。
これらを使って回転後の画像の幅と高さを算出します。
回転後の画像の幅と高さが求まれば、幅方向、高さ方向のズレを求めることができて、平行移動の補正を行うことができます。
計算式は下記のコードとなります。

# 回転後の画像のサイズを計算
cos_theta = np.abs(M[0, 0])
sin_theta = np.abs(M[0, 1])
new_width = int(width * cos_theta + height * sin_theta)
new_height = int(width * sin_theta + height * cos_theta)

# 回転の中心のズレを修正
M[0,2] += (new_width - width)/2.0
M[1,2] += (new_height - height)/2.0

また、cv2.warpAffine関数のborderModeをいくつか変更した結果を下に示します。

これらの指定は次のコードの様にcv2.warpAffine関数のborderModeに値を設定します。
cv2.BORDER_CONSTANTの場合はborderValueに色をタプルで設定します。

# borderMode=cv2.BORDER_CONSTANT
rotated_image = cv2.warpAffine(image, M, (width, height), borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 0, 255)

# borderMode=cv2.BORDER_REPLICATE
rotated_image = cv2.warpAffine(image, M, (width, height), borderMode=cv2.BORDER_REPLICATE)

# borderMode=cv2.BORDER_REFLECT
rotated_image = cv2.warpAffine(image, M, (width, height), borderMode=cv2.BORDER_REFLECT)

# borderMode=cv2.BORDER_WRAP
rotated_image = cv2.warpAffine(image, M, (width, height), borderMode=cv2.BORDER_WRAP)

# borderMode=cv2.BORDER_REFLECT_101
rotated_image = cv2.warpAffine(rgb_image, M, (width, height), borderMode=cv2.BORDER_REFLECT_101)

# borderMode=cv2.BORDER_DEFAULT
rotated_image = cv2.warpAffine(image, M, (width, height), borderMode=cv2.BORDER_DEFAULT)

回転後の画像のサイズ・中心のズレの修正を含むサンプルコードの全体を下に示します。

import cv2
import numpy as np
from matplotlib import pyplot as plt

def warpAffine_rotaion(src, angle, scale):
    """
    指定された回転角度に反時計回りに画像を回転し、回転の画像サイズに合わせてサイズを変化した画像を返す。

    src: 入力画像
    angle: 回転角度
    scale: 画像の拡大・縮小を指定する倍率

    戻り値: 回転した画像
    """
    # 入力画像のサイズを取得
    height, width = src.shape[:2]
    # 回転の中心座標を指定
    center = (width // 2, height // 2)

    # 変換行列(回転行列)を計算
    M = cv2.getRotationMatrix2D(center, angle, scale)

    # 回転後の画像のサイズを計算
    cos_theta = np.abs(M[0, 0])
    sin_theta = np.abs(M[0, 1])
    new_width = int(width * cos_theta + height * sin_theta)
    new_height = int(width * sin_theta + height * cos_theta)

    # 回転の中心のズレを修正
    M[0,2] += (new_width - width)/2.0
    M[1,2] += (new_height - height)/2.0

    # 画像を反時計回りにangle°回転する
    return cv2.warpAffine(src, M, (new_width, new_height))


def main():
    # 画像を読み込む
    image = cv2.imread("image.jpg")

    # BGRのチャンネル並びをRGBの並びに変更(matplotlibで結果を表示するため)
    rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    # スケールの指定(拡大・縮小しないため、1倍)
    scale = 1.0

    # ---- 反時計回りに30°回転 -------
    # 回転角度を指定
    angle = 30
    # 回転を実行
    rotated_ccw30 = warpAffine_rotaion(rgb_image, angle, scale)
    # ----------------------------

    # ---- 反時計回りに90°回転 -------
    # 回転角度を指定
    angle = 90
    # 回転を実行
    rotated_ccw90 = warpAffine_rotaion(rgb_image, angle, scale)
    # ----------------------------

    # ---- 反時計回りに125°回転 -------
    # 回転角度を指定
    angle = 125
    # 回転を実行
    rotated_ccw125 = warpAffine_rotaion(rgb_image, angle, scale)
    # ----------------------------

    # ---- 反時計回りに180°回転 -------
    # 回転角度を指定
    angle = 180
    # 画像を反時計回りに180°回転する
    rotated_ccw180 = warpAffine_rotaion(rgb_image, angle, scale)
    # ----------------------------

    # ---- 反時計回りに345°回転 -------
    # 回転角度を指定
    angle = 345
    # 画像を反時計回りに345°回転する
    rotated_ccw345 = warpAffine_rotaion(rgb_image, angle, scale)
    # ----------------------------

    # 結果の可視化
    plt.rcParams["figure.figsize"] = [12,6]                             # 表示領域のアスペクト比を設定
    title = "Rotation with cv2.warpAffine: codevace.com"
    plt.figure(title)                                                   # ウィンドウタイトルを設定
    plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95)   # 余白を設定

    plt.subplot(231)                                                    # 2行3列の1番目の領域にプロットを設定
    plt.imshow(rgb_image)                                               # 入力画像を表示
    plt.title("Original Image")                                         # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.subplot(232)                                                    # 2行3列の2番目の領域にプロットを設定
    plt.imshow(rotated_ccw30)                                           # 時計回りに30°回転した画像を表示
    plt.title("30° roteted")                                            # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.subplot(233)                                                    # 2行3列の3番目の領域にプロットを設定
    plt.imshow(rotated_ccw90)                                           # 90°回転した画像を表示
    plt.title("90° roteted")                                            # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.subplot(234)                                                    # 2行3列の4番目の領域にプロットを設定
    plt.imshow(rotated_ccw125)                                          # 反時計回りに125°回転した画像を表示
    plt.title("125° roteted")                                           # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.subplot(235)                                                    # 2行3列の5番目の領域にプロットを設定
    plt.imshow(rotated_ccw180)                                          # 反時計回りに180°回転した画像を表示
    plt.title("180° roteted")                                           # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.subplot(236)                                                    # 2行3列の6番目の領域にプロットを設定
    plt.imshow(rotated_ccw345)                                          # 反時計回りに345°回転した画像を表示
    plt.title("345° roteted")                                           # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す

    plt.show()                                                          # ウィンドウを表示する


if __name__ == "__main__":
    main()

おわりに

OpenCVの画像回転はcv2.rotate関数、cv2.warpAffine関数を組み合わせれば、ほとんどのケースの回転に対応できます。
上手に使い分けることで、演算負荷の軽減や見通しの良いコードの作成が期待できます。

また、cv2.warpAffine関数は変換行列により、回転以外の効果も得られますので、もう少し深掘りした別の投稿で取り上げたいと思います。

ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。

■(広告)OpenCVの参考書としてどうぞ!■

参考リンク

-Python, プログラミング, 画像処理
-