3. 画像の幾何変換#
本節では、OpenCVを用いた画像の幾何変換の方法について紹介する。
3.1. 単純な幾何変換#
まずは、単純な幾何変換の例として、
画像の拡大・縮小
水平・垂直方向の反転
90度, 180度, 270度の回転
について見ていこう。
3.1.1. 画像の拡大・縮小#
画像の拡大・縮小には、 cv2.resize を用いる。この関数には、入力画像、出力画像のサイズ、そして補間方法を指定する。
出力サイズの指定方法には二種類あり、出力の画像サイズを直接指定する方法と、拡大率、縮小率を指定する方法がある。
# 出力サイズを指定する方法
img_half = cv2.resize(img, (100, 100), interpolation=cv2.INTER_AREA)
# 拡大・縮小の比率を指定する方法
img_half = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
通常、画像を縮小する場合は、 cv2.INTER_AREA を補間方法として指定する。この補間方法では、縮小後の画素がカバーする元画像の範囲の画素値を平均して、縮小後の画素値を決定する。
一方で、画像を拡大する場合は、 cv2.INTER_NEAREST (最近傍補間)、 cv2.INTER_LINEAR (バイリニア補間)、 cv2.INTER_CUBIC (バイキュービック補間) のいずれかを指定することが多い。
図 3.1 画像の縮小における補間法の比較#
少々、分かりづらいかもしれないが、 図 3.1 に示したバイリニア補間とインターエリア法の結果を比較してみると、バイリニア補間の結果では、縮小された画像にジャギーが見られるのに対して、インターエリア法の結果は適度に画像がぼやけており、ジャギーのような高周波のノイズは見られないことが分かる。
3.1.2. 水平・垂直方向の反転#
画像を水平あるいは垂直方向に反転させるには、 cv2.flip を用いる。
# 水平方向の反転
img_flipped_horizontal = cv2.flip(img, 1)
# 垂直方向の反転
img_flipped_vertical = cv2.flip(img, 0)
また、水平・垂直の両方向について画像を反転させるには、 cv2.flip の第二引数に -1 を指定する。
# 水平・垂直両方向の反転
img_flipped_both = cv2.flip(img, -1)
一方、この方法は、第2引数に指定すべき値を覚えておく必要があるため、少々扱いづらい。Pythonを利用しているのであれば、スライスを利用したり、 np.flip を利用するほうが分かりやすいだろう。
# 水平方向の反転 (スライス)
img_flipped_horizontal = img[:, ::-1]
# 垂直方向の反転 (スライス)
img_flipped_vertical = img[::-1, :]
# 水平・垂直両方向の反転 (スライス)
img_flipped_both = img[::-1, ::-1]
# 水平方向の反転 (np.flip)
img_flipped_horizontal = np.flip(img, axis=1)
# 垂直方向の反転 (np.flip)
img_flipped_vertical = np.flip(img, axis=0)
# 水平・垂直両方向の反転 (np.flip)
img_flipped_both = np.flip(img, axis=(0, 1))
上記の np.flip を用いる方法を見てみると、 cv2.flip に指定している第2引数の値が、画像の軸のインデックスに対応していることが分かる。
図 3.2 各軸を対象とした画像反転の結果#
3.1.3. 90度・180度・270度の回転#
画像を回転させる時、回転角度が90度の倍数であれば、元の画像の画素の値を補間することなく再配置することで回転後の画像を得ることができる。
OpenCVでは、 cv2.rotate を用いると、90度の倍数の回転を行うことができる。
回転の量や方向については、次のいずれかの定数を指定する。
cv2.ROTATE_90_CLOCKWISE: 90度時計回りに回転cv2.ROTATE_90_COUNTERCLOCKWISE: 90度反時計回りに回転cv2.ROTATE_180: 180度回転
# 90度回転
img_rot = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
# 180度回転
img_rot = cv2.rotate(img, cv2.ROTATE_180)
# 270度回転
img_rot = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
また、NumPyの配列操作を使っても、同様の結果を得ることができる。具体的には、 np.transpose と np.flip を組み合わせれば良い。ただし、以下の例では、画像の回転角度が反時計回りであることに注意すること。
# 90度回転 (反時計回り)
img_rot = np.transpose(img, (1, 0, 2)) # 転置
img_rot = np.flip(img_rot, axis=0) # 上下反転
# 180度回転
img_rot = np.flip(img, axis=(0, 1)) # 上下反転 + 左右反転
# 270度回転 (反時計回り)
img_rot = np.transpose(img, (1, 0, 2)) # 転置
img_rot = np.flip(img_rot, axis=1) # 左右反転
ただし、上記のNumPyによる画像の回転は、画像のデータが、(height, width, channels) の順で格納されていることを前提としているので、画像のデータの格納順には注意が必要である。
図 3.3 90度の倍数で画像を回転させた結果#
回転方向に関する注意点
通常、数学的な議論では、座標系のx軸は右向きが正、y軸は上向きが正であり、回転方向は反時計回りを正とする。
一方、OpenCVでは、画像の座標系の原点が左上にあり、さらに、x軸が右向き、y軸が下向きに取られている。しかしながら、回転方向は反時計回りを正のままであることに注意してほしい。
3.2. 一般的な画像の回転#
画像を任意の角度や回転中心に対して回転させるには、このような行列がアフィン変換であることを利用して cv2.warpAffine を用いる方法がある。
特に、画像を回転させる操作は、画素の座標を画像の左上にある原点を中心に回転させるので、任意の回転中心を指定するためには、通常、回転中心として利用したい画素を原点に移動するような平行移動を行う必要がある。
例えば、画像を位置 \(\mathbf{c} = (c_x, c_y)\) を回転中心として、角度 \(\theta\) だけ回転させるには、次のようなアフィン変換行列 \(\mathbf{A}\) を用いる。
ただし、行列 \(\left( \mathbf{R} \mid \mathbf{t} \right)\) は、回転行列 \(\mathbf{R}\) と平行移動ベクトル \(\mathbf{t}\) を組み合わせたアフィン変換行列を表し、 \(\mathbf{I}\) は単位行列を表す。
このような行列を自身で計算して cv2.warpAffine に渡すこともできるが、OpenCVには、回転中心と回転角度を指定するだけで、上記のようなアフィン変換行列を計算してくれる関数 cv2.getRotationMatrix2D が用意されている。
# 回転行列の計算 (回転中心は画像の中心とする)
height, width = img.shape[:2]
cx, cy = width / 2, height / 2
theta = 30 # 回転角度
R = cv2.getRotationMatrix2D((cx, cy), theta, 1.0) # 回転行列の計算
# 画像の回転
img_rot = cv2.warpAffine(img, R, (width, height))
cv2.getRotationMatrix2D の第3引数は、回転後の画像の拡大・縮小率を指定するものなので、サイズを変更せずに回転だけしたい場合には 1.0 を指定しておけば良い。
図 3.4 cv2.warpAffine を用いて、画像を30度回転させた結果#
cv2.warpAffine の第3引数には、出力画像のサイズを指定できる。なお、ここに指定するサイズは (width, height) の順にしなければならないので注意してほしい。
この出力サイズは、画像を回転させた時に、画像の端が切れてしまうのを防ぐのに利用できる。少々、煩雑にはなるが、画像を中心周りに回転した時に端が切れるのを防ぐには、回転後の画像の中心を、新しい画像サイズの中心に合わせるような平行移動を加える必要がある。
img = skimage.data.cat()
height, width = img.shape[:2]
theta = 30
rad = np.radians(theta)
# 回転後の画像サイズを計算
new_width = int(height * np.abs(np.sin(rad)) + width * np.abs(np.cos(rad)))
new_height = int(height * np.abs(np.cos(rad)) + width * np.abs(np.sin(rad)))
# 原点中心の回転
M = cv2.getRotationMatrix2D((width / 2, height / 2), theta, 1.0)
# 回転後の画像中心を新しい画像の中心に平行移動
M[0, 2] += (new_width - width) // 2
M[1, 2] += (new_height - height) // 2
# 画像を幾何変換する
res = cv2.warpAffine(img, M, (new_width, new_height))
図 3.5 画像サイズを大きくして、回転後の画像全体を表示した結果#
3.3. 射影変換#
一例として、次のような数独の画像を正規化して、数独の問題だけが写った画像を得る方法を見てみよう。
図 3.6 斜め方向から撮影された数独の画像#
この画像において、数独の問題を含む正方形の4頂点は、次の座標を持っている。
左上: (x, y) = (670, 71)
右上: (x, y) = (1115, 302)
右下: (x, y) = (621, 707)
左下: (x, y) = (221, 239)
正規化された画像のサイズが 512x512 であるとすると、正規化された画像において、数独の問題を含む正方形の4頂点は、次の座標を持つことになる。
左上: (x, y) = (0, 0)
右上: (x, y) = (512, 0)
右下: (x, y) = (512, 512)
左下: (x, y) = (0, 512)
このような対応関係を利用して、元の画像を正規化された画像に射影変換することができる。OpenCVでは、 cv2.getPerspectiveTransform を用いて、上記の対応関係を満たす射影変換行列を計算することができる。
import cv2
import numpy as np
src_pts = np.array([[670, 71], [1115, 302], [621, 707], [221, 239]], dtype=np.float32)
dst_pts = np.array([[0, 0], [512, 0], [512, 512], [0, 512]], dtype=np.float32)
H = cv2.getPerspectiveTransform(src_pts, dst_pts)
こうして得られた射影変換行列 H を用いて、元の画像を正規化された画像に射影変換するには、 cv2.warpPerspective を用いる。この時、関数の第3引数には、出力画像のサイズ、すなわち 512x512 を指定する。
img_dst = cv2.warpPerspective(img, H, (512, 512))
すると、以下の 図 3.7 に示すように、指定した4点が正方形の各頂点に対応する形で画像を正規化することができる。
図 3.7 射影変換による画像の正面化#