插值

圖像縮放算法是數字圖像處理算法中經常遇到的問題。我們經常會將某種尺寸的圖像轉換爲其他尺寸的圖像,如放大或者縮小圖像。OpenCV中的Resize() 函數非常方便而且效率非常高。下面是OPENCV提供的cvResize函數原型。

/****************************************************************************************************/
圖像大小變換 
void cvResize( const CvArr* src, CvArr* dst, int interpolation=CV_INTER_LINEAR );
src 
輸入圖像. 
dst 
輸出圖像. 
interpolation 
插值方法: 
CV_INTER_NN - 最近鄰插值, 
CV_INTER_LINEAR - 雙線性插值 (缺省使用) 
CV_INTER_AREA - 使用象素關係重採樣。當圖像縮小時候,該方法可以避免波紋出現。當圖像放大時,類似於 CV_INTER_NN 方法.. 
CV_INTER_CUBIC - 立方插值. 
函數 cvResize 將圖像 src 改變尺寸得到與 dst 同樣大小。若設定 ROI,函數將按常規支持 ROI.
/****************************************************************************************************/

  相信使用過Opencv的朋友都知道如何使用此函數。下面根據我自己的理解,用VC++ 來實現圖像縮放算法 ,希望大家能從中理解圖像縮放算法的原理。

 


第1節最近鄰插值

 

  最簡單的圖像縮放算法就是最近鄰插值。顧名思義,就是將目標圖像各點的像素值設爲源圖像中與其最近的點。假設源圖像的寬度和高度分別爲w0和h0, 縮放後的目標圖像的寬度和高度分別爲w1和h1, 那麼比例就是float fw = float(w0)/w1; float fh =float(h0)/h1; 對於目標圖像中的(x,y)點座標對應着源圖像中的(x0,y0)點。其中:x0= int(x*fw), y0 = int(y*fh)。

  示例1:現在將一張670*503的BMP圖像縮放到200*160,代碼和效果如下。

void ResizeNear01(CImage &src, CImage &dst)
{
    int w0 = src.GetWidth();
    int h0 = src.GetHeight();

    int w1 = dst.GetWidth();
    int h1 = dst.GetHeight();

    float fw = float(w0) / w1;
    float fh = float(h0) / h1;

    int x0, y0;
    for(int y=0; y<h1; y++)
    {
        y0 = int(y * fh);
        for(int x=0; x<w1; x++)
        {
            x0 = int(x * fw);
            dst.SetPixel(x, y, src.GetPixel(x0, y0));
        }
    }
}

分析:對於此程序,我們將執行此ResizeNear01函數的語句加上一個for循環,使其執行100次,看它的速度怎麼樣。

#include <time.h>
void CResizeDemoDlg::OnBnClickedButton1()
{
    // TODO: Add your control notification handler code here
    CImage src, dst;
    src.Load(L"d:\\1.bmp");
    dst.Create(200, 160, 24);

    clock_t start = clock();
    for(int i=0; i<100; i++)
    {
        ResizeNear01(src, dst);
    }
    float end = float(clock() - start)/CLOCKS_PER_SEC;
    CString str;
    str.Format(L"%6.2f", end);
    MessageBox(str);


    dst.Save(L"d:\\rs.jpg");
}

  顯示程序執行所用的時間爲20.59秒,平均一次需要0.2秒,看起來速度還可以,是因爲時間複雜度較低。目標圖像的尺寸大小是200*160。


  示例2:示例1中的算法可以改進速度的地方有兩個。第一,因爲最近鄰插值算法求源圖像中的座標是固定的,可以把每個目標每個x和每個y對應的值通過一次循環先求出來,再利進入雙重循環。第二,使用指針效率更高,如果使用CImage提供的GetPixel和SetPixel是費時間的。

  

//優化後的最近鄰插值算法
void ResizeNear02(CImage &src, CImage &dst)
{
    int w0 = src.GetWidth();
    int h0 = src.GetHeight();
    int pitch0 = src.GetPitch();

    int w1 = dst.GetWidth();
    int h1 = dst.GetHeight();
    int pitch1 = dst.GetPitch();

    float fw = float(w0) / w1;
    float fh = float(h0) / h1;

    int *arr_x = newint[w1];
    int *arr_y = newint[h1];
    for(int y=0; y<h1; y++)
    {
        arr_y[y] = int(y*fh);
    }
    for(int x=0; x<w1; x++)
    {
        arr_x[x] = int(x*fw);
    }

    BYTE* pSrc = (BYTE*)src.GetBits();
    BYTE* pDst = (BYTE*)dst.GetBits();
    BYTE* p0, *p1;
    for(int y=0; y<h1; y++)
    {
        p0 = pSrc + pitch0 * arr_y[y];
        p1 = pDst + pitch1 * y;
        for(int x=0; x<w1; x++)
        {
            //dst.SetPixel(x, y, src.GetPixel(arr_x[x], arr_y[y]));
            memcpy(p1 + 3*x, p0 + arr_x[x]*3, 3);
        }
    }

    delete []arr_x;
    delete []arr_y;
}

  同樣執行示例1中的測試程序,讓ResizeNear02也循環一百次,在我機器上測試得到的結果是0.05秒,速度提高了400倍,這是一件多麼讓人興奮的事情啊。
