本文为毛星云《OpenCV3编程入门》学习记录。
平滑处理(smoothing)也称模糊处理(bluring),是一种简单且使用频率很高的图像处理方法。平滑处理的用途有很多,最常见的是用来减少图像上的噪点或者失真。在涉及到降低图像分辨率时,平滑处理是非常好用的方法。
图像滤波,指在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理和分析的有效性和可靠性。
消除图像中的噪声成分叫作图像的平滑化或滤波操作。信号或图像的能量大部分集中在幅度谱的低频和中频段,而在较高频段,有用的信息经常被噪声淹没。因此一个能降低高频成分幅度的滤波器就能够减弱噪声的影响。
图像滤波的目的有两个:一个是抽出对象的特征作为图像识别的特能模式:另一个是为适应图像处理的要求,消除图像数字化时所混入的噪声。
而对滤波处理的要求也有两条:一是不能损坏图像的轮廓及边缘等重要信息;二是使图像清晰视觉效果好。
平滑滤波是低频增强的空间域滤波技术。它的目的有两类:一类是模糊:另一类是消除噪音。
空间域的平滑滤波一般采用简单平均法进行,就是求邻近像元点的平均亮度值。邻域的大小与平滑的效果直接相关,邻域越大平滑的效果越好,但邻域过大,平滑也会使边缘信息损失的越大,从而使输出的图像变得模糊,因此需合理选择邻域的大小生关于滤波器,一种形象的比喻是:可以把滤波器想象成一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就把这个窗口放到图像之上,透过这个窗口来看我们得到的图像。
滤波器的种类有很多,在新版本的OpenCV中,提供了如下5种常用的图像平滑处理操作方法,它们分别被封装在单独的函数中,使用起来非常方便.
线性滤波器:线性滤波器经常用于剔除输入信号中不想要的频率或者从许多频率中选择一个想要的频率。
几种常见的线性滤波器如下。
关于滤波和模糊,大家往往在初次接触的时候会弄混淆:“一会儿说滤波,一会儿又说模糊,似乎不太清楚。”
不过,没关系,在这里,我们就来分析一下,为大家扫清障碍。
上文已经提到过,滤波是将信号中特定波段频率滤除的操作,是抑制和防止干扰的一项重要措施。
为了方便说明,就拿我们经常用的高斯滤波来作例了吧。滤波可分低通滤波和高通滤波两种:高斯滤波是指用高斯函数作为滤波函数的滤波操作,至于是不是模糊,要看是高斯低通还是高斯高通,低通就是模糊,高通就是锐化。
其实说白了是很简单的:
邻域算子(局部算子)是利用给定像素周围的像索值的决定此像素的最终输出值的一种算子。而线性邻域滤波就是一种常用的邻域算子,像素的输出值取决于输入像素的权和,具体过程如下图所示。
邻域算子除了用于局部色调调整以外,还可以用于图像滤波,以实现图像的平滑和锐化,图像边缘增强或者图像噪声的去除。本节我们介绍的主角是线性邻域滤波算子,即用不同的权重去结合一个小邻域内的像素,来得到应有的处理效果。
图注:
邻域滤波(卷积)一一左边图像与中间图像的卷积产生右边图像。目标图像中蓝色标记的像素是利用原图像中红色标记的像素计算得到的。
线性滤波处理的输出像素值 g ( i , j ) g(i,j) g(i,j)是输入像素值 f ( i + k , j + I ) f(i+k,j+I) f(i+k,j+I)的加权和,如下
g ( i , j ) = ∑ k , I f ( i + k , j + I ) h ( k , I ) g(i,j)=\sum_{k,I}f(i+k,j+I)h(k,I) g(i,j)=∑k,If(i+k,j+I)h(k,I)
其中的 h ( k , I ) h(k,I) h(k,I),我们称其为“核”,是滤波器的加权系数,即滤波器的“滤波系数”。
上而的式子可以简单写作:
g = f ⨂ h g=f\bigotimes h g=f⨂h
其中f表示输入像素值,h表示加权系数“核”,g表示输出像素值。
在新版本的OpenCV中,提供了如下三种常用的线性滤波操作,它们分别被封装在单独的函数中,使用起来非常方便。
下面我们来对它们进行一一介绍。
方框滤波(boxFilter)被封装在一个名为boxblur的函数中,即boxblur函数的作用是使用方框滤波器(boxfilter)来模糊一张图片,从src输入,从dst输出。
函数原型如下。
C++:
void boxFilter(InputArray src,OutputArray dst,int ddepth,Size ksize,Point anchor=Point(-1,-1),bool normalize=true,int borderType=BORDER_DEFAULT)
参数详解如下。
BoxFilter()函数方框滤波所用的核表示如下。
K = a [ 1 1 1 . . . 1 1 1 1 1 . . . 1 1 . . . . . . . . . . . . . . . . . . 1 1 1 . . . 1 1 ] K=a\begin{bmatrix} 1&1 &1 &... &1 &1 \\ 1&1 &1 &... &1 &1 \\ ...&... &... &... &... &... \\ 1&1 &1 &... &1 &1 \end{bmatrix} K=a⎣⎢⎢⎡11...111...111...1............11...111...1⎦⎥⎥⎤
其中:
a { 1 k s i z e . w e i g h t ∗ k s i z e . h e i g h t n o r r m a l i z e = t u r e 1 n o r r m a l i z e = f a l s e a\left\{\begin{matrix} \frac{1}{ksize.weight*ksize.height} & norrmalize=ture \\ 1& norrmalize=false \end{matrix}\right. a{ksize.weight∗ksize.height11norrmalize=turenorrmalize=false
上式中f表示原图,h表示核,g表示目标图,当normalize=true的时候,方框滤波就变成了我们熟悉的均值滤波。也就是说,均值滤波是方框滤波归一化(normahzed)后的特殊情况。其中,归一化就是把要处理的量都缩放到一个范围内,比如(0,1),以便统一处理和直观量化。而非归一化(Unnormalized)的方框滤波用于计算每个像素邻域内的积分特性,比如密集光流算法(dense optical flow algorithms)中用到的图像倒数的协方差矩阵(covariance matrices of image derivatives)。
如果我们要在可变的窗口中计算像素总和,可以使用integral()函数。
均值滤波,是最简单的一种滤波操作,输出图像的每一个像素是核窗口内输入图像对应像索的平均值(所有像素加权系数相等),其实说白了它就是归一化后的方框滤波。我们在下文进行源码剖析时会发现,blur函数内部中其实就是调用了一下boxFilter。
下面开始讲均值滤波的内容吧。
均值滤波的理论简析
均值滤波是典型的线性滤波算法,主要方法为邻域平均法,即用一片图像区域的各个像索的均值来代替原图像中的各个像素值。一般需要在图像上对目标像素给出一个模板(内核),该模板包括了其周围的临近像素(比如以目标像素为中心的周8(3x3)个像素,构成一个滤波模板,即去掉目标像素本身)。再用模板中的个体像索的平均值来代替原来像素值。即对待处理的当前像素点(x,y),选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点(x,y),作为处理后图像在该点上的灰度值 g ( x , y ) g(x,y) g(x,y)即 g ( x , y ) = m 1 ∑ f ( x , y ) g(x,y)=m\frac{1}{\sum{f(x,y)}} g(x,y)=m∑f(x,y)1,其中m为该模板中包含当前像素在内的像素总个数。
均值滤波的缺陷
均值滤汲本身存在着固有的缺陷,即它不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声点。
在OpenCV中使用均值滤波——blur函数
blur函数的作用是:对输入的图像src进行均值滤波后用输出。
blur函数在OpenCV官方文档中,给出的其核是这样的:
K = 1 k s i z e . w i d t h + k s i z e . h e i g h t [ 1 1 1 . . . 1 1 1 1 1 . . . 1 1 . . . . . . . . . . . . . . . . . . 1 1 1 . . . 1 1 ] K=\frac{1}{ksize.width+ksize.height}{\begin{bmatrix} 1&1 &1 &... &1 &1 \\ 1&1 &1 &... &1 &1 \\ ... &... &... &... &... &... \\ 1&1 &1 &... &1 &1 \end{bmatrix}} K=ksize.width+ksize.height1⎣⎢⎢⎡11...111...111...1............11...111...1⎦⎥⎥⎤
这个内核一看就明了,就是在求均值,即blur函数封装的就是均值滤波。
blur函数的原型如下。
C++:
void blur(InputArray rc,OutputArray dst,Size ksize,Point anchor=Point(-1,-1),int borderType = BORDER_DEFAULT);
高斯滤波的理论简析
高斯滤波是一种线性平滑滤波,可以消除高斯噪声,广泛应用于图像处理的减噪过程。通俗地讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像索值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
大家常说高斯滤波是最有用的滤波操作,虽然它用起来效率往往不是最高的。高斯模糊技术生成的图像,其视觉效果就像是经过一个半透明屏幕在观察图像,这与镜头焦外成像效果散景以及普通照明阴影中的效果都明显不同。高斯平滑也用于计算机视觉算法中的预先处理阶段,以增强图像在不同比例大小下的图像效果(参见尺度窄间表示以及尺度空间实现),从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。
图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅里叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波操作。
高斯滤波器是一类根据高斯函数的形状来选择权值的线性平滑滤波器。高斯平滑滤波器对于抑制服从正态分布的噪声非常有效。一维零均值高斯函数如下。
G ( x ) = e x p ( − x 2 / ( 2 s i g m a 2 ) ) G(x)=exp(-x^{2}/(2sigma^{2})) G(x)=exp(−x2/(2sigma2))
其中,高斯分布参数Sigma决定了高斯函数的宽度,对于图像处理来说,常用二维零均值离散高斯函数作平滑滤波器。
二维高斯函数如下。
G 0 ( x , y ) = A e − ( x − u x ) 2 2 σ x 2 + − ( y − u y ) 2 2 σ y 2 G_0(x,y)=Ae\frac{-(x-u_{x})^{2}}{2\sigma _{x}^{2}}+\frac{-(y-u_{y})^{2}}{2\sigma_{y}^{2}} G0(x,y)=Ae2σx2−(x−ux)2+2σy2−(y−uy)2
高斯滤波:GaussianBlur函数
GaussianBlur函数的作用是用高斯滤波器来模糊一张图片,对输入的图像src进行高斯滤波后用dst输出。它将图像和指定的高斯核函数做卷积运算,并且支持就地过滤(ln-placefiltering)。
C++:
void GaussianBlur(lnputArray src,OutputArray dst,Size ksize,double sigmaX,double sigmaY=0,int borderType=BORDER_DEFAULT)
为了结果的正确性着想,最好是把第三个参数Size、第四个参数sigmaX和第五个参数sigmaY全部指定到。
在这一部分中,笔者将带领大家领略OpenCV的开源魅力,对OpenCV中讲解到的线性滤波函数一一boxFliter、blur和GaussianBlur函数以及周边所涉及到的源码进行适当的剖析。
OpenCV中boxFilter函数源码解析
在opencv的安装路径\sources\modules\imgprooc\src下的smooth.cpp源文件中找到boxFliter函数的源代码,如下:
void cv::boxFilter( InputArray _src, OutputArray _dst, int ddepth,
Size ksize, Point anchor,
bool normalize, int borderType )
{
Mat src = _src.getMat();
int sdepth = src.depth(), cn = src.channels();
if( ddepth < 0 )
ddepth = sdepth;
_dst.create( src.size(), CV_MAKETYPE(ddepth, cn) );
Mat dst = _dst.getMat();
if( borderType != BORDER_CONSTANT && normalize && (borderType & BORDER_ISOLATED) != 0 )
{
if( src.rows == 1 )
ksize.height = 1;
if( src.cols == 1 )
ksize.width = 1;
}
#ifdef HAVE_TEGRA_OPTIMIZATION
if ( tegra::box(src, dst, ksize, anchor, normalize, borderType) )
return;
#endif
Ptr<FilterEngine> f = createBoxFilter( src.type(), dst.type(),
ksize, anchor, normalize, borderType );
f->apply( src, dst );
}
FilterEngjne类解析:OpenCV图像滤波核心引擎
FilterEngine类是OpenCV关于图像滤波的主力军类,是OpenCV图像滤波功能的核心引擎。各种滤波函数如blur、GaussianBlur,其实是就是在数末尾处定义了一个Ptr类型的f,然后f->apply(src,dst)了一下而已。
这个类可以把几乎所有的滤波操作施加到图像上,它包含了所有必要的中间缓存器。有很多和滤波相关的create系函数的返回值直接就是Ptr。
比如:
cv::createSeparableLinearFilter()
cv:createLinearFilter(),cv::createGaussianFilter(),cv::createDerivFilter()
cv::createBoxFilter()
cv::createMorphologyFilter()
下面给出其中一个函数的原型。
C++: void boxFilter(InputArray src,OutputArray dst, int ddepth, Size ksize, Point anchor=Point(-1,-1), boolnormalize=true, int borderType=BORDER_DEFAULT )
上面提到过,其中的Ptr是用来动态分配的对象的智能指针模板类,而尖括号里的模板参数就是FilterEngine。
使用FilterEngine类可以分块处理大量的图像,构建复杂的管线,其中就包含一些进行滤波阶段。如果我们需要使用预先定义好的的滤波操作,有cv::filter2D()、cv::erode()和cv:.dilate()可以选择,它们不依赖于FilterEngine,在自己函数体内部就实现了FilterEngine提供的功能;不像其他诸如我们今天讲的blur系列函数,依赖于FilterEngine引擎。
我们来看一下其类声明经过详细注释的源码,如下:
//-----------------------------------【FilterEngine类中文注释版源代码】----------------------------
// 代码作用:FilterEngine类,OpenCV图像滤波功能的核心引擎
// 说明:以下代码为来自于计算机开源视觉库OpenCV的官方源代码
// OpenCV源代码版本:2.4.9
// 源码路径:…\opencv\sources\modules\imgproc\include\opencv2\imgproc\imgproc.hpp
// 源文件中如下代码的起始行数:222行
// 中文注释by浅墨
//--------------------------------------------------------------------------------------------------------
class CV_EXPORTS FilterEngine
{
public:
//默认构造函数
FilterEngine();
//完整的构造函数。 _filter2D 、_rowFilter 和 _columnFilter之一,必须为非空
FilterEngine(const Ptr<BaseFilter>& _filter2D,
constPtr<BaseRowFilter>& _rowFilter,
constPtr<BaseColumnFilter>& _columnFilter,
int srcType, int dstType, intbufType,
int_rowBorderType=BORDER_REPLICATE,
int _columnBorderType=-1,
const Scalar&_borderValue=Scalar());
//默认析构函数
virtual ~FilterEngine();
//重新初始化引擎。释放之前滤波器申请的内存。
void init(const Ptr<BaseFilter>& _filter2D,
constPtr<BaseRowFilter>& _rowFilter,
constPtr<BaseColumnFilter>& _columnFilter,
int srcType, int dstType, intbufType,
int_rowBorderType=BORDER_REPLICATE, int _columnBorderType=-1,
const Scalar&_borderValue=Scalar());
//开始对指定了ROI区域和尺寸的图片进行滤波操作
virtual int start(Size wholeSize, Rect roi, int maxBufRows=-1);
//开始对指定了ROI区域的图片进行滤波操作
virtual int start(const Mat& src, const Rect&srcRoi=Rect(0,0,-1,-1),
bool isolated=false, intmaxBufRows=-1);
//处理图像的下一个srcCount行(函数的第三个参数)
virtual int proceed(const uchar* src, int srcStep, int srcCount,
uchar* dst, intdstStep);
//对图像指定的ROI区域进行滤波操作,若srcRoi=(0,0,-1,-1),则对整个图像进行滤波操作
virtual void apply( const Mat& src, Mat& dst,
const Rect&srcRoi=Rect(0,0,-1,-1),
Point dstOfs=Point(0,0),
bool isolated=false);
//如果滤波器可分离,则返回true
boolisSeparable() const { return (const BaseFilter*)filter2D == 0; }
//返回输入和输出行数
int remainingInputRows() const;
intremainingOutputRows() const;
//一些成员参数定义
int srcType, dstType, bufType;
Size ksize;
Point anchor;
int maxWidth;
Size wholeSize;
Rect roi;
int dx1, dx2;
int rowBorderType, columnBorderType;
vector<int> borderTab;
int borderElemSize;
vector<uchar> ringBuf;
vector<uchar> srcRow;
vector<uchar> constBorderValue;
vector<uchar> constBorderRow;
int bufStep, startY, startY0, endY, rowCount, dstY;
vector<uchar*> rows;
Ptr<BaseFilter> filter2D;
Ptr<BaseRowFilter> rowFilter;
Ptr<BaseColumnFilter> columnFilter;
};
OpenCV中blur函数源码剖析
在opencv的安装路径\sources\modules\imgprooc\src下的smooth.cpp源文件中找到blur函数的源代码,如下:
void cv::blur( InputArray src, OutputArray dst,
Size ksize, Point anchor, int borderType )
{
boxFilter( src, dst, -1, ksize, anchor, true, borderType );
}
均值滤波是均一化的方框滤波。
void cv::GaussianBlur( InputArray _src, OutputArray _dst, Size ksize,
double sigma1, double sigma2,
int borderType )
{
Mat src = _src.getMat();
_dst.create( src.size(), src.type() );
Mat dst = _dst.getMat();
if( borderType != BORDER_CONSTANT )
{
if( src.rows == 1 )
ksize.height = 1;
if( src.cols == 1 )
ksize.width = 1;
}
if( ksize.width == 1 && ksize.height == 1 )
{
src.copyTo(dst);
return;
}
#ifdef HAVE_TEGRA_OPTIMIZATION
if(sigma1 == 0 && sigma2 == 0 && tegra::gaussian(src, dst, ksize, borderType))
return;
#endif
#if defined HAVE_IPP && (IPP_VERSION_MAJOR >= 7)
if(src.type() == CV_32FC1 && sigma1 == sigma2 && ksize.width == ksize.height && sigma1 != 0.0 )
{
IppiSize roi = {src.cols, src.rows};
int bufSize = 0;
ippiFilterGaussGetBufferSize_32f_C1R(roi, ksize.width, &bufSize);
AutoBuffer<uchar> buf(bufSize+128);
if( ippiFilterGaussBorder_32f_C1R((const Ipp32f *)src.data, (int)src.step,
(Ipp32f *)dst.data, (int)dst.step,
roi, ksize.width, (Ipp32f)sigma1,
(IppiBorderType)borderType, 0.0,
alignPtr(&buf[0],32)) >= 0 )
return;
}
#endif
Ptr<FilterEngine> f = createGaussianFilter( src.type(), ksize, sigma1, sigma2, borderType );
f->apply( src, dst );
}