補正値を計算してアフィン変換する
はじめに
画像に対して、平行移動、拡大縮小、回転の各処理を行うための手法としてアフィン変換があります。
この手法に関する詳しい説明はWEB上に多く公開されており、例えば
のような記事が公開されています。
openCVでAPI提供されており、Pythonで容易にアフィン変換を実現する事も可能です。
しかしながら、公開されている記事の大半はアフィン変換の説明が丁寧に行われている一方で、補正値をどうやって計算するのかに触れられていないと感じています。
補正値の計算方法はタスクに依存すると思いますが、この記事では補正値の計算方法の一例について説明したいと思います。
問題設定
下記のような白黒の画像を入力として考えます。
オブジェクト(この例の場合は楕円)を画像の中心に移動させ、傾きを補正し下記のような状態に補正します。
さらに、オブジェクトの形状は矩形や楕円など、不定ととします。
どうやればよいか?
これを実現するには、
- 画像の中心とオブジェクトの重心のズレを計算して、オブジェクトを画像の中心に移動させる
- オブジェクトの傾きを計算して、傾きを補正する
の2ステップの処理が必要となってきます。
この二つのステップについて、作成したプログラムに沿って説明していきます。
プログラム自体は簡単なモノですが、githubで公開しています。
中心位置の補正
画像の中心座標とオブジェクトの中心座標を計算し、その差をアフィン変換(平行移動)します。
画像の中心座標は、画像の解像度/2で容易に計算する事ができます。
# ① 画像を読み込む
src_img = cv2.imread(src_path, cv2.IMREAD_GRAYSCALE)
# ② 画像の中心座標を求める
height,width = src_img.shape[:2]
gy = height / 2
gx = width / 2
print("画像の中心:y={0},x={1}\n".format(gy, gx))
877×564の画像に対する画像の中心は下記のように計算されています。
画像の中心:y=282.0,x=438.5
次にオブジェクトの中心座標を計算します。
今回のお題は、ピクセル値=255の部分をオブジェクトと定義できます。
そこで、ピクセル値=255となるピクセル座標の重心を計算する事でオブジェクトの中心座標を計算できます。
下記処理では輝度値が255となるピクセル座標をnumpy.whereで抽出し、numpy.meanで重心値を計算しています。(numpy.meanではなく、statisticsのmeanを使っても良かったかもしれませんが・・・)
# ③ オブジェクトの重心を求める
object_g = np.array(np.where(src_img == 255)).mean(axis=1)
print("オブジェクトの中心座標:y={0}, x={1}\n".format(object_g[0], object_g[1]))
計算結果は下記のような感じです。
オブジェクトの中心座標:y=143.85395537525355, x=659.172244759973
手書きで恐縮ですが、赤丸で示したあたりの座標が計算されています。
2つの中心座標の誤差を計算し、アフィン変換(平行移動を行います)
# ④ 重心のズレを補正する
dy = gy - object_g[0]
dx = gx - object_g[1]
print("中心座標とのズレ:y={0}, x={1}\n".format(dy, dx))
mat_g = np.array([[1, 0, dx], [0, 1, dy]], dtype=np.float32)
affine_img_g = cv2.warpAffine(src_img, mat_g, (width,height))
これで平行移動は完了です。
なお、WEBに公開されているアフィン変換の平行移動式は、
のような感じになっていますが、cv2.warpAffineは、
のような感じで、不要な計算を省略しているようです。(mat_gは、これにあわせて行列式を定義しています)
傾きの補正
次に傾きを補正するのですが、少し学習的なやり方を使ってみたいと思います。
下記のコードではピクセル値が255となるピクセル座標に対する共分散行列を計算して、固有値問題を解いています。いわゆる主成分分析(PCA)を行っています。
# ⑤ 確度のズレを計算する
index_vector = np.array(np.where(affine_img_g == 255))
Cov = np.cov(index_vector)
eigne_value, eigen_vector = LA.eig(Cov)
PCAを行うと下記図の黄色で示す座標空間を赤色の軸で示す座標空間に変換します。
これは、オブジェクトの重心を中心に回転処理を加えるのと同じ意味合いを持つことになります。
今回は、この方式で画像の傾きを補正しています。
次にこの補正値を利用して画像を回転させます。
気を付けなければいけないのは、アフィン変換(回転処理)は原点座標を中心とした回転を行っている事です。
このため、上記で計算した補正値で画像を回転するには、
- オブジェクトの重心が原点座標になるように画像を平行移動させる
- 画像を回転させる
- オブジェクトの重心が元の座標に戻るように画像を平行移動させる
の処理を行う必要があります。
数式書くのが面倒なので省略しますが
(変換後座標)=(3の変換行列)(2の変換行列)(1の変換行列)(元座標)
みたいな変換を行う必要があります。
cv2.GetRotationMatrix2Dを使うと上記1~3の流れを考慮した回転行列を生成してくれるのですが、cosとsinを度単位に変換するのがややこしそうだったので、平行移動と同様に自前で変換行列を宣言しました。
計算式は、下記を参考にしています。
これをふまえて、コードは下記のようになります。
# ⑤ 確度のズレを計算する
index_vector = np.array(np.where(affine_img_g == 255))
Cov = np.cov(index_vector)
eigne_value, eigen_vector = LA.eig(Cov)
mat_delta = np.array([[eigen_vector[0][0], eigen_vector[0][1], (1.0 - eigen_vector[0][0]) * gx - eigen_vector[0][1] * gy],
[eigen_vector[1][0], eigen_vector[1][1], (1.0 - eigen_vector[0][0])*gy + eigen_vector[0][1]*gx]], dtype=np.float32)
print("変換行列")
print(mat_delta)
affine_img_delta = cv2.warpAffine(affine_img_g, mat_delta, (width, height))
cv2.imwrite(dest_path, affine_img_delta)
回転補正に関する補足
原点を中心として、30°回転させたい場合、cv2.getRotationMatrix2Dを使って下記のような感じで変換行列を取得できます。
mat = cv2.getRotationMatrix2D((0, 0), 30, 1.0)
この変換行列をprint出力すると
[[ 0.8660254 0.5 0. ]
[-0.5 0.8660254 0. ]]
となっています。
世の中に公開されている画像回転に関する資料を見ると下記のような行列式で画像を回転させるような説明がなされています。
なぜ、
[[ 0.8660254 -0.5 0. ]
[0.5 0.8660254 0. ]]
とならないのでしょうか?
数学的には下記左側の図のように左下座標が原点となるのに対して、画像処理では右側の図のように左上が原点となります。
cv2.getRotationMatrix2Dはこの違いを考慮した回転行列を出力しているようです。
実行結果
この様な感じで、対象の形状は位置を考慮する事無く位置を補正できるのが今回の手法の良い所です。
このプログラム課題点
今回作成したプログラムの課題点を最後に列挙しておきます。
- 傾きの補正の所で気になった、不明点を明確化出来なかった。
- 下記のように補正されるケースもあって、補正後の向きを整える事が考慮されていない
- オブジェクト座標が一様である場合や、偏りのある場合に上手く動かない(かもしれない。試していません。)
おわりに
openCVやscikit-learnは非常に便利なツールです。
ただ、少し高度な事をしようとして自前実装と組み合わせると、意図と異なる挙動を取り出す事もあります。
このため、便利なツールが裏側でどのような事をやっているかというのをきちんと理解するのは大事だと思います。
便利なツールは有効活用しつつ、裏側はしっかりと理解する。
そして、自分のアイデアをプロダクト化していくという事に興味がある方は、弊社のメンバー募集を是非ご覧ください。
メンバー募集中です
アダコテックは上記のような画像処理技術を使って、大手メーカーの検査ラインを自動化するソフトウェアを開発している会社です。
機械学習や画像処理の内部ロジックに興味がある方、ご連絡下さい!
我々と一緒にモノづくりに革新を起こしましょう!
Discussion