前两个part讲了一些常用的基础类和函数、操作方法之类。那我们要完整地能够处理一幅图像需要进行哪些操作步骤呢?首先我们要明确进行图像处理的目的:获得目标区域所在的位置。无论是颜色追踪、对直线或者圆的追踪、还是对某个固定图案的追踪,无论是单目标还是多目标,最终都需要获取最后的目标所在的位置。那么如何从原本的图像得到目标的位置呢?这就需要按照以下的操作步骤来进行:

获取图像->去噪(滤波、阈值化之类)->边缘检测->提取固定区域->给出目标点坐标

获取图像就像part2中“代码流程”讲解的一样,定义一个VideoCapture类型的变量,使用>>运算符来获取实时图像存储到Mat容器中。去噪是图像处理中相当重要的一个环节,我们需要在一幅图像中将干扰物排除在外,得到最终所需要的区域,part3部分将围绕“去噪”的操作进行展开。常用的操作是“滤波”操作。在图像的模糊中,滤波分为线性滤波、非线性滤波和形态学滤波。在part1中已经介绍了形态学滤波的基本操作,大家自行回顾复习;这个部分会讲线性滤波和非线性滤波的原理以及其在OpenCV中所对应的API函数。

一、关于图像滤波

1、图像的模糊和锐化

首先我们引入两个概念,“信噪比”和“滤波器”。“信噪比”,顾名思义,就是信号电压与噪声电压的比值,而图像的信噪比应该等于信号与噪声的功率谱之比(不知道什么是功率谱的戳蓝色字体)。我们进行模糊与锐化处理的目的就是提高信噪比,使最后信息提取的准确度更高。“滤波器”是一种选频装置,可以使信号中特定的频率成分通过,而极大地衰减其他频率成分。滤波器包含了“高通滤波器”和“低通滤波器”,顾名思义就是允许高频分量通过的滤波器和允许低频分量通过的滤波器——在下面也会提到这两者。在图像处理中,滤波就是用滤波器模糊图像的过程。

一般图像的能量主要集中在其低频部分,而图像的边缘部分以及噪声主要分布在高频部分。于是,图像的“模糊”就是对图像进行平滑处理,削弱其高频部分(目的是去除高频部分中的噪声),但是这同时也会模糊图像的边缘信息;而图像的“锐化”则是为了使图像的边缘、轮廓变得清晰,便于提取图像中需要的特征。我们进行图像的模糊与锐化的目的就是——提高信噪比,便于最后的特征提取的处理。

在“图像”的处理中,我们通常所说的“滤波”指的是对图像的平滑处理(就是图像的模糊),也就是低通滤波;如果指的是图像的锐化,一般来说会强调是“高通”滤波。因此本教程中如果没有强调是高通滤波,则默认指低通滤波。

图像滤波的目的是在尽量保留图像的细节特征的前提下对目标图像的噪声进行抑制。这一部分的处理好坏直接决定了所得结果的准确性(因为搞不好信噪比就会下降,有用信息没法被有效提取)。因此,记住图像处理中的操作步骤——先模糊、再锐化,模糊是为了提高信噪比,而锐化是为了突出图像边缘、便于最后提取目标区域。

还要明确的一点就是滤波的操作是怎样进行的——就是对原图像的每个像素周围一定范围内的像素进行运算(比如part1中所讲的腐蚀就是对一定区域内的像素求最小值的运算),运算的范围就称为掩膜邻域

2、(线性滤波VS非线性滤波)&&(相关VS卷积运算)

在讨论线性滤波与非线性滤波的区别、相关运算和卷积运算的区别之前,我们首先要介绍很多杂七杂八的关于图像滤波概念,比如邻域运算、锚点、归一化等等:

邻域运算是指当输出图象中每个像素是由对应的输入像素及其一个邻域内的像素共同决定时的图像运算。意思就是,输出图像中的像素点由|输入图像中的相应位置的像素点|及其周围的像素点进行一些奇奇怪怪的运算得到。而邻域运算用到了一个东西叫做“邻域算子”,邻域算子决定了邻域运算进行的是怎样的运算操作(就是这个输入图像的像素点和周围的像素点进行的是怎样的奇奇怪怪的运算)。

线性运算就是算子做加权求和的运算,用到的运算方式就是加减乘除之类;而非线性运算则不止使用简单的基本运算,还包括逻辑运算(比如位运算)、求最大值最小值、中值操作等等不能通过简单的基本运算法则而得到的运算。显然,线性滤波就是采用线性运算的算子进行的滤波,非线性滤波就是采用非线性运算的算子进行的滤波。这里大家可能会有疑惑,那形态学滤波操作,比如腐蚀与膨胀不就是非线性运算中的一种吗(最大最小值操作)?确实是这样,但是因为形态学滤波操作在图像处理中很常用也很重要,因此在各种教材中都是单独作为一个部分来讲解。下面还介绍了线性邻域算子的两种运算的区别——