本人接觸圖像處理雖然也快一年了,但還是新手,望大家多多指正。

 

 

雙線性插值作爲OpenCV中默認使用的圖像縮放算法,其效果和速度都是不錯的。並且效果也比較穩定,計算複雜度並不算太高。我看了很多網上的算法,自己也沒看太懂,下面是從網上找的雙線性插值算法的講解。
    “圖像的雙線性插值放大算法中,目標圖像中新創造的象素值,是由源圖像位置在它附近的2*2區域4個鄰近象素的值通過加權平均計算得出的。雙線性內插值算法放大後的圖像質量較高,不會出現像素值不連續的的情況。然而次算法具有低通濾波器的性質,使高頻分量受損,所以可能會使圖像輪廓在一定程度上變得模糊。”


    下面還是根據我自己的理解來繼續講述吧,相信讀者中有很多高手,希望讀者能給予我指點一下,讓我也能更明白一些。
    雙線性插值 算法和最近鄰插值算法比較類似。在最近鄰插值算法中,目標圖像中的某個點(x,y)是去源圖像中找最鄰近的一個點(x0, y0)即可。目標圖像中的點(x, y)對應於源圖像中的點(x0',y0'),x0'、y0'很可能不是整數,而是小數,而最近鄰插值算法是找其鄰近整型值(int(x0'+0.5f),int(y0'+0.5f))(上篇文章中沒有進行四捨五入)。我們現在找x0', y0'所在位置旁邊的四個點,根據這四個點與(x0',y0')距離的關係計算目標圖像中(x,y)一點的像素值。算法描述如下:

 

 

(1)計算源圖像與目標圖像寬與高的比例
w0 : 表示源圖像的寬度
h0 : 表示源圖像的高度
w1 : 表示目標圖像的寬度
h1 : 表示目標圖像的高度
float fw = float(w0-1)/(w1-1);
float fh = float(h0-1)/(h1-1);
(2)針對目標圖像的一個點(x, y),計算在源圖像中的對應座標,結果爲浮點數。
float x0 = x * fw;
float y0 = y * fh;

int x1 = int(x0);
int x2 = x1 + 1;
int y1 = int(y0);
int y2 = y1+1;

所求的源圖像中的四個點座標爲(x1,y1) (x1, y2) (x2, y1) (x2,y2)
(3)求周圍四個點所佔的權重比值
如上圖,
fx1 = x0 - x1;
fx2 = 1.0f - fx1;
fy1 = y0 - y1; 
fy2 = 1.0f - fy1;

float s1 = fx1*fy1;
float s2 = fx2*fy1;
float s3 = fx2*fy2;
float s4 = fx1*fy2;

我們以value(座標)來代表取得此點的座標值,則:
value(x0,y0) = value(x2,y2)*s1+value(x1,y2)*s2+value(x1,y1)*s3+value(x2,y1)*s4;

如果 對上述運算不夠明白 的話,可以這樣來求。
我們先要求得(x0,y1) 和(x0,y2)的像素值。
則floatvalue(x0,y1) = value(x1,y1)*fx2 + value(x2,y1)*fx1;
float value(x0,y2) = value(x1,y2)*fx2 + value(x2,y2)*fx1;
註釋:離某點越近,離權重越大,故取其與1的差值。
float value(x0,y0) = value(x0,y1)*fy2 + value(x0,y2)*fy1;
驗證後與上邊公式一樣。
(4)求得值後填充到目標圖像上就可以了。

爲了能讓人更容易理解,咱還是使用GetPixel和SetPixel進行取值和賦值。

 

void ResizeLinear01(CImage& src, CImage& dst)
{
    int w0 = src.GetWidth();
    int h0 = src.GetHeight();
    int pitch0 = src.GetPitch();

    int w1 = dst.GetWidth();
    int h1 = dst.GetHeight();
    int pitch1 = dst.GetPitch();

    float fw = float(w0) / w1;
    float fh = float(h0) / h1;

    int y1,y2, x1,x2, x0,y0;
    float fx1,fx2, fy1, fy2;
    for(int y=0; y<h1; y++)
    {
        y0 = y*fh;
        y1 = int(y0);
        if(y1 == h0-1)    y2 = y1;
        else y2 = y1 + 1;

        fy1 = y1-y0; 
        fy2 = 1.0f - fy1;
        for(int x=0; x<w1; x++)
        {
            x0 = x*fw;
            x1 = int(x0);
            if(x1 == w0-1)    x2 = x1;
            else x2 = x1+1;

            fx1 = y1-y0;
            fx2 = 1.0f - fx1;

            float s1 = fx1*fy1;
            float s2 = fx2*fy1;
            float s3 = fx2*fy2;
            float s4 = fx1*fy2;

            COLORREF c1,c2,c3,c4, color;
            c1 = src.GetPixel(x1,y1);
            c2 = src.GetPixel(x2,y1);
            c3 = src.GetPixel(x1,y2);
            c4 = src.GetPixel(x2,y2);
            BYTE r,g,b;
            r = (BYTE)(GetRValue(c1)*s3) + (BYTE)(GetRValue(c2)*s4) + (BYTE)(GetRValue(c3)*s2) + (BYTE)(GetRValue(c4)*s1);
            g = (BYTE)(GetGValue(c1)*s3) + (BYTE)(GetGValue(c2)*s4) + (BYTE)(GetGValue(c3)*s2) + (BYTE)(GetGValue(c4)*s1);
            b = (BYTE)(GetBValue(c1)*s3) + (BYTE)(GetBValue(c2)*s4) + (BYTE)(GetBValue(c3)*s2) + (BYTE)(GetBValue(c4)*s1);

            dst.SetPixelRGB(x, y, r, g, b);
        }
    }

}

 

