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

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

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

前回は、自前の行列クラスを使用して射影変換を行いましたが、Javascriptの行列演算用のライブラリが有志によって公開されています。

この中で特にメジャーであり、enchantjsにも使用されている行列演算ライブラリのglMatrix.jsを使用して射影変換のパラメータを求めてみます。
glMatrix.js内の4x4の正方行列の作成(mat4.create)と逆行列(mat4.inverse)を使用します。

function getParam(src, dest) {

    function Z(val){
        return val == 0 ? 0.5 : val;
    }

    var X1 = Z(src[0][0]);
    var X2 = Z(src[1][0]);
    var X3 = Z(src[2][0]);
    var X4 = Z(src[3][0]);
    var Y1 = Z(src[0][1]);
    var Y2 = Z(src[1][1]);
    var Y3 = Z(src[2][1]);
    var Y4 = Z(src[3][1]);
    var x1 = Z(dest[0][0]);
    var x2 = Z(dest[1][0]);
    var x3 = Z(dest[2][0]);
    var x4 = Z(dest[3][0]);
    var y1 = Z(dest[0][1]);
    var y2 = Z(dest[1][1]);
    var y3 = Z(dest[2][1]);
    var y4 = Z(dest[3][1]);

    //X point
    var tx = mat4.create([
    X1, Y1, -X1 * x1, -Y1 * x1, // 1st column
    X2, Y2, -X2 * x2, -Y2 * x2, // 2nd column
    X3, Y3, -X3 * x3, -Y3 * x3, // 3rd column
    X4, Y4, -X4 * x4, -Y4 * x4  // 4th column
    ])

    mat4.inverse(tx);
    var kx1 = tx[0] * x1 + tx[1] * x2 + tx[2] * x3 + tx[3] * x4;
    var kc1 = tx[0] + tx[1] + tx[2] + tx[3];
    var kx2 = tx[4] * x1 + tx[5] * x2 + tx[6] * x3 + tx[7] * x4;
    var kc2 = tx[4] + tx[5] + tx[6] + tx[7];
    var kx3 = tx[8] * x1 + tx[9] * x2 + tx[10] * x3 + tx[11] * x4;
    var kc3 = tx[8] + tx[9] + tx[10] + tx[11];
    var kx4 = tx[12] * x1 + tx[13] * x2 + tx[14] * x3 + tx[15] * x4;
    var kc4 = tx[12] + tx[13] + tx[14] + tx[15];

    //Y point
    var ty = mat4.create([
    X1, Y1, -X1 * y1, -Y1 * y1, // 1st column
    X2, Y2, -X2 * y2, -Y2 * y2, // 2nd column
    X3, Y3, -X3 * y3, -Y3 * y3, // 3rd column
    X4, Y4, -X4 * y4, -Y4 * y4  // 4th column
    ]);

    mat4.inverse(ty);
    var ky1 = ty[0] * y1 + ty[1] * y2 + ty[2] * y3 + ty[3] * y4;
    var kf1 = ty[0] + ty[1] + ty[2] + ty[3];
    var ky2 = ty[4] * y1 + ty[5] * y2 + ty[6] * y3 + ty[7] * y4;
    var kf2 = ty[4] + ty[5] + ty[6] + ty[7];
    var ky3 = ty[8] * y1 + ty[9] * y2 + ty[10] * y3 + ty[11] * y4;
    var kf3 = ty[8] + ty[9] + ty[10] + ty[11];
    var ky4 = ty[12] * y1 + ty[13] * y2 + ty[14] * y3 + ty[15] * y4;
    var kf4 = ty[12] + ty[13] + ty[14] + ty[15];
    var det_1 = kc3 * (-kf4) - (-kf3) * kc4;

    if(det_1 == 0) {
        det_1 = 0.0001;
    }
    det_1 = 1 / det_1;
    var param = new Array(8);
    var C = (-kf4 * det_1) * (kx3 - ky3) + (kf3 * det_1) * (kx4 - ky4);
    var F = (-kc4 * det_1) * (kx3 - ky3) + (kc3 * det_1) * (kx4 - ky4);
    param[2] = C;
    param[5] = F;
    param[6] = kx3 - C * kc3;
    param[7] = kx4 - C * kc4;
    param[0] = kx1 - C * kc1;
    param[1] = kx2 - C * kc2;
    param[3] = ky1 - F * kf1;
    param[4] = ky2 - F * kf2;

    return param;
}

※getParam関数内のクロージャ関数である「Z」は何をしているかというと、逆行列を求める際に対角成分が0の場合、処理の途中でゼロ除算が発生して値が求まらないため、座標に0がある場合は0.5にすることで回避しています。
微妙に違いが出る分は画像補完でなんとかなるかなと思ってます。

射影変換パラメータによる描画処理

function draw(param) {
    var ctx = this.context;
    var imgW = this.image.width;
    var imgH = this.image.height;

    var input = ctx.getImageData(0, 0, imgW, imgH);
    var output = ctx.createImageData(imgW, imgH);

    for (var i = 0; i < imgH; ++i) {
        for (var j = 0; j < imgW; ++j) {
            var tmp = j * param[6] + i * param[7] + param[8];
            var tmpX = (j * param[0] + i * param[1] + param[2]) / tmp;
            var tmpY = (j * param[3] + i * param[4] + param[5]) / tmp;

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

            if (floorX >= 0 && floorX < imgW && floorY >= 0 && floorY < imgH) {
                var pixelData = this.getPixel(input, floorX, floorY);
                var R = pixelData.R;
                var G = pixelData.G;
                var B = pixelData.B;

                this.setPixel(output, j, i, R, G, B, 255);
            }
        }
    }

    ctx.putImageData(output, 512, 0);
}

射影変換パラメータを取得し、描画用に転送元座標と転送先座標の逆行列を求めて描画する。
逆行列を求めるのにパラメータが8個なので3x3の行列でもいいのですが、大は小を兼ねるので4x4の行列にして求めています。

    //射影変換パラメータを取得
    var param = getParam(origin, markers);

    //描画処理用に射影変換パラメータの逆行列を取得
    var mx = mat4.create([
    param[0], param[1], param[2], 0, // 1st column
    param[3], param[4], param[5], 0, // 2nd column
    param[6], param[7],        1, 0, // 3rd column
           0,        0,        0, 1  // 4th column
    ])
    mat4.inverse(mx);

    var inv_param = new Array(9);
    inv_param[0] = mx[0];
    inv_param[1] = mx[1];
    inv_param[2] = mx[2];
    inv_param[3] = mx[4];
    inv_param[4] = mx[5];
    inv_param[5] = mx[6];
    inv_param[6] = mx[8];
    inv_param[7] = mx[9];
    inv_param[8] = mx[10];

    //描画処理
    draw(inv_param);
};


次回は、画像の補完をして画像表示するように対応してみます。