デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理を基礎からやり直す

射影変換(ホモグラフィ)について理解してみる その3

前回は、代数ライブラリを使って8個のパラメータを取得しました。今回は、8個のパラメータを使って画像を変形して表示させます。
CSSでは、matrix3dに3x3の正方行列として9(8+1)個のパラメータをセットして画像を変形して表示していましたが、これをmatrix3dのAPIが存在しないCanvasでどうやるのか?
参考になったのが、下記のサイトです。
射影変換

変換式
u = (x*a + y*b + c) / (x*g + y*h + 1)
v = (x*d + y*e + f) / (x*g + y*h + 1)
の通りに8個のパラメータが分かったので、転送元座標から転送先座標を求めればいいのです。

function draw(){
   var imgW = imgObj.width;
   var imgH = imgObj.height;
   var canvas = document.getElementById('ctx');
   var context = canvas.getContext("2d");
   var input = context.getImageData(0, 0, imgW, imgH);
   var output =context.createImageData(imgW, imgH);
   for(var i = 0; i < imgH; ++i){
       for(var j = 0; j < imgW; ++j){
           //u = (x*a + y*b + c) / (x*g + y*h + 1)
           //v = (x*d + y*e + f) / (x*g + y*h + 1)
           
           var tmp = j * ans.e(7) + i * ans.e(8) + 1.0;
           var tmpX = (j * ans.e(1) + i * ans.e(2) + ans.e(3)) / tmp;
           var tmpY = (j * ans.e(4) + i * ans.e(5) + ans.e(6)) / tmp;

           var floorX = tmpX | 0;
           var floorY = tmpY | 0;

           if (floorX >= 0 && floorX < imgW && floorY >= 0 && floorY < imgH) {
               // 左上 + 右上 + 左下 + 右下
               var pixelData = getPixel(input, j, i);   // ピクセル値を取得する
               var R = pixelData.R;
               var G = pixelData.G;
               var B = pixelData.B;
               setPixel(output, floorX, floorY, R, G, B, 255);
           }
       }
   }

   // CanvasのコンテキストにImageDataを描画
   context.putImageData(output, 512, 0);
}

function getPixel(imageData, x, y){
   var pixels = imageData.data;
   var index = (imageData.width * y * 4) + (x * 4);
   if(index < 0 || index + 3 > pixels.length) return undefined;
   return { R:pixels[index + 0], G:pixels[index + 1], B:pixels[index + 2], A:pixels[index + 3] };
}

function setPixel(imageData, x, y, r, g, b, a){
   var pixels = imageData.data;
   var index = (imageData.width * y * 4) + (x * 4);
   if(index < 0 || index + 3 > pixels.length) return false;
   pixels[index + 0] = r;
   pixels[index + 1] = g;
   pixels[index + 2] = b;
   pixels[index + 3] = a;
   return true;
}

ところが実際に表示された画像は下記のように穴が空いた画像になったのです。
※転送先画像は転送元画像の右下座標を(Width-20,Height-20)に位置をずらしただけです。
f:id:Yaju3D:20130811162931j:plain

そういえば参考サイトでは、バイリニア補間をしておりました。では、補完すれば穴が埋まるのかと思って調べてみると違いました。
参考:画像フィルタ処理 - 画像処理ソリューション
画像を拡大や回転した際に、転送元の点近くの複数箇所から輝度値を求めて美しくするためのもので穴を埋めるといった用途ではありません。
穴が空いた画像といえば、以前の記事「画像の回転処理 穴が空かない方法」でやりました。
転送元から転送先にすると誤差で穴が空いてしまうって。
f:id:Yaju3D:20130430230125j:plain
今回も考え方は同じで、転送先から転送元にすれば穴が開かなくなるはずです。
f:id:Yaju3D:20130430234347j:plain
そうすると、転送先から転送元の逆行列を求める必要があるんですね。
早速、「射影変換 逆行列」で検索してみました。
するとYahoo 知恵袋に「 射影変換式の逆式」があり、回答として「関数に与える引数を逆にすれば良い」とありました。
転送元と転送先で射影変換パラメータを求める際には、転送元と転送先を内部で入れ替えることで逆行列を求めたことになる。その上で描画処理も合わせて変更する。

var i, M = [], V = [];
var x, y, X, Y;
for(i=0;i<4;i++) {
        //転送元と転送先を入れ替える
	//x = origin[i][0];
	//y = origin[i][1];
	//X = markers[i].x() - imgx;
	//Y = markers[i].y() - imgy;
	x = markers[i].x() - imgx;
	y = markers[i].y() - imgy;
	X = origin[i][0];
	Y = origin[i][1];
	M.push([x, y, 1, 0, 0, 0, -x*X, -y*X]);
	M.push([0, 0, 0, x, y, 1, -x*Y, -y*Y]);
	V.push(X);
	V.push(Y);
}

//射影変換を求める
var ans = $M(M).inv().x($V(V));
console.log($M(M).inspect());
console.log($V(V).inspect());
console.log(ans.inspect());
//描画
draw();

function draw(){
   var imgW = imgObj.width;
   var imgH = imgObj.height;
   var canvas = document.getElementById('ctx');
   var context = canvas.getContext("2d");
   var input = context.getImageData(0, 0, imgW, imgH);
   var output =context.createImageData(imgW, imgH);
   for(var i = 0; i < imgH; ++i){
       for(var j = 0; j < imgW; ++j){
           //u = (x*a + y*b + c) / (x*g + y*h + 1)
           //v = (x*d + y*e + f) / (x*g + y*h + 1)
           
           var tmp = j * ans.e(7) + i * ans.e(8) + 1.0;
           var tmpX = (j * ans.e(1) + i * ans.e(2) + ans.e(3)) / tmp;
           var tmpY = (j * ans.e(4) + i * ans.e(5) + ans.e(6)) / tmp;

           var floorX = tmpX | 0;
           var floorY = tmpY | 0;

           if (floorX >= 0 && floorX < imgW && floorY >= 0 && floorY < imgH) {
               // 左上 + 右上 + 左下 + 右下
               //var pixelData = getPixel(input, j, i);   // ピクセル値を取得する
               var pixelData = getPixel(input, floorX, floorY);   // ピクセル値を取得する     
               var R = pixelData.R;
               var G = pixelData.G;
               var B = pixelData.B;
               //setPixel(output, floorX, floorY, R, G, B, 255);
               setPixel(output, j, i, R, G, B, 255);
           }
       }
   }

結果的に穴が空かない画像にはなりましたが、上部が少し欠けてるので補正は必要そうです
f:id:Yaju3D:20130812192333j:plain

下記サイトはC#ですが、転送元座標と転送先座標を内部的に入れ替えて処理しているようです。
Homography.pde - CS 583 - Introduction to Computer Vision

次回は、転送元座標と転送先座標を内部的に入れ替えないで、直接射影変換の逆行列を求める方法を説明していきます。