雙線性插值算法

1、線性插值的解釋

雙線性插值,又稱爲雙線性內插。在數學上,雙線性插值是有兩個變量的插值函數的線性插值擴展,其核心思想是在兩個方向分別進行一次線性插值。一下來自於[1]

單線性插值法

已知數據 (x0, y0) 與 (x1, y1),要計算 [x0, x1] 區間內某一位置 x 在直線上的y值。
這裏寫圖片描述
這裏寫圖片描述
上面比較好理解吧,仔細看就是用x和x0,x1的距離作爲一個權重,用於y0和y1的加權。雙線性插值本質上就是在兩個方向上做線性插值。

雙線性插值法

在數學上,雙線性插值是有兩個變量的插值函數的線性插值擴展,其核心思想是在兩個方向分別進行一次線性插值[1]。見下圖:
這裏寫圖片描述

假如我們想得到未知函數 f 在點 P = (x, y) 的值,假設我們已知函數 f 在 Q11 = (x1, y1)、Q12 = (x1, y2), Q21 = (x2, y1) 以及 Q22 = (x2, y2) 四個點的值。最常見的情況,f就是一個像素點的像素值。首先在 x 方向進行線性插值,得到
這裏寫圖片描述
然後在 y 方向進行線性插值,得到
這裏寫圖片描述
綜合起來就是雙線性插值最後的結果:
這裏寫圖片描述

由於圖像雙線性插值只會用相鄰的4個點,因此上述公式的分母都是1。opencv中的源碼如下,用了一些優化手段,比如用整數計算代替float(下面代碼中的*2048就是變11位小數爲整數,最後有兩個連乘,因此>>22位),以及源圖像和目標圖像幾何中心的對齊
- SrcX=(dstX+0.5)* (srcWidth/dstWidth) -0.5
- SrcY=(dstY+0.5) * (srcHeight/dstHeight)-0.5
這個要重點說一下,源圖像和目標圖像的原點(0,0)均選擇左上角,然後根據插值公式計算目標圖像每點像素,假設你需要將一幅5x5的圖像縮小成3x3,那麼源圖像和目標圖像各個像素之間的對應關係如下。如果沒有這個中心對齊,根據基本公式去算,就會得到左邊這樣的結果;而用了對齊,就會得到右邊的結果:
這裏寫圖片描述

2、另一位牛人講的比較易懂

這博文來自文件[2]

1.雙線性插值

假設源圖像大小爲mxn,目標圖像爲axb。那麼兩幅圖像的邊長比分別爲:m/a和n/b。注意,通常這個比例不是整數,編程存儲的時候要用浮點型。目標圖像的第(i,j)個像素點(i行j列)可以通過邊長比對應回源圖像。其對應座標爲(i*m/a,j*n/b)。
顯然,這個對應座標一般來說不是整數,而非整數的座標是無法在圖像這種離散數據上使用的。雙線性插值通過尋找距離這個對應座標最近的四個像素點,來計算該點的值(灰度值或者RGB值)。如果你的對應座標是(2.5,4.5),那麼最近的四個像素是(2,4)、(2,5)、(3,4),(3,5)。
若圖像爲灰度圖像,那麼(i,j)點的灰度值可以通過一下公式計算:
f(i,j)=w1*p1+w2*p2+w3*p3+w4*p4;
其中,pi(i=1,2,3,4)爲最近的四個像素點,wi(i=1,2,3,4)爲各點相應權值。關於權值的計算,在維基百科和百度百科上寫的很明白。

2.存在的問題

