本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/26157633
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
知乎:http://www.zhihu.com/people/mao-xing-yun
写作当前博文时配套使用的OpenCV版本号: 2.4.9
这篇文章里,我们将一起探讨图像金字塔的一些基本概念,怎样使用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函数,最后是一个综合演示样例程序。
二、关于图像金字塔
图像金字塔是图像中多尺度表达的一种,最主要用于图像的切割,是一种以多分辨率来解释图像的有效但概念简单的结构。
图像金字塔最初用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步减少,且来源于同一张原始图的图像集合。其通过梯次向下採样获得,直到达到某个终止条件才停止採样。
金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。
我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。
素材图是帅气的美剧《绿箭侠》里面的绿箭侠Oliver Queen。
普通情况下有两种类型的图像金字塔经常出如今文献和以及实际运用中。他们各自是:
- 高斯金字塔(Gaussianpyramid): 用来向下採样,基本的图像金字塔
- 拉普拉斯金字塔(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中。
非常多时候,我们并不用考虑第二个參数dst的初始图像尺寸和类型(即直接定义一个Mat类型,不用对其初始化),由于其尺寸和类型能够由src,dsize,fx和fy这其它的几个參数来确定。
看一下它的函数原型:
C++: void resize(InputArray src,OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR )
- 第一个參数,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的调用范例。
方式一,调用范例:
Mat dst=Mat::zeros(512 ,512, CV_8UC3 );//新建一张512x512尺寸的图片 Mat src=imread(“1.jpg”); //显式指定dsize=dst.size(),那么fx和fy会其计算出来,不用额外指定。 resize(src, dst, dst.size());方式二、调用范例:
Mat dst; Mat src=imread(“1.jpg”) //指定fx和fy,让函数计算出目标图像的大小。 resize(src, dst, Size(), 0.5, 0.5);接着我们看看完整的演示样例程序:
//-----------------------------------【头文件包括部分】--------------------------------------- // 描写叙述:包括程序所依赖的头文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【命名空间声明部分】--------------------------------------- // 描写叙述:包括程序所使用的命名空间 //----------------------------------------------------------------------------------------------- using namespace cv; //-----------------------------------【main( )函数】-------------------------------------------- // 描写叙述:控制台应用程序的入口函数,我们的程序从这里開始 //----------------------------------------------------------------------------------------------- int main( ) { //加载原始图 Mat srcImage = imread("1.jpg"); //project文件夹下应该有一张名为1.jpg的素材图 Mat tmpImage,dstImage1,dstImage2;//暂时变量和目标图的定义 tmpImage=srcImage;//将原始图赋给暂时变量 //显示原始图 imshow("【原始图】", srcImage); //进行尺寸调整操作 resize(tmpImage,dstImage1,Size( tmpImage.cols/2, tmpImage.rows/2 ),(0,0),(0,0),3); resize(tmpImage,dstImage2,Size( tmpImage.cols*2, tmpImage.rows*2 ),(0,0),(0,0),3); //显示效果图 imshow("【效果图】之中的一个", dstImage1); imshow("【效果图】之二", dstImage2); waitKey(0); return 0; }
程序执行截图:
四、pyrUp()函数剖析
pyrUp( )函数的作用是向上採样并模糊一张图像,说白了就是放大一张图片。
C++: 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的内核做卷积,就是这样。
直接看完整的演示样例程序:
//-----------------------------------【头文件包括部分】--------------------------------------- // 描写叙述:包括程序所依赖的头文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【命名空间声明部分】--------------------------------------- // 描写叙述:包括程序所使用的命名空间 //----------------------------------------------------------------------------------------------- using namespace cv; //-----------------------------------【main( )函数】-------------------------------------------- // 描写叙述:控制台应用程序的入口函数,我们的程序从这里開始 //----------------------------------------------------------------------------------------------- int main( ) { //加载原始图 Mat srcImage = imread("1.jpg"); //project文件夹下应该有一张名为1.jpg的素材图 Mat tmpImage,dstImage;//暂时变量和目标图的定义 tmpImage=srcImage;//将原始图赋给暂时变量 //显示原始图 imshow("【原始图】", srcImage); //进行向上取样操作 pyrUp( tmpImage, dstImage, Size( tmpImage.cols*2, tmpImage.rows*2 ) ); //显示效果图 imshow("【效果图】", dstImage); waitKey(0); return 0; }
程序执行截图:
![]()
五、pyrDown()函数剖析
pyrDown( )函数的作用是向下採样并模糊一张图片,说白了就是缩小一张图片。
C++: 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函数执行了高斯金字塔建造的向下採样的步骤。首先,它将源图像与例如以下内核做卷积运算:
接着,它便通过对图像的偶数行和列做插值来进行向下採样操作。
依旧是看看完整的演示样例程序:
//-----------------------------------【头文件包括部分】--------------------------------------- // 描写叙述:包括程序所依赖的头文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【命名空间声明部分】--------------------------------------- // 描写叙述:包括程序所使用的命名空间 //----------------------------------------------------------------------------------------------- using namespace cv; //-----------------------------------【main( )函数】-------------------------------------------- // 描写叙述:控制台应用程序的入口函数,我们的程序从这里開始 //----------------------------------------------------------------------------------------------- int main( ) { //加载原始图 Mat srcImage = imread("1.jpg"); //project文件夹下应该有一张名为1.jpg的素材图 Mat tmpImage,dstImage;//暂时变量和目标图的定义 tmpImage=srcImage;//将原始图赋给暂时变量 //显示原始图 imshow("【原始图】", srcImage); //进行向下取样操作 pyrDown( tmpImage, dstImage, Size( tmpImage.cols/2, tmpImage.rows/2 ) ); //显示效果图 imshow("【效果图】", dstImage); waitKey(0); return 0; }
程序执行截图:
![]()
六、综合演示样例篇——在实战中熟稔
依旧是每篇文章都会配给大家的一个具体凝视的博文配套演示样例程序,把这篇文章中介绍的知识点以代码为载体,展现给大家。
这个演示样例程序中,分别演示了用resize,pryUp,pryDown来让源图像进行放大缩小的操作,分别用键盘按键1、2、3、4、A、D、W、S来控制图片的放大与缩小:
OK,上具体凝视的代码吧:
//-----------------------------------【程序说明】---------------------------------------------- // 程序名称::《 【OpenCV新手教程之十三】OpenCV图像金字塔:高斯金字塔、拉普拉斯金字塔与图片尺寸缩放》 博文配套源码 // 开发所用IDE版本号:Visual Studio 2010 // 开发所用OpenCV版本号: 2.4.9 // 2014年5月18日 Create by 浅墨 //---------------------------------------------------------------------------------------------- //-----------------------------------【头文件包括部分】--------------------------------------- // 描写叙述:包括程序所依赖的头文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【宏定义部分】-------------------------------------------- // 描写叙述:定义一些辅助宏 //------------------------------------------------------------------------------------------------ #define WINDOW_NAME "【程序窗体】" //为窗体标题定义的宏 //-----------------------------------【命名空间声明部分】-------------------------------------- // 描写叙述:包括程序所使用的命名空间 //----------------------------------------------------------------------------------------------- using namespace std; using namespace cv; //-----------------------------------【全局变量声明部分】-------------------------------------- // 描写叙述:全局变量声明 //----------------------------------------------------------------------------------------------- Mat g_srcImage, g_dstImage, g_tmpImage; //-----------------------------------【全局函数声明部分】-------------------------------------- // 描写叙述:全局函数声明 //----------------------------------------------------------------------------------------------- static void ShowHelpText(); //-----------------------------------【ShowHelpText( )函数】---------------------------------- // 描写叙述:输出一些帮助信息 //---------------------------------------------------------------------------------------------- static void ShowHelpText() { //输出一些帮助信息 printf(" 欢迎来到OpenCV图像金字塔和resize演示样例程序~ "); printf( " 按键操作说明: " " 键盘按键【ESC】或者【Q】- 退出程序 " " 键盘按键【1】或者【W】- 进行基于【resize】函数的图片放大 " " 键盘按键【2】或者【S】- 进行基于【resize】函数的图片缩小 " " 键盘按键【3】或者【A】- 进行基于【pyrUp】函数的图片放大 " " 键盘按键【4】或者【D】- 进行基于【pyrDown】函数的图片缩小 " " by浅墨 " ); } //-----------------------------------【main( )函数】-------------------------------------------- // 描写叙述:控制台应用程序的入口函数,我们的程序从这里開始 //----------------------------------------------------------------------------------------------- int main( ) { //改变console字体颜色 system("color 2F"); //显示帮助文字 ShowHelpText(); //加载原图 g_srcImage = imread("1.jpg");//project文件夹下须要有一张名为1.jpg的測试图像,且其尺寸需被2的N次方整除,N为能够缩放的次数 if( !g_srcImage.data ) { printf("Oh,no,读取srcImage错误~! "); return false; } // 创建显示窗体 namedWindow( WINDOW_NAME, CV_WINDOW_AUTOSIZE ); imshow(WINDOW_NAME, g_srcImage); //參数赋值 g_tmpImage = g_srcImage; g_dstImage = g_tmpImage; int key =0; //轮询获取按键信息 while(1) { key=waitKey(9) ;//读取键值到key变量中 //依据key变量的值,进行不同的操作 switch(key) { //======================【程序退出相关键值处理】======================= case 27://按键ESC return 0; break; case 'q'://按键Q return 0; break; //======================【图片放大相关键值处理】======================= case 'a'://按键A按下,调用pyrUp函数 pyrUp( g_tmpImage, g_dstImage, Size( g_tmpImage.cols*2, g_tmpImage.rows*2 ) ); printf( ">检測到按键【A】被按下,開始进行基于【pyrUp】函数的图片放大:图片尺寸×2 " ); break; case 'w'://按键W按下,调用resize函数 resize(g_tmpImage,g_dstImage,Size( g_tmpImage.cols*2, g_tmpImage.rows*2 )); printf( ">检測到按键【W】被按下,開始进行基于【resize】函数的图片放大:图片尺寸×2 " ); break; case '1'://按键1按下,调用resize函数 resize(g_tmpImage,g_dstImage,Size( g_tmpImage.cols*2, g_tmpImage.rows*2 )); printf( ">检測到按键【1】被按下,開始进行基于【resize】函数的图片放大:图片尺寸×2 " ); break; case '3': //按键3按下,调用pyrUp函数 pyrUp( g_tmpImage, g_dstImage, Size( g_tmpImage.cols*2, g_tmpImage.rows*2 )); printf( ">检測到按键【3】被按下,開始进行基于【pyrUp】函数的图片放大:图片尺寸×2 " ); break; //======================【图片缩小相关键值处理】======================= case 'd': //按键D按下,调用pyrDown函数 pyrDown( g_tmpImage, g_dstImage, Size( g_tmpImage.cols/2, g_tmpImage.rows/2 )); printf( ">检測到按键【D】被按下,開始进行基于【pyrDown】函数的图片缩小:图片尺寸/2 " ); break; case 's' : //按键S按下,调用resize函数 resize(g_tmpImage,g_dstImage,Size( g_tmpImage.cols/2, g_tmpImage.rows/2 )); printf( ">检測到按键【S】被按下,開始进行基于【resize】函数的图片缩小:图片尺寸/2 " ); break; case '2'://按键2按下,调用resize函数 resize(g_tmpImage,g_dstImage,Size( g_tmpImage.cols/2, g_tmpImage.rows/2 ),(0,0),(0,0),2); printf( ">检測到按键【2】被按下,開始进行基于【resize】函数的图片缩小:图片尺寸/2 " ); break; case '4': //按键4按下,调用pyrDown函数 pyrDown( g_tmpImage, g_dstImage, Size( g_tmpImage.cols/2, g_tmpImage.rows/2 ) ); printf( ">检測到按键【4】被按下,開始进行基于【pyrDown】函数的图片缩小:图片尺寸/2 " ); break; } //经过操作后,显示变化后的图 imshow( WINDOW_NAME, g_dstImage ); //将g_dstImage赋给g_tmpImage,方便下一次循环 g_tmpImage = g_dstImage; } return 0; }放一些程序执行截图。
原始图:
经过多次按键后的效果图:
另外,还能够放大图像到非常大的尺寸,上图的话会非常凶残并且不美观。所以就不放出超过原图尺寸的截图了。
好的,就放出这些效果图,具体很多其它的执行效果大家就自己下载演示样例程序回去玩~
本篇文章的配套源码请点击这里下载:
OK,今天的内容大概就是这些,我们下篇文章见:)