長方形を任意の四角形に変換する行列

この前、長方形の画像を、任意の4頂点からなる凸四角形に変形して表示したくなった。

長方形を、2x2の行列による一次変換や、それに平行移動を加えた3x3のアフィン変換で、任意の凸四角形に移すことは不可能である。これらの一次変換では、長方形を平行四辺形に変換することはできるが、台形には変換できないからである。

しかし、変換行列を任意の3x3行列とし、変換後のベクトルの3番目の要素を拡大係数として斉次座標系を構成する、アフィン変換を拡張したものとも言える、射影変換(perspective transform)なら、それは可能である。

可能なはずなのだが、Webを検索しても、具体的な変換行列の数式が意外と見つからない。筆者の検索方法が悪いからだとは思うが、どうにも見つけられなかったので、自分で計算してみた。

0≦x≦W, 0≦y≦Hの領域にある長方形を0≦x≦1, 0≦y≦1の正方形に移す射影行列は自明(diag(1/W,1/H,1)である)なので、(0,0),(1,0),(0,1),(1,1)の4点を(Px,Py),(Qx,Qy),(Rx,Ry),(Sx,Sy)に移す射影変換を考える。

(≡は列ベクトルが斉次座標(homogeneous coordinate)として等価であることを表す)を満たすsx〜w2があるかどうかを考える。

上の合同式を展開する。

射影変換の斉次座標の定義より、(x y w)T≡(x/w y/w 1)Tなので、上の合同式は

という等式にできる。つまり、

という連立方程式であり、これを解くと、

と求まる。(力づくで解くなら、tx,tyの次にw0,w1をまとめて求めると良い。)(一見複雑だが、分母は全て同じ)
従って、w2=SxQy-SyQx+QxRy-QyRx+RxSy-RySxとすると、(0,0),(W,0),(0,H),(H,H)の4点を(Px,Py),(Qx,Qy),(Rx,Ry),(Sx,Sy)に移す射影変換は、

である。

■テストプログラム
Javaアプレットの起動用のページ
ソースコード
・使い方
 四角形の頂点の赤い丸をドラッグすると、イメージがそれに合わせて変形します。
 四角形に凹みができる(鈍角の内角ができる)と、正しく動作しません。


サンプルプログラムは、画像の射影変換をサポートするOpenVGやOpenCV等のライブラリを使って作ろうと思っていたし、実際にOpenVGにて

vgSeti(VG_MATRIX_MODE, VG_MATRIX_IMAGE_USER_TO_SURFACE);
vgLoadMatrix(matrix);
vgDrawImage(...);

とすることによって、大まかに動作確認もできていたのだが、JavaからOpenVGを使う手段が見つからなかったので、ここに公開するのは諦めた。
それを探している途中で、Java Advanced Imaging APIという、Java SEに組み込まれているライブラリに、PerspectiveTransformというクラスを見つけたので、今回はそれを使うことにした。

しかし、これの使い方がよくわからない。回転やアフィン変換なら、ParameterBlockクラスのインスタンスにパラメーターをaddしてJAI.create()すると、結果がPlanarImageのインスタンスとして得られるのだが、射影変換はParameterBlockにaddしてもJAI.create()で結果が取り出せないようである。巷のサンプルコードでも、射影変換の場合は、その逆変換をWarpPerspectiveにセットして、JAI.create()でwarping transformationの結果を取り出すようになっている。Warping transformationだと逆行列が必要になるので、射影変換をする為に逆行列が存在しない場合を考慮しないといけなくなるのが不便である。
なぜJAI.create()はPerspectiveTransformをサポートしないのだろう。
WarpPerspectiveを使わずにPerspectiveTransformの結果を得る手段は無いのだろうか?

参考リンク:
Programming in Java Advanced Imaging
§8.4にPerspectiveTransformの説明、§3.6にJAI.create()のoperatorの説明あり

さて、JAIのPerspectiveTransformのAPI referenceを読んでいると、getQuadToQuad()という、任意の四角形を任意の四角形に移す射影変換を作成するメソッドがあり、ガックリしてしまった。
上記のテストプログラムの

PerspectiveTransform transform = new PerspectiveTransform(
createRectangleToArbitraryQuadrangleTransform(
W, H,
points[0].x, points[0].y,
points[1].x, points[1].y,
points[2].x, points[2].y,
points[3].x, points[3].y)
);

この部分は、

PerspectiveTransform transform = PerspectiveTransform.getQuadToQuad(
0, 0, W, 0,
0, H, W, H,
points[0].x, points[0].y,
points[1].x, points[1].y,
points[2].x, points[2].y,
points[3].x, points[3].y);

これに替えても同じ動作をする。
JAIを使うのであれば、テストプログラムの射影変換作成関数は無用なのである。
しかも、OpenVGにもvguComputeWarpQuadToQuad()、OpenCVにもgetPerspectiveTransform()というそれらしい関数があるのを、今見つけた。
どうやら、本稿に記載した射影変換の式が役に立つことは無さそうである。