1. 程式人生 > >最近鄰插值和雙線性插值的基本原理 以及OpenCV中resize函式的用法改變影象的大小

最近鄰插值和雙線性插值的基本原理 以及OpenCV中resize函式的用法改變影象的大小

最近鄰插值和雙線性插值的基本原理

影象的縮放很好理解,就是影象的放大和縮小。傳統的繪畫工具中,有一種叫做“放大尺”的繪畫工具,畫家常用它來放大圖畫。當然,在計算機上,我們不再需要用放大尺去放大或縮小影象了,把這個工作交給程式來完成就可以了。下面就來講講計算機怎麼來放大縮小圖象;在本文中,我們所說的影象都是指點陣圖,也就是用一個畫素矩陣來描述影象的方法,對於另一種影象:用函式來描述影象的向量圖,不在本文討論之列。
越是簡單的模型越適合用來舉例子,我們就舉個簡單的影象:3X3 的256級灰度圖,也就是高為3個象素,寬也是3個象素的影象,每個象素的取值可以是 0-255,代表該畫素的亮度,255代表最亮,也就是白色,0代表最暗,即黑色。假如影象的象素矩陣如下圖所示(這個原始圖把它叫做源圖,Source):
234 38 22
67 44 12
89 65 63
這個矩陣中,元素座標(x,y)是這樣確定的,x從左到右,從0開始,y從上到下,也是從零開始,這是圖象處理中最常用的座標系,就是這樣一個座標:
---------------------->X
|
|
|
|
|
∨Y
如果想把這副圖放大為 4X4大小的影象,那麼該怎麼做呢?那麼第一步肯定想到的是先把4X4的矩陣先畫出來再說,好了矩陣畫出來了,如下所示,當然,矩陣的每個畫素都是未知數,等待著我們去填充(這個將要被填充的圖的叫做目標圖,Destination):
? ? ? ?
? ? ? ?
? ? ? ?
? ? ? ?

然後要往這個空的矩陣裡面填值了,要填的值從哪裡來來呢?是從源圖中來,好,先填寫目標圖最左上角的象素,座標為(0,0),那麼該座標對應源圖中的座標可以由如下公式得出:
srcX=dstX* (srcWidth/dstWidth) , srcY = dstY * (srcHeight/dstHeight)
好了,套用公式,就可以找到對應的原圖的座標了(0*(3/4),0*(3/4))=>(00.75,00.75)=>(0,0)
,找到了源圖的對應座標,就可以把源圖中座標為(0,0)處的234象素值填進去目標圖的(0,0)這個位置了。
接下來,如法炮製,尋找目標圖中座標為(1,0)的象素對應源圖中的座標,套用公式:
(10.75,0

0.75)=>(0.75,0)
結果發現,得到的座標裡面竟然有小數,這可怎麼辦?計算機裡的影象可是數字影象,象素就是最小單位了,象素的座標都是整數,從來沒有小數座標。這時候採用的一種策略就是採用四捨五入的方法(也可以採用直接舍掉小數位的方法),把非整數座標轉換成整數,好,那麼按照四捨五入的方法就得到座標(1,0),完整的運算過程就是這樣的:
(10.75,00.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) 公式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也體現出了這一特點。

OpenCV提供了resize函式來改變影象的大小,函式原型如下:

void resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR );


先解釋一下各個引數的意思:

src:輸入,原影象,即待改變大小的影象;

dst:輸出,改變大小之後的影象,這個影象和原影象具有相同的內容,只是大小和原影象不一樣而已;

dsize:輸出影象的大小。如果這個引數不為0,那麼就代表將原影象縮放到這個Size(width,height)指定的大小;如果這個引數為0,那麼原影象縮放之後的大小就要通過下面的公式來計算:

dsize = Size(round(fx*src.cols), round(fy*src.rows))

其中,fx和fy就是下面要說的兩個引數,是影象width方向和height方向的縮放比例。

fx:width方向的縮放比例,如果它是0,那麼它就會按照(double)dsize.width/src.cols來計算;

fy:height方向的縮放比例,如果它是0,那麼它就會按照(double)dsize.height/src.rows來計算;

interpolation:這個是指定插值的方式,影象縮放之後,肯定畫素要進行重新計算的,就靠這個引數來指定重新計算畫素的方式,有以下幾種:

  • INTER_NEAREST - 最鄰近插值
  • INTER_LINEAR - 雙線性插值,如果最後一個引數你不指定,預設使用這種方法
  • INTER_AREA - resampling using pixel area relation. It may be a preferred method for image decimation, as it gives moire’-free results. But when the image is zoomed, it is similar to the INTER_NEAREST method.
  • INTER_CUBIC - 4x4畫素鄰域內的雙立方插值
  • INTER_LANCZOS4 - 8x8畫素鄰域內的Lanczos插值


