Pythonで任意の四角形を正方形に変換する方法(射影変換・ホモグラフィ)
以前、台形補正についての記事を書きました。
plant-raspberrypi3.hatenablog.com
今回は、台形だけではなく、任意の四角形を正方形(または別の任意の四角形)に変換しよう、という試み。
射影変換の原理を勉強して、Pythonで実装してみました。
PythonライブラリのSympyとScikit-imageを使う方法です。
1. 射影変換の原理
射影変換(ホモグラフィ)の原理にはついて、たくさんの解説記事が見つかります。 この記事では、私が自分なりに理解したことについて書いていきます。 ベクトルをベクトルにうつす線形写像を求める問題を考えます。
これは とおくと が成り立つを求める問題と置き換えることができます。 次に、四隅の座標を基準にして変形する場合を考えます。変換前の座標を 変換後の座標を とします。ただし、 です。 具体的には、以下の図のような変換を考えます。
ここで、四隅の座標が与えられている場合、各について以下の式が成り立ちます。 これは、を計算することで得られます。 また、同様に も得られます。これを上の2つの式に代入して左辺を右辺に移動すると となるので、これをについて解くことで〜が得られ、が求まります。 求めたを使い、 の関係から画像の変換を行います。クリックすると展開されます
・ステップ1
・ステップ2
2. Sympyで連立方程式を解きHを求める
前述の通り、を求めるためには8元連立方程式を解く必要があります。 しかし、手計算で解くのはとても面倒なので、Sympyで解いていきます。 Sympyの使い方についてはこの辺りを参照。 3.2. Sympy : Python での代数計算 — Scipy lecture notes 環境は、MacOS Sierra, Python3.6です。
まずは必要なパッケージをインポートします。 〜を変数として設定します。 次に、のセットをnumpyアレイ を解くための連立方程式を以下のように記述します。 各ごとに方程式を求め、リスト 全ての方程式の入ったリストexpを引数としてSympyのsolve関数に渡し、連立方程式を解き、解をHに代入します。 仮に以下のような図の変換の場合
方程式は となり、Sympyによってを求めると となることがわかりました。 このが正しいかは、の値を用いてを計算して確かめることができます。クリックすると展開されます
from sympy import solve,symbols
import numpy as np
str_list = [f"h{i}" for i in [11,12,13,21,22,23,31,32]]
str_symbol = " ".join(str_list)
h11, h12, h13, h21, h22, h23, h31, h32 = symbols(str_symbol)
xy
に、のセットをnumpyアレイuv
に代入します。exp
に加えます。exp = []
for i in range(4):
exp_x = h11*xy[i,0]+h12*xy[i,1]+h13-(h31*xy[i,0]+h32*xy[i,1]+1)*uv[i,0]
exp_y = h21*xy[i,0]+h22*xy[i,1]+h23-(h31*xy[i,0]+h32*xy[i,1]+1)*uv[i,1]
exp.append(exp_x)
exp.append(exp_y)
result = solve(exp,dict=True)
r = result[0]
H = np.array([[r[h11],r[h12],r[h13]],
[r[h21],r[h22],r[h23]],
[r[h31],r[h32],1]],dtype=np.float64)
1: 10.0*h11 + 20.0*h12 + h13
2: 10.0*h21 + 20.0*h22 + h23
3: 20.0*h11 + 70.0*h12 + h13
4: 20.0*h21 + 70.0*h22 + h23 - 2000.0*h31 - 7000.0*h32 - 100.0
5: 90.0*h11 + 90.0*h12 + h13 - 9000.0*h31 - 9000.0*h32 - 100.0
6: 90.0*h21 + 90.0*h22 + h23 - 9000.0*h31 - 9000.0*h32 - 100.0
7: 90.0*h11 + 10.0*h12 + h13 - 9000.0*h31 - 1000.0*h32 - 100.0
8: 90.0*h21 + 10.0*h22 + h23
array([[ 2.06775593e+00, -4.13551186e-01, -1.24065356e+01],
[ 2.13237330e-01, 1.70589864e+00, -3.62503462e+01],
[ 8.18794424e-03, -4.13551186e-03, 1.00000000e+00]])
for i in range(4):
xy1 = np.append(xy[i],1)
uv1 = np.dot(H,xy1)
uv1 = np.round(uv1/uv1[-1]).astype(int)
uv1 = uv1[:-1]
print(f"{xy[i]} -> {uv1}")
#出力
[20 10] -> [0 0]
[70 20] -> [100 0]
[90 90] -> [100 100]
[10 90] -> [ 0 100]
3. 求めたHとScikit-imageを使って図形を変換する
を図形に適用して、実際に変換していきます。 変換がわかりやすいように多角形を描いてみました。
これを変換していきます。 前の項目で述べた、を求める過程を関数にまとめます。 この関数を使って解きます。四隅の変換前と変換後の座標を設定します。 早速、を解いていきます。 が、そのままでやると何故かわからないですがうまくいかないことがわかったので、xとyを入れ替えたりゴソゴソやっています。numpyのxyと画像にした時のxyの方向が異なるせいかなぁ、と思っていますが、もし詳しい方がいらっしゃったらコメントいただけるとありがたいです。。。 最後に求めたを、skimageのtransformモジュールの機能を使って、画像に適用します。 変換によって伸展した場所がモヤっとしてしまうので、最後に四捨五入によってバイナリ画像に修正してから表示します。 変換前
変換後
うまくいきました! 原理的には正方形だけでなく、別の任意の四角形にも変換できるので、色々と面白いことができそうです。 以上です。クリックすると展開されます
polygon = np.array([[20,10],[50,40],[70,20],[90,90],[50,60],[10,90]])
img = np.zeros((100,100),dtype=np.float64)
rr,cc = draw.polygon(polygon.T[0],polygon.T[1])
img[rr,cc] = 1
plt.imshow(img)
plt.show()
from sympy import solve,symbols
import numpy as np
from skimage import draw,transform
import matplotlib.pyplot as plt
from pprint import pprint
def solve_H(xy,uv):
print("XY to UV")
for i in range(4):
print(f"{xy[i]} -> {uv[i]}")
str_list = [f"h{i}" for i in [11,12,13,21,22,23,31,32]]
str_symbol = " ".join(str_list)
h11, h12, h13, h21, h22, h23, h31, h32 = symbols(str_symbol)
exp = []
for i in range(4):
exp_x = h11*xy[i,0]+h12*xy[i,1]+h13-(h31*xy[i,0]+h32*xy[i,1]+1)*uv[i,0]
exp_y = h21*xy[i,0]+h22*xy[i,1]+h23-(h31*xy[i,0]+h32*xy[i,1]+1)*uv[i,1]
exp.append(exp_x)
exp.append(exp_y)
print("")
print("Equations")
for i,e in enumerate(exp):
print(f"{i+1}: {e}")
result = solve(exp,dict=True)
r = result[0]
H = np.array([[r[h11],r[h12],r[h13]],
[r[h21],r[h22],r[h23]],
[r[h31],r[h32],1]],dtype=np.float64)
print("")
print("Result_array")
pprint(H)
return H
xy = np.array([[20,10],[70,20],[90,90],[10,90]])
uv = np.array([[0,0],[100,0],[100,100],[0,100]])
xy_t = xy[:,::-1]
uv_t = uv[:,::-1]
H = solve_H(xy_t,uv_t)
#出力
XY to UV
[10 20] -> [0 0]
[20 70] -> [ 0 100]
[90 90] -> [100 100]
[90 10] -> [100 0]
Equations
1: 10.0*h11 + 20.0*h12 + h13
2: 10.0*h21 + 20.0*h22 + h23
3: 20.0*h11 + 70.0*h12 + h13
4: 20.0*h21 + 70.0*h22 + h23 - 2000.0*h31 - 7000.0*h32 - 100.0
5: 90.0*h11 + 90.0*h12 + h13 - 9000.0*h31 - 9000.0*h32 - 100.0
6: 90.0*h21 + 90.0*h22 + h23 - 9000.0*h31 - 9000.0*h32 - 100.0
7: 90.0*h11 + 10.0*h12 + h13 - 9000.0*h31 - 1000.0*h32 - 100.0
8: 90.0*h21 + 10.0*h22 + h23
Result_array
array([[ 2.06775593e+00, -4.13551186e-01, -1.24065356e+01],
[ 2.13237330e-01, 1.70589864e+00, -3.62503462e+01],
[ 8.18794424e-03, -4.13551186e-03, 1.00000000e+00]])
使う機能はProjectiveTransform
とwarp
です。ProjectiveTransform
はinverse
にすると想定通りの変換となります。tform = transform.ProjectiveTransform(matrix=H).inverse
warped = transform.warp(img,tform,order=3,mode="edge")
plt.imshow(img)
plt.show()
plt.imshow(np.round(warped).astype(int))
plt.show()