本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/23184547
作者:毛星云(浅墨) 邮箱: happylifemxy@163.com
写作当前博文时配套使用的OpenCV版本: 2.4.8
正如我们上一篇文章中讲到的,线性滤波可以实现很多种不同的图像变换。然而非线性滤波,如中值滤波器和双边滤波器,有时可以达到更好的实现效果。邻域算子的其他一些例子还有对二值图像进行操作的形态学算子,用于计算距离变换和寻找连通量的半全局算子。
先上一张截图:
一、理论与概念讲解——从现象到本质
1.1 非线性滤波概述
之前的那篇文章里,我们所考虑的滤波器都是线性的,即两个信号之和的响应和他们各自响应之和相等。换句话说,每个像素的输出值是一些输入像素的加权和,线性滤波器易于构造,并且易于从频率响应角度来进行分析。
其实在很多情况下,使用邻域像素的非线性滤波也许会得到更好的效果。比如在噪声是散粒噪声而不是高斯噪声,即图像偶尔会出现很大的值的时候。在这种情况下,用高斯滤波器对图像进行模糊的话,噪声像素是不会被去除的,它们只是转换为更为柔和但仍然可见的散粒。
这就到了中值滤波登场的时候了。
1.2 中值滤波
中值滤波(Median filter)是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值,该方法在去除脉冲噪声、椒盐噪声的同时又能保留图像边缘细节,.
中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,其基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值的中值代替,让周围的像素值接近的真实值,从而消除孤立的噪声点,对于斑点噪声(speckle noise)和椒盐噪声(salt-and-pepper noise)来说尤其有用,因为它不依赖于邻域内那些与典型值差别很大的值。中值滤波器在处理连续图像窗函数时与线性滤波器的工作方式类似,但滤波过程却不再是加权运算。
中值滤波在一定的条件下可以克服常见线性滤波器如最小均方滤波、方框滤波器、均值滤波等带来的图像细节模糊,而且对滤除脉冲干扰及图像扫描噪声非常有效,也常用于保护边缘信息, 保存边缘的特性使它在不希望出现边缘模糊的场合也很有用,是非常经典的平滑噪声处理方法。
●中值滤波与均值滤波器比较
中值滤波器与均值滤波器比较的优势:在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了噪声的影响,但是在中值滤波器中,由于噪声成分很难选上,所以几乎不会影响到输出。因此同样用3x3区域进行处理,中值滤波消除的噪声能力更胜一筹。中值滤波无论是在消除噪声还是保存边缘方面都是一个不错的方法。
中值滤波器与均值滤波器比较的劣势:中值滤波花费的时间是均值滤波的5倍以上。
顾名思义,中值滤波选择每个像素的邻域像素中的中值作为输出,或者说中值滤波将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。
例如,取3 x 3的函数窗,计算以点[i,j]为中心的函数窗像素中值步骤如下:
(1) 按强度值大小排列像素点.
(2) 选择排序像素集的中间值作为点[i,j]的新值.
这一过程如图下图所示.
一般采用奇数点的邻域来计算中值,但如果像素点数为偶数
时,中值就取排序像素中间两点的平均值.采用大小不同邻域的中值滤波器的结果如图。
中值滤波在一定条件下,可以克服线性滤波器(如均值滤波等)所带来的图像细节模糊,
而且对滤除脉冲干扰即图像扫描噪声最为有效。在实际运算过程中并不需要图像的统计特
性,也给计算带来不少方便。但是对一些细节多,特别是线、尖顶等细节多的图像不宜采用
中值滤波。
1.3 双边滤波
双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。
双边滤波器的好处是可以做边缘保存(edge preserving),一般过去用的维纳滤波或者高斯滤波去降噪,都会较明显地模糊边缘,对于高频细节的保护效果并不明显。双边滤波器顾名思义比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离的较远的像素不会太多影响到边缘上的像素值,这样就保证了边缘附近像素值的保存。但是由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净的滤掉,只能够对于低频信息进行较好的滤波。
在双边滤波器中,输出像素的值依赖于邻域像素值的加权值组合:
而加权系数w(i,j,k,l)取决于定义域核和值域核的乘积。
其中定义域核表示如下(如图):
定义域滤波对应图示:
值域核表示为:
值域滤波:
两者相乘后,就会产生依赖于数据的双边滤波权重函数:
二、深入——OpenCV源码分析溯源
首先让我们一起领略medianBlur()函数的源码,其于…\opencv\sources\modules\imgproc\src\smooth.cpp的第1653行开始。
- //-----------------------------------【medianBlur()函数中文注释版源代码】-------------------------
- // 代码作用:进行中值滤波操作的函数
- // 说明:以下代码为来自于计算机开源视觉库OpenCV的官方源代码
- // OpenCV源代码版本:2.4.8
- // 源码路径:…\opencv\sources\modules\imgproc\src\smooth.cpp
- // 源文件中如下代码的起始行数:1653行
- // 中文注释by浅墨
- //--------------------------------------------------------------------------------------------------------
- void cv::medianBlur( InputArray _src0,OutputArray _dst, int ksize )
- {
- //拷贝形参Mat数据到临时变量,用于稍后的操作
- Mat src0 = _src0.getMat();
- _dst.create( src0.size(), src0.type() );
- Mat dst = _dst.getMat();
- //处理特定的ksize值的不同情况
- if( ksize <= 1 )
- {
- src0.copyTo(dst);
- return;
- }
- CV_Assert( ksize% 2 == 1 );
- //若之前有过HAVE_TEGRA_OPTIMIZATION优化选项的定义,则执行宏体中的tegra优化版函数并返回
- #ifdef HAVE_TEGRA_OPTIMIZATION
- if (tegra::medianBlur(src0, dst, ksize))
- return;
- #endif
- bool useSortNet = ksize == 3 || (ksize == 5
- #if !CV_SSE2//若CV_SSE2为假,则执行宏体中语句
- && src0.depth() > CV_8U
- #endif
- );
- //开始正式进行滤波操作
- Mat src;
- if( useSortNet )
- {
- if( dst.data != src0.data )
- src = src0;
- else
- src0.copyTo(src);
- //根据不同的位深,给medianBlur_SortNet函数取不同的模板类型值
- if( src.depth() == CV_8U )
- medianBlur_SortNet<MinMax8u, MinMaxVec8u>( src, dst, ksize );
- else if( src.depth() == CV_16U )
- medianBlur_SortNet<MinMax16u, MinMaxVec16u>( src, dst, ksize );
- else if( src.depth() == CV_16S )
- medianBlur_SortNet<MinMax16s, MinMaxVec16s>( src, dst, ksize );
- else if( src.depth() == CV_32F )
- medianBlur_SortNet<MinMax32f, MinMaxVec32f>( src, dst, ksize );
- else
- CV_Error(CV_StsUnsupportedFormat, "");
- return;
- }
- else
- {
- cv::copyMakeBorder( src0, src, 0, 0, ksize/2, ksize/2, BORDER_REPLICATE);
- int cn = src0.channels();
- CV_Assert( src.depth() == CV_8U && (cn == 1 || cn == 3 || cn ==4) );
- double img_size_mp = (double)(src0.total())/(1 << 20);
- if( ksize <= 3 + (img_size_mp < 1 ? 12 : img_size_mp < 4 ? 6 :2)*(MEDIAN_HAVE_SIMD && checkHardwareSupport(CV_CPU_SSE2) ? 1 : 3))
- medianBlur_8u_Om( src, dst, ksize );
- else
- medianBlur_8u_O1( src, dst, ksize );
- }
- }
仔细阅读源码我们可以发现,正式进入滤波操作时,根据图像不同的位深,我们会给medianBlur_SortNet函数模板取不同的模板类型值,或者调用medianBlur_8u_Om或medianBlur_8u_O1来进行操作。
上面我们刚说到,medianBlur_SortNet 是一个函数模板,其源码于smooth.cpp的1439行开始,由于其函数体很长,我们在此只贴出它的函数声明。
- template<class Op, class VecOp>
- static void medianBlur_SortNet( constMat& _src, Mat& _dst, int m );
另外,bilateralFilter函数的源码也比较冗长,在D:\Program Files\opencv\sources\modules\imgproc\src\smooth.cpp源码文件中。
从1714行到2273行都是。我们在这里只给出路径,和一张概况图,大家有兴趣自己去看源代码。
再提一点,smooth.cpp源码的第2275行到2552行是OpenCV中自适应双边滤波器(adaptiveBilateralFilter)的源代码,有兴趣和精力的童鞋可以去探究探究。
三、浅出——API函数快速上手
3.1 中值滤波——medianBlur函数
medianBlur函数使用中值滤波器来平滑(模糊)处理一张图片,从src输入,而结果从dst输出。
且对于多通道图片,每一个通道都单独进行处理,并且支持就地操作(In-placeoperation)。
- C++: void medianBlur(InputArray src,OutputArray dst, int ksize)
参数详解:
- 第一个参数,InputArray类型的src,函数的输入参数,填1、3或者4通道的Mat类型的图像;当ksize为3或者5的时候,图像深度需为CV_8U,CV_16U,或CV_32F其中之一,而对于较大孔径尺寸的图片,它只能是CV_8U。
- 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。我们可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
- 第三个参数,int类型的ksize,孔径的线性尺寸(aperture linear size),注意这个参数必须是大于1的奇数,比如:3,5,7,9 ...
调用范例:
- //载入原图
- Mat image=imread("1.jpg");
- //进行中值滤波操作
- Mat out;
- medianBlur( image, out, 7);
用上面三句核心代码架起来的完整程序代码:
- //-----------------------------------【程序说明】----------------------------------------------
- // 说明:【中值滤波medianBlur函数的使用示例程序】
- // 开发所用OpenCV版本:2.4.8
- // 2014年4月3 日 Create by 浅墨
- //------------------------------------------------------------------------------------------------
- //-----------------------------------【头文件包含部分】---------------------------------------
- // 描述:包含程序所依赖的头文件
- //----------------------------------------------------------------------------------------------
- #include "opencv2/core/core.hpp"
- #include"opencv2/highgui/highgui.hpp"
- #include"opencv2/imgproc/imgproc.hpp"
- //-----------------------------------【命名空间声明部分】---------------------------------------
- // 描述:包含程序所使用的命名空间
- //-----------------------------------------------------------------------------------------------
- using namespace cv;
- //-----------------------------------【main( )函数】--------------------------------------------
- // 描述:控制台应用程序的入口函数,我们的程序从这里开始
- //-----------------------------------------------------------------------------------------------
- int main( )
- {
- //载入原图
- Mat image=imread("1.jpg");
- //创建窗口
- namedWindow("中值滤波【原图】" );
- namedWindow("中值滤波【效果图】");
- //显示原图
- imshow("中值滤波【原图】", image );
- //进行中值滤波操作
- Mat out;
- medianBlur( image, out, 7);
- //显示效果图
- imshow("中值滤波【效果图】" ,out );
- waitKey(0 );
- }
运行效果图(孔径的线性尺寸为7):
3.2 双边滤波——bilateralFilter函数
用双边滤波器来处理一张图片,由src输入图片,结果于dst输出。
- C++: void bilateralFilter(InputArray src, OutputArraydst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT)
- 第一个参数,InputArray类型的src,输入图像,即源图像,需要为8位或者浮点型单通道、三通道的图像。
- 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
- 第三个参数,int类型的d,表示在过滤过程中每个像素邻域的直径。如果这个值我们设其为非正数,那么OpenCV会从第五个参数sigmaSpace来计算出它来。
- 第四个参数,double类型的sigmaColor,颜色空间滤波器的sigma值。这个参数的值越大,就表明该像素邻域内有更宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
- 第五个参数,double类型的sigmaSpace坐标空间中滤波器的sigma值,坐标空间的标注方差。他的数值越大,意味着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。
- 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
调用代码示范如下:
- //载入原图
- Mat image=imread("1.jpg");
- //进行双边滤波操作
- Mat out;
- bilateralFilter( image, out, 25, 25*2, 25/2 );
用一个完整的示例程序把bilateralFilter函数熟悉一下:
运行效果图:
- //-----------------------------------【程序说明】----------------------------------------------
- // 说明:【双边滤波bilateralFilter函数的使用示例程序】
- // 开发所用OpenCV版本:2.4.8
- // 2014年4月3 日 Create by 浅墨
- //------------------------------------------------------------------------------------------------
- //-----------------------------------【头文件包含部分】---------------------------------------
- // 描述:包含程序所依赖的头文件
- //----------------------------------------------------------------------------------------------
- #include "opencv2/core/core.hpp"
- #include"opencv2/highgui/highgui.hpp"
- #include"opencv2/imgproc/imgproc.hpp"
- //-----------------------------------【命名空间声明部分】---------------------------------------
- // 描述:包含程序所使用的命名空间
- //-----------------------------------------------------------------------------------------------
- using namespace cv;
- //-----------------------------------【main( )函数】--------------------------------------------
- // 描述:控制台应用程序的入口函数,我们的程序从这里开始
- //-----------------------------------------------------------------------------------------------
- int main( )
- {
- //载入原图
- Mat image=imread("1.jpg");
- //创建窗口
- namedWindow("双边滤波【原图】" );
- namedWindow("双边滤波【效果图】");
- //显示原图
- imshow("双边滤波【原图】", image );
- //进行双边滤波操作
- Mat out;
- bilateralFilter( image, out, 25, 25*2, 25/2 );
- //显示效果图
- imshow("双边滤波【效果图】" ,out );
- waitKey(0 );
- }
四、综合示例——在实战中熟稔
依然是每篇文章都会配给大家的一个详细注释的博文配套示例程序,把这篇文章中介绍的知识点以代码为载体,展现给大家。
这个示例程序中可以用轨迹条来控制各种滤波(方框滤波、均值滤波、高斯滤波、中值滤波、双边滤波)的参数值,通过滑动滚动条,就可以控制图像在各种平滑处理下的模糊度,有一定的可玩性。废话不多说,上代码吧:
- //-----------------------------------【程序说明】----------------------------------------------
- // 程序名称::《【OpenCV入门教程之九】非线性滤波专场:中值滤波、双边滤波 》 博文配套源码
- // 开发所用IDE版本:Visual Studio 2010
- // 开发所用OpenCV版本: 2.4.8
- // 2014年4月8日 Create by 浅墨
- //------------------------------------------------------------------------------------------------
- //-----------------------------------【头文件包含部分】---------------------------------------
- // 描述:包含程序所依赖的头文件
- //----------------------------------------------------------------------------------------------
- #include <opencv2/core/core.hpp>
- #include<opencv2/highgui/highgui.hpp>
- #include<opencv2/imgproc/imgproc.hpp>
- #include <iostream>
- //-----------------------------------【命名空间声明部分】---------------------------------------
- // 描述:包含程序所使用的命名空间
- //-----------------------------------------------------------------------------------------------
- using namespace std;
- using namespace cv;
- //-----------------------------------【全局变量声明部分】--------------------------------------
- // 描述:全局变量声明
- //-----------------------------------------------------------------------------------------------
- Mat g_srcImage,g_dstImage1,g_dstImage2,g_dstImage3,g_dstImage4,g_dstImage5;
- int g_nBoxFilterValue=6; //方框滤波内核值
- int g_nMeanBlurValue=10; //均值滤波内核值
- int g_nGaussianBlurValue=6; //高斯滤波内核值
- int g_nMedianBlurValue=10; //中值滤波参数值
- int g_nBilateralFilterValue=10; //双边滤波参数值
- //-----------------------------------【全局函数声明部分】--------------------------------------
- // 描述:全局函数声明
- //-----------------------------------------------------------------------------------------------
- //轨迹条回调函数
- static void on_BoxFilter(int, void *); //方框滤波
- static void on_MeanBlur(int, void *); //均值块滤波器
- static void on_GaussianBlur(int, void *); //高斯滤波器
- static void on_MedianBlur(int, void *); //中值滤波器
- static void on_BilateralFilter(int, void*); //双边滤波器
- //-----------------------------------【main( )函数】--------------------------------------------
- // 描述:控制台应用程序的入口函数,我们的程序从这里开始
- //-----------------------------------------------------------------------------------------------
- int main( )
- {
- system("color 5E");
- //载入原图
- g_srcImage= imread( "1.jpg", 1 );
- if(!g_srcImage.data ) { printf("Oh,no,读取srcImage错误~!\n"); return false; }
- //克隆原图到四个Mat类型中
- g_dstImage1= g_srcImage.clone( );
- g_dstImage2= g_srcImage.clone( );
- g_dstImage3= g_srcImage.clone( );
- g_dstImage4= g_srcImage.clone( );
- g_dstImage5= g_srcImage.clone( );
- //显示原图
- namedWindow("【<0>原图窗口】", 1);
- imshow("【<0>原图窗口】",g_srcImage);
- //=================【<1>方框滤波】=========================
- //创建窗口
- namedWindow("【<1>方框滤波】", 1);
- //创建轨迹条
- createTrackbar("内核值:", "【<1>方框滤波】",&g_nBoxFilterValue, 50,on_BoxFilter );
- on_MeanBlur(g_nBoxFilterValue,0);
- imshow("【<1>方框滤波】", g_dstImage1);
- //=====================================================
- //=================【<2>均值滤波】==========================
- //创建窗口
- namedWindow("【<2>均值滤波】", 1);
- //创建轨迹条
- createTrackbar("内核值:", "【<2>均值滤波】",&g_nMeanBlurValue, 50,on_MeanBlur );
- on_MeanBlur(g_nMeanBlurValue,0);
- //======================================================
- //=================【<3>高斯滤波】===========================
- //创建窗口
- namedWindow("【<3>高斯滤波】", 1);
- //创建轨迹条
- createTrackbar("内核值:", "【<3>高斯滤波】",&g_nGaussianBlurValue, 50,on_GaussianBlur );
- on_GaussianBlur(g_nGaussianBlurValue,0);
- //=======================================================
- //=================【<4>中值滤波】===========================
- //创建窗口
- namedWindow("【<4>中值滤波】", 1);
- //创建轨迹条
- createTrackbar("参数值:", "【<4>中值滤波】",&g_nMedianBlurValue, 50,on_MedianBlur );
- on_MedianBlur(g_nMedianBlurValue,0);
- //=======================================================
- //=================【<5>双边滤波】===========================
- //创建窗口
- namedWindow("【<5>双边滤波】", 1);
- //创建轨迹条
- createTrackbar("参数值:", "【<5>双边滤波】",&g_nBilateralFilterValue, 50,on_BilateralFilter);
- on_BilateralFilter(g_nBilateralFilterValue,0);
- //=======================================================
- //输出一些帮助信息
- cout<<endl<<"\t嗯。好了,请调整滚动条观察图像效果~\n\n"
- <<"\t按下“q”键时,程序退出~!\n"
- <<"\n\n\t\t\t\tby浅墨";
- while(char(waitKey(1))!= 'q') {}
- return 0;
- }
- //-----------------------------【on_BoxFilter( )函数】------------------------------------
- // 描述:方框滤波操作的回调函数
- //-----------------------------------------------------------------------------------------------
- static void on_BoxFilter(int, void *)
- {
- //方框滤波操作
- boxFilter(g_srcImage, g_dstImage1, -1,Size( g_nBoxFilterValue+1, g_nBoxFilterValue+1));
- //显示窗口
- imshow("【<1>方框滤波】", g_dstImage1);
- }
- //-----------------------------【on_MeanBlur( )函数】------------------------------------
- // 描述:均值滤波操作的回调函数
- //-----------------------------------------------------------------------------------------------
- static void on_MeanBlur(int, void *)
- {
- blur(g_srcImage, g_dstImage2, Size( g_nMeanBlurValue+1, g_nMeanBlurValue+1),Point(-1,-1));
- imshow("【<2>均值滤波】", g_dstImage2);
- }
- //-----------------------------【on_GaussianBlur( )函数】------------------------------------
- // 描述:高斯滤波操作的回调函数
- //-----------------------------------------------------------------------------------------------
- static void on_GaussianBlur(int, void *)
- {
- GaussianBlur(g_srcImage, g_dstImage3, Size( g_nGaussianBlurValue*2+1,g_nGaussianBlurValue*2+1 ), 0, 0);
- imshow("【<3>高斯滤波】", g_dstImage3);
- }
- //-----------------------------【on_MedianBlur( )函数】------------------------------------
- // 描述:中值滤波操作的回调函数
- //-----------------------------------------------------------------------------------------------
- static void on_MedianBlur(int, void *)
- {
- medianBlur( g_srcImage, g_dstImage4, g_nMedianBlurValue*2+1 );
- imshow("【<4>中值滤波】", g_dstImage4);
- }
- //-----------------------------【on_BilateralFilter( )函数】------------------------------------
- // 描述:双边滤波操作的回调函数
- //-----------------------------------------------------------------------------------------------
- static void on_BilateralFilter(int, void *)
- {
- bilateralFilter( g_srcImage, g_dstImage5, g_nBilateralFilterValue, g_nBilateralFilterValue*2,g_nBilateralFilterValue/2 );
- imshow("【<5>双边滤波】", g_dstImage5);
- }
放出一些效果图。
原图:
方框滤波(盒式滤波):
均值滤波:
高斯滤波:
中值滤波(参数调的有些猛,妹子完全面目全非- -):
双边滤波(和原图差别不大,要仔细看才看得出效果):
OK,就放出这些吧,具体更多的运行效果大家就自己下载示例程序回去玩吧。
本篇文章到这里就基本结束了,最后放出文章配套示例程序的打包下载地址,
本篇文章的配套源代码请点击这里下载:
OK,今天的内容大概就是这些,我们下篇文章见:)