読者です 読者をやめる 読者になる 読者になる

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

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

行列による2Dアフィン変換を理解してみる

2D プログラム

前回、回転と拡大縮小と平行移動を3×3行列に合わせた式が以下になります。

 回転行列          拡大縮小行列    平行移動行列
 | cos -sin 0 |   | sx  0  0 |   | 1  0 t.x |
 | sin  cos 0 |   | 0  sy  0 |   | 0  1 t.y |
 |  0    0  1 |   | 0   0  1 |   | 0  0  1  |

そもそも、何故行列にしたのか?
本やWebサイト等を読んで、アフィン変換には行列を使用するといった理解のみで前回まで進んできた。
行列を使わずにプログラムを組んでも回転と拡大縮小と平行移動は出来るのだから、わざわざ行列を使用する本質を実際に組むまで理解できていなかった。

実際に組んでみて分かったのは、「平行移動してから拡大縮小して回転させる」といった複数の操作が行列と行列の掛け算によって、一つにまとめて出来てしまうといった利点があることが分かった。

「平行移動→図形オブジェクト→拡大縮小→図形オブジェクト→回転→図形オブジェクト」
のように、1つ1つの操作を図形オブジェクトで処理してから次の操作をすると処理コストがかかる。
「平行移動→拡大縮小→回転→図形オブジェクト」
のように図形オブジェクトに反映される最終結果が先ほどと同じであるなら、まとめて操作した方が処理コストは大幅に減らせることが出来るのである。

このように複数の変換行列を一つの組み合わせ行列にまとめることを「行列の合成」という。
回転と拡大縮小と平行移動を3×3行列に合わせたのも合成を行いやすくするためでもある。

さて、合成するために行列数を合わせたならば、3×3行列ではなく以下のように2×3行列にすれば行列の掛け算の計算量が少なくて済みそうな気がするんですが・・・

 回転行列          拡大縮小行列    平行移動行列
 | cos -sin 0 |   | sx  0  0 |   | 1  0 t.x |
 | sin  cos 0 |   | 0  sy  0 |   | 0  1 t.y |

実は、行列の掛け算では掛けられる行列の列数と掛ける行列の行数が同じである必要があるため、2×3行列同士の掛け算は出来ません。
よって、ダミーの行を追加して3×3行列の正方行列(行の数と列の数が同じ行列)にする必要があるわけです。


今回2Dアフィン変換のクラスを作成するにあたり、傾斜行列(平行四辺形)を追加しています。

 回転行列          傾斜行列           拡大縮小行列    平行移動行列
 | cos -sin 0 |   |  1   tanY 0 |   | sx  0  0 |   | 1  0 t.x |
 | sin  cos 0 |   | tanX   1  0 |   | 0  sy  0 |   | 0  1 t.y |
 |  0    0  1 |   |  0     0  1 |   | 0   0  1 |   | 0  0  1  |

アフィン変換では平行四辺形にまでは変形できるものの、台形には変形できません、台形に変形できる処理は射影変換(ホモグラフィ)と呼びます。
http://imagingsolution.net/imaging/affine-transformation/


行列による2Dアフィン変換のソースリスト

function Matrix(){
    this.initialize.apply(this,arguments);
};

Matrix.prototype = {
    // | m00 m01 m02 |
    // | m10 m11 m12 |
    // | m20 m21 m22 |

    initialize : function(arg){
        var count = arguments.length;
        if (count == 0) {
            //単位行列を作成
            this.m00 = this.m11 = this.m22 = 1;
            this.m01 = this.m02 = this.m10 = this.m12 = this.m20 = this.m21 = 0;
        }
    },

    set : function(mx) {
        this.m00 = mx.m00; this.m01 = mx.m01; this.m02 = mx.m02;
        this.m10 = mx.m10; this.m11 = mx.m11; this.m12 = mx.m12;
        this.m20 = mx.m20; this.m21 = mx.m21; this.m22 = mx.m22;
        return this;
    },

    //行列同士の掛け算
    // | a00 a01 a02 || b00 b01 b02 |
    // | a10 a11 a12 || b10 b11 b12 |
    // | a20 a21 a22 || b20 b21 b22 |
    // | a00×b00 + a01×b10 + a02×b20 a00×b01 + a01×b11 + a02×b21 a00×b02 + a01×b12 + a02×b22 |
    // | a10×b00 + a11×b10 + a12×b20 a10×b01 + a11×b11 + a12×b21 a10×b02 + a11×b12 + a12×b22 |
    // | a20×b00 + a21×b10 + a22×b20 a20×b01 + a21×b11 + a22×b21 a20×b02 + a21×b12 + a22×b22 |
    multi : function(mx) {
        var tx,ty,tz;

        tx = this.m00; ty = this.m01; tz = this.m02;
        this.m00 = tx * mx.m00 + ty * mx.m10 + tz * mx.m20;
        this.m01 = tx * mx.m01 + ty * mx.m11 + tz * mx.m21;
        this.m02 = tx * mx.m02 + ty * mx.m12 + tz * mx.m22;

        tx = this.m10; ty = this.m11; tz = this.m12;
        this.m10 = tx * mx.m00 + ty * mx.m10 + tz * mx.m20;
        this.m11 = tx * mx.m01 + ty * mx.m11 + tz * mx.m21;
        this.m12 = tx * mx.m02 + ty * mx.m12 + tz * mx.m22;

        tx = this.m20; ty = this.m21; tz = this.m22;
        this.m20 = tx * mx.m00 + ty * mx.m10 + tz * mx.m20;
        this.m21 = tx * mx.m01 + ty * mx.m11 + tz * mx.m21;
        this.m22 = tx * mx.m02 + ty * mx.m12 + tz * mx.m22;

        return this;
    },

    //回転
    rotate : function(rad) {
        var mx = new Matrix();
        mx.m00 = Math.cos(rad); mx.m01 = -Math.sin(rad);
        mx.m10 = Math.sin(rad); mx.m11 = Math.cos(rad);
        return this.multi(mx);
    },

    //傾斜
    skew : function(qx,qy) {
        var mx = new Matrix();
        mx.m01 = Math.tan(qy);
        mx.m10 = Math.tan(qx);
        return this.multi(mx);
    },

    //拡大縮小
    scale : function(sx, sy) {
        var mx = new Matrix();
        mx.m00 = sx;
        mx.m11 = sy;
        return this.multi(mx);
    },

    //平行移動
    translate : function(tx, ty) {
        var mx = new Matrix();
        mx.m02 = tx;
        mx.m12 = ty;
        return this.multi(mx);
    },

    //行列変換の結果
    transformPoint : function(p) {
        return new Point(this.m00 * p.x + this.m01 * p.y + this.m02,
                         this.m10 * p.x + this.m11 * p.y + this.m12);
    }
};

今回は理解を優先するためにかなりベタな作りで、無駄な計算も含まれております。
他のプログラムなどは効率性を重視したアフィン変換をしていたりします。

2Dゲーム用の3行3列のマトリックスの関数を作ろう
http://hakuhin.jp/as/matrix_33.html