线性邻域运算包括相关运算与卷积运算两种,两者原理类似,但实现上存在着细微差别。其实严格意义上来讲,滤波指的是相关运算——但是在图像处理的邻域中,很多教材已经把滤波和卷积的概念模糊掉了,因此图像处理中的滤波可以理解成是包含了相关运算与卷积运算两种的(并且在各种教材中常常用的名词是”卷积“,将滤波与卷积反而等价处理了)。其实这也是有一定道理的,因为图像处理中所用的邻域算子通常是180°对称的,且在卷积之前会对图像进行填充,因此在运算里面它俩的结果就没有差别了,这里仅仅是明确一下概念,其实搞混了也不影响最后结果。

简单来说,相关运算就是将输入图像的对应像素点与掩膜作“加权求和”运算,并将运算结果填入输出图像中的”锚点“中(锚点就是被平滑的那个点,尽管这两者的解释有点”A解释B,B解释A“的循环在里面,嗯大家自行体会体会理解一下就好)。在OpenCV的几乎所有函数中,让你填锚点的地方,如果锚点填写负值,则表示锚点取卷积核的中央(这也很容易理解嘛,就是以输入图像中对应位置的那个像素点为中心,取邻域算子的范围);并且在函数原型中都填了锚点的默认值是(-1,-1),所以,函数调用中碰到要你写锚点的地方,不填就完事了。

相关运算就是直接用掩膜与输入图像进行加权求和,并填入锚点位置。

下面是用掩膜进行相关运算,其中c=a1*b1+a2*b2+a3*b3+a4*b4+a5*b5+a6*b6+a7*b7+a8*b8+a9*b9

opencv 将固定位置的像素标记为红色 opencv图像定位_c++

要注意的是,相关运算不改变图像大小,边缘部分将进行填充(填充的内容根据算法的不同而异)。

而卷积运算则要稍微复杂一些,先要将卷积核进行180°的旋转(在平面内部旋转哈,你要是硬要向着纸面前面或后面进行翻转那我也没办法/摊手);然后再进行加权求和的运算。

下面是用卷积核进行相关运算,其中c=a9*b1+a8*b2+a7*b3+a6*b4+a5*b5+a4*b6+a3*b7+a2*b8+a1*b9

opencv 将固定位置的像素标记为红色 opencv图像定位_c++_02

这是一个卷积运算的完整栗子——

opencv 将固定位置的像素标记为红色 opencv图像定位_卷积_03

可以看到,卷积运算会改变图像大小,因为卷积运算不会对图像的边缘部分进行填充——但是图像处理中进行的卷积运算一般都是先进行填充再进行卷积的,因此如果你调用OpenCV中的相关函数,你会发现图像大小并没有改变。(你在各种API函数中会看见一个叫做“边界模式”的东西,就是对图像边缘部分进行填充的方式啦)

还有一个东西叫做“归一化”,就是要把需要处理的数据经过处理后限制在你需要的一定范围内——一般灰度值范围为[0,255],归一化的意思就是将运算后的图像的范围缩减到这个范围之内(显然,经过滤波运算后,很可能得出的结果超出了255的范围)。在很多OpenCV中提供的很多API会有是否要归一化的选项。

二、线性滤波

线性滤波的过程,就是用线性滤波算子整个儿“走”过一遍输入图像,并将计算结果填入输出图像的过程(不同的线性滤波利用不同的卷积核进行卷积,因此能得到不同的效果)。有关线性滤波与非线性滤波的综合代码示例在随附程序part3中。

1、方框滤波

方框滤波所用的核长这样:
opencv 将固定位置的像素标记为红色 opencv图像定位_opencv_04
其中
opencv 将固定位置的像素标记为红色 opencv图像定位_opencv_05
也就是说,你可以选择对结果进行归一化或者不归一化。前面说了归一化可以把数据限定在所需要的一定范围内——那进行归一化的目的是显然的,为了能够让结果仍然是一幅能够正常显示的图像嘛;那不进行归一化的目的是什么呢?非归一化的方框滤波用于计算每个像素邻域内的积分特性,像素的积分特性在某些场合要用到,比如计算某一可变区域的像素值总和的时候,利用积分图来运算比正常累加要快很多,且图像越大效果越显著。

方框滤波所提供的API函数原型:

boxFilter(
    InputArray src,		//输入图像
    OutputArray dst,	//输出图像
    int ddepth,			//输出图像的深度,-1代表使用原图的深度
    Size ksize(n,n),	//内核尺寸,n越大效果越明显
    Point anchor=Point(-1,-1),	//锚点,若点坐标为负值,则表示锚点在核的核心
    bool normalize=true,			//ture表示被归一化,false表示没有被归一化
    int borderType=BORDER_DEFAULT);//用于推断图像外部像素的某种边界模式,一般不用管它