測試程序仍是將670*503尺寸的圖片縮放爲200*160。經過測試,上邊的算法執行一次就需要0.5秒左右,可以說是非常的慢。如果將其縮放成2000*1600大小的圖片,一次就需要50秒。這是非常可怕的,假如現在我們做一個類似PHOTOSHOP的軟件,用戶想將其擴大若干倍,卻要等50秒,估計用戶已經沒有耐心使用您的軟件了。
我們來分析一下怎樣可以優化程序。在每一步優化算法後,我們都再用上邊同樣的程序和步驟進行測試,觀察運行所用時間。
(1)改函數調用 爲指針操作如第1節。(進行完此步後,程序只需要0.2秒左右,速度提高了250倍,哈哈!)
(2)將x0,y0座標的計算提取到 循環外,因爲第二層循環裏的x座標每次循環都要重複一次,並且是重複的。(仍然是需要0.2秒左右。我們讓其循環一百次,再比較下所用時間。使用ResizeLinear02方法用時19.6秒,使用ResizeLinear02方法都是用時17.4秒,看來還是有作用的。)

下面是最終的代碼。

void ResizeLinear04(CImage& src, CImage& dst)
{
    int w0 = src.GetWidth();
    int h0 = src.GetHeight();
    int pitch0 = src.GetPitch();

    int w1 = dst.GetWidth();
    int h1 = dst.GetHeight();
    int pitch1 = dst.GetPitch();

    BYTE* pSrc = (BYTE*)src.GetBits();
    BYTE* pDst = (BYTE*)dst.GetBits();
    BYTE* p0, *p1 = pDst;

    float fw = float(w0-1) / (w1-1);
    float fh = float(h0-1) / (h1-1);

    float x0, y0;
    int y1, y2, x1, x2;
    float fx1, fx2, fy1, fy2;
    
    int* arr_x1 = newint[w1];
    int* arr_x2 = newint[w1];
    float* arr_fx1 = newfloat[w1];

    for(int x=0; x<w1; x++)
    {
        x0 = x*fw;
        arr_x1[x] = int(x0);
        arr_x2[x] = int(x0+0.5f);
        arr_fx1[x] = x0 - arr_x1[x];
        //TRACE(L"x=%6d; x0=%6.3f; x1=%6d; x2=%6d; fx1=%6.3f;\n", x, x0, arr_x1[x], arr_x2[x], arr_fx1[x]);
    }    
    for(int y=0; y<h1; y++)
    {
        y0 = y*fh;
        y1 = int(y0);
        y2 = int(y0+0.5f);
        fy1 = y0-y1; 
        fy2 = 1.0f - fy1;
        //TRACE(L"y=%6d; y0=%6.3f; y1=%6d; y2=%6d; fy1=%6.3f;\n", y, y0, y1, y2, fy1);
        for(int x=0; x<w1; x++)
        {
            x1 = arr_x1[x];
            x2 = arr_x2[x];
            fx1 = arr_fx1[x];
            fx2 = 1.0f-fx1;

            float s1 = fx2*fy2;
            float s2 = fx1*fy2;
            float s3 = fx1*fy1;
            float s4 = fx2*fy1;
            //TRACE(L"s1=%6.3f; s2=%6.3f; s3=%6.3f; s4=%6.3f; sum=%6.3f\n", s1,s2,s3,s4, s1+s2+s3+s4);
            BYTE* p11 = pSrc + pitch0*y1 + 3*x1;
            BYTE* p12 = pSrc + pitch0*y1 + 3*x2;
            BYTE* p21 = pSrc + pitch0*y2 + 3*x1;
            BYTE* p22 = pSrc + pitch0*y2 + 3*x2;
            
            *p1 = BYTE((*p11)*s1 + (*p12)*s2 + (*p21)*s4 + (*p22)*s3);    p1++;    p11++; p12++; p21++; p22++;
            *p1 = BYTE((*p11)*s1 + (*p12)*s2 + (*p21)*s4 + (*p22)*s3);    p1++;    p11++; p12++; p21++; p22++;
            *p1 = BYTE((*p11)*s1 + (*p12)*s2 + (*p21)*s4 + (*p22)*s3);  p1++;

        }
        p1 = pDst + y*pitch1;
    }

    delete []arr_x1;
    delete []arr_x2;
    delete []arr_fx1;
}

 

發佈了0 篇原創文章 · 獲贊 6 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章