アウトプットブログ

主にプログラミングで学んだことをアウトプットします。

ホモグラフィ変換の実装

Javaを使用して画像のホモグラフィ変換を実装しました。
任意の四角形を矩形に補正する機能です(所謂、台形補正のようなもの)。


ちょうどよさそうな画像がiPhoneの写真に保存されていました。
この下から撮影したズムスタのバックスクリーンを、正面から撮影した写真のように補正することをゴールとします。
f:id:fuhito615:20211203155754p:plain

モグラフィ変換

モグラフィ変換の式は、変換前の座標を (x,y)、変換後の座標を (x',y')とすると、以下。

 
s\begin{pmatrix}
x'\\y'\\1
\end{pmatrix}
=
\begin{pmatrix}
a&b&c\\
d&e&f\\
g&h&1
\end{pmatrix}
\begin{pmatrix}
x\\y\\1
\end{pmatrix}
式変形。

x'=\frac{ax+by+c}{gx+hy+1}\\
y'=\frac{dx+ey+f}{gx+hy+1}


パラメータ a hを求めるには、変換前と変換後の座標のペアが4つあればよく、それらを
 (x_0, y_0)(x_1, y_1)(x_2, y_2)(x_3, y_3)  (x_0', y_0')(x_1', y_1')(x_2', y_2')(x_3', y_3')
とすると最終的には以下の連立方程式を解けばよいとのこと。(導出の過程は割愛)

 
\begin{pmatrix}
a\\b\\c\\d\\e\\f\\g\\h\\
\end{pmatrix}
=
\begin{pmatrix}
x_0&y_0&1&0&0&0&-x_0x_0'&-y_0x_0'\\
0&0&0&x_0&y_0&1&-x_0y_0'&-y_0y_0'\\
x_1&y_1&1&0&0&0&-x_1x_1'&-y_1x_1'\\
0&0&0&x_1&y_1&1&-x_1y_1'&-y_1y_1'\\
x_2&y_2&1&0&0&0&-x_2x_2'&-y_2x_2'\\
0&0&0&x_2&y_2&1&-x_2y_2'&-y_2y_2'\\
x_3&y_3&1&0&0&0&-x_3x_3'&-y_3x_3'\\
0&0&0&x_3&y_3&1&-x_3y_3'&-y_3y_3'\\
\end{pmatrix}^{-1}
\begin{pmatrix}
x_0'\\y_0'\\x_1'\\y_1'\\x_2'\\y_2'\\x_3'\\y_3'\\
\end{pmatrix}

ソースコード

モグラフィ変換を行うクラス。
行列の計算にはND4Jライブラリを使用。

package homography;

import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.inverse.InvertMatrix;

public class Homography {
	/** 変換行列 */
	private double[] param;

	/**
	 * コンストラクタ
	 * @param src 変換前の4点
	 * @param dst 変換後の4点
	 */
	public Homography(double[][] src, double[][] dst) {
		setParam(src, dst);
	}

	/**
	 * ホモグラフィ変換行列を設定する。
	 * @param src 変換前の4座標
	 * @param dst 変換後の4座標
	 */
	public void setParam(double[][] src, double[][] dst) {
		if (src.length != 4) throw new IllegalArgumentException();
		if (dst.length != 4) throw new IllegalArgumentException();
		for (int i = 0; i < src.length; i++) if (src[i].length != 2) throw new IllegalArgumentException();
		for (int i = 0; i < dst.length; i++) if (dst[i].length != 2) throw new IllegalArgumentException();

		INDArray mat = Nd4j.create(new double[][] {
			{ src[0][0], src[0][1], 1,         0,         0, 0, -1*src[0][0]*dst[0][0], -1*src[0][1]*dst[0][0] },
			{         0,         0, 0, src[0][0], src[0][1], 1, -1*src[0][0]*dst[0][1], -1*src[0][1]*dst[0][1] },
			{ src[1][0], src[1][1], 1,         0,         0, 0, -1*src[1][0]*dst[1][0], -1*src[1][1]*dst[1][0] },
			{         0,         0, 0, src[1][0], src[1][1], 1, -1*src[1][0]*dst[1][1], -1*src[1][1]*dst[1][1] },
			{ src[2][0], src[2][1], 1,         0,         0, 0, -1*src[2][0]*dst[2][0], -1*src[2][1]*dst[2][0] },
			{         0,         0, 0, src[2][0], src[2][1], 1, -1*src[2][0]*dst[2][1], -1*src[2][1]*dst[2][1] },
			{ src[3][0], src[3][1], 1,         0,         0, 0, -1*src[3][0]*dst[3][0], -1*src[3][1]*dst[3][0] },
			{         0,         0, 0, src[3][0], src[3][1], 1, -1*src[3][0]*dst[3][1], -1*src[3][1]*dst[3][1] }
		});

		INDArray dstArray = Nd4j.create(new double[][] {
			{ dst[0][0], dst[0][1], dst[1][0], dst[1][1], dst[2][0], dst[2][1], dst[3][0], dst[3][1] }
		}).transpose();

		// ( a, b, c, d, e, f, g, h )
		INDArray result = InvertMatrix.invert(mat, true).mmul(dstArray);

		this.param = result.toDoubleVector();
	}