示例:

boxFilter(srcImage,dstImage,-1,Size(3,3));
2、均值滤波

嗯均值滤波,顾名思义就是在运算邻域内求均值——看一眼上面方框滤波的核你会发现,均值滤波就是归一化后的方框滤波/摊手(所以用不到积分特性的时候,它俩没区别,嗯)。显然,它的核长这样:
opencv 将固定位置的像素标记为红色 opencv图像定位_图像处理_06
你还可能总会见到什么4邻域平均、8邻域平均之类的概念,意思就是以这样的核进行卷积运算:

opencv 将固定位置的像素标记为红色 opencv图像定位_线性滤波_07

均值滤波的函数原型在这里:

blur(
    InputArray src,		//输入图像
    OutputArray dst,	//输出图像
    Size ksize(n,n),	//内核尺寸,n越大效果越明显
    Point anchor=Point(-1,-1),		//锚点,若点坐标为负值,则表示锚点在核的核心
	int borderType=BORDER_DEFAULT);	//用于推断图像外部像素的某种边界模式,一般不用管它

呐,示例:

blur(srcImage,dstImage,Size(5,5));
3、高斯滤波

嗯,高斯滤波就是利用正态分布的算子和图像进行卷积的过程,也称高斯模糊。因为在一般图像的噪声分布概率中,高斯噪声出现的概率是相当高的,于是与之相应的高斯滤波在去噪的过程中效果也是非常好的。其缺陷是效率不是最高的。

其中,一维零均值高斯函数长这样:
opencv 将固定位置的像素标记为红色 opencv图像定位_c++_08
二维零均值高斯函数长这样:
opencv 将固定位置的像素标记为红色 opencv图像定位_c++_09
其中,高斯分布参数Sigma决定了高斯函数的宽度。sigma的取值决定了高斯函数窗口的大小。在实际中经常看到sigma取值0.8或者1。正常情况下我们由高斯函数计算得到的模板是浮点型数,但是有些情况我们为了加快计算需要将模板处理成整数,对于常见的3x3或者5x5其整数模板长成这样:
opencv 将固定位置的像素标记为红色 opencv图像定位_opencv_10

GaussianBlur(
    InputArray src,		//输入图像
    OutputArray dst,	//输出图像
    Size ksize(n,n),	//内核尺寸,n越大效果越明显
    double sigmaX,		//高斯核函数在X方向的标准偏差
    double sigmaY=0;	//高斯核函数在Y方向的标准偏差
    int borderType=BORDER_DEFAULT);//用于推断图像外部像素的某种边界模式,一般不用管它

/*其中,若sigmaY为零,就将它设为sigmaX;
若sigmaX和sigmaY都是0,那么就由ksize.width和ksize.height计算出来*/

要注意的一点是,**ksize.heightksize.width必须为正奇数!!!**示例:

GaussianBlur(srcImage,dstImage,Size(3,3),0,0);

三、非线性滤波

线性滤波器中,每个像素点的输出值是一些输入像素的加权和,因此线性滤波器容易构造,且更方便我们从频率响应的角度来分析。但是如果图像中的噪声不是高斯噪声,而是散粒噪声(意思就是存在一些像素点的值远远高于或低于周围像素点的值),这个时候线性滤波的弊端就体现出来了——线性滤波无法排除“极端值”对于图像的影响,即图像中的散粒噪声无法通过线性滤波而去除。因此我们引入了“中值滤波”——

1、中值滤波

中值滤波的思想就是用像素点的一个邻域中各点值的中值代替它原来的值,让图像的像素值接近的真实值。中学数学中就学过什么平均值、极值、中位数、方差等等之类的概念,中值就是中位数啦,好处也很显然——能够排除极端值的干扰。因此,在图像处理中,如果在邻域运算中采用“中值滤波”操作,能够排除极端值的干扰,也就是能够消除散粒噪声。不仅如此,中值滤波还能够克服线性滤波模糊图像边缘的缺点,能够比较好的保留图像边缘信息。

主要的缺点就是——慢。贼慢,大概是均值滤波花费时间的5倍以上(因为均值滤波只需要作加权和,可是中值滤波要排序,效率最高的内排序算法的平均时间复杂度大概是。。。opencv 将固定位置的像素标记为红色 opencv图像定位_c++_11)。还有就是,对于细节过多的图像,中值滤波没法很好地保留完整图像的细节信息。

嗯我们来区分一下高斯噪声和散粒噪声。高斯噪声的定义是概率密度函数符合高斯分布(就是正态分布)的噪声,包括。。。等会,我发现一个问题!百度百科上是这么说的:

opencv 将固定位置的像素标记为红色 opencv图像定位_卷积_12

毛星云的那本OpenCV教材上是这么说的:

opencv 将固定位置的像素标记为红色 opencv图像定位_卷积_13

嗯,所以它俩到底是包含关系还是对立关系???(满脸问号)好了我们不管它,我们只要记住,中值滤波能消除与周围值格格不入的极值点,好的就是这样。

来,我们看下OpenCV提供的中值滤波接口的API函数原型:

void medianBlur(
    InputArray src,		//输入图像
    OutputArray dst,	//输出图像
    int ksize);		//注意啊!!!这里是int类型的,不是Size类的啊!!!
					//还有!!!这里必须要填大于1的奇数啊!!!不然会报错啊!!!

重要的事情说三遍!!!ksize只能填大于1的奇数!!!ksize只能填大于1的奇数!!!(加上上面注释里的一遍,嗯三遍了)示例:

medianBlur(srcImage,dstImage,7);
2、双边滤波

首先,引入双边滤波里的两个权重域的概念:空间域(spatial domain S)和像素范围域(range domain R),分别受空间权重与相似权重的影响。

顾名思义,空间权重与像素位置有关,为像素之间的距离(欧式距离,空间度量);相似权重与像素值大小有关,为像素值之间的距离(辐射距离,相似性度量)。意思就是,从两个不同的角度来分析——一个是像素点之间隔得多远,一个是邻域像素之间相差有多大。

双边滤波就是由空间域与像素范围域共同影响所得到的滤波效果——当空间权重远大于相似权重时(也就是邻域内基本只受空间距离的影响下),双边滤波的效果就基本类似于高斯滤波,都由高斯函数主要决定算子的权值,这样就能有效去除低频噪声;但在图像的边缘地区,这时候相似权重的比重加大(因为像素点之间的差异大了嘛),因此能够很好地保留图像的边缘信息,不至于使在高斯滤波中一样将边缘模糊掉。

但是因为双边滤波会很好地保留图像的边缘信息——因此,随之带来的缺点就是,高频噪声无法被有效消除(毕竟边缘信息与高频噪声都处于高频段)。

那么,空间权重与相似权重各自的影响分别有多大呢?双边滤波器受3个参数的控制:滤波器半宽N、参数δs和δr。N越大,平滑作用越强;δd和δr分别控制着空间邻近度因子Wd和亮度相似度因子Wr的衰减程度。

呐,这是空间邻近度因子:
opencv 将固定位置的像素标记为红色 opencv图像定位_c++_14
其对应的滤波的图示:

opencv 将固定位置的像素标记为红色 opencv图像定位_卷积_15

这是亮度相似度因子:
opencv 将固定位置的像素标记为红色 opencv图像定位_线性滤波_16
其对应的滤波的图示:

opencv 将固定位置的像素标记为红色 opencv图像定位_线性滤波_17

两者相乘后,就会产生依赖于数据的双边滤波权重函数:
opencv 将固定位置的像素标记为红色 opencv图像定位_opencv_18
下面是其对应的API函数:

CV_EXPORTS_W void bilateralFilter( 
    InputArray src,		//输入图像
    OutputArray dst,	//输出图像 
    int d,	//每个像素邻域的直径范围。若这个值非正数,则函数会从第五个参数sigmaSpace计算该值
    double sigmaColor, 	//颜色空间过滤器的sigma值,越大表明该像素邻域内有越宽广的颜色会被混合到一起
    double sigmaSpace,	//坐标空间滤波器的sigma值,越大表明颜色相近的较远的像素将相互影响,
    					//当d>0时,d指定了邻域大小且与sigmaSpace无关,否则d正比于sigmaSpace. 
    int borderType=BORDER_DEFAULT );//用于推断图像外部像素的某种边界模式,一般不用管它

例子:

bilateralFilter(srcImage, dstImage, 25, 25 * 2, 25 / 2);

要注意的一点是,双边滤波尽管考虑的很全面,但非常慢(我刚刚试了一下,用电脑跑的时候如果d>20帧率已经低于10了,树莓派会比电脑慢很多。。。)这是这五种滤波里面最慢的orz慎重使用(一般也不需要那么高的准确度,效率很重要啊喂)。

嗯这一篇讲了图像的线性/非线性滤波操作,侧重于讲解其原理部分,因为我认为只要懂了原理,对于其对应的API函数调用是很轻松的。并且只有真正“懂”了图像处理,才能自己独立的写完一个图像相关的项目,而不是碰到题目就上网搜现成的算法,尽管大部分时候也能用,但总归不是自己的东西(而且很多搜到的算法我认为准确度并不高,那也是人家业余时间学了会图像就写了放在网上的居多)。代码想要熟悉一下的话点开随附的part3代码,并自己动手操作一遍。