1. 程式人生 > >用線性插值演算法實現影象縮放

用線性插值演算法實現影象縮放

  為了方便理解,先考慮一維情況下的線性插值 
對於一個數列c,我們假設c[a]到c[a+1]之間是線性變化的 
那麼對於浮點數x(a<=x<a+1),c(x)=c[a+1]*(x-a)+c[a]*(1+a-x); 
這個好理解吧?

把這種插值方式擴充套件到二維情況 
對於一個二維陣列c,我們假設對於任意一個浮點數i,c(a,i)到c(a+1,i)之間是線性變化的,c(i,b)到c(i,b+1)之間也是線性變化的(a,b都是整數) 
那麼對於浮點數的座標(x,y)滿足(a<=x<a+1,b<=y<b+1),我們可以先分別求出c(x,b)和c(x,b+1): 
c(x,b) = c[a+1]*(x-a)+c[a]*(1+a-x); 
c(x,b+1) = c[a+1][b+1]*(x-a)+c[a][b+1]*(1+a-x); 
好,現在已經知道c(x,b)和c(x,b+1)了,而根據假設c(x,b)到c(x,b+1)也是線性變化的,所以: 
c(x,y) = c(x,b+1)*(y-b)+c(x,b)*(1+b-y) 

這就是雙線性插值

原理簡述

在對影象進行空間變換的過程中,典型的情況是在對影象進行放大處理的時候,影象會出現失真的現象。這是由於在變換之後的影象中,存在著一些變換之前的影象中沒有的畫素位置。為了說明這個問題,不妨假設有一副大小為64x64的灰度影象A,現在將影象放大到256x256,不妨令其為影象B,如圖 1所示。顯然,根據簡單的幾何換算關係,可以知道B影象中(x,y)處的畫素值應該對應著A影象中的(x/4,y/4)處的象素值,即
B(x,y) = A(x/4,y/4) (式1)
對於B中的(4,4),(4,8),(4,16)…(256,256)這些位置,通過式1就可以計算出其在A中的位置,從而可以得到灰度值。但是,對於B中的(1,1),(1,2),(1,3)…等等這些座標點而言,如果按照式1計算的話,那麼它們在A中對應的座標不再是整數。比如,對於B中的座標點(1,1),其在A中的對應座標就變成了(0.25,0.25)。對於數字影象而言,小數座標是沒有意義的。因此,必須考慮採用某種方法來得到B中畫素點在A中對應位置上的灰度級。
處理這一問題的方法被稱為影象灰度級插值。常用的插值方式有三種:最近鄰域插值、雙線性插值、雙三次插值。理論上來講,最近鄰域插值的效果最差,雙三次插值的效果最好,雙線性插值的效果介於兩者之間。不過對於要求不是非常嚴格的影象插值而言,使用雙線性插值通常就足夠了。
本文中將採用matlab實現一個雙線性插值的程式。
雙線性插值的原理如圖 2所示。影象之間座標對映有兩種方式:如果是從原影象的座標對映到目標影象,稱為前向對映,反之則稱為後向對映。顯然,雙線性插值採用的是後向對映方式。
下面對圖 2的具體含義進行說明。首先,根據幾何關係,從B影象中的座標(x,y)得到A影象中的座標(x/4,y/4),但是,對映得到的這個座標(x/4,y/4)並沒有剛好位於A影象中的整數座標上,而是對映到了四個畫素座標(a,b)、(a+1,b)、(a,b+1)、(a+1,b+1)所圍成的矩形之間,其中,a、b是A影象的整數座標。現在的問題就是如何根據A(a,b)、A(a+1,b)、A(a,b+1)、A(a+1,b+1)這四個點上的灰度級求出A(x/4,y/4)處的灰度級。雙線性插值技術採用的方法是:假設A影象的灰度級變化在縱向方向上是線性變化的,這樣根據直線方程或者幾何比例關係就能夠求得(a,y/4)和(a+1,y/4)座標處的灰度級A(a,y/4)和A(a+1,y/4)。然後,再假設在((a,y/4),A(a,y/4))和(a+1,y/4),A(a+1,y/4))這兩點所確定的直線上,灰度級仍然是線性變化的。求出直線方程,於是就可以求得(x/4,y/4)處的灰度級A(x/4,y/4)。這就是雙線性插值的基本思路。其中用到的兩個基本假設是:首先灰度級在縱向方向上是線性變化的,然後假定灰度級在橫向方向上也是線性變化的。