	/**
	 * 座標を変換する。
	 * @param srcCoord 変換元座標
	 * @return 変換先座標
	 */
	public double[] convert(double[] srcCoord) {
		if (srcCoord.length != 2) throw new IllegalArgumentException();

		double x = (this.param[0]*srcCoord[0] + this.param[1]*srcCoord[1] + this.param[2])
					/(this.param[6]*srcCoord[0] + this.param[7]*srcCoord[1] + 1);
		double y = (this.param[3]*srcCoord[0] + this.param[4]*srcCoord[1] + this.param[5])
					/(this.param[6]*srcCoord[0] + this.param[7]*srcCoord[1] + 1);

		return new double[] {x, y};
	}
}


画像を処理するクラス。
元の座標をホモグラフィ変換して先の座標に色を設定していくと、ピクセルが飛んだり重なったりしてドット落ちが発生する。
そのため、先の座標をホモグラフィ変換して元の座標から色を取得し、設定していく。

package homography;

import java.awt.image.BufferedImage;

public class HomographyImage {

	/**
	 * 指定の4点の範囲を矩形変換する(ホモグラフィ変換)。
	 * @param img 画像
	 * @param in 切り抜く4点(左上の点から時計周り)
	 * @param width 変換後の幅
	 * @param height 変換後の高さ
	 * @return 変換した画像
	 */
	public static BufferedImage convert(BufferedImage img, int[][] in, int width, int height) {
		if (in.length != 4) throw new IllegalArgumentException();
		for (int i = 0; i < in.length; i++) if (in[i].length != 2) throw new IllegalArgumentException();

		double[][] src = new double[4][2];
		double[][] dst = new double[4][2];

		src[0][0] = 0;
		src[0][1] = 0;
		src[1][0] = width;
		src[1][1] = 0;
		src[2][0] = width;
		src[2][1] = height;
		src[3][0] = 0;
		src[3][1] = height;

		for (int i = 0; i < in.length; i++) {
			for (int j = 0; j < in[i].length; j++) {
				dst[i][j] = in[i][j];
			}
		}

		Homography h = new Homography(src, dst);

		BufferedImage outImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				double[] coord = h.convert(new double[] {x, y});

				int rgb = img.getRGB((int)Math.round(coord[0]), (int)Math.round(coord[1]));

				outImg.setRGB(x, y, rgb);
			}
		}

		return outImg;
	}
}

実行結果

f:id:fuhito615:20211203155749p:plain
赤枠部分を切り取り、矩形に変換します。

BufferedImage inImg = ImageIO.read(new File("img\\in.png"));
int[][] in = {
	{255, 309},
	{876, 221},
	{991, 420},
	{110, 496}
};

BufferedImage outImg = HomographyImage.convert(inImg, in, 600, 300);
ImageIO.write(outImg, "png", new File("img\\out.png"));


出力された画像ファイルがこちら。
f:id:fuhito615:20211203155757p:plain


期待通りの変換ができていました。
ただ若干ギザギザしていて、画質が粗い・・・


変換した座標を四捨五入して元の画像から色を拾っているのですが、これは「最近傍補間」と言われる補間方法のよう。
「バイリニア補間」と呼ばれる方法を使用すると滑らかになるとのことなので、そのうち修正しようと思います。

参考にしたサイト

下記サイトを参考にさせていただきました。
imagingsolution.net
www.kennzo.net