約一年ぶりに画像の補間手法について語ります。
前回はニアレストネイバー(最近傍)法について語りましたが、今回は一歩進んでバイリニア(線形または1次補間)法のプログラムを紹介します。
まぁ、今更って感じですが。
概要
ニアレストネイバーでは画像を拡大・縮小したときのジャギーやモアレが目立ちます。そこで画像を滑らかにする効果を持つ補間方法の一つとしてバイリニアがあります。
他にもよく用いられる補間方法としてBicubic(バイキュービック)法やLanczos(ランチョス)法が有名ですが、特にバイリニアは処理に掛かる計算量が比較的少なく、軽量の補間フィルタとしてポピュラーな存在となっています。
バイリニアはニアレストネイバーと違い、元画像のピクセルの色をそのまま採用するのではなく、隣接するピクセルの色データを混ぜ合わせて新しいピクセルの色を決定します。
詳しくはWikipediaあたりを参照してください。
プログラム
さて、早速作成したバイリニアリサイズ関数のプログラムコードを以下に示します。よければご自由にお使いください。
(前回紹介したニアレストネイバーは24bitビットマップ画像用でしたが、今回は32bit用となっています。)
procedure ResizeBilinear(SrcBmp, DstBmp :TBitmap; Width, Height: Integer);
type
PRGBQuadArray = ^TRGBQuadArray;
TRGBQuadArray = array[0..32767] of TRGBQuad;
var
wfactor, hfactor: Double;
x0, x1, y0, y1 : Integer;
coefx0, coefx1 : Double;
coefy0, coefy1 : Double;
ix, iy : Integer;
srcWidth, srcHeight : Integer;
d, d00, d01, d10, d11: PRGBQuad;
dst, src1, src2 : PRGBQuadArray;
r0, r1 : Double;
g0, g1 : Double;
b0, b1 : Double;
x, y : Double;
begin
if (Width < 1) or (Height < 1) then Exit;
if (DstBmp.PixelFormat <> pf32bit)
or (SrcBmp.PixelFormat <> pf32bit) then Exit;
DstBmp.Width := Width;
DstBmp.Height := Height;
srcWidth := SrcBmp.Width;
srcHeight := SrcBmp.Height;
hfactor := srcHeihgt / Height;
wfactor := srcWidth / Width;
for iy:= 0 to Height - 1 do
begin
y := hfactor * iy;
y0 := Trunc(y);
y1 := Min(y0 + 1, srcHeight - 1);
coefy1 := y - y0;
coefy0 := 1 - coefy1;
dst := DstBmp.ScanLine[iy];
src1 := SrcBmp.ScanLine[y0];
src2 := SrcBmp.ScanLine[y1];
for ix := 0 to Width - 1 do
begin
x := wfactor * ix;
x0 := Trunc(x);
x1 := Min(x0 + 1, srcWidth - 1);
coefx1 := x - x0;
coefx0 := 1 - coefx1;
d := @dst[ix];
d00 := @src1[x0]; d01 := @src2[x0];
d10 := @src1[x1]; d11 := @src2[x1];
r0 := d00^.rgbRed*coefx0 + d10^.rgbRed*coefx1;
r1 := d01^.rgbRed*coefx0 + d11^.rgbRed*coefx1;
g0 := d00^.rgbGreen*coefx0 + d10^.rgbGreen*coefx1;
g1 := d01^.rgbGreen*coefx0 + d11^.rgbGreen*coefx1;
b0 := d00^.rgbBlue*coefx0 + d10^.rgbBlue*coefx1;
b1 := d01^.rgbBlue*coefx0 + d11^.rgbBlue*coefx1;
d^.rgbRed := Min($FF, Round((r0*coefy0 + r1*coefy1)));
d^.rgbGreen := Min($FF, Round((g0*coefy0 + g1*coefy1)));
d^.rgbBlue := Min($FF, Round((b0*coefy0 + b1*coefy1)));
end;
end;
end;
実行結果
前回と同じようにWindowsXP付属のサンプル画像『Water lilies.jpg』(800×600ピクセル)をソース画像とし、320×240ピクセルへの縮小、1280×960ピクセルへの拡大を行いました。拡大画像の方は一部分だけ切り取りました。
リサイズ | Bilinear | NearestNeighbor |
---|---|---|
1280×960拡大 | ||
320×240縮小 |
縮小時は分かりにくいかもしれませんが、花びらのふちなどが若干滑らかになっているのが分かると思います。拡大時は明確な違いがでているのが分かると思います。簡単なアルゴリズムでありながらこの差は大きいです。
しかし、処理時間はどうでしょう。
上記のソースはまだ最適化処理を行っていないので遅いのですが、拡大時の処理時間がニアレストネイバーが約9msなのに対し、バイリニアは約180msもの処理時間を食っています。20倍弱の差があります。これではちょっと重過ぎるのでとりあえず、前回同様の高速化を行ってみます。
改良高速化
前回同様に一度計算したx軸のオフセット位置を保持する手段を適用してみます。
procedure ResizeBilinear(SrcBmp, DstBmp :TBitmap; Width, Height: Integer);
type
PRGBQuadArray = ^TRGBQuadArray;
TRGBQuadArray = array[0..32767] of TRGBQuad;
var
wfactor, hfactor: Double;
x0, x1 :TIntegerDynArray;
coefx0, coefx1 :TDoubleDynArray;
y0, y1 :Integer;
coefy0, coefy1 :Double;
ix, iy :Integer;
srcWidth, srcHeight :Integer;
d, d00, d01, d10, d11 :PRGBQuad;
dst, src1, src2 :PRGBQuadArray;
r0, r1 :Double;
g0, g1 :Double;
b0, b1 :Double;
x, y :Double;
begin
if (Width < 1) or (Height < 1) then Exit;
if (DstBmp.PixelFormat <> pf32bit)
or (SrcBmp.PixelFormat <> pf32bit) then Exit;
DstBmp.Width := Width;
DstBmp.Height := Height;
srcWidth := SrcBmp.Width;
srcHeihgt := SrcBmp.Height;
hfactor := srcHeihgt / Height;
wfactor := srcWidth / Width;
SetLength(x0, Width); SetLength(coefx0, Width);
SetLength(x1, Width); SetLength(coefx1, Width);
for ix := 0 to Width - 1 do
begin
x := wfactor * ix;
x0[ix] := Trunc(x);
x1[ix] := Min(x0[ix] + 1, srcWidth - 1);
coefx1[ix] := x - x0[ix];
coefx0[ix] := 1 - coefx1[ix];
end;
for iy:= 0 to Height - 1 do
begin
y := hfactor * iy;
y0 := Trunc(y);
y1 := Min(y0 + 1, srcHeight - 1);
coefy1 := y - y0;
coefy0 := 1 - coefy1;
dst := DstBmp.ScanLine[iy];
src1 := SrcBmp.ScanLine[y0];
src2 := SrcBmp.ScanLine[y1];
for ix := 0 to Width - 1 do
begin
d := @dst[ix];
d00 := @src1[x0[ix]]; d01 := @src2[x0[ix]];
d10 := @src1[x1[ix]]; d11 := @src2[x1[ix]];
r0 := d00^.rgbRed*coefx0[ix] + d10^.rgbRed*coefx1[ix];
r1 := d01^.rgbRed*coefx0[ix] + d11^.rgbRed*coefx1[ix];
g0 := d00^.rgbGreen*coefx0[ix] + d10^.rgbGreen*coefx1[ix];
g1 := d01^.rgbGreen*coefx0[ix] + d11^.rgbGreen*coefx1[ix];
b0 := d00^.rgbBlue*coefx0[ix] + d10^.rgbBlue*coefx1[ix];
b1 := d01^.rgbBlue*coefx0[ix] + d11^.rgbBlue*coefx1[ix];
d^.rgbRed := Min($FF, Round((r0*coefy0 + r1*coefy1)));
d^.rgbGreen := Min($FF, Round((g0*coefy0 + g1*coefy1)));
d^.rgbBlue := Min($FF, Round((b0*coefy0 + b1*coefy1)));
end;
end;
end;
結果150ms程度になりました。うーん、微妙デスねぇ。やはり処理を重くしている原因は実数計算の回数が多いことにあります。Double型などを使った実数計算は通常『浮動小数点数』を用いて計算されます。その名の通り小数点が移動する型の事です。詳しくはめんどくさいのでWikipediaをご参照ください。
昔と違って現在の一般的なPCのCPUは浮動小数点演算の為のユニットを持っており、高速に計算が出来るのですが、やはり整数演算に比べるとかなり遅いです。なので浮動小数点演算をなるべく減らす事が高速化の鍵となってきます。
では実際にどのようにやるかは次回のテーマ『Bilinear(バイリニア法) ~2.固定小数点演算編~』で紹介します。
いつになるのやら。。。
コメント