在 Windows 中做過影象方面程式的人應該都知道 Windows 的 GDI 有一個 API 函式: StretchBlt ,對應在 VCL 中是 TCanvas 類的 StretchDraw 方法。它可以很簡單地實現影象的縮放操作。但問題是它是用了速度最快,最簡單但效果也是最差的“最近鄰域法”,雖然在大多數情況下,它也夠用了,但對於要求較高的情況就不行了。

不久前我做了一個小玩意兒(見 《人個資訊助理之我的相簿》 ),用於管理我用 DC 拍的一堆照片,其中有一個外掛提供了縮放功能,目前的版本就是用了 StretchDraw ,有時效果不能令人滿意,我一直想加入兩個更好的:線性插值法和三次樣條法。經過研究發現三次樣條法的計算量實在太大,不太實用,所以決定就只做線性插值法的版本了。

從數字影象處理的基本理論,我們可以知道:影象的變形變換就是源影象到目標影象的座標變換。簡單的想法就是把源影象的每個點座標通過變形運算轉為目標影象的相應點的新座標,但是這樣會導致一個問題就是目標點的座標通常不會是整數,而且像放大操作會導致目標影象中沒有被源影象的點對映到,這是所謂“向前對映” 方法的缺點。所以一般都是採用“逆向對映”法。

但是逆向對映法同樣會出現對映到源影象座標時不是整數的問題。這裡就需要“重取樣濾波器”。這個術語看起來很專業,其實不過是因為它借用了電子訊號處理中的慣用說法(在大多數情況下,它的功能類似於電子訊號處理中的帶通濾波器),理解起來也不復雜,就是如何確定這個非整數座標處的點應該是什麼顏色的問題。前面說到的三種方法:最近鄰域法,線性插值法和三次樣條法都是所謂的“重取樣濾波器”。

所謂“最近鄰域法”就是把這個非整數座標作一個四捨五入,取最近的整數點座標處的點的顏色。而“線性插值法”就是根據周圍最接近的幾個點(對於平面圖像來說,共有四點)的顏色作線性插值計算(對於平面圖像來說就是二維線性插值)來估計這點的顏色,在大多數情況下,它的準確度要高於最近鄰域法,當然效果也要好得多,最明顯的就是在放大時,影象邊緣的鋸齒比最近鄰域法小非常多。當然它同時還帶業個問題:就是影象會顯得比較柔和。這個濾波器用專業術語來說(呵呵,賣弄一下偶的專業 ^_^ )叫做:帶阻效能好,但有帶通損失,通帶曲線的矩形係數不高。至於三次樣條法我就不說了,複雜了一點,可自行參考數字影象處理方面的專業書籍,如本文的參考文獻。

再來討論一下座標變換的演算法。簡單的空間變換可以用一個變換矩陣來表示:

[x’,y’,w’]=[u,v,w]*T

其中: x’,y’ 為目標影象座標, u,v 為源影象座標, w,w’ 稱為齊次座標,通常設為 1 , T 為一個 3X3 的變換矩陣。

這種表示方法雖然很數學化,但是用這種形式可以很方便地表示多種不同的變換,如平移,旋轉,縮放等。對於縮放來說,相當於:

                                [Su  0  0 ]

[x, y, 1] = [u, v, 1] *  [0  Sv  0 ]
                                [0   0   1 ]

其中 Su,Sv 分別是 X 軸方向和 Y 軸方向上的縮放率,大於 1 時放大,大於 0 小於 1 時縮小,小於 0 時反轉。

矩陣是不是看上去比較暈?其實把上式按矩陣乘法展開就是:

