画像拡大、縮小時の補間

 GDI Plus の使用

GDI補間画像の拡大縮小を行う時、各ドットの中間の値が必要になります。
その時の画像の補間方法として、色々な方法があり、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)法
1、、X、X、Y、Y、Y、Y  は求める位置から参照する画素までの距離を表します。
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)/πDsinc関数と呼ばれるもので、Dを無限遠点にまで応用すれば、理論的には最も完全な補間となり、バイキュービックの補間式はこれを近似したものですが、無限遠点まで用いるのは現実的ではないため、普通は使用されません。
 この計算方法は、計算に非常に時間がかかり バイキュービック(BiCubic)法 に比べ四倍ぐらい時間が掛かり、あまり使用されていませんが、現在CPUの計算速度が上がっているので、必要に応じて使用しても良いかと思います。
重み係数に負の値が出現するので、場合によって結果が負になる時があるので、負の時は値をゼロとするような処理が必要です。


ランツォシュ
補間サンプルVCL
 サンプル画像のアンシャープ+ランツォシュは、プログラムに組み込まれていません。
最初にアンシャープ処理をして保存をし、その後、ランツォシュ補間をして拡大しています。
 サンプルプログラムは、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.



    download interpolation.zip

画像処理一覧へ戻る

      最初に戻る