在這篇文章里,我們一起學習下 圖像金字塔 的一些基本概念,如何使用OpenCV函數pyrUp和pyrDown 對圖像進行向上和向下采樣,以及了解專門用於縮放圖像尺寸的resize函數的用法。此博文一共有四個配套的簡短的示例程序,其詳細注釋過的代碼都在文中貼出,且文章最后提供了綜合示例程序的下載。
一、引言
我們經常會將某種尺寸的圖像轉換為其他尺寸的圖像,如果放大或者縮小圖片的尺寸,籠統的來說,可以使用OpenCV為我們提供的以下倆種方式:
(1)resize函數,這是最直接的方式
(2)pyrUp(),pyrDown()函數。即圖像金字塔相關的倆個函數,對圖像進行向上,向下采樣的操作
pyrUp、pyrDown其實和專門用作放大縮小圖像尺寸的resize在功能上差不多,披着圖像金字塔的皮,說白了還是在對圖像進行放大和縮小操作。
另外需要指出的是,pyrUp、pyrDown在OpenCV的imgproc模塊中的Image Filtering子模塊里,而resize在imgproc 模塊的Geometric Image Transformations子模塊里。
在這篇文章中,我們將先介紹圖像金字塔的原理,接着介紹resize函數,然后是pyrUp和pyrDown函數,最后是一個綜合示例程序。
二、關於圖像金字塔
圖像金字塔是圖像尺度表達的一種,最主要用於圖像的分割,是一種以多分辨率來解釋圖像的有效但概念簡單地結構。
圖像金字塔最初用於 機器視覺 和 圖像壓縮 ,一幅圖像的金字塔是一系列以金字塔形狀排列的分辨率逐步降低,且來源同一張原始圖的圖像集合。
其次通過梯次向下采樣獲得,直到達到某個終止條件才停止采樣。
金字塔的底部是待處理圖像的高分辨率表示,而頂部是低分辨率的近似
我們將一層一層的圖像比喻成金字塔,層級越高,則圖像越小,分辨率越低。

一般情況下有兩種類型的圖像金字塔常常出現在文獻和以及實際運用中。他們分別是:
<1> 高斯金字塔(Gaussianpyramid):用來向下采樣,主要的圖像金字塔
<2> 拉普拉斯金字塔(Laplacianpyramid):用來從金字塔低層圖像重建上層未采樣圖像,在數字圖像處理中也即是預測殘差,可以對圖像進行最大程度的還原,配合高斯金字塔一起使用。
兩者的簡要區別:高斯金字塔用來向下降采樣圖像,而拉普拉斯金字塔則用來從金字塔底層圖像中向上采樣重建一個圖像。
要從金字塔第i層生成第i+1層(我們表示第i+1層為G_i+1),我們先要用高斯核對G_1進行卷積,然后刪除所有偶數行和偶數列。當然的是,新得到圖像面積會變為源圖像的四分之一。按上述過程對輸入圖像G_0執行操作就可產生出整個金字塔。
當圖像向金字塔的上層移動時,尺寸和分辨率就降低。OpenCV中,從金字塔中上一級圖像生成下一級圖像的可以用PryDown。而通過PryUp將現有的圖像在每個維度都放大兩遍。
圖像金字塔中的向上和向下采樣分別通過OpenCV函數 pyrUp 和 pyrDown 實現。
概括起來就是:
-----對圖像向上采樣:pyrUp函數
------對圖像向下采樣:pyrDown函數
這里的向下與向上采樣,是對圖像的尺寸而言(和金字塔的方向相反),向上就是圖像尺寸加倍,向下就是圖像尺寸減半。而如果我們按上圖中演示的金字塔方向來理解,金字塔向上圖像其實在縮小,這樣剛剛是反過來了。
但需要注意的是,PryUp和PryDown不是互逆的,即PryUp不是降采樣的逆操作。這種情況下,圖像首先在每個維度上擴大為原來的兩倍,新增的行(偶數行)以0填充。然后給指定的濾波器進行卷積(實際上是一個在每個維度都擴大為原來兩倍的過濾器)去估計“丟失”像素的近似值。
PryDown( )是一個會丟失信息的函數。為了恢復原來更高的分辨率的圖像,我們要獲得由降采樣操作丟失的信息,這些數據就和拉普拉斯金字塔有關系了。
2.1 高斯金字塔
高斯金字塔是通過高斯平滑和亞采樣獲得一系列采樣圖像,也就是說第K層高斯金字塔通過平滑,亞采樣就可以獲得K+1層高斯圖像,高斯金字塔包含了一系列低通濾波器,其截止頻率從上一層到下一層是以因子2逐漸增加,所以高斯金字塔就可以跨越很大的頻率范圍,金字塔的圖像如下:

另外,每一層都按從下到上的次序編號, 層級 G_i+1 (表示為 G_i+1尺寸小於第i層G_i)。
2.1.1 對圖像的向下取樣
為了獲取層級為 G_i+1 的金字塔圖像,我們采用如下方法:
(1)對圖像G_i進行高斯內核卷積
(2)將所有偶數行和列去除
得到的圖像即為G_i+1的圖像,顯而易見,結果圖像只有原圖的四分之一。通過對輸入圖像G_i(原始圖像)不停迭代以上步驟就會得到整個金字塔。同時我們也可以看到,向下取樣會逐漸丟失圖像的信息。
以上就是對圖像的向下取樣操作,即縮小圖像
2.1.2 對圖像的向上取樣
如果想放大圖像,則需要通過向上取樣操作得到,具體做法如下:
(1)將圖像在每個方向擴大為原來的倆倍,新增的行和列以0填充
(2)使用先前同樣的內核(乘以4)與放大后的圖像卷積,獲得新增像素的近似值
得到的圖像即為放大后的圖像,但是與原來的圖像相比會發覺比較模糊,因為在縮放的過程中已經丟失了一些信息,如果想在縮小和放大整個過程中減少信息的丟失,這些數據形成了拉普拉斯金字塔。
那么,我們接下來一起看一看拉普拉斯金字塔的概念吧。
2.2 拉普拉斯金字塔
下式是拉普拉斯金字塔第i層的數學定義:

式中的
表示第i層的圖像。而UP()操作是將源圖像中位置為(x,y)的像素映射到目標圖像的(2x+1,2y+1)位置,即在進行向上取樣。符號
表示卷積,
為5x5的高斯內核。
我們下文將要介紹的pryUp,就是在進行上面這個式子的運算。
因此,我們可以直接用OpenCV進行拉普拉斯運算:
![]()
也就是說,拉普拉斯金字塔是通過源圖像減去先縮小后再放大的圖像的一系列圖像構成的
整個拉普拉斯金字塔運算過程可以通過下圖來概括:

所以,我們可以將拉普拉斯金字塔理解為高斯金字塔的逆形式。
另外再提一點,關於圖像金字塔非常重要的一個應用就是實現圖像分割。圖像分割的話,先要建立一個圖像金字塔,然后在G_i和G_i+1的像素直接依照對應的關系,建立起”父與子“關系。而快速初始分割可以先在金字塔高層的低分辨率圖像上完成,然后逐層對分割加以優化。
三、resize( )函數剖析
resize( )為OpenCV中專職調整圖像大小的函數。
此函數將源圖像精確的轉換為指定尺寸的目標圖像。如果源圖像中設置了ROI(region of interest,感興趣區域),那么resize()函數會對源圖像的ROI區域進行調整圖像尺寸的操作,來輸出到目標圖像中,若目標圖像中已經設置ROI區域,不難理解resize()將會對源圖像進行尺寸調整並填充到目標圖像的ROI中。
1 void resize(InputArray src,OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR );
很多時候,我們並不用考慮第二個參數dst的初始圖像尺寸和類型(即直接定義一個Mat類型,不用對其初始化),因為其尺寸和類型可以由src,dsize,fx和fy這其他的幾個參數來確定。
- 第一個參數,InputArray類型的src,輸入圖像,即源圖像,填Mat類的對象即可。
- 第二個參數,OutputArray類型的dst,輸出圖像,當其非零時,有着dsize(第三個參數)的尺寸,或者由src.size()計算出來。
- 第三個參數,Size類型的dsize,輸出圖像的大小;如果它等於零,由下式進行計算:
其中,dsize,fx,fy都不能為0。
- 第四個參數,double類型的fx,沿水平軸的縮放系數,有默認值0,且當其等於0時,由下式進行計算:
![]()
- 第五個參數,double類型的fy,沿垂直軸的縮放系數,有默認值0,且當其等於0時,由下式進行計算:
![]()
- 第六個參數,int類型的interpolation,用於指定插值方式,默認為INTER_LINEAR(線性插值)。
可選的插值方式如下:
- INTER_NEAREST - 最近鄰插值
- INTER_LINEAR - 線性插值(默認值)
- INTER_AREA - 區域插值(利用像素區域關系的重采樣插值)
- INTER_CUBIC –三次樣條插值(超過4×4像素鄰域內的雙三次插值)
- INTER_LANCZOS4 -Lanczos插值(超過8×8像素鄰域的Lanczos插值)
若要縮小圖像,一般情況下最好用CV_INTER_AREA來插值,
而若要放大圖像,一般情況下最好用CV_INTER_CUBIC(效率不高,慢,不推薦使用)或CV_INTER_LINEAR(效率較高,速度較快,推薦使用)。
關於插值,我們看幾張圖就能更好地理解。先看原圖:

