了解插值算法與常見幾何變換之間的關係
理解插值算法的原理
掌握OpenCV框架下插值算法API的使用
插值算法原理介紹
近鄰插值算法
1.原理簡介
將目標圖像中的點,對應到原圖像中後,找到最相鄰的整數坐標點的像素值,作為該點的像素值輸出。
如上圖所示,目標圖像中的某點投影到原圖像中的位置為點P,與P距離最近的點為Q11,此時易知,f(P)=f(Q11)。
2.例子說明
如圖所示:
將一幅3*3圖像放大到4*4,用f(x , y)表示原圖像,h(x ,y)表示目標圖像,我們有如下公式:
3.缺點
由最鄰近插值法,放大後的圖像有很嚴重的馬賽克,會出現明顯的塊狀效應;縮小後的圖像有很嚴重的失真。
這是一種最基本、最簡單的圖像縮放方式。變換後的每個像素點的像素值,只由原圖像中的一個像素點確定。例如上面,點(0,0.75)的像素只由(0,1)確定,這樣的效果顯然不好。點(0,0.75)的像素不止和(0,1)有關,和(0,0)也有關,只是(0,1)的影響更大。如果可以用附近的幾個像素點按權重分配,共同確定目標圖像某點的像素,效果會更好。下面的雙線性插值就解決了這個問題。
雙線性插值算法
1.線性插值
在講雙線性插值之前先了解一下線性插值。線性插值:使用連接兩個已知量的直線來確定在這兩個已知量之間的一個未知量的值。線性插值形式:
如下圖所示:
線性插值多項式:
其實,即使x不在x0到x1之間,這個公式也是成立的。在這種情況下,這種方法叫作線性外插。
線性插值的誤差:線性插值其實就是拉格朗日插值有2個結點時的情況。插值餘項為:
從插值餘項可以看出,隨著二階導數的增大,線性插值的誤差增大。即函數的曲率越大,線性插值近似的誤差也越大。
舉個例子。下圖中,左邊為原圖像,拉伸後,理想的輸出圖像的像素分布應該為綠色箭頭指向的,但是按照線性插值,會得到紅色箭頭指向的結果。
2.雙線性插值
雙線性插值形式:
雙線性插值是線性插值在二維時的推廣,在兩個方向上共做了三次線性插值。定義了一個雙曲拋物面與四個已知點擬合。
具體操作為在X方向上進行兩次線性插值計算,然後在Y方向上進行一次插值計算。如下圖所示:
首先,f(x,y)為二元函數,假設我們知道f(x0,y0),f(x1,y1),f(x0,y1),f(x1,y0)四個點的值。這四個點確定一個矩形,我們希望通過插值得到矩形內任意點的函數值。
先在x方向上進行兩次線性插值,得到:
再在y方向上進行一次線性插值,得到:
綜合起來,就是雙線性插值的結果:
如果選擇一個坐標系統,使f(x)已知的四個點的坐標分別為(0,0),(0,1),(1,0),(1,1),那麼確定一個單位正方形,四個點分別為正方形的四個頂點:
3.原圖像和目標圖像的幾何中心對稱
在計算目標圖像中,對應原圖像的虛擬坐標點時,一般的變換是:
這種變換下,原圖像的有些點沒有參與計算。舉個例子,把9∗9的原圖像縮小成3∗3,原圖像的原點(0,0)和目標圖像的原點(0,0)都為左上角,目標圖像右上角的坐標為(0,2),對應原圖像的坐標為(0∗(9/3),2∗(9/3))=(0,6)。目標圖像右邊已經沒有點了,(0,6)右邊的像素點也就用不到了。
原圖像和目標圖像的像素之間的對應關係如下:
從圖片可以看出,只有圈出來的紅色部分參與運算了。目標圖像的每個像素點的灰度值相對於原圖像偏左上方,右下角的元素實際上沒有參與運算。
為了讓原圖像和目標圖像的中心對齊,我們規定另外一種變換方式:
就是在原來的變換後面加了調節因子:
0.5(src_width/dst_width−1)
這種變換下,目標圖像的中心點(1,1),對應了原圖像的中心點(4,4),兩個圖像的幾何中心重合,能充分利用原圖像的點,並且目標圖像的每個像素點之間都是等間隔的,也都和兩邊有一定的邊距。實際上,在openCv中也是這種變換方式。
4.cv.resize()的計算過程
對於縮小圖像,目標圖像中每個點都能找到原圖像中包圍它的四個臨近點,每個點都進行雙線性插值即可。
對於放大圖像,邊界附近的點經過坐標變換可能超出了原圖像的範圍。舉個例子,把3∗3的原圖像放大成4∗4。
中間的點都能在原圖像中找到包圍它的四個臨近點,做雙線性插值即可。
例如,目標圖像中的點(1,3),對應原圖像的點為(0.625,2.125),原圖像的縱坐標最大為2,找不到包圍(0.625,2.125)四個點,所以用它最鄰近的兩個點(0,2)和(1,2)做線性插值(外插),得到目標圖像中(1,3)的像素值。
例如,目標圖像右上角的頂點(0,3),對於原圖像的點為(0,2.125),直接用原圖像右上角的頂點(0,2)作為它的值即可。
計算過程:
用h(x,y)表示目標圖像,f(x,y)表示原圖像
可以用代碼舉例子測試:
import cv2import numpy as npsrc = np.array([[56,23,15],[65,32,78],[12,45,62]],dtype=np.uint8)print(src)dst = cv2.resize(src,dsize=(4,4),interpolation=cv2.INTER_LINEAR)print(dst)
三次樣條插值算法
給定n+1個點,a=x_0<x_1<...<x_n=b,以及他們的函數值f(x_i),i=0,1,2,...n上,確定一個三次多項式:
每個三次多項式中有四個未知參數,有n個區間,n個多項式,共4n個未知參數。我們知道「n個未知數需要n個已知條件確定唯一解」,所以要確定這4n個未知參數,共需要4n個已知條件。
每個三次多項式滿足如下條件:
以上共4n−2個條件,還差2個條件,由如下三種邊界條件確定:
4n個條件有了,就可以確定每個區間上的三次多項式。
對於每個區間內的點,就可以用Si(x)得到插值結果。三次樣條插值具有良好的收斂性,穩定性和光滑性,優點明顯,是非常重要的插值工具。
這裡主要了解三次樣條插值的作用,具體的推導過程比較繁瑣,想了解的可以查閱資料。
兩種映射方法
向前映射和向後映射都是將一個圖像經過幾何變換得到另一個圖像的過程,它們的目的都是得到目標圖像的像素,只是方式不同。
向前映射
圖像變換的本質是將像素點的坐標通過某一種函數關係,映射到另外的位置。
向前映射的過程可以分解為兩步:坐標變換+分配像素值
向前映射的坐標變換:由原圖像坐標推算該像素在目標圖像的位置。
例如,我們知道原圖像的某個像素點的坐標(x,y),變換後在新圖像的坐標為(x′,y′),變換後的坐標一般為非整數的,而非整數的坐標是沒意義的,所以將這個點的像素按權重分配給周圍四個像素點。對於變換後坐標仍為整數的點,直接把其像素值分配給目標圖像中對應的點即可。將原圖像的所有像素點都進行這種坐標變換和分配像素值,就得到了新圖像。
所以,新圖像的每個像素點的像素值,都是由它周圍的非整數坐標的點的像素分配給它併疊加得到的(或者直接等於某個整數坐標點的像素值)。由於這個分配、疊加的特性,向前映射法有時也叫像素移交映射。
對於向前映射,雖然原圖像中的每個點分配係數之和為1。但目標圖像上每個點的像素值是多個分配值疊加而成的,所以不能保證所有分配到其上的權重之和為1。因此必須記錄下所有分配到其上的權重並累加起來,最後利用累加權重進行歸一化,才能得到正確的插值結果。所以,確定目標圖像某一點的像素值,需要遍歷原圖像的所有像素值,進行坐標變換和分配像素值。這是向前映射法的缺點。
向後映射
向後映射的過程可以分解為兩步:坐標變換+插值。
向後映射的坐標變換:由輸出圖像坐標反過來推算該像素在原圖像的位置
前面說的幾種插值方式,就是向後映射的例子。是由目標圖像的坐標算出在原圖像的坐標,再確定它的像素值由原圖像的哪幾個點按權重分配得到。然後進行插值操作,得到該點的像素值。某一點的像素值進行一次操作就可以得到,不需要遍歷全部像素點。向後映射法也叫像素填充算法。向後映射法解決了漏點的問題,出現了馬賽克。
動手實現
c++實現
1. 函數原型
void cv::resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR )
2. 插值方式
註:目標圖像大小可以通過「參數dsize」和「參數fx和fy」兩種方式確定。兩種參數不能同時為0。
dsize的第一個參數為列數,第二個參數為行數,都為整數。若指定了dsize的值,無論是否指定了fx和fy的值,都由參數dsize來決定目標圖像的大小。
如果dsize的值None,目標圖像的大小通過fx和fy確定。fx為列數縮放的倍數,fy為行數縮放的倍數。
3.代碼實現
#include <opencv2/opencv.hpp>#include <iostream>
using namespace cv;using namespace std;
int main(int argc, char* argv[]){ Mat img = imread(""C:/Users/94890/Desktop/picture/luelue.jpg""); if (img.empty()) { cout << "無法讀取圖像" << endl; return 0; }
int height = img.rows;//原圖像行數 int width = img.cols;//原圖像列數 // 縮小圖像,比例為(0.2, 0.2),行列數必須為整數 Size dsize = Size(round(0.2 * width), round(0.2 * height)); Mat shrink; //使用雙線性插值 resize(img, shrink, dsize, 0, 0, INTER_LINEAR);
// 在縮小圖像的基礎上,放大圖像,比例為(1.5, 1.5) float fx = 1.5; float fy = 1.5; Mat enlarge1, enlarge2; resize(shrink, enlarge1, Size(), fx, fy, INTER_NEAREST); resize(shrink, enlarge2, Size(), fx, fy, INTER_LINEAR);
// 顯示 imshow("src", img); imshow("shrink", shrink); imshow("INTER_NEAREST", enlarge1); imshow("INTER_LINEAR", enlarge2);
//保存圖像 imwrite("C:/Users/94890/Desktop/picture/shrink2.jpg",shrink); imwrite("C:/Users/94890/Desktop/picture/INTER_NEAREST2.jpg", enlarge1); imwrite("C:/Users/94890/Desktop/picture/INTER_LINEAR2.jpg", enlarge2); waitKey(0); return 0;}原圖像
0.2倍縮小,雙線性插值
縮小後的圖像1.5倍放大,最近鄰插值
縮小後的圖像1.5倍放大,雙線性插值
python實現
1. 函數原型
#dst為輸出圖像,類型與原圖像相同dst = cv2.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]])
2. 插值方式
通常,縮小圖像使用區域插值(cv.INTER_AREA),放大圖像使用三次樣條插值(cv.INTER_CUBIC)和雙線性插值(cv.INTER_LINEAR)。三次樣條插值方式速度較慢,雙線性插值方式較快且效果也不錯。
3. 代碼實現
import cv2
if __name__ == "__main__": img = cv2.imread('C:/Users/94890/Desktop/smile.jpg', cv2.IMREAD_UNCHANGED) print('Original shape : ', img.shape)#img.shape屬性種第一個值對應行數,第二個值對應列數 width = int(img.shape[1] * 0.3)#列數必須是整數 height = int(img.shape[0] * 0.3)#行數必須是整數 dsize = (width, height)#dsize屬性值第一個數對應列數,第二個數對應行數 # resize image resized = cv2.resize(img, dsize, interpolation=cv2.INTER_LINEAR)#雙線性插值方式 print('Resized shape : ', resized.shape)
fx = 1.5#列數變為原來的1.5倍 fy = 1.5#行數變為原來的1.5倍 resized1 = cv2.resize(resized, dsize=None, fx=fx, fy=fy, interpolation=cv2.INTER_NEAREST)#最鄰近插值 resized2 = cv2.resize(resized, dsize=None, fx=fx, fy=fy, interpolation=cv2.INTER_LINEAR)#雙線性插值 print('Resized1 shape : ', resized1.shape)
#顯示圖像 cv2.imshow("Resized image", resized) cv2.imshow("INTER_NEAREST image", resized1) cv2.imshow("INTER_LINEAR image", resized2) #保存圖像 cv2.imwrite("C:/Users/94890/Desktop/Resized_image.jpg", resized) cv2.imwrite("C:/Users/94890/Desktop/INTER_NEAREST_image.jpg", resized1) cv2.imwrite("C:/Users/94890/Desktop/INTER_LINEAR_image.jpg", resized2) cv2.waitKey(0) cv2.destroyAllWindows()原圖像
0.3倍縮小,雙線性插值
縮小後的圖像1.5倍放大,最近鄰插值
縮小後的圖像1.5倍放大,雙線性插值
——END——