3x3 画像1 画像2

さて、今日は昨日紹介したアルゴリズムである、
線形補間法をコードで実装してみよう。

ピクセルを領域として考えないと、
画像の端にあるピクセルの情報量が減ってしまうことは、
以前、「ピクセルは点か領域か」の日記で考察した。
なので、ピクセルを領域として計算する方法で行いたい。
その場合、コードを書く前に座標の問題を考える必要がある。

何が問題かというと、画像の端に当たるピクセルの計算時に、
元の画像の範囲を超えてしまう可能性が考えられるのだ。
これは、近傍法では起こらない。

例を挙げよう。

3 x 3 の画像を、15 x 15 に拡大するとする。

黒赤黒
青白黄
黒緑黒

試しに、拡大先の画像の右上に当たるピクセルで考える。
右上なので、ピクセルの位置(添字)は、(14, 0) だ。
領域と考えると、(14, 0) - (15, 1) の範囲となる。

座標変換を行うために、範囲から代表となる点を選べば、
範囲の中心である、(14.5, 0.5) となる。

次に、この点に対して座標変換を行う。
新しい画像の座標から、元の画像の座標に変換するのだ。
14.5 / 15 * 3 = 2.9, 0.5 / 15 * 3 = 0.1
つまり、対応する元の座標は、(2.9, 0.1) となる。

この座標も、代表となる点の座標と考えないといけないため、
この点を代表とする領域を考えると、
(2.4, -0.4) - (3.4, 0.6) という範囲となる。
そして、この範囲をピクセルの位置(添字)に戻せば、
(2.4, -0.4) という要素番号が求まる。

近傍法の場合は、これに最も近い点を使うため、
四捨五入して、(2, 0) となるため、元の画像内に収まる。

線形補間法の場合、算出位置に最も近い 4 点を使うため、
(2, -1), (3, -1), (2, 0), (3, 0) の 4 つの点が必要だ。
(2, 0) 以外は、元の画像の範囲外となってしまった。
つまり、座標の範囲チェックが必要になるってことだ。

では、実装に入ろう。


# 負の方向に小数を切り捨てる関数 floor を使う。
use POSIX;

# BMP 読み込み。
my $src = read_bmp;

# 画像の幅と高さを取得。
my $scx = @{$src->[0]};
my $scy = @$src;

# 新しい画像の幅と高さを決める。
my $dcx = 105;
my $dcy = 90;

# 新しい画像のデータを準備する。
my $dest = [ map { [ (0) x $dcx ] } 1 .. $dcy ];

for (my $dy = 0; $dy < $dcy; ++$dy) {
    for (my $dx = 0; $dx < $dcx; ++$dx) {

        no integer;

        # 新しい点の位置に対応する元の点の位置を求める。
        my $sx = ($dx + 0.5) / $dcx * $scx - 0.5;
        my $sy = ($dy + 0.5) / $dcy * $scy - 0.5;

        # 位置を整数化し、最も近い左上の点を得る。
        # 計算結果が負の場合があるので、
        # int 関数は使えない。そこで POSIX::floor を使う。
        my $px = floor($sx);
        my $py = floor($sy);

        # 色を累算する配列。
        my @rgb = (0, 0, 0);

        # 周辺の 4 点に対して処理をする。
        foreach my $yy ($py, $py + 1) {
            foreach my $xx ($px, $px + 1) {

                # 縦と横の影響力を求め、積を重みとする。
                my $weight = (1 - abs($yy - $sy))
                          * (1 - abs($xx - $sx));

                # 座標が範囲外であれば、範囲内に収める。
                my ($tx, $ty) = ($xx, $yy);
                $tx = 0 if $tx < 0;
                $tx = $scx - 1 if $tx >= $scx;
                $ty = 0 if $ty < 0;
                $ty = $scy - 1 if $ty >= $scy;

                # RGB に分解し、それぞれ重みを掛けて累算する。
                if ($weight) {
                    my @clr = unpack_rgb($src->[$ty][$tx]);
                    $rgb[$_] += $clr[$_] * $weight foreach 0 .. 2;
                }

            }
        }

        # RGB それぞれ累計値を整数化する。
        $rgb[$_] = floor($rgb[$_] + 0.5) foreach 0 .. 2;

        # 32 ビット整数値に戻しておく。
        $dest->[$dy][$dx] = pack_rgb($rgb[0], $rgb[1], $rgb[2]);
    }
}

# BMP を書き出す。
write_bmp($dest, 24);