當進行6次圖像縮小接着6次圖像放大操作后,兩種不同的插值方式得到的效果圖:

效果很明顯,第一張全是一個個的像素,非常影響美觀。另外一張卻有霧化的朦朧美感,所以插值方式的選擇,對經過多次放大縮小的圖片最終得到的效果是有很大影響的。
接着我們來看兩種resize的調用范例。
方式一,調用范例:
1 Mat dst = Mat::zeros(512,512,CV_8UC3); //新建一張512*512尺寸的圖片
2 Mat src = imread("1.jpg"); 3 //顯示指定dsize=dst.size(),那么fx和fy會其計算出來,不用額外指定
4 resize(src,dst,dst.size());
方式一,調用范例:
1 Mat dst; 2 Mat src = imread("1.jpg"); 3 //指定fx和fy,讓函數計算出目標圖像的大小
4 resize(src,dst,Size(),0.5,0.5);
接着我們看看完整的示例程序:
1 #include <opencv2/core/core.hpp>
2 #include <opencv2/opencv.hpp>
3 #include <opencv2/imgproc/imgproc.hpp>
4 #include <iostream>
5
6 using namespace std; 7 using namespace cv; 8
9 int main() 10 { 11 //載入原圖
12 Mat srcImage = imread("1.jpg"); 13 Mat tmpImage, dstImage1, dstImage2; //臨時變量和目標圖的定義
14 tmpImage = srcImage; //將原始圖賦給臨時變量 15
16 //顯示原始圖
17 imshow("【原始圖】", srcImage); 18
19 //進行尺寸調整操作
20 resize(tmpImage,dstImage1,Size(tmpImage.cols/2,tmpImage.rows/2),(0,0),(0,0),3); 21 resize(tmpImage,dstImage2,Size(tmpImage.cols*2,tmpImage.rows*2),(0,0),(0,0),3); 22
23 //顯示原始圖
24 imshow("【效果圖】之一", dstImage1); 25 imshow("【效果圖】之二", dstImage2); 26
27 waitKey(); 28 return 0; 29 }
程序是們問題的,但是VS編譯不過,總是提示找不到庫文件,但明明是加了的,待解決。。。。
四、pyrUp()函數剖析
pyrUp()函數的作用是向上采樣並模糊一張圖像,其實就是放大一張圖片。。
void pyrUp(InputArray src, OutputArraydst, const Size& dstsize=Size(), int borderType=BORDER_DEFAULT );
- 第一個參數,InputArray類型的src,輸入圖像,即源圖像,填Mat類的對象即可。
- 第二個參數,OutputArray類型的dst,輸出圖像,和源圖片有一樣的尺寸和類型。
- 第三個參數,const Size&類型的dstsize,輸出圖像的大小;有默認值Size(),即默認情況下,由Size(src.cols*2,src.rows*2)來進行計算,且一直需要滿足下列條件:

- 第四個參數,int類型的borderType,又來了,邊界模式,一般我們不用去管它
pyrUp函數執行高斯金字塔的采樣操作,其實它也可以用於拉普拉斯金字塔的。
首先,它通過插入可為零的行與列,對源圖像進行向上取樣操作,然后將結果與pyrDown()乘以4的內核做卷積,就是這樣。
直接看完整的示例程序:
1 /*------------------------------------------ 2 pyrUp()函數 3 --------------------------------------------*/
4 int main() 5 { 6 //載入原始圖
7 Mat srcImage = imread("1.jpg"); 8 Mat tmpImage, dstImage; //臨時變量和目標圖的定義
9 tmpImage = srcImage; //將原始圖賦給臨時變量 10
11 //顯示原始圖
12 imshow("【原始圖】",srcImage); 13
14 //進行向上采樣操作
15 pyrUp(tmpImage,dstImage,Size(tmpImage.cols*2,tmpImage.rows*2)); 16
17 //顯示效果圖
18 imshow("【效果圖】",dstImage); 19
20 waitKey(0); 21 return 0; 22 }
與上一個程序的問題一樣,反復檢查配置沒錯啊。。。這是嗶了狗了。。難道我粗心弄錯了什么?????????
五、pyrDown()函數剖析
pyrDown()函數的作用是向下采樣並模糊一張圖片,說白了就是一張圖片。。
1 void pyrDown(InputArray src,OutputArray dst, const Size& dstsize=Size(), int borderType=BORDER_DEFAULT);
- 第一個參數,InputArray類型的src,輸入圖像,即源圖像,填Mat類的對象即可。
- 第二個參數,OutputArray類型的dst,輸出圖像,和源圖片有一樣的尺寸和類型。
- 第三個參數,const Size&類型的dstsize,輸出圖像的大小;有默認值Size(),即默認情況下,由Size Size((src.cols+1)/2, (src.rows+1)/2)來進行計算,且一直需要滿足下列條件:

該pyrDown函數執行了高斯金字塔建造的向下采樣的步驟。首先,它將源圖像與如下內核做卷積運算:

接着,它便通過對圖像的偶數行和列做插值來進行向下采樣操作。
依然是看看完整的示例程序:
1 /*------------------------------------------ 2 【3】 pyrDown()函數 3 --------------------------------------------*/
4 int main() 5 { 6 //載入原始圖
7 Mat srcImage = imread("1.jpg"); 8 Mat tmpImage, dstImage; //臨時變量和目標圖的定義
9 tmpImage = srcImage; //將原始圖賦給臨時變量 10
11 //顯示原始圖
12 imshow("【原始圖】",srcImage); 13
14 //進行向下采樣操作
15 pyrDown(tmpImage,dstImage,Size(tmpImage.cols/2,tmpImage.rows/2)); 16
17 //顯示效果圖
18 imshow("【效果圖】",dstImage); 19
20 waitKey(); 21 return 0; 22 }
六、綜合示例篇——在實戰中熟稔
依然是每篇文章都會配給大家的一個詳細注釋的博文配套示例程序,把這篇文章中介紹的知識點以代碼為載體,展現給大家。
這個示例程序中,分別演示了用resize,pryUp,pryDown來讓源圖像進行放大縮小的操作,分別用鍵盤按鍵1、2、3、4、A、D、W、S來控制圖片的放大與縮小:
1 /*------------------------------------------------------------------ 2 【4】OpenCV圖像金字塔:高斯金字塔、拉普拉斯金字塔與圖片尺寸縮放 3 ------------------------------------------------------------------*/
4
5 #define WINDOW_NAME "【程序窗口】"
6
7 Mat g_srcImage, g_dstImage, g_tmpImage; 8
9 static void ShowHelpText(); 10
11 static void ShowHelpText() 12 { 13 //輸出一些幫助信息
14 printf("\n\n\n\t歡迎來到OpenCV圖像金字塔和resize示例程序~\n\n"); 15 printf("\n\n\t按鍵操作說明: \n\n"
16 "\t\t鍵盤按鍵【ESC】或者【Q】- 退出程序\n"
17 "\t\t鍵盤按鍵【1】或者【W】- 進行基於【resize】函數的圖片放大\n"
18 "\t\t鍵盤按鍵【2】或者【S】- 進行基於【resize】函數的圖片縮小\n"
19 "\t\t鍵盤按鍵【3】或者【A】- 進行基於【pyrUp】函數的圖片放大\n"
20 "\t\t鍵盤按鍵【4】或者【D】- 進行基於【pyrDown】函數的圖片縮小\n"
21 "\n\n\t\t\t\t\t\t\t\t by hehhehheh \n\n\n"
22 ); 23 } 24
25 int main() 26 { 27 //改變console字體顏色
28 system("color 1f"); 29
30 //顯示幫助文字
31 ShowHelpText(); 32
33 //載入原圖
34 g_srcImage = imread("1.jpg"); 35 if (!g_srcImage.data) 36 { 37 printf("Oh,no,讀取srcImage錯誤~! \n"); 38 return false; 39 } 40
41 //創建顯示窗口
42 namedWindow(WINDOW_NAME, CV_WINDOW_AUTOSIZE); 43 imshow(WINDOW_NAME, g_srcImage); 44
45 //參數賦值
46 g_tmpImage = g_srcImage; 47 g_dstImage = g_tmpImage; 48
49 int key = 0; 50
51 //輪詢獲取按鍵值
52 while (1) 53 { 54 key = waitKey(9); //讀取鍵值到key變量中 55
56 //根據key變量的值,進行不同的操作、
57 switch (key) 58 { 59 //程序退出操作
60 case 27: //按鍵ESC
61 return 0; 62 break; 63
64 case 'q': //按鍵Q
65 return 0; 66 break; 67
68 //【程序放大相關鍵值處理】
69 case 'a': //按鍵a按下,調用pyrUp函數
70 pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); 71 printf(">檢測到按鍵【A】被按下,開始進行基於【pyrUp】函數的圖片放大:圖片尺寸×2 \n"); 72 break; 73
74 case 'w'://按鍵W按下,調用resize函數
75 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); 76 printf(">檢測到按鍵【W】被按下,開始進行基於【resize】函數的圖片放大:圖片尺寸×2 \n"); 77 break; 78
79 case '1'://按鍵1按下,調用resize函數
80 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); 81 printf(">檢測到按鍵【1】被按下,開始進行基於【resize】函數的圖片放大:圖片尺寸×2 \n"); 82 break; 83
84 case '3': //按鍵3按下,調用pyrUp函數
85 pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2)); 86 printf(">檢測到按鍵【3】被按下,開始進行基於【pyrUp】函數的圖片放大:圖片尺寸×2 \n"); 87 break; 88
89 //【程序縮小相關鍵值處理】
90 case 'd': //按鍵D按下,調用pyrDown函數
91 pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); 92 printf(">檢測到按鍵【D】被按下,開始進行基於【pyrDown】函數的圖片縮小:圖片尺寸/2\n"); 93 break; 94
95 case 's': //按鍵S按下,調用resize函數
96 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); 97 printf(">檢測到按鍵【S】被按下,開始進行基於【resize】函數的圖片縮小:圖片尺寸/2\n"); 98 break; 99
100 case '2'://按鍵2按下,調用resize函數
101 resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2), (0, 0), (0, 0), 2); 102 printf(">檢測到按鍵【2】被按下,開始進行基於【resize】函數的圖片縮小:圖片尺寸/2\n"); 103 break; 104
105 case '4': //按鍵4按下,調用pyrDown函數
106 pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2)); 107 printf(">檢測到按鍵【4】被按下,開始進行基於【pyrDown】函數的圖片縮小:圖片尺寸/2\n"); 108 break; 109 } 110
111 //經過操作后,顯示變化后的圖
112 imshow(WINDOW_NAME, g_dstImage); 113
114 //將g_dstImage賦給g_tmpImage,方便下一次循環
115 g_tmpImage = g_dstImage; 116 } 117
118 return 0; 119 }
我又查了幾遍,發現配置確實沒問題,打開以前的工程可以正常的運行,配置是一模一樣的,真是醉了。。。。。。。。。。。。。。
待我有時間去請教大神或者執行解決,這里就不多說啦,主要看代碼就好了。。