{ x = u * Su

{ y = v * Sv

就這麼簡單。 ^_^

有了上面三個方面的準備,就可以開始編寫程式碼實現了。思路很簡單:首先用兩重迴圈遍歷目標影象的每個點座標,通過上面的變換式(注意:因為是用逆向對映,相應的變換式應該是: u = x / Su 和 v = y / Sv )取得源座標。因為源座標不是整數座標,需要進行二維線性插值運算:

P = n*b*PA + n * ( 1 – b )*PB + ( 1 – n ) * b * PC + ( 1 – n ) * ( 1 – b ) * PD

其中: n 為 v (對映後相應點在源影象中的 Y 軸座標,一般不是整數)下面最接近的行的 Y 軸座標與 v 的差;同樣 b 也類似,不過它是 X 軸座標。 PA-PD 分別是 (u,v) 點周圍最接近的四個(左上,右上,左下,右下)源影象點的顏色(用 TCanvas 的 Pixels 屬性)。 P 為 (u,v) 點的插值顏色,即 (x,y) 點的近似顏色。

這段程式碼我就不寫的,因為它的效率實在太低:要對目標影象的每一個點的 RGB 進行上面那一串複雜的浮點運算。所以一定要進行優化。對於 VCL 應用來說,有個比較簡單的優化方法就是用 TBitmap 的 ScanLine 屬性,按行進行處理,可以避免 Pixels 的畫素級操作,對效能可以有很大的改善。這已經是算是用 VCL 進行影象處理的基本優化常識了。不過這個方法並不總是管用的,比如作影象旋轉的時候,這時需要更多的技巧。

無論如何,浮點運算的開銷都是比整數大很多的,這個也是一定要優化掉的。從上面可以看出,浮點數是在變換時引入的,而變換引數 Su,Sv 通常就是浮點數,所以就從它下手優化。一般來說, Su,Sv 可以表示成分數的形式:

Su = ( double )Dw / Sw; Sv = ( double )Dh / Sh

其中 Dw, Dh 為目標影象的寬度和高度, Sw, Sh 為源影象的寬度和高度(因為都是整數,為求得浮點結果,需要進行型別轉換)。

將新的 Su, Sv 代入前面的變換公式和插值公式,可以匯出新的插值公式:

因為:

b = 1 – x * Sw % Dw / ( double )Dw;  n = 1 – y * Sh % Dh / ( double )Dh

設:

B = Dw – x * Sw % Dw; N = Dh – y * Sh % Dh

則:

b = B / ( double )Dw; n = N / ( double )Dh

用整數的 B , N 代替浮點的 b, n ,轉換插值公式:

P = ( B * N * ( PA – PB – PC + PD ) + Dw * N * PB + DH * B * PC + ( Dw * Dh – Dh * B – Dw * N ) * PD ) / ( double )( Dw * Dh )

這裡最終結果 P 是浮點數,對其四捨五入即可得到結果。為完全消除浮點數,可以用這樣的方法進行四捨五入:

P = ( B * N … * PD + Dw * Dh / 2 ) / ( Dw * Dh )

這樣, P 就直接是四捨五入後的整數值,全部的計算都是整數運算了。

簡單優化後的程式碼如下:

int __fastcall TResizeDlg::Stretch_Linear(Graphics::TBitmap * aDest, Graphics::TBitmap * aSrc)

{

    int sw = aSrc->Width - 1, sh = aSrc->Height - 1, dw = aDest->Width - 1, dh = aDest->Height - 1;

    int B, N, x, y;

    int nPixelSize = GetPixelSize( aDest->PixelFormat );

    BYTE * pLinePrev, *pLineNext;

    BYTE * pDest;

    BYTE * pA, *pB, *pC, *pD;

    for ( int i = 0; i <= dh; ++i )

    {

        pDest = ( BYTE * )aDest->ScanLine[i];

        y = i * sh / dh;

        N = dh - i * sh % dh;

        pLinePrev = ( BYTE * )aSrc->ScanLine[y++];

        pLineNext = ( N == dh ) ? pLinePrev : ( BYTE * )aSrc->ScanLine[y];

        for ( int j = 0; j <= dw; ++j )

        {

             x = j * sw / dw * nPixelSize;

            B = dw - j * sw % dw;

            pA = pLinePrev + x;

            pB = pA + nPixelSize;

            pC = pLineNext + x;

            pD = pC + nPixelSize;

            if ( B == dw )

            {

                pB = pA;

                pD = pC;

            }

            for ( int k = 0; k < nPixelSize; ++k )

                *pDest++ = ( BYTE )( int )(

                    ( B * N * ( *pA++ - *pB - *pC + *pD ) + dw * N * *pB++

                    + dh * B * *pC++ + ( dw * dh - dh * B - dw * N ) * *pD++

                    + dw * dh / 2 ) / ( dw * dh )

                );

        }

    }

    return 0;

}

應該說還是比較簡潔的。因為寬度高度都是從 0 開始算,所以要減一, GetPixelSize 是根據 PixelFormat 屬性來判斷每個畫素有多少位元組,此程式碼只支援 24 或 32 位色的情況(對於 15 或 16 位色需要按位拆開 — 因為不拆開的話會在計算中出現不期望的進位或借位,導致影象顏色混亂 — 處理較麻煩;對於 8 位及 8 位以下索引色需要查調色盤,並且需要重索引,也很麻煩,所以都不支援;但 8 位灰度影象可以支援)。另外程式碼中加入一些在影象邊緣時防止訪問越界的程式碼。

通過比較,在 PIII-733 的機器上,目標影象小於 1024x768 的情況下,基本感覺不出速度比 StretchDraw 有明顯的慢(用浮點時感覺比較明顯)。效果也相當令人滿意,不論是縮小還是放大,影象質量比 StretchDraw 方法有明顯提高。

不過由於採用了整數運算,有一個問題必須加以重視,那就是溢位的問題:由於式中的分母是 dw * dh ,而結果應該是一個 Byte 即 8 位二進位制數,有符號整數最大可表示 31 位二進位制數,所以 dw * dh 的值不能超過 23 位二進位制數,即按 2:1 的寬高比計算目標影象解析度不能超過 4096*2048 。當然這個也是可以通過用無符號數(可以增加一位)及降低計算精度等方法來實現擴充套件的,有興趣的朋友可以自己試試。

當然這段程式碼還遠沒有優化到極致,而且還有很多問題沒有深入研究,比如抗混疊( anti-aliasing )等,有興趣的朋友可以自行參考相關書籍研究,如果你有什麼研究成果,非常歡迎你為我的程式編寫外掛實現。

[ Mental Studio ] 猛禽

2004-3-28

參考文獻:

崔屹《數字影象處理技術與應用》電子工業出版社, 1997

//上面的只能是24位的點陣圖,現在修改了以個Delphi版本的,支援多種點陣圖格式了,應該。測試,32和24位都可

procedure StretchBitmap(Dest, Src: TBitmap);
var
  sw, sh, dw, dh, B, N, x, y, i, j, k, nPixelSize: DWord;
  pLinePrev, pLineNext, pDest, pA, pB, pC, pD: PByte;
begin
  sw := Src.Width -1;
  sh := Src.Height -1;
  dw := Dest.Width -1;
  dh := Dest.Height -1;
  //獲得顯示模式
  nPixelSize := Integer(Src.PixelFormat);
  if nPixelSize < 4 then
    nPixelSize := 4
  else if nPixelSize = 4 then
    inc(nPixelSize)
  else if nPixelSize > 7 then
    nPixelSize := 7;
  Dest.PixelFormat := TPixelFormat(nPixelSize);
  nPixelSize := nPixelSize - 3;
  for i := 0 to dh do
  begin
    pDest := Dest.ScanLine[i];
    y := i * sh div dh;
    N := dh - i * sh mod dh;
    pLinePrev := Src.ScanLine[y];
    Inc(y);
    if N = dh then
      pLineNext := pLinePrev
    else
      pLineNext := Src.ScanLine[y];
    for j := 0 to dw do
    begin
      x := j * sw div dw * nPixelSize;
      B := dw - j * sw mod dw;
      pA := pLinePrev;
      Inc(pA, x);
      pB := pA;
      Inc(pB, nPixelSize);
      pC := pLineNext;
      Inc(pC, x);
      pD := pC;
      Inc(pD, nPixelSize);
      if B = dw then begin
        pB := pA;
        pD := pC;
      end;
      for k := 0 to nPixelSize -1 do
      begin
        pDest^ := Byte(DWord( (B * N * DWord(pA^ - pB^ - pC^ + pD^) + dw * N * pB^
                              + dh * B * pC^ + (dw * dh - dh * B - dw * N)* pD^
                              + dw * dh div 2) div (dw * dh) ));
        Inc(pDest);
        Inc(pA);
        Inc(pB);
        Inc(pC);
        Inc(pD);
      end;
    end;
  end;
end;