這部分的前提是,你已經明白什麼是雙線性插值並且在給定源圖像和目標圖像尺寸的情況下,可以用筆計算出目標圖像某個像素點的值。當然,最好的情況是你已經用某種語言實現了網上一大堆博客上原創或轉載的雙線性插值算法,然後發現計算出來的結果和matlab、openCV對應的resize()函數得到的結果完全不一樣。
那這個究竟是怎麼回事呢?
其實答案很簡單,就是座標系的選擇問題,或者說源圖像和目標圖像之間的對應問題。
按照網上一些博客上寫的,源圖像和目標圖像的原點(0,0)均選擇左上角,然後根據插值公式計算目標圖像每點像素,假設你需要將一幅5x5的圖像縮小成3x3,那麼源圖像和目標圖像各個像素之間的對應關係如下:
這裏寫圖片描述
只畫了一行,用做示意,從圖中可以很明顯的看到,如果選擇右上角爲原點(0,0),那麼最右邊和最下邊的像素實際上並沒有參與計算,而且目標圖像的每個像素點計算出的灰度值也相對於源圖像偏左偏上。
那麼,讓座標加1或者選擇右下角爲原點怎麼樣呢?很不幸,還是一樣的效果,不過這次得到的圖像將偏右偏下。
最好的方法就是,兩個圖像的幾何中心重合,並且目標圖像的每個像素之間都是等間隔的,並且都和兩邊有一定的邊距,這也是matlab和openCV的做法。如下圖:
這裏寫圖片描述
如果你不懂我上面說的什麼,沒關係,只要在計算對應座標的時候改爲以下公式即可,

int x=(i+0.5)*m/a-0.5
int y=(j+0.5)*n/b-0.5

instead of

int x=i*m/a
int y=j*n/b

利用上述公式,將得到正確的雙線性插值結果

3、又是另一位講的通俗易懂

這位大神講openCV ——雙線性插值(Bilinear interpolation)[3]

1,原理

  在圖像的仿射變換中,很多地方需要用到插值運算,常見的插值運算包括最鄰近插值,雙線性插值,雙三次插值,蘭索思插值等方法,OpenCV提供了很多方法,其中,雙線性插值由於折中的插值效果和運算速度,運用比較廣泛。
  越是簡單的模型越適合用來舉例子,我們就舉個簡單的圖像:3*3 的256級灰度圖。假如圖像的象素矩陣如下圖所示(這個原始圖把它叫做源圖,Source):
234 38 22
67 44 12
89 65 63
  這 個矩陣中,元素座標(x,y)是這樣確定的,x從左到右,從0開始,y從上到下,也是從零開始,這是圖象處理中最常用的座標系。
  如果想把這副圖放大爲 4*4大小的圖像,那麼該怎麼做呢?那麼第一步肯定想到的是先把4*4的矩陣先畫出來再說,好了矩陣畫出來了,如下所示,當然,矩陣的每個像素都是未知數,等待着我們去填充(這個將要被填充的圖的叫做目標圖,Destination):
  ? ? ? ?
  ? ? ? ?
  ? ? ? ?
  ? ? ? ?
  然後要往這個空的矩陣裏面填值了,要填的值從哪裏來來呢?是從源圖中來,好,先填寫目標圖最左上角的象素,座標爲(0,0),那麼該座標對應源圖中的座標可以由如下公式得出srcX=dstX* (srcWidth/dstWidth) , srcY = dstY * (srcHeight/dstHeight)
  好了,套用公式,就可以找到對應的原圖的座標了(0*(3/4),0*(3/4))=>(0*0.75,0*0.75)=>(0,0),找到了源圖的對應座標,就可以把源圖中座標爲(0,0)處的234象素值填進去目標圖的(0,0)這個位置了。
  接下來,如法炮製,尋找目標圖中座標爲(1,0)的象素對應源圖中的座標,套用公式:
(1*0.75,0*0.75)=>(0.75,0) 結果發現,得到的座標裏面竟然有小數,這可怎麼辦?計算機裏的圖像可是數字圖像,象素就是最小單位了,象素的座標都是整數,從來沒有小數座標。這時候採用的一種策略就是採用四捨五入的方法(也可以採用直接舍掉小數位的方法),把非整數座標轉換成整數,好,那麼按照四捨五入的方法就得到座標(1,0),完整的運算過程就是這樣的:(1*0.75,0*0.75)=>(0.75,0)=>(1,0) 那麼就可以再填一個象素到目標矩陣中了,同樣是把源圖中座標爲(1,0)處的像素值38填入目標圖中的座標。
  依次填完每個象素,一幅放大後的圖像就誕生了,像素矩陣如下所示:
  234 38 22 22
  67 44 12 12
  89 65 63 63
  89 65 63 63
  這種放大圖像的方法叫做最臨近插值算法,這是一種最基本、最簡單的圖像縮放算法,效果也是最不好的,放大後的圖像有很嚴重的馬賽克,縮小後的圖像有很嚴重的失真;效果不好的根源就是其簡單的最臨近插值方法引入了嚴重的圖像失真,比如,當由目標圖的座標反推得到的源圖的的座標是一個浮點數的時候,採用了四捨五入的方法,直接採用了和這個浮點數最接近的象素的值,這種方法是很不科學的,當推得座標值爲 0.75的時候,不應該就簡單的取爲1,既然是0.75,比1要小0.25 ,比0要大0.75 ,那麼目標象素值其實應該根據這個源圖中虛擬的點四周的四個真實的點來按照一定的規律計算出來的,這樣才能達到更好的縮放效果。
  雙線型內插值算法就是一種比較好的圖像縮放算法,它充分的利用了源圖中虛擬點四周的四個真實存在的像素值來共同決定目標圖中的一個像素值,因此縮放效果比簡單的最鄰近插值要好很多。
