2006 年 3 月 9 日 23 時 56 分

背景色への最適化


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


[写真] [写真]


さて、いよいよ大詰めだ。今日で最後となる。

今日は、画像のサイズをうまく調整し、
背景との間のぎざぎざを取り払って完成させよう。

まず、回転後の画像のサイズだが、
実はあらかじめ計算しておくことができる。
そうすれば、背景色だけの領域を最低限にすることができる。

ではどうやって求めるか。

回転によるサイズの変更を考える場合、
元の画像の頂点がどう動くかに着目すればよい。

図を見てほしい。頂点●と、頂点■の動きをみてみよう。
少し分かりにくいが、0~90 度の間の回転で考えると、
頂点●は常に回転後の右辺にある。
同様に頂点■は、常に回転後の下辺にある。

つまり、回転後の●の x 座標を 2 倍すれば幅となり、
回転後の頂点■の、y 座標を 2 倍すれば高さとなるのだ。

では、0~90 度以外の範囲ではどうなるか。

180 度の回転は、回転しない場合と同じサイズのはずだ。
-30 度の回転と 30 度の回転は上下対象だから、
やはり同じサイズになるはずだ。
120 度の回転と 60 度の回転は左右対象だから、
これまた同じサイズになるはずだ。

つまり、結果のサイズだけを考えた場合、
どの角度でも、0~90 度の範囲で考えることができるのだ。

次に、背景との間のぎざぎざを取り払う。
これは簡単で、4 点を使って線形補間を行う際に、
範囲外になった点のを背景色として計算すればいいのだ。


では、やってみよう。


use POSIX;
no integer;

# 円周率を得る。
my $PI = atan2(0, -1);


# 回転角を定義。今回は -5 度。
my $angle  = -5;

# 背景色を定義。今回は赤色。
my $bgcolor = pack_rgb(255, 0, 0);


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

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

# 元の画像の中心を求める。
my $sox = $scx / 2;
my $soy = $scy / 2;


# 新しい画像の幅と高さを求める。

# 回転角を 0~90 の指標へ。

# 180 で割ったあまりを求める。
my $co = $angle - floor($angle / 180) * 180;

# 90~180 度は 90~0 度と左右対称。
$co = 180 - $co if $co >= 90;

# 指標をラジアンに変更し、
my $cor = $co * $PI / 180;

# 指標を元にサイズを求め、周辺に 1 ピクセルの余裕を持たせる。
# 回転の計算式は、回転後から回転前を求めるものだったので、
# 回転方向が逆になるため、ラジアン値を符号反転する。
# (このあたり、最適化の余地は十分にあります)

# 幅は、右上の点の x 座標の 2 倍。
my $dcx = ceil(cos(-$cor) * $sox + sin(-$cor) * -$soy) * 2 + 2;

# 高さは、右下の点の y 座標の 2 倍。
my $dcy = ceil(cos(-$cor) * $soy - sin(-$cor) * $sox) * 2 + 2;


# 新しい画像の中心を求める。
my $dox = $dcx / 2;
my $doy = $dcy / 2;

# 新しい画像のデータを背景色で初期化する。
my $dest = [ map { [ ($bgcolor) x $dcx ] } 1 .. $dcy ];


# 回転角をラジアンに変換し、
my $radian = $angle * $PI / 180;

# サインとコサインを求めておく。
my $sin = sin($radian);
my $cos = cos($radian);


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

        no integer;

        # 新しい点の位置に対応する元の点の位置を求める。
        my $sx = $sox + $cos * ($dx - $dox + 0.5)
                      + $sin * ($dy - $doy + 0.5) - 0.5;
        my $sy = $soy + $cos * ($dy - $doy + 0.5)
                      - $sin * ($dx - $dox + 0.5) - 0.5;

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

        # 計算対象の 4 点が全て範囲外なら転送しない。
        # ($px, $py) が (-1, -1) でも右下の点は範囲内。
        next if $px < -1 or $px >= $scx or $py < -1 or $py >= $scy;

        # 色を累算する。
        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 $color;

                # 座標が範囲外であれば、背景色で計算。
                if ($xx < 0 or $xx >= $scx
                  or $yy < 0 or $yy >= $scy) {
                    $color = $bgcolor;
                } else {
                    $color = $src->[$yy][$xx];
                }

                # RGB に分解しそれぞれ重みを掛けて累算する。
                if ($weight) {
                    my @pixel = unpack_rgb($color);
                    $rgb[$_] += $pixel[$_] * $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);



Copyright (c) 1994-2007 Project Loafer. All rights reserved.