画像拡大、縮小時の補間
GDI Plus の使用
画像の拡大縮小を行う時、各ドットの中間の値が必要になります。
その時の画像の補間方法として、色々な方法があり、Windows XP3以降のGDI Plus を使用すれば、簡単に画像の補間を行う事が出来ます。
Delphi では、uses文に GDIPAPIと GDIPOBJの二つを追加すれば、GDI
Plusの利用が可能となります。
TGPGraphicsに
InterpolationModeを指定して、TGPBitmapに 書き出せば、指定した補間方法で、画像が書き出されます。
Bicubic
双三次補間、バイキュービック法 事前フィルター処理は実行されません。このモードは、イメージを元のサイズの 25%
以下にするような縮小処理には適していません。
Bilinear
双一次補間、バイリニア法
事前フィルター処理は実行されません。このモードは、イメージを元のサイズの 50% 以下にするような縮小処理には適していません。
Default
既定のモード。
HighQuality
高品質補間
LowQuality
低品質補間
HighQualityBicubic
高品質双三次補間
事前フィルター処理が適用され、高品質の縮小拡大処理が実行されます。このモードを使用すると、変換後のイメージが高品質になります。
HighQualityBilinear
高品質双一次補間 事前フィルター処理が適用され、高品質の縮小拡大処処理が実行されます。
Invalid QualityMode
列挙体の要素Invalid(無効なモードを指定)と等価。
NearestNeighbor
最近傍補間、ニアレストネイバー法
変換部分のプログラムを下記に示します。
ダウロード圧縮ファイルの中のBicubicNホルダーにサンプルプログラムがあります。
procedure TForm1.Button1Click(Sender: TObject); var SrcRect : TRect; GDIGraphic : TGPGraphics; GDIBitmap : TGPBitmap; PolaMode : TInterpolationMode; Scal : Double; ABitmap : TBitmap; WIC : TWICImage; AStream : TMemoryStream; begin Scal := GWidth / IWidth; // 元画像と表示画像の倍率 SrcRect := Rect(Round(Origin.X * Scal), // マウスの選択範囲を元画像範囲に設定 Round(Origin.Y * Scal), Round(EndPoint.X * Scal), Round(EndPoint.Y * Scal)); // 画像の一部をImage1に描画 if FOpenF then begin ABitmap := TBitmap.Create; WIC := TWICImage.Create; // TWICImageの生成 try WIC.LoadFromFile(InFilename); // 画像の読み込み ABitmap.Assign(WIC); // ビットマップABitmapに設定 Image1.Canvas.CopyRect(Rect(0, 0, Image1.Width, Image1.Height), // 指定された範囲をVCLでImage1に表示 ABitmap.Canvas, SrcRect); finally ABitmap.Free; WIC.Free; end; end; // 以下GDI+ の補間モードを指定しての描画 // RadioGruupのItemに応じて補間モードを選択 PolaMode := InterpolationModeInvalid; case RadioGroup1.ItemIndex of 0: PolaMode := InterpolationModeInvalid; 1: PolaMode := InterpolationModeDefault; 2: PolaMode := InterpolationModeLowQuality; 3: PolaMode := InterpolationModeHighQuality; 4: PolaMode := InterpolationModeBilinear; 5: PolaMode := InterpolationModeBicubic; 6: PolaMode := InterpolationModeNearestNeighbor; 7: PolaMode := InterpolationModeHighQualityBilinear; 8: PolaMode := InterpolationModeHighQualityBicubic; end; // Image2の画像を消去 Image2.Picture.Assign(nil); GDIGraphic := TGPGraphics.Create(Image2.Canvas.Handle); AStream := TMemoryStream.Create; if FOpenF then begin GDIBitmap := TGPBitmap.Create; GDIBitmap.Create(InFilename); end else begin Image1.Canvas.CopyRect(Rect(0, 0, Image1.Width, Image1.Height), // 指定された範囲をVCLでImage1に表示 Image4.Canvas,SrcRect); // メモリストリームにImage4の画像を保存 Image4.Picture.Graphic.SaveToStream(AStream); // GDI+のTGPBitmapを,そのメモリストリームで生成 GDIBitmap := TGPBitmap.Create(TStreamAdapter.Create(AStream, soReference)); end; // 補間モード設定 GDIGraphic.SetInterpolationMode(PolaMode); // GDIBitmapをImage2に描画 // TCanvasのStretchDrawまたはCopyRectに相当する GDIGraphic.DrawImage(GDIBitmap, MakeRect(0, 0, Image2.Width, Image2.Height), SrcRect.Left, SrcRect.Top, SrcRect.Right - SrcRect.Left, SrcRect.Bottom - SrcRect.Top, UnitPixel); GDIBitmap.Free; GDIGraphic.Free; AStream.Free; end;
GDI Pius を使用すれば、画像の補間が簡単に行えますが、通常のVclでの画像補間について検討してみます。
どの様にして補間されているかがプログラムを組むことによって理解が出来るでしょう。
ニアレストネイバー(Nearest Neighbor)法
これは一番簡単な方法で最近傍法とも言われています、計算された拡大縮小時の元画像座標 xf、yf が浮動小数点なので、この座標に一番近い座標の値を新しい座標の値にする方法です。
単純に、座標を四捨五入して、その座標の値を取得して新しい座標の値にするだけです。
補間計算をしているわけではありません。
バイリニア(BiLinear)法
周囲四点からの距離によって重み付けをしてその点の値とする方法です。
比較的計算が速く行われると同時に、きれいな画像を得ることが出来ます。
求める値をDとすると
の様になります。
バイキュービック(BiCubic)法
X1、X2、X3、X4、Y1、Y2、Y3、Y4
は求める位置から参照する画素までの距離を表します。
Wは重みで、X1~X4、Y1~Y4をそれぞれ計算加算して重みとします。
関数W(x)はsinc関数(sinc(x) = sin(πx)/πx) W(y)はsinc関数(sinc(y) =
sin(πy)/πy) をテイラー展開により三次の項まで近似した関数で、Lanczos(ランツォシュ)補間法のLanczo-2(N=2)が基本になっているようです。
その為、結果が負になる時があるので、負の時は値をゼロにするような処理が必要です。
Lanczos(ランツォシュ)補間法
距離は上図式と同じです。
使用する重み関数は、距離をDとすると、各座標毎に
W(D)
= (sin(πD)/πD)/(sin(πD/N)/(πD/N))
で示されます。
ちなみにsin(πD)/πDはsinc関数と呼ばれるもので、Dを無限遠点にまで応用すれば、理論的には最も完全な補間となり、バイキュービックの補間式はこれを近似したものですが、無限遠点まで用いるのは現実的ではないため、普通は使用されません。
この計算方法は、計算に非常に時間がかかり バイキュービック(BiCubic)法 に比べ四倍ぐらい時間が掛かり、あまり使用されていませんが、現在CPUの計算速度が上がっているので、必要に応じて使用しても良いかと思います。
重み係数に負の値が出現するので、場合によって結果が負になる時があるので、負の時は値をゼロとするような処理が必要です。
サンプル画像のアンシャープ+ランツォシュは、プログラムに組み込まれていません。
最初にアンシャープ処理をして保存をし、その後、ランツォシュ補間をして拡大しています。
サンプルプログラムは、Bicubic、BicubicO、BicubicPのホルダーの中にあります。
Bicubicホルダーにあるのは、バイキュービック法だけで、BicubicO、BicubicPには、上記四種類の方法が組み込んであります。
TBitmapのデーターに TBitmap.Canvas.Pixels[X, Y: Integer]:
TColor でアクセスをすると、非常に遅いので、アクセス方法を変更する必要があります。
そこで、二次元配列を利用すれば、高速にアクセスする事ができます。
BicubicOにあるものは、元画像データーを二次元配列に変換してから、拡大縮小補間を行っていて、BicubicPにあるものは、PRGBarry(ポインタ型RGB配列)とScanLineを利用して
TBitmap のデーターを二次元配列データーとして利用することにより、補間作業を行っています。
PRGBarryを利用すると、使用するメモリーの量が少なくて済みます。
テストした結果、縮小拡大時の実行時間には差がありませんでしたので、PRGBarryの利用が効率的です。
次のサンプルリストは、PRGBarryを利用した場合です。
TBitmapのデーターが24ビットフォーマットの場合は、TRGBTripleを使用し、32ビットフォーマットの場合は、TRGBQuadを使用します。
配列の大きさを[0..0] 1として宣言していますが、スキャンラインを使用してポインタを割り付けると実際の配列の大きさは、TBitmapのWidthとなります。
type
TRGBArray = array[0..0] of TRGBTriple;
PRGBArray = ^TRGBArray;
var
srcmat : array of PRGBArray;
begin
Length(srcmat, Bitmap.Height);
// ポインタ配列にデーターセット
for
H := 0 to Bitmap.Height - 1 do begin
Srcmat[H] := Src.ScanLine[H];
end;
この様にする事で、配列として各色の値にアクセスする事が出来ます。
Srcmat[H][W] .rgbtBlue;
Srcmat[H][W]
.rgbtGreen;
Srcmat[H][W] .rgbtRed;
画像の補間は、必要な座標の周囲4点、16点
又はそれ以上の点をアクセスしますが、範囲外へのアクセスを防止するため、アクセスする範囲を狭めてしまうと、特に拡大時は、画像の無い部分が大きくなってしまいます。
それを防止する為、範囲外となる部分は、一番外側の値で代用する事にして、画像の無い部分がないようにします。
他の画像処理のサンプルプログラムは、バイリニアしか使用していないので、一番右側の縦と、一番下の横ラインは、画像として作画されないことになっています。
縦横1ラインなので無視をしていますが、気になるようであれば、此処で行っている一番外側の値で代用するように変更をすれば良いでしょう。
unit Main; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, System.Diagnostics; type TRGBArray = array[0..0] of TRGBTriple; // RGB 三色(3バイト) 一次元配列であればサイズは1でもよい PRGBArray = ^TRGBArray; // 実際の配列のメモリー領域はBitmap幅領域内となります type TForm1 = class(TForm) Button1: TButton; Image1: TImage; Image2: TImage; Edit1: TLabeledEdit; Edit2: TLabeledEdit; RadioGroup1: TRadioGroup; Label1: TLabel; nEdit: TLabeledEdit; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } function AdjustByte(Value: Double): Byte; // オーバーフロー、アンダーフロー対策 function Bicubic(XD, YD, a: double): TRGBTriple; function Bilinear(XD, YD: double): TRGBTriple; function NearestNeighbor(XD, YD: double): TRGBTriple; function Lanczos(XD, YD: double; n: integer): TRGBTriple; public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} var srcmat : array of PRGBArray; Src, Dest : TBitmap; function TForm1.AdjustByte(Value: double): Byte; // オーバーアンダーフロー対策 begin result := Round(Value); if Value < 0 then result := 0; if Value > 255 then result := 255; end; function TForm1.Lanczos(XD, YD: double; n: integer): TRGBTriple; var xBase, yBase : Integer; i, j : Integer; w_total : Double; xC, yC : Integer; distX, distY : Double; weight, weightx : Double; dPIx, dPIy : Double; dPIxn, dPIyn : Double; CpBo, CpGo, CpRo : Double; nx : Integer; begin nx := n - 1; xBase := Trunc(XD); yBase := Trunc(YD); CpBo := 0; CpGo := 0; CpRo := 0; w_total := 0; // 周辺(a*2)^2画素を取得して処理 for i := -nx to n do begin xC := xBase + i; // X距離決定 distX := abs(xC - XD); // X重み付け // distX = 0 の時は weightx=1 // distX < n の時は 0 < weightx < 1 // distX = n の時は weightx = 0 なので次の計算省略 weightx := 1; if distX <> 0 then if distX < n then begin dPIx := PI * distX; dPIxn := dPIx / n; weightx := sin(dPIx) * sin(dPIxn) / (dPIx * dPIxn); end else continue; // weightx = 0 なので次の計算省略 for j := -nx to n do begin yC := yBase + j; // Y距離決定 distY := abs(yC - YD); // Y重み付け // disty = 0 の時は weightx * 1 // disty < n の時は weightx * (0 < weight < 1) // disty = n の時は weightx * 0 なので次の計算省略 weight := weightx; if distY <> 0 then if distY < n then begin dPIy := PI * distY; dPIyn := dPIy / n; weight := weightx * sin(dPIy) * sin(dPIyn) / (dPIy * dPIyn); end else continue; // weight = weightx * 0 なので次の計算省略 // 範囲外へのアクセス防止 ランツォシュの処理範囲が枠外となる場合は一番外側の値を採用します if xC < 0 then xC := 0; if xC > Src.Width - 1 then xC := Src.Width - 1; if yC < 0 then yC := 0; if yC > Src.Height - 1 then yC := Src.Height - 1; // 画素取得 CpBo := CpBo + weight * SrcMat[yC][xC].rgbtBlue; CpGo := CpGo + weight * SrcMat[yC][xC].rgbtGreen; CpRo := CpRo + weight * SrcMat[yC][xC].rgbtRed; w_total := w_total + weight; end; end; if w_total<> 0 then begin Result.rgbtBlue := AdjustByte(CpBo / w_total); Result.rgbtGreen := AdjustByte(CpGo / w_total); Result.rgbtRed := AdjustByte(CpRo / w_total); end else begin Result.rgbtBlue := 0; Result.rgbtGreen := 0; Result.rgbtRed := 0; end; end; function TForm1.NearestNeighbor(XD, YD: double): TRGBTriple; var X, Y : Integer; begin X := Trunc(XD + 0.5); // 四捨五入 Y := Trunc(YD + 0.5); // 四捨五入 if X > Src.Width - 1 then X := Src.Width - 1; // 範囲外へのアクセス防止 if Y > Src.Height - 1 then Y := Src.Height - 1; // 範囲外へのアクセス防止 Result := SrcMat[Y][X]; end; function TForm1.Bilinear(XD, YD: double): TRGBTriple; var R, G, B : array[0..1] of array[0..1] of byte; dX, dY : Double; X, Y : Integer; K, L, SK, SL : Integer; MdX, MdY : Double; MW, MH : Integer; begin X := Trunc(XD); Y := Trunc(YD); dX := XD - X; dY := YD - Y; MW := Src.Width - 1; MH := Src.height -1; // 線形補間 for K := 0 to 1 do for L := 0 to 1 do begin SK := K + X; if SK > MW then SK := MW; // 範囲外へのアクセス防止 SL := L + Y; if SL > MH then SL := MH; // 範囲外へのアクセス防止 R[k, L] := SrcMat[SL][SK].rgbtRed; G[K, L] := SrcMat[SL][SK].rgbtGreen; B[k, L] := SrcMat[SL][SK].rgbtBlue; end; MdX := 1 - dX; MdY := 1 - dY; Result.rgbtRed := Trunc(MdX * (MdY * R[0, 0] + dY * R[0, 1]) + dX * (MdY * R[1, 0] + dY * R[1, 1])); Result.rgbtGreen := Trunc(MdX * (MdY * G[0, 0] + dY * G[0, 1]) + dX * (MdY * G[1, 0] + dY * G[1, 1])); Result.rgbtBlue := Trunc(MdX * (MdY * B[0, 0] + dY * B[0, 1]) + dX * (MdY * B[1, 0] + dY * B[1, 1])); end; function TForm1.Bicubic(XD, YD, a: double): TRGBTriple; var SH, SW : Integer; DX, DY, PX, PY: Double; XI, YI, X, Y : Integer; R, G, B : Double; begin // R、G、B 初期化 R := 0; G := 0; B := 0; // 垂直方向の補間係数処理 YI := Trunc(YD); // ゼロ方向へまるめ XI := Trunc(XD); // ゼロ方向へまるめ for Y := YI - 1 to YI + 2 do begin DY := Abs(YD - Y); // Y座標差分計算 // バイキュービック補間係数計算 if DY <= 1 then PY := (a + 2) * DY * DY * DY - (a + 3) * DY * DY + 1 else if DY < 2 then PY := a * DY * DY * DY - 5 * a * DY * DY + 8 * a * DY - 4 * a else PY := 0; // 2以上DY の値に基本的に2以上はないが浮動小数点桁落ち計算誤差対策 // 元座標縦方向領域外へのアクセス防止 SH := Y; if (SH < 0) or (SH > Src.Height -1) then SH := YI; // 水平方向の補間係数処理 for X := XI - 1 to XI + 2 do begin DX := Abs(XD - X); // X座標差分計算 // バイキュービック補間係数計算 if DX <= 1 then PX := (a + 2) * DX * DX * DX - (a + 3) * DX * DX + 1 else if DX < 2 then PX := a * DX * DX * DX - 5 * a * DX * DX + 8 * a * DX - 4 * a else PX := 0; // 2以上DX の値に基本的に2以上はないが浮動小数点桁落ち計算誤差対策 // 元座標横方向領域外へのアクセス防止 SW := X; if (SW < 0) or (SW > Src.Width -1) then SW := XI; // Srcからの読み込み及び補間計算 R := R + Srcmat[SH,SW].rgbtRed * PX * PY; G := G + Srcmat[SH,SW].rgbtGreen * PX * PY; B := B + Srcmat[SH,SW].rgbtBlue * PX * PY; end; end; Result.rgbtRed := AdjustByte(R); Result.rgbtGreen := AdjustByte(G); Result.rgbtBlue := AdjustByte(B); end; procedure TForm1.Button1Click(Sender: TObject); var Rate : Double; SPDH, SPDW : Double; CH : Integer; DH, DW : Integer; XD, YD : Double; a : Double; Pbitmap : PRGBArray; Atime : TStopwatch; n : Integer; begin Atime := TStopwatch.StartNew; Val(Edit1.Text, Rate, CH); // 倍率セット if CH <> 0 then Rate := 1; a := -1; if RadioGroup1.ItemIndex = 1 then begin Val(Edit2.Text, a, CH); // aの値はシャープ値-0.5 ~-1 が標準 if Ch <> 0 then a := -1; end; n := 2; if RadioGroup1.ItemIndex = 3 then begin val(nEdit.Text, n, ch); if ch <> 0 then n := 2; if (n < 1) then begin application.MessageBox('Nの値が小さすぎます','注意', 0); exit; end; if (n > 10) then begin application.MessageBox('Nの値が大きすぎて計算に時間が掛かります','注意', 0); exit; end; end; try Src := TBitmap.Create; Dest := TBitmap.Create; // 倍率セット // 24bitフォーマットに設定 Src.PixelFormat := pf24bit; Dest.PixelFormat := pf24bit; // Srcにコピー Src.Assign(Image1.Picture.Bitmap); // ソース画像用配列確保 SetLength(Srcmat, Src.Height); // ポインタ配列にデーターセット for DH := 0 to Src.Height - 1 do begin Pbitmap := Src.ScanLine[DH]; Srcmat[DH] := Pbitmap; end; // 倍率からDestサイズの設定 Dest.Width := Round(Src.Width * Rate); Dest.Height := Round(Src.Height * Rate); SPDH := Src.Height / Dest.Height; // 縦倍率の逆数 SPDW := Src.Width / Dest.Width; // 横倍率の逆数 // 垂直方向の処理 for DH := 0 to Dest.Height -1 do begin YD := DH * SPDH; // 縦Y方向元座標の計算 Pbitmap := Dest.ScanLine[DH]; // 水平方向の処理 for DW := 0 to Dest.Width -1 do begin XD := DW * SPDW; // 横X方向元座標の計算 // Destへ書き出し case RadioGroup1.ItemIndex of 0 : Pbitmap[DW] := Bilinear(XD, YD); // バイリニア 1 : Pbitmap[DW] := Bicubic(XD, YD, a); // バイキュービック補間処理 2 : Pbitmap[DW] := NearestNeighbor(XD, YD); // 最近傍補間 3 : Pbitmap[DW] := Lanczos(XD, YD, n); // ランツォシュ end; end; end; Image2.Width := Dest.Width; // 表示サイズ設定 Image2.Height := Dest.Height; Image2.Picture.Bitmap.Handle := Dest.ReleaseHandle; // Image2のビットマップハンドルにDestを設定 Destを表示 finally Dest.Free; Src.Free; end; ATime.Stop; Label1.Caption := intTostr(ATime.ElapsedMilliseconds) + ' mSec'; end; procedure TForm1.FormCreate(Sender: TObject); begin Form1.AutoScroll := True; Edit1.Text := '5'; Edit2.Text := '-1'; nEdit.Text := '2'; with RadioGroup1.Items do begin Add('Bilinear'); Add('Bicubic'); Add('NearestNeighbor'); add('Lanczos'); end; RadioGroup1.ItemIndex := 0; end; end.