はじめに
画像処理の基本的な操作のひとつである画像の回転は、さまざまな効果を出すことができます。
例えば、風景写真を回転することで、視点を変えて新たな景色を楽しむことができます。
また、人物の写真を回転することで、正面を向いた顔を正しく表示することができます。
OpenCVでは、画像を回転する方法として、cv2.rotate
関数とcv2.warpAffine
関数の2つを使うことができます。
本記事では、cv2.rotate
関数とcv2.warpAffine
関数の違いや使い分けについて解説します。
画像の回転
画像の回転は、以下の様々な場面で利用されます。例えば、以下のようなケースがあります。
- 補正と調整:画像が正しい向きでない場合、回転して正しい方向に修正することがあります。
- オブジェクト検出:物体検出アルゴリズムでは、画像内のオブジェクトを検出する前に、画像を適切な向きに回転させることがあります。
- データ拡張:機械学習モデルのトレーニングデータを増やすために、画像に対してランダムな回転を適用して汎化性能を向上させることがあります。
使用される場面が少なくない画像の回転ですが、画像のサイズが変化する場合があります。
そのため、画像のサイズが変化することを想定して、処理を行う必要があります。
cv2.rotate関数とcv2.warpAffine関数
OpenCVで画像の回転を行うcv2.rotate
関数とcv2.warpAffine
関数を紹介します。cv2.rotate
関数は画像を回転することが目的の関数ですが、cv2.warpAffine
関数は画像の回転のみが目的ではなく、アフィン変換を行う関数です。
これらの使い分けは以下の様になると思います。
処理速度が重要な場合はcv2.rotate
関数、任意の回転が必要な場合はcv2.warpAffine
関数を使い分けるとよいでしょう。
では、以下に詳しく解説します。
cv2.rotate関数
cv2.rotate
関数は、90°刻みの回転を行います。
引数
名称 | 説明 |
入力画像 | 入力画像。この画像を回転します。 |
rotateCode | 画像を回転する方法として、RotateFlagsを指定します。 |
dst(オプション) | 出力画像。指定された場合、結果がこの変数に格納されます。指定されない場合は、新しい配列が作成されて結果が格納され戻り値として取得できます。 |
rotateCode
に指定できるRotateFlagsは下の3つのいずれかとなります。
フラグの種類 | 説明 |
cv2.ROTATE_90_CLOCKWISE | 時計回りに 90°回転 |
cv2.ROTATE_180 | 時計回りに 180°回転 |
cv2.ROTATE_90_COUNTERCLOCKWISE | 時計回りに 270°回転(反時計回りに90°回転) |
戻り値
戻り値は回転された画像データです。
サンプルコード
下に示すサンプルコードの処理の手順は次の通りです。
- 入力画像の読み込み
- BGRのチャンネル並びをRGBの並びに変更
この処理は結果の表示にmatplotlibを使うために行なっています。 - 変換行列を求める
- 画像を回転
時計回りに90°、180°、反時計回りに90°の順番で実行しています。 - 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
関数の構文が下になります。
引数
名称 | 説明 |
入力画像(必須) | 変換前の入力画像 |
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_LANCZOS4 | 8x8近傍に対するのLanczos補間法。 |
cv2.INTER_LINEAR_EXACT | Bit exact バイリニア補間法。 |
cv2.INTER_NEAREST_EXACT | Bit exact 最近傍法。この方法はPIL, scikit-imageまたはMatlabの最近傍法と同じ結果となります。 |
補間方法の違いはcv2.resize関数の解説記事で説明しているのでチェックして下さい。
borderMode
に指定できるBorderTypesは次の通りです。
borderType | 説明 |
cv2.BORDER_CONSTANT | 境界ピクセルを指定した定数値に設定します。定数値は引数のborderValue で指定します。 |
cv2.BORDER_REPLICATE | 画像の端のピクセルをそのままコピーします。 |
cv2.BORDER_REFLECT | 画像の端のピクセルを反射させます。 |
cv2.BORDER_WRAP | 画像の端のピクセルが反対側にWrappingされます。 具体的には以下のような動作になります: ・画像の左端のピクセルは右端からコピーされる ・画像の右端のピクセルは左端からコピーされる ・画像の上端のピクセルは下端からコピーされる ・画像の下端のピクセルは上端からコピーされる つまり、画像がタイルのように繰り返されるイメージです。 |
cv2.BORDER_REFLECT_101 | cv2.BORDER_REFLECT と同じですが、端の1画素は反射せずにそのままコピーします。 |
cv2.BORDER_TRANSPARENT | 境界ピクセルを透明にします。アルファチャンネルの設定が必要です。 |
cv2.BORDER_REFLECT101 | cv2.BORDER_REFLECT_101 と同じです。 |
cv2.BORDER_DEFAULT | cv2.BORDER_REFLECT_101 と同じです。 |
cv2.BORDER_ISOLATED | 境界ピクセルを0にします。 |
戻り値
戻り値は回転された画像データです。
変換行列を作成するcv2.getRotationMatrix2D関数
cv2.warpAffine
関数の引数M
である2x3の変換行列を事前に計算する必要があります。
2x3の変換行列はcv2.getRotationMatrix2D
関数を使用して作成します。
引数
名称 | 説明 |
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
関数は、透視変換に特化した関数であることを念頭において利用するのが望ましいでしょう。
サンプルコード
下に示すサンプルコードの処理の手順は次の通りです。
- 入力画像の読み込み
- BGRのチャンネル並びをRGBの並びに変更
この処理は結果の表示にmatplotlibを使うために行なっています。 - 変換行列を求める
- 回転後の画像のサイズを計算
- 回転の中心のズレを修正
- 画像を回転
30°、90°、125°、180°、345°(全て時計回り)の順番で回転を実行しています。 - matplotlibを使って、結果を表示
結果は下に示す通りです。
cv2.warpAffine
関数を使用する場合、任意の角度で画像を回転することができますが、変換行列を求める必要があるためcv2.rotate
関数ほど簡単ではありません。
また、回転後に画像サイズが変わります。90°刻みのであれば高さと幅が入れ替わる程度ですが、任意の角度で回転した場合は画像サイズを計算するプロセスが発生します。
画像サイズを変更しない場合は以下の様な結果となります。
アフィン変換における回転を行う場合の変換行列の各要素には以下の様になります。
$$M=\begin{bmatrix} a & b & c \\ d & e & f \end{bmatrix}$$
- aとe:拡大縮小因子
- aは x 軸方向の拡大縮小を表します。
- eは y 軸方向の拡大縮小を表します。
- 回転の場合はa=eとなり、(拡大率)*cos(回転角度)で表されます。
- 拡大率は等しい1より大きい場合は拡大、1より小さい場合は縮小となります。
- bとd:せん断因子または回転因子
- bは x 軸方向のせん断または回転を表します。
- dは y 軸方向のせん断または回転を表します。
- 回転の場合はb=dとなり、(拡大率)*sin(回転角度) で表されます。
- これらの値が0であれば回転なし、0以外であればせん断がかかります。
- cとf:平行移動因子
- cは x 軸方向の平行移動を表し、
(1-a)*(回転中心のx座標) - b*(回転中心のy座標) で表されます。 - fは y 軸方向の平行移動を表、
b*(回転中心のx座標) - (1-a)*(回転中心のy座標) で表されます。
- cは x 軸方向の平行移動を表し、
以上より、変換行列はこの様になります。
$$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の参考書としてどうぞ!■
参考リンク
OpenCV: rotate()
Rotates a 2D array in multiples of 90 degrees. The function cv::rotate rotates the array in one of three different ways: Rotate by 90 degrees clockwise (rotateCode = ROTATE_90_CLOCKWISE). Rotate by 180 degrees clockwise (rotateCode = ROTATE_180). Rotate by 270 degrees clockwise (rotateCode = ROTATE_90_COUNTERCLOCKWISE).
OpenCV: warpAffine()
Applies an affine transformation to an image.
OpenCV: getRotationMatrix2D()
Calculates an affine matrix of 2D rotation.