圖像縮放

 

第一篇文章.

插值算法對於縮放比例較小的情況是完全可以接受的,令人信服的。一般的,縮小0.5倍以上或放大3.0倍以下,對任何圖像都是可以接受的。


最鄰近插值(近鄰取樣法):
  最臨近插值的的思想很簡單。對於通過反向變換得到的的一個浮點座標,對其進行簡單的取整,得到一個整數型座標,這個整數型座標對應的像素值就是目的像素的像素值,也就是說,取浮點座標最鄰近的左上角點(對於DIB是右上角,因爲它的掃描行是逆序存儲的)對應的像素值。可見,最鄰近插值簡單且直觀,但得到的圖像質量不高


雙線性內插值:
  對於一個目的像素,設置座標通過反向變換得到的浮點座標爲(i+u,j+v),其中i、j均爲非負整數,u、v爲[0,1)區間的浮點數,則這個像素得值 f(i+u,j+v) 可由原圖像中座標爲 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所對應的周圍四個像素的值決定,即:

    f(i+u,j+v) = (1-u)(1-v)f(i,j) + (1-u)vf(i,j+1) + u(1-v)f(i+1,j) + uvf(i+1,j+1)

其中f(i,j)表示源圖像(i,j)處的的像素值,以此類推
  這就是雙線性內插值法。雙線性內插值法計算量大,但縮放後圖像質量高,不會出現像素值不連續的的情況。由於雙線性插值具有低通濾波器的性質,使高頻分量受損,所以可能會使圖像輪廓在一定程度上變得模糊

 


  三次卷積法能夠克服以上兩種算法的不足,計算精度高,但計算亮大,他考慮一個浮點座標(i+u,j+v)周圍的16個鄰點,目的像素值f(i+u,j+v)可由如下插值公式得到:

    f(i+u,j+v) = [A] * [B] * [C]

[A]=[ S(u + 1) S(u + 0) S(u - 1) S(u - 2) ]

  ┏ f(i-1, j-1) f(i-1, j+0) f(i-1, j+1) f(i-1, j+2) ┓
[B]=┃ f(i+0, j-1) f(i+0, j+0) f(i+0, j+1) f(i+0, j+2) ┃
  ┃ f(i+1, j-1) f(i+1, j+0) f(i+1, j+1) f(i+1, j+2) ┃
  ┗ f(i+2, j-1) f(i+2, j+0) f(i+2, j+1) f(i+2, j+2) ┛

  ┏ S(v + 1) ┓
[C]=┃ S(v + 0) ┃
  ┃ S(v - 1) ┃
  ┗ S(v - 2) ┛

   ┏ 1-2*Abs(x)^2+Abs(x)^3      , 0<=Abs(x)<1
S(x)={ 4-8*Abs(x)+5*Abs(x)^2-Abs(x)^3 , 1<=Abs(x)<2
   ┗ 0                , Abs(x)>=2
S(x)是對 Sin(x*Pi)/x 的逼近(Pi是圓周率——π)


最鄰近插值(近鄰取樣法)、雙線性內插值、三次卷積法 等插值算法對於旋轉變換、錯切變換、一般線性變換 和 非線性變換 都適用。

第二篇文章.

“插值”最初是電腦的術語,後來引用到數碼圖像上來。圖像放大時,像素也相應地增加,但這些增加的像素從何而來?這時插值就派上用場了:插值就是在不生成像素的情況下增加圖像像素大小的一種方法,在周圍像素色彩的基礎上用數學公式計算丟失像素的色彩(也有的有些相機使用插值,人爲地增加圖像的分辨率)。所以在放大圖像時,圖像看上去會比較平滑、乾淨。但必須注意的是插值並不能增加圖像信息。
1.         最近像素插值算法(Nearest Neighbour interpolation)
  最近像素插值算法是最簡單的一種插值算法,當圖片放大時,缺少的像素通過直接使用與之最接近的原有的像素的顏色生成,也就是說照搬旁邊的像素,這樣做的結果是產生了明顯可見的鋸齒。
2.         雙線性插值(Bilinear interpolation)
  這種算法輸出的圖像的每個像素都是原圖中四個像素(2×2)運算的結果,這種算法極大地消除了鋸齒現象
3.         雙三次插值算法(Bicubic interpolation)
  這種算法是上一種算法的改進算法,它輸出圖像的每個像素都是原圖16個像素(16×16)運算的結果。這種算法是一種很常見的算法,普遍用在圖像編輯軟件、打印機驅動和數碼相機上。
4.         分形算法(Fractal interpolation)
  這是Altamira Group提出的一種算法,這種算法得到的圖像跟其它算法相比更清晰、銳利。
  現在有許多數碼相機生產商將插值算法用在了數碼相機上,並將通過算法得到的分辨率值大肆宣傳,固然他們的算法比雙三次插值算法等算法先進很多,但是事實是:圖像的細節是不能憑空造出來的。對數碼相機不是很熟悉的消費者在購買時一定要注意這個問題。
 
在Windows中做過圖像方面程序的人應該都知道Windows的GDI有一個API函數:StretchBlt,對應在VCL中是TCanvas類的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)等,有興趣的朋友可以自行參考相關書籍研究


 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章