使用注意事項:

1. dsize和fx/fy不能同時為0,要麼你就指定好dsize的值,讓fx和fy空置直接使用預設值,就像

resize(img, imgDst, Size(30,30));

要麼你就讓dsize為0,指定好fx和fy的值,比如fx=fy=0.5,那麼就相當於把原圖兩個方向縮小一倍!


2. 至於最後的插值方法,正常情況下使用預設的雙線性插值就夠用了。

幾種常用方法的效率是:最鄰近插值>雙線性插值>雙立方插值>Lanczos插值;

但是效率和效果成反比,所以根據自己的情況酌情使用。


3. 正常情況下,在使用之前dst影象的大小和型別都是不知道的,型別從src影象繼承而來,大小也是從原影象根據引數計算出來。但是如果你事先已經指定好dst影象的大小,那麼你可以通過下面這種方式來呼叫函式:

resize(src, dst, dst.size(), 0, 0, interpolation);


程式碼如下:::::

/*
#include <opencv2\opencv.hpp>
#include<cv.h>
#include<highgui.h>
using namespace cv;
int g_nCurrRowsSize = 0, g_nCurrColsSize = 0;
int main()
{
 Mat src =imread("C:\\1.bmp");   
 Mat dst; 
 double scale=0.5;  //縮放倍數

 g_nCurrRowsSize = src.rows* scale;
 g_nCurrColsSize = src.cols* scale;

 dst.create( Size(g_nCurrColsSize, g_nCurrRowsSize), src.type()); //構造目標圖象
 resize(src, dst,Size(g_nCurrColsSize, g_nCurrRowsSize), 0, 0, 3); //縮放源影象到目標影象 

 namedWindow( "src",WINDOW_AUTOSIZE ); //建立用於顯示源影象的視窗
 namedWindow( "dst",WINDOW_AUTOSIZE ); //建立用於顯示目標影象的視窗
 imshow( "src", src );  //顯示源影象
 imshow( "dst", dst );  //顯示目標影象
 waitKey(0);   
 return 0;
}
*/



#include<iostream>
#include<opencv2/opencv.hpp> 
using namespace std;
using namespace cv; 
int g_nCurrRowsSize = 0, g_nCurrColsSize = 0;
int main()
{
	Mat srcImage = imread("C:\\1.bmp");
	imshow("【原圖】", srcImage);
 
	/*Mat dstImage;
	resize(srcImage, dstImage, Size(50, 50), 0, 0, 3);
	imshow("【處理後的圖片】", dstImage);
	waitKey(0);*/
	
	Mat midImage, dstImage;
	srcImage.copyTo(midImage);
 
	g_nCurrRowsSize = srcImage.rows;
	g_nCurrColsSize = srcImage.cols;
	namedWindow("【滾動條】");
	createTrackbar("rows", "【滾動條】", &g_nCurrRowsSize, srcImage.rows * 2, 0);
	createTrackbar("cols", "【滾動條】", &g_nCurrColsSize, srcImage.cols * 2, 0);
 
	char key;
	while (1)
	{
		resize(srcImage, dstImage, Size(g_nCurrColsSize, g_nCurrRowsSize), 0, 0, INTER_LINEAR);
        /*
        INTER_NEAREST - 最鄰近插值
        INTER_LINEAR - 雙線性插值,如果最後一個引數你不指定,預設使用這種方法
        INTER_AREA 使用象素關係重取樣。當影象縮小時候,該方法可以避免波紋出現。
                   當影象放大時,類似於 CV_INTER_NN 方法
        INTER_CUBIC - 4x4畫素鄰域內的雙立方插值
        INTER_LANCZOS4 - 8x8畫素鄰域內的Lanczos插值
       */
		imshow("【變換尺寸】", dstImage);
		key = waitKey(10);
 
		switch (key)
		{
		case	27:
			return 0;
			break;
			//如果檢測到鍵盤值為 'a' 則恢復原圖
		case	'a':
			midImage.copyTo(dstImage);
			g_nCurrColsSize = srcImage.cols;
			g_nCurrRowsSize = srcImage.rows;
			break;
		default:
			break;
		}
	}
	return 0;
}

測試結果:在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

執行第一個註釋的程式碼 把scale倍數改成0.5 結果如下:
在這裡插入圖片描述