【Python】OpenCVで画像をアフィン変換【移動・拡大・回転・剪断】

アフィン変換とは
アフィン変換とは、画像の移動や回転、拡大縮小、剪断といった処理を行うものです。
この記事では数学の専門的な用語や、数式についてはさらっと紹介する程度にとどめておき、PythonのOpenCVを使ってアフィン変換で画像処理する実例を中心に紹介していきます。
アフィン変換を使うと、この動画のように簡単に図形を回転させたりできます。
はじめに
フリー画像サイト 「pixabay」 さんからお借りした画像をOpenCVで加工していきます。巷のアフィン変換の記事を拝見すると、皆さんなぜかゴリラ系の画像を使ってらっしゃいます。私も慣習に習って、こちらのオラウータンを選んでみました。

ここではPython3.xとOpenCVを使用しました。
$ pip list | grep opencv
opencv-contrib-python 4.6.0.66
opencv-python 4.5.5.62
OpenCVがまだインストールされてない場合は、pipコマンドでインストールしましょう。numpyも使いますので合わせてインストールします。
$ pip install opencv-python
$ pip install opencv-contrib-python
$ pip install numpy
平行移動のアフィン変換
平行移動のアフィン変換を行列式で表すと次の通りです。
\begin{align}
\begin{pmatrix}
x'\\
y'\\
1 \\
\end{pmatrix}
=
\begin{pmatrix}
1 & 0 & T{x} \\
0 & 1 & T{y} \\
0 & 0 & 1 \\
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
1 \\
\end{pmatrix}
\end{align}
PythonのOpenCVで warpAffine
関数を使って平行移動を行います。次はアフィン変換で画像の平行移動の例です:
import cv2
import os
import numpy as np
= "../orang-utan.jpg"
input_path = cv2.imread(input_path)
img
= '../' + os.path.basename(input_path).split('.')[0] + '-' + os.path.basename(__file__).split('.')[0] + '.jpg'
output_path
= img.shape[:2]
h, w =200
tx=300
ty
= np.float32([
M 1,0,tx],
[0,1,ty],
[
])= cv2.warpAffine(img, M, (w, h))
img
'img', img)
cv2.imshow(
cv2.imwrite(output_path, img)
0)
cv2.waitKey( cv2.destroyAllWindows()
出力結果がこちらです。
プログラミングでは行列の3行目は省略します。
線形代数の話
ここでは行列への理解を深めるために、線形代数の初歩的なことについて触れておきます。
xとyを含む次の連立1次方程式の解を考えてみます。
\begin{align}
\left\{
\begin{array}{l}
x+2y=2\\
-x+y=1
\end{array}
\right.
\end{align}
この連立1次方程式を実際に解いてみましょう。
上+下で次が得られます:
\begin{align}
3y=3
\end{align}
この式の両辺を3で割れば(y=1)が得られます。 後でやる行列の計算の都合上、この両辺を二倍して連立1次方程式の上段から引き算するとxの解が得られます。
\begin{align}
\begin{array}{rr}
& x+2y=2\\
+) & 2y=2\\
\hline
&x=0
\end{array}
\end{align}
(x=0,y=1)の解が得られました。
行列で解く
今度は先ほどの連立1次方程式を行列で解いてみます。 最初の連立1次方程式を行列で表すと次の通りです。x、yの変数を取り除いて係数を並べただけです。
\begin{align}
\begin{bmatrix}
1 & 2 & 2 \\
-1 & 1 & 1
\end{bmatrix}
\end{align}
これを手動で計算したように、行列ないで計算していきます。
まず、1行目を2行目に足し算します(行和)。
\begin{align}
\begin{bmatrix}
1 & 2 & 2 \\
0 & 3 & 3
\end{bmatrix}
\end{align}
2行目を1/3倍します(行倍)。
\begin{align}
\begin{bmatrix}
1 & 2 & 2 \\
0 & 1 & 1
\end{bmatrix}
\end{align}
2行目を2倍にした値を1行目から引きます(行和)。
\begin{align}
\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 1
\end{bmatrix}
\end{align}
1列目がx、2列目がy、3列目が解となり、手計算と同じく(x=0,y=1)の解が得られました。
行列式の理解の手がかりになれば幸いです。
拡大縮小のアフィン変換
X軸、Y軸方向の拡大率をそれぞれSx、Syとすると、拡大縮小のアフィン変換を行列式で表すと次の通りです。
\begin{align}
\begin{pmatrix}
x'\\
y'\\
1 \\
\end{pmatrix}
=
\begin{pmatrix}
S{x} & 0 & 0 \\
0 & S{y} & 0 \\
0 & 0 & 1 \\
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
1 \\
\end{pmatrix}
\end{align}
次はアフィン変換で画像の拡大縮小の例です:
import cv2
import os
import numpy as np
= "../orang-utan.jpg"
input_path = cv2.imread(input_path)
img
= '../' + os.path.basename(input_path).split('.')[0] + '-' + os.path.basename(__file__).split('.')[0] + '.jpg'
output_path
= img.shape[:2]
h, w =2
sx=0.5
sy
= np.float32([
M 0,0],
[sx,0,1,sy]
[
])= cv2.warpAffine(img, M, (w, h))
img
'img', img)
cv2.imshow(
cv2.imwrite(output_path, img)
0)
cv2.waitKey( cv2.destroyAllWindows()
出力結果がこちらです。

回転のアフィン変換
原点を中心に反時計回りにθ°回転させるアフィン変換を、行列式で表すと次の通りです。
\begin{align}
\begin{pmatrix}
x'\\
y'\\
1 \\
\end{pmatrix}
=
\begin{pmatrix}
cos\theta & -sin\theta & 0 \\
sin\theta & cos\theta & 0 \\
0 & 0 & 1 \\
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
1 \\
\end{pmatrix}
\end{align}
三角関数を計算してもよいのですが、GetRotationMatrix2D(center, angle, scale)
を使うと便利です。
パラメータ | 意味 |
---|---|
center | 回転の中心位置 |
angle | 度単位で表される回転角度 |
scale | 等方性スケーリング係数 |
次はアフィン変換で画像の拡大縮小の例です:
import cv2
import os
import numpy as np
= "../orang-utan.jpg"
input_path = cv2.imread(input_path)
img
= '../' + os.path.basename(input_path).split('.')[0] + '-' + os.path.basename(__file__).split('.')[0] + '.jpg'
output_path
= img.shape[:2]
h, w
= cv2.getRotationMatrix2D((0,1000), 60, 1.0)
M
= cv2.warpAffine(img, M, (w, h))
img
'img', img)
cv2.imshow(
cv2.imwrite(output_path, img)
0)
cv2.waitKey( cv2.destroyAllWindows()
出力結果がこちらです。
剪断(スキュー)のアフィン変換
剪断またはスキューとは、画像を平行四辺形に変形する処理のことです。 剪断のアフィン変換を行列式で表すと次の通りです。
\begin{align}
\begin{pmatrix}
x'\\
y'\\
1 \\
\end{pmatrix}
=
\begin{pmatrix}
1 & 0 & 0 \\
tan\theta & 1 & 0 \\
0 & 0 & 1 \\
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
1 \\
\end{pmatrix}
\end{align}
または
\begin{align}
\begin{pmatrix}
x'\\
y'\\
1 \\
\end{pmatrix}
=
\begin{pmatrix}
1 & tan\theta & 0 \\
0 & 1 & 0 \\
0 & 0 & 1 \\
\end{pmatrix}
\begin{pmatrix}
x \\
y \\
1 \\
\end{pmatrix}
\end{align}
ここではOpenCVの関数
getAffineTransform(src, dts)
を使って剪断を行ってみます。src,
dtsには3点分のxy座標を3x2行列のnumpy配列で与えます。
次はアフィン変換で画像の剪断を行う例です:
import cv2
import os
import numpy as np
= "../orang-utan.jpg"
input_path = cv2.imread(input_path)
img
= '../' + os.path.basename(input_path).split('.')[0] + '-' + os.path.basename(__file__).split('.')[0] + '.jpg'
output_path
= img.shape[:2]
h, w
= 500
shear = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
src = src.copy()
dts 0] += (shear / h * (h - src[:,1])).astype(np.float32)
dts[:,= cv2.getAffineTransform(src,dts)
M
= cv2.warpAffine(img, M, (w, h))
img
'img', img)
cv2.imshow(
cv2.imwrite(output_path, img)
0)
cv2.waitKey( cv2.destroyAllWindows()
出力結果がこちらです。
4点以上のアフィン変換、estimateAffine2D(estimateRigidTransform)
getAffineTransform
のように、アフィン行列は対応する3点があれば求められます。しかし、4点以上の対応点群がある場合には、getAffineTransform
では求められません。
行列解は一意に決まらず推定が入ります。OpenCVでやるにはestimateRigidTransform
が利用できます。ただし、この関数はOpenCV4以降で非推奨となってます。代わりにOpenCV4ではestimateAffine2D
とestimateAffinePartial2D
が用意されてます。
4点以上のアフィン変換の実用例としては、映像処理における手ぶれ補正が挙げられます。
で紹介したSIMPLE VIDEO STABILIZATION USING OPENCV
のC++ソースコードを読むと、映像から特徴点を抽出し、オプティカルフローで前フレームの特徴点と紐付けてestimateAffinePartial2D
でアフィン行列を求めることで手ぶれ補正を実現してるのが分かります。実際はこの後にカルマンフィルタやローパスフィルタなどで平滑化する処理が入ります。
SIMPLE VIDEO STABILIZATION USING OPENCV
estimateAffine2D(from, to)
の使い方もgetAffineTransform(src, dts)
とほぼ同じです。先ほどの剪断された画像を元に戻してみましょう。3点座標の行列ですが、estimateAffine2D
の使い方に慣れるためにちょうど良いでしょう。
先ほどの剪断された画像を estimateAffine2D
で元の位置に戻した例です:
import cv2
import os
import numpy as np
= "../orang-utan-affine-shear.jpg"
input_path = cv2.imread(input_path)
img
= '../' + os.path.basename(input_path).split('.')[0] + '-' + os.path.basename(__file__).split('.')[0] + '.jpg'
output_path
= img.shape[:2]
h, w
= 500
shear = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
src = src.copy()
dts 0] += (shear / h * (h - src[:,1])).astype(np.float32)
dts[:,
= cv2.estimateAffine2D(dts, src)
M
= cv2.warpAffine(img, M[0], (w, h))
img
'img', img)
cv2.imshow(
cv2.imwrite(output_path, img)
0)
cv2.waitKey( cv2.destroyAllWindows()
出力結果がこちらになります。
画面からはみ出た部分はトリミングされてしまいましたが、元の写真と同じ形へアフィン変換で戻すことができました。
estimateAffine2D
へ入力しているdts
とsrc
は本来逆ですが、元に戻すという意味で使ってますのでご理解ください。
関連記事
- Diagramsを使ってサクッとインフラ図を描く【Python】
- 【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)
- fdupes で内容が重複しているファイルを見つける|シェル
- ImageMagickで画像加工|シェル
- 【Python】OpenCVでコーナーの検出【Harris/Shi-Tomasi】