雙線性內插值算法描述如下:
  對於一個目的像素,設置座標通過反向變換得到的浮點座標爲(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)處的的像素值,以此類推。
  比如,象剛纔的例子,現在假如目標圖的象素座標爲(1,1),那麼反推得到的對應於源圖的座標是(0.75 , 0.75), 這其實只是一個概念上的虛擬象素,實際在源圖中並不存在這樣一個象素,那麼目標圖的象素(1,1)的取值不能夠由這個虛擬象素來決定,而只能由源圖的這四個象素共同決定:(0,0)(0,1)(1,0)(1,1),而由於(0.75,0.75)離(1,1)要更近一些,那麼(1,1)所起的決定作用更大一些,這從公式1中的係數uv=0.75×0.75就可以體現出來,而(0.75,0.75)離(0,0)最遠,所以(0,0)所起的決定作用就要小一些,公式中係數爲(1-u)(1-v)=0.25×0.25也體現出了這一特點。

2,計算方法

這裏寫圖片描述
 首先,在X方向上進行兩次線性插值計算,然後在Y方向上進行一次插值計算。
 這裏寫圖片描述
在圖像處理的時候,我們先根據
  srcX=dstX* (srcWidth/dstWidth),
  srcY = dstY * (srcHeight/dstHeight)
來計算目標像素在源圖像中的位置,這裏計算的srcX和srcY一般都是浮點數,比如f(1.2, 3.4)這個像素點是虛擬存在的,先找到與它臨近的四個實際存在的像素點
  (1,3) (2,3)
  (1,4) (2,4)
寫成f(i+u,j+v)的形式,則u=0.2,v=0.4, i=1, j=3
在沿着X方向差插值時,f(R1)=u(f(Q21)-f(Q11))+f(Q11)
沿着Y方向同理計算。
或者,直接整理一步計算,
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)

3,加速以及優化策略

 單純按照上文實現的插值算法只能勉強完成插值的功能,速度和效果都不會理想,在具體代碼實現的時候有些小技巧。參考OpenCV源碼以及網上博客整理如下兩點:
源圖像和目標圖像幾何中心的對齊
將浮點運算轉換成整數運算

3.1 源圖像和目標圖像幾何中心的對齊  

 方法:在計算源圖像的虛擬浮點座標的時候,一般情況:
  srcX=dstX* (srcWidth/dstWidth) ,
  srcY = dstY * (srcHeight/dstHeight)
  中心對齊(OpenCV也是如此):
  SrcX=(dstX+0.5)* (srcWidth/dstWidth) -0.5
  SrcY=(dstY+0.5) * (srcHeight/dstHeight)-0.5
  原理:
雙線性插值算法及需要注意事項這篇博客解釋說“如果選擇右上角爲原點(0,0),那麼最右邊和最下邊的像素實際上並沒有參與計算,而且目標圖像的每個像素點計算出的灰度值也相對於源圖像偏左偏上。”我有點保持疑問。
  將公式變形:
  srcX=dstX* (srcWidth/dstWidth)+0.5*(srcWidth/dstWidth-1)
  相當於我們在原始的浮點座標上加上了0.5*(srcWidth/dstWidth-1)這樣一個控制因子,這項的符號可正可負,與srcWidth/dstWidth的比值也就是當前插值是擴大還是縮小圖像有關,有什麼作用呢?看一個例子:假設源圖像是3*3,中心點座標(1,1)目標圖像是9*9,中心點座標(4,4),我們在進行插值映射的時候,儘可能希望均勻的用到源圖像的像素信息,最直觀的就是(4,4)映射到(1,1)現在直接計算srcX=4*3/9=1.3333!=1,也就是我們在插值的時候所利用的像素集中在圖像的右下方,而不是均勻分佈整個圖像。現在考慮中心點對齊,srcX=(4+0.5)*3/9-0.5=1,剛好滿足我們的要求。

3.2 將浮點運算轉換成整數運算

  參考圖像處理界雙線性插值算法的優化
  直接進行計算的話,由於計算的srcX和srcY 都是浮點數,後續會進行大量的乘法,而圖像數據量又大,速度不會理想,解決思路是:
  浮點運算→→整數運算→→”<<左右移按位運算”
  放大的主要對象是u,v這些浮點數,OpenCV選擇的放大倍數是2048“如何取這個合適的放大倍數呢,要從三個方面考慮,
  第一:精度問題,如果這個數取得過小,那麼經過計算後可能會導致結果出現較大的誤差。
  第二,這個數不能太大,太大會導致計算過程超過長整形所能表達的範圍。
  第三:速度考慮。假如放大倍數取爲12,那麼算式在最後的結果中應該需要除以12*12=144,但是如果取爲16,則最後的除數爲16*16=256,這個數字好,我們可以用右移來實現,而右移要比普通的整除快多了。”我們利用左移11位操作就可以達到放大目的。

4,代碼

uchar* dataDst = matDst1.data;
    int stepDst = matDst1.step;
    uchar* dataSrc = matSrc.data;
    int stepSrc = matSrc.step;
    int iWidthSrc = matSrc.cols;
    int iHiehgtSrc = matSrc.rows;

    for (int j = 0; j < matDst1.rows; ++j)
    {
        float fy = (float)((j + 0.5) * scale_y - 0.5);
        int sy = cvFloor(fy);
        fy -= sy;
        sy = std::min(sy, iHiehgtSrc - 2);
        sy = std::max(0, sy);

        short cbufy[2];
        cbufy[0] = cv::saturate_cast<short>((1.f - fy) * 2048);
        cbufy[1] = 2048 - cbufy[0];

        for (int i = 0; i < matDst1.cols; ++i)
        {
            float fx = (float)((i + 0.5) * scale_x - 0.5);
            int sx = cvFloor(fx);
            fx -= sx;

            if (sx < 0) {
                fx = 0, sx = 0;
            }
            if (sx >= iWidthSrc - 1) {
                fx = 0, sx = iWidthSrc - 2;
            }

            short cbufx[2];
            cbufx[0] = cv::saturate_cast<short>((1.f - fx) * 2048);
            cbufx[1] = 2048 - cbufx[0];

            for (int k = 0; k < matSrc.channels(); ++k)
            {
                *(dataDst+ j*stepDst + 3*i + k) = (*(dataSrc + sy*stepSrc + 3*sx + k) * cbufx[0] * cbufy[0] + 
                    *(dataSrc + (sy+1)*stepSrc + 3*sx + k) * cbufx[0] * cbufy[1] + 
                    *(dataSrc + sy*stepSrc + 3*(sx+1) + k) * cbufx[1] * cbufy[0] + 
                    *(dataSrc + (sy+1)*stepSrc + 3*(sx+1) + k) * cbufx[1] * cbufy[1]) >> 22;
            }
        }
    }
    cv::imwrite("linear_1.jpg", matDst1);

    cv::resize(matSrc, matDst2, matDst1.size(), 0, 0, 1);
    cv::imwrite("linear_2.jpg", matDst2);

參考文件:
[1]三十分鐘理解:線性插值,雙線性插值Bilinear Interpolation算法
[2] 雙線性插值算法及需要注意事項
[3]OpenCV ——雙線性插值(Bilinear interpolation)
[4]FPGA雙線性插值算法(圖像的縮放



轉載自:https://blog.csdn.net/lovexlsforever/article/details/79508602

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