在靜態(tài)圖像和視頻處理的編程中經(jīng)常遇到位圖縮放的操作, 一般可用windows API或者一些SDK來實(shí)現(xiàn), 或者是用線型插值法.
1. windows API一般是Bitblt來完成, 在之前做的一個(gè)項(xiàng)目中用的就是這種方法,效率很好,在雙核2.0,1G內(nèi)存的機(jī)器上所耗時(shí)間小于1ms.
2. 利用開源的CxImage來實(shí)現(xiàn).這種方法沒有試過.
3. 第3種就是線性插值法,這個(gè)一直不是很理解,比如說800X600縮放到1024X768, 插值的大小可以用前后2個(gè)象素的平均值來計(jì)算. 但是這些值怎么插法, 因?yàn)殚L寬不是2倍.如果是整數(shù)倍則很好處理每個(gè)象素后插一個(gè)即可.一直對(duì)這個(gè)疑惑不解,今天又想起這個(gè)問題,因此把它寫下來以做日后思考.???
以下的文字可以解釋上面的問題(摘自網(wǎng)頁)
在Windows中做過圖像方面程序的人應(yīng)該都知道Windows的GDI有一個(gè)API函數(shù):StretchBlt,對(duì)應(yīng)在VCL中是TCanvas類的StretchDraw方法。它可以很簡單地實(shí)現(xiàn)圖像的縮放操作。但問題是它是用了速度最快,最簡單但效果也是最差的“最近鄰域法”,雖然在大多數(shù)情況下,它也夠用了,但對(duì)于要求較高的情況就不行了。
不久前我做了一個(gè)小玩意兒(見《人個(gè)信息助理之我的相冊(cè)》),用于管理我用DC拍的一堆照片,其中有一個(gè)插件提供了縮放功能,目前的版本就是用了StretchDraw,有時(shí)效果不能令人滿意,我一直想加入兩個(gè)更好的:線性插值法和三次樣條法。經(jīng)過研究發(fā)現(xiàn)三次樣條法的
計(jì)算量實(shí)在太大,不太實(shí)用,所以決定就只做線性插值法的版本了。
從數(shù)字圖像處理的基本理論,我們可以知道:圖像的變形變換就是源圖像到目標(biāo)圖像的坐標(biāo)變換。簡單的想法就是把源圖像的每個(gè)點(diǎn)坐標(biāo)通過變形運(yùn)算轉(zhuǎn)為目標(biāo)圖像的相應(yīng)點(diǎn)的新坐標(biāo),但是這樣會(huì)導(dǎo)致一個(gè)問題就是目標(biāo)點(diǎn)的坐標(biāo)通常不會(huì)是整數(shù),而且像放大操作會(huì)導(dǎo)致目標(biāo)圖像中沒有被源圖像的點(diǎn)映射到,這是所謂“向前映射 ”方法的缺點(diǎn)。所以一般都是采用“逆向映射”法。
但是逆向映射法同樣會(huì)出現(xiàn)映射到源圖像坐標(biāo)時(shí)不是整數(shù)的問題。這里就需要“重采樣濾波器”。這個(gè)術(shù)語看起來很專業(yè),其實(shí)不過是因?yàn)樗栌昧穗娮有盘?hào)處理中的慣用說法(在大多數(shù)情況下,它的功能類似于電子信號(hào)處理中的帶通濾波器),理解起來也不復(fù)雜,就是如何確定這個(gè)非整數(shù)坐標(biāo)處的點(diǎn)應(yīng)該是什么顏色的問題。前面說到的三種方法:最近鄰域法,線性插值法和三次樣條法都是所謂的“重采樣濾波器”。
所謂“最近鄰域法”就是把這個(gè)非整數(shù)坐標(biāo)作一個(gè)四舍五入,取最近的整數(shù)點(diǎn)坐標(biāo)處的點(diǎn)的顏色。而“線性插值法”就是根據(jù)周圍最接近的幾個(gè)點(diǎn)(對(duì)于平面圖像來說,共有四點(diǎn))的顏色作線性插值計(jì)算(對(duì)于平面圖像來說就是二維線性插值)來估計(jì)這點(diǎn)的顏色,在大多數(shù)情況下,它的準(zhǔn)確度要高于最近鄰域法,當(dāng)然效果也要好得多,最明顯的就是在放大時(shí),圖像邊緣的鋸齒比最近鄰域法小非常多。當(dāng)然它同時(shí)還帶業(yè)個(gè)問題:就是圖像會(huì)顯得比較柔和。這個(gè)濾波器用專業(yè)術(shù)語來說(呵呵,賣弄一下偶的專業(yè)^_^)叫做:帶阻性能好,但有帶通損失,通帶曲線的矩形系數(shù)不高。至于三次樣條法我就不說了,復(fù)雜了一點(diǎn),可自行參考數(shù)字圖像處理方面的專業(yè)書籍,如本文的參考文獻(xiàn)。
再來討論一下坐標(biāo)變換的算法。簡單的空間變換可以用一個(gè)變換矩陣來表示:
[x’,y’,w’]=[u,v,w]*T
其中:x’,y’為目標(biāo)圖像坐標(biāo),u,v為源圖像坐標(biāo),w,w’稱為齊次坐標(biāo),通常設(shè)為1,T為一個(gè)3X3的變換矩陣。這種表示方法雖然很數(shù)學(xué)化,但是用這種形式可以很方便地表示多種不同的變換,如平移,旋轉(zhuǎn),縮放等。對(duì)于縮放來說,相當(dāng)于:
[Su 0 0 ]
[x, y, 1] = [u, v, 1] * | 0 Sv 0 |
[0 0 1 ]
其中Su,Sv分別是X軸方向和Y軸方向上的縮放率,大于1時(shí)放大,大于0小于1時(shí)縮小,小于0時(shí)反轉(zhuǎn)。
矩陣是不是看上去比較暈?其實(shí)把上式按矩陣乘法展開就是:
{ x = u * Su
{ y = v * Sv
就這么簡單。^_^
有了上面三個(gè)方面的準(zhǔn)備,就可以開始編寫代碼實(shí)現(xiàn)了。思路很簡單:首先用兩重循環(huán)遍歷目標(biāo)圖像的每個(gè)點(diǎn)坐標(biāo),通過上面的變換式(注意:因?yàn)槭怯媚嫦蛴成,相?yīng)的變換式應(yīng)該是:u = x / Su 和v = y / Sv)取得源坐標(biāo)。因?yàn)樵醋鴺?biāo)不是整數(shù)坐標(biāo),需要進(jìn)行二維線性插值運(yùn)算:
P = n*b*PA + n * ( 1 – b )*PB + ( 1 – n ) * b * PC + ( 1 – n ) * ( 1 – b ) * PD
其中:n為v(映射后相應(yīng)點(diǎn)在源圖像中的Y軸坐標(biāo),一般不是整數(shù))下面最接近的行的Y軸坐標(biāo)與v的差;同樣b也類似,不過它是X軸坐標(biāo)。PA-PD分別是(u,v)點(diǎn)周圍最接近的四個(gè)(左上,右上,左下,右下)源圖像點(diǎn)的顏色(用TCanvas的Pixels屬性)。P為(u,v)點(diǎn)的插值顏色,即(x,y)點(diǎn)的近似顏色。這段代碼我就不寫的,因?yàn)樗男蕦?shí)在太低:要對(duì)目標(biāo)圖像的每一個(gè)點(diǎn)的RGB進(jìn)行上面那一串復(fù)雜的浮點(diǎn)運(yùn)算。所以一定要進(jìn)行優(yōu)化。對(duì)于VCL應(yīng)用來說,有個(gè)比較簡單的優(yōu)化方法就是用TBitmap的ScanLine屬性,按行進(jìn)行處理,可以避免Pixels的像素級(jí)操作,對(duì)性能可以有很大的改善。這已經(jīng)是算是用VCL進(jìn)行圖像處理的基本優(yōu)化常識(shí)了。不過這個(gè)方法并不總是管用的,比如作圖像旋轉(zhuǎn)的時(shí)候,這時(shí)需要更多的技巧。
無論如何,浮點(diǎn)運(yùn)算的開銷都是比整數(shù)大很多的,這個(gè)也是一定要優(yōu)化掉的。從上面可以看出,浮點(diǎn)數(shù)是在變換時(shí)引入的,而變換參數(shù)Su,Sv通常就是浮點(diǎn)數(shù),所以就從它下手優(yōu)化。一般來說,Su,Sv可以表示成分?jǐn)?shù)的形式:Su = ( double )Dw / Sw; Sv = ( double )Dh / Sh
其中Dw, Dh為目標(biāo)圖像的寬度和高度,Sw, Sh為源圖像的寬度和高度(因?yàn)槎际钦麛?shù),為求得浮點(diǎn)結(jié)果,需要進(jìn)行類型轉(zhuǎn)換)。將新的Su, Sv代入前面的變換公式和插值公式,可以導(dǎo)出新的插值公式:
因?yàn)椋篵 = 1 – x * Sw % Dw / ( double )Dw; n = 1 – y * Sh % Dh / ( double )Dh
設(shè):B = Dw – x * Sw % Dw; N = Dh – y * Sh % Dh
則:b = B / ( double )Dw; n = N / ( double )Dh
用整數(shù)的B,N代替浮點(diǎn)的b, n,轉(zhuǎn)換插值公式:
P = ( B * N * ( PA – PB – PC + PD ) + Dw * N * PB + DH * B * PC + ( Dw * Dh – Dh * B – Dw * N ) * PD ) / ( double )( Dw * Dh )
這里最終結(jié)果P是浮點(diǎn)數(shù),對(duì)其四舍五入即可得到結(jié)果。為完全消除浮點(diǎn)數(shù),可以用這樣的方法進(jìn)行四舍五入:P = ( B * N … * PD + Dw * Dh / 2 ) / ( Dw * Dh )
這樣,P就直接是四舍五入后的整數(shù)值,全部的計(jì)算都是整數(shù)運(yùn)算了。
簡單優(yōu)化后的代碼如下:
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;
}
應(yīng)該說還是比較簡潔的。因?yàn)閷挾雀叨榷际菑?開始算,所以要減一,GetPixelSize是根據(jù)PixelFormat屬性來判斷每個(gè)像素有多少字節(jié),此代碼只支持24或32位色的情況(對(duì)于15或16位色需要按位拆開—因?yàn)椴徊痖_的話會(huì)在計(jì)算中出現(xiàn)不期望的進(jìn)位或借位,導(dǎo)致圖像顏色混亂—處理較麻煩;對(duì)于8位及8位以下索引色需要查調(diào)色板,并且需要重索引,也很麻煩,所以都不支持;但8位灰度圖像可以支持)。另外代碼中加入一些在圖像邊緣時(shí)防止訪問越界的代碼。
通過比較,在PIII-733的機(jī)器上,目標(biāo)圖像小于1024×768的情況下,基本感覺不出速度比StretchDraw有明顯的慢(用浮點(diǎn)時(shí)感覺比較明顯)。效果也相當(dāng)令人滿意,不論是縮小還是放大,圖像質(zhì)量比StretchDraw方法有明顯提高。不過由于采用了整數(shù)運(yùn)算,有一個(gè)問題必須加以重視,那就是溢出的問題:由于式中的分母是dw * dh,而結(jié)果應(yīng)該是一個(gè)Byte即8位二進(jìn)制數(shù),有符號(hào)整數(shù)最大可表示31位二進(jìn)制數(shù),所以dw * dh的值不能超過23位二進(jìn)制數(shù),即按2:1的寬高比計(jì)算目標(biāo)圖像分辨率不能超過
4096*2048。當(dāng)然這個(gè)也是可以通過用無符號(hào)數(shù)(可以增加一位)及降低計(jì)算精度等方法來實(shí)現(xiàn)的,有興趣的朋友可以自己試試。
當(dāng)然這段代碼還遠(yuǎn)沒有優(yōu)化到極致,而且還有很多問題沒有深入研究,比如抗混疊(anti-aliasing)等
|