第6章 图像处理
到这里,我们已经了解了OpenCV 库的结构,知道了用来表示图像的基本数 据结构,也熟悉了运行程序并将结果显示到屏幕上的Highgui 接口。现在,我们 可以利用这些控制图像结构的基本方法,来学习更加复杂的图像处理方法。
所谓的学习高级的处理方法,即把图像以“图像”的方式来处理,而不是作 为只由颜色值或灰度图组成的数组。而我们在提到“图像处理”时,就是表示使 用图像结构中所定义的高层处理方法来完成特殊任务,而这些任务,就是图形和 视觉范畴的任务。
本 章 中 , 你 将 学 到 :
- 三种线性滤波:方框滤波、均值滤波、高斯滤波
- 两种非线性滤波:中值滤波、双边滤波
- 7种图像处理形态学:腐蚀、膨胀,开运算、闭运算、形态学梯度、顶帽、
黑帽 - 漫水填充
- 图像缩放
- 图像金字塔
- 阈值化
6.1 线性滤波:方框滤波、均值滤波、高斯滤波
6.1.1 平滑处理
平滑处理 (smoothing) 也称模糊处理(bluring), 是一种简单且使用频率很 高的图像处理方法。平滑处理的用途有很多,最常见的是用来减少图像上的噪点 或者失真。在涉及到降低图像分辨率时,平滑处理是非常好用的方法。
6.1.2 图像滤波与滤波器
图像滤波,指在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制, 是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理 和分析的有效性和可靠性。
消除图像中的噪声成分叫作图像的平滑化或滤波操作。信号或图像的能量大 部分集中在幅度谱的低频和中频段,而在较高频段,有用的信息经常被噪声淹没。 因此一个能降低高频成分幅度的滤波器就能够减弱噪声的影响。
图像滤波的目的有两个:一个是抽出对象的特征作为图像识别的特征模式; 另一个是为适应图像处理的要求,消除图像数字化时所混入的噪声。
而对滤波处理的要求也有两条:一是不能损坏图像的轮廓及边缘等重要信息; 二是使图像清晰视觉效果好。
平滑滤波是低频增强的空间域滤波技术。它的目的有两类: 一类是模糊;另 一类是消除噪音。
空间域的平滑滤波一般采用简单平均法进行,就是求邻近像元点的平均亮度 值。邻域的大小与平滑的效果直接相关,邻域越大平滑的效果越好,但邻域过大, 平滑也会使边缘信息损失的越大,从而使输出的图像变得模糊,因此需合理选择 邻域的大小。
关于滤波器,一种形象的比喻是:可以把滤波器想象成一个包含加权系数的 窗口,当使用这个滤波器平滑处理图像时,就把这个窗口放到图像之上,透过这 个窗口来看我们得到的图像。
滤波器的种类有很多,在新版本的OpenCV 中,提供了如下5种常用的图像 平滑处理操作方法,它们分别被封装在单独的函数中,使用起来非常方便。
- 方框滤波——BoxBlur函数
- 均值滤波(邻域平均滤波)——Blur函数
- 高斯滤波——GaussianBlur函数
- 中值滤波——medianBlur函数
- 双边滤波——bilateralFilter函数
本节要讲解的是作为线性滤波的方框滤波、均值滤波和高斯滤波。其他两种非线性滤波操作——中值滤波和双边滤波,我们留待下节讲解。
6.1.3 线性滤波器的简介
线性滤波器:线性滤波器经常用于剔除输入信号中不想要的频率或者从许多 频率中选择一个想要的频率。
几种常见的线性滤波器如下。
- 低通滤波器:允许低频率通过;
- 高通滤波器:允许高频率通过;
- 带通滤波器:允许一定范围频率通过;
- 带阻滤波器:阻止一定范围频率通过并且允许其他频率通过;
- 全通滤波器:允许所有频率通过,仅仅改变相位关系;
- 陷波滤波器(Band-Stop Filter):阻止一个狭窄频率范围通过,是一种特殊 带阻滤波器。
6.1.4 滤波和模糊
关于滤波和模糊,大家往往在初次接触的时候会弄混淆:“一会儿说滤波, 一 会儿又说模糊,似乎不太清楚。”
没关系,在这里,我们就来分析一下,为大家扫清障碍。
上文已经提到过,滤波是将信号中特定波段频率滤除的操作,是抑制和防止 干扰的一项重要措施。
为了方便说明,就拿我们经常用的高斯滤波来作例子吧。滤波可分低通滤波 和高通滤波两种:高斯滤波是指用高斯函数作为滤波函数的滤波操作,至于是不 是模糊,要看是高斯低通还是高斯高通,低通就是模糊,高通就是锐化。
其实说白了是很简单的:
- 高斯滤波是指用高斯函数作为滤波函数的滤波操作;
- 高斯模糊就是高斯低通滤波。
6.1.5 邻域算子与线性邻域滤波
邻域算子(局部算子)是利用给定像素周围的像素值的决定此像素的最终输 出值的一种算子。而线性邻域滤波就是一种常用的邻域算子,像素的输出值取决 于输入像素的加权和,具体过程如图6.1所示。
邻域算子除了用于局部色调调整以外,还可以用于图像滤波,以实现图像的 平滑和锐化,图像边缘增强或者图像噪声的去除。本节我们介绍的主角是线性邻 域滤波算子,即用不同的权重去结合一个小邻域内的像素,来得到应有的处理效果。
图注:邻域滤波(卷积)——左边图像与中间图像的卷积产生右边图像。目 标图像中蓝色标记的像素是利用原图像中红色标记的像素计算得到的。
线性滤波处理的输出像素值g(i,j) 是输入像素值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,I∑f(i+k,j+I)h(k,I)
其中的 h(k,D), 我们称其为“核”,是滤波器的加权系数,即滤波器的“滤波 系数”。
上面的式子可以简单写作:
g = f ⊗ h g=f \otimes h g=f⊗h
其中f 表示输入像素值,h 表示加权系数“核”,g 表示输出像素值。
在新版本的OpenCV 中,提供了如下三种常用的线性滤波操作,它们分别被 封装在单独的函数中,使用起来非常方便。
- 方框滤波—boxblur 函数
- 均值滤波—blur 函数
- 高斯滤波——GaussianBlur函数 下面我们来对它们进行一一介绍。
6.1.6 方 框 滤 波(box Filter)
方框滤波 (box Filter) 被封装在一个名为boxblur 的函数中,即boxblur 函数 的作用是使用方框滤波器(box filter)来模糊一张图片,从src输入,从dst输出。
函数原型如下。
void boxFilter(InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1, -1), boolnormalize = true, int borderType = BORDER_DEFAULT)
参数详解如下。
- 第一个参数,InputArray类型的src, 输入图像,即源图像,填Mat 类的对象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但 需要注意,待处理的图片深度应该为CV_8U 、CV_ 16U 、CV_ 16S 、CV_32F 以及CV_64F 之一。
- 第二个参数,OutputArray 类型的dst, 即目标图像,需要和源图片有一样 的尺寸和类型。
- 第三个参数,int类型的ddepth,输出图像的深度, -1代表使用原图深度, 即 src.depth()。
- 第四个参数,Size 类 型 ( 对Size类型稍后有讲解)的ksize,内核的大小。 一般用Size(w,h) 来表示内核的大小,其中w 为像素宽度,h 为像素高度。 Size(3,3) 就表示3x3的核大小,Size(5,5) 就表示5x5的核大小。
- 第五个参数,Point 类型的anchor, 表示锚点(即被平滑的那个点)。注意 它有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心 为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。
- 第六个参数,bool类型的normalize,默认值为true,一个标识符,表示内 核是否被其区域归一化(normalized) 了 。
- 第七个参数,int类型的borderType,用于推断图像外部像素的某种边界模 式。有默认值BORDER_DEFAULT, 我们一般不去管它。
BoxFilter()函数方框滤波所用的核表示如下。
其中:
上式中f 表示原图,h 表示核,g 表示目标图,当normalize=true 的时候,方 框滤波就变成了我们熟悉的均值滤波。也就是说,均值滤波是方框滤波归一化 (normalized)后的特殊情况。其中,归一化就是把要处理的量都缩放到一个范围 内,比如(0,1),以便统一处理和直观量化。而非归一化(Unnormalized) 的方框 滤波用于计算每个像素邻域内的积分特性,比如密集光流算法(dense optical flow algorithms) 中用到的图像倒数的协方差矩阵 (covariance matrices of image derivatives)。
如果我们要在可变的窗口中计算像素总和,可以使用integral()函数。
6.1.7 均值滤波
均值滤波,是最简单的一种滤波操作,输出图像的每一个像素是核窗口内输 入图像对应像素的平均值(所有像素加权系数相等),其实说白了它就是归一化后的方框滤波。我们在下文进行源码剖析时会发现,blur 函数内部中其实就是调用 了一下boxFilter。
下面开始讲均值滤波的内容吧。
1. 均值滤波的理论简析
均值滤波是典型的线性滤波算法,主要方法为邻域平均法,即用一片图像区 域的各个像素的均值来代替原图像中的各个像素值。一般需要在图像上对目标像 素给出一个模板(内核),该模板包括了其周围的临近像素(比如以目标像素为中 心的周围8(3x3-1) 个像素,构成一个滤波模板,即去掉目标像素本身)。再用模 板中的全体像素的平均值来代替原来像素值。即对待处理的当前像素点(x,y),
选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再 把该均值赋予当前像素点(x,y), 作为处理后图像在该点上的灰度点g(x,y), 即 g(x,y)=1/m Zf(x,y),其 中m 为该模板中包含当前像素在内的像素总个数。
2. 均值滤波的缺陷
均值滤波本身存在着固有的缺陷,即它不能很好地保护图像细节,在图像去 噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声 点。
3. 在OpenCV 中使用均值滤波——blur函数
blur函数的作用是:对输入的图像src进行均值滤波后用dst输出。 blur函数在OpenCV 官方文档中,给出的其核是这样的:
这个内核一看就明了,就是在求均值,即blur 函数封装的就是均值滤波。 blur函数的原型如下。
void blur(InputArray src, OutputArraydst, Size ksize, Point anchor = Point(-1, -1), int borderType = BORDER_DEFAULT)
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但 需要注意的是,待处理的图片深度应该为 CV_8U 、CV_ 16U 、CV_ 16S、
CV_32F 以 及CV_64F 之一。 - 第二个参数,OutputArray 类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。比如可以用 Mat::Clone, 以源图片为模板,来初始化得到 如假包换的目标图。
- 第三个参数,Size类 型 ( 对Size类型稍后有讲解)的ksize,内核的大小。
一般写作Size(w,h) 来表示内核的大小(其中,w 为像素宽度,h 为像素高 度 ) 。Size(3,3) 就表示3×3的核大小,Size(5,5) 就表示5×5的核大小
- 第四个参数,Point类型的anchor, 表示锚点(即被平滑的那个点),注意 它有默认值Point(-1,-1)。如果这个点坐标是负值,就表示取核的中心为锚 点,所以默认值Point(-1,-1)表示这个锚点在核的中心。
- 第五个参数,int类型的borderType,用于推断图像外部像素的某种边界模 式。有默认值BORDER_DEFAULT, 我们一般不去管它。
6.1.8 高 斯 滤 波
1. 高斯滤波的理论简析
高斯滤波是一种线性平滑滤波,可以消除高斯噪声,广泛应用于图像处理的 减噪过程。通俗地讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像 素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的 具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板 确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
大家常说高斯滤波是最有用的滤波操作,虽然它用起来效率往往不是最高的。 高斯模糊技术生成的图像,其视觉效果就像是经过一个半透明屏幕在观察图像, 这与镜头焦外成像效果散景以及普通照明阴影中的效果都明显不同。高斯平滑也 用于计算机视觉算法中的预先处理阶段,以增强图像在不同比例大小下的图像效 果(参见尺度空间表示以及尺度空间实现)。从数学的角度来看,图像的高斯模糊 过程就是图像与正态分布做卷积。由于正态分布又叫作高斯分布,所以这项技术 就叫作高斯模糊。
图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函 数的傅里叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通 滤波操作。
高斯滤波器是一类根据高斯函数的形状来选择权值的线性平滑滤波器。高斯 平滑滤波器对于抑制服从正态分布的噪声非常有效。一维零均值高斯函数如下。
G
(
x
)
=
e
x
p
(
−
x
2
2
s
i
g
m
a
2
)
G(x)=exp(\frac{-x^2}{2sigma^2})
G(x)=exp(2sigma2−x2)
其中,高斯分布参数Sigma 决定了高斯函数的宽度。对于图像处理来说,常 用二维零均值离散高斯函数作平滑滤波器。
二维高斯函数如下。
2 .高斯滤波:GaussianBlur 函 数
GaussianBlur函数的作用是用高斯滤波器来模糊一张图片,对输入的图像src进行高斯滤波后用dst 输出。它将源图像和指定的高斯核函数做卷积运算,并且 支持就地过滤(In-placefiltering)。
C++:void GaussianBlur(InputArray src,OutputArray dst,Size ksize, double sigmaX,double sigmaY=0,intborderType=BORDER_DEFAULT)
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。它可以是单独的任意通道数的图片,但需要注意的是,其图片深 度应该为CV_8U 、CV_ 16U 、CV_ 16S 、CV_32F 以 及CV_64F 之一。
- 第二个参数,OutputArray 类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。比如可以用 Mat::Clone, 以源图片为模板,来初始化得到 如假包换的目标图。
- 第三个参数,Size 类型的 ksize 高斯内核的大小。其中 ksize.width 和 ksize.height可以不同,但它们都必须为正数和奇数,或者是零,这都由sigma 计算而来。
- 第四个参数,double类型的sigmaX, 表示高斯核函数在X 方向的的标准偏 差。
- 第五个参数,double类型的sigmaY, 表示高斯核函数在Y 方向的的标准偏 差。若sigmaY 为零,就将它设为sigmaX; 如 果sigmaX 和 sigmaY 都是0, 那么就由ksize.width 和 ksize.height 计算出来。
为了结果的正确性着想,最好是把第三个参数Size、第四个参数sigmaX 和第 五个参数sigmaY 全部指定到。 - 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模 式。有默认值BORDER_DEFAULT, 我们一般不去管它。
6.1.9 线性滤波相关OpenCV源码剖析
在这一部分中,笔者将带领大家领略OpenCV 的开源魅力,对OpenCV 中 节 讲解到的线性滤波函数——boxFilter、blur和 GaussianBlur 函数以及周边所涉及到 的源码进行适当的剖析。
通过本章的学习,我们可以对 OpenCV 有更加深刻的理解,成为一个高端大 气的OpenCV 使用者。
1.OpenCV中 boxFilter 函数源码解析
可以在OpenCV 的安装路径\sources\moduleslimgproclsrc 下 的smooth.cpp 源 文 件中找到boxFilter 函数的源代码,如下。
void cv::boxFilter(InputArray _src, OutputArray _dst, int ddepth, Size ksize, Point anchor, bool normalize, int borderType)
{
Mat src = _src.getMat(); // 复制源图的形参Mat 数据到临时变量,用于稍后的操
int sdepth = src.depth(), cn = src.channels(); // 定义 int型临时变量,代表源图深度的sdepth, 源图通道的引用cn
// 处理ddepth 小于零的情况
if (ddepth < 0)
ddepth = sdepth;
dst.create(src.size(), cV_MAKETYPE(ddepth, cn)); // 初始化目标图
Mat dst = _dst.getMat(); // 复制目标图的形参Mat 数据到临时变量,用于稍后的操作
// 处理 borderType 不 为 BORDER_CONSTANT 且 normalize 为真的情况
if (borderType != BORDER_CONSTANT && normalize)
{
if (src.rows == 1)
ksize.height;
if (src.cols == 1)
ksize.width = 1;
}
// 若之前有过HAVE_TEGRA_OPTIMIZATION优化选项的定义,则执行宏体中的tegra,优化版函数并返回
#ifdef HAVE_TEGRA_OPTIMIZATION
if (tegra::box(src, dst, ksize, anchor, normalize, borderType))
return;
#endif
// 调用FilterEngine 滤波引擎,正式开始滤波操作
Ptr<FilterEngine> f = createBoxFilter(src.type(), dst.type(), ksize, anchor, normalize, borderType);
f->apply(src, dst);
}
其中,Ptr 是用来动态分配的对象的智能指针模板类。可以发现,函数的内部 代码思路是很清晰的:先复制源图的形参Mat 数据到临时变量,定义一些临时变量,
再处理ddepth 小于零的情况,接着处理 borderType 不为 BORDER_CONSTANT且 normalize为真的情况,最终调用FilterEngine 滤波引擎创建一个BoxFilter, 正式开 始滤波操作 。
这里的FilterEngine 是 OpenCV 图像滤波功能的核心引擎,我们有必要详细剖 析看其源代码 。
2.FilterEngine类解析: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()
下面给出其中 一 个函数的原型。
Ptr<FilterEngine> createLinearFilter(int srcType, int dstType,
InputArray kernel, Point_anchor = Point(-1, -1), double delta = 0, int rowBorderType = BORDER_DEFAULT, intcolumnBorderType = -1, const Scalar &borderValue = Scalar())
上面提到过,其中的Ptr 是用来动态分配的对象的智能指针模板类,而尖括 号里的模板参数就是FilterEngine。
使用FilterEngine类可以分块处理大量的图像,构建复杂的管线,其中就包含 一 些进行滤波阶段。如果我们需要使用预先定义好的的滤波操作,有cv::filter2DO、
cv::erode()和 cv::dilate()可以选择,它们不依赖于FilterEngine, 在 自 己 函 数 体 内 部 就实现了FilterEngine 提供的功能;不像其他诸如我们今天讲的blur 系列函数, 依 赖 于FilterEngine 引 擎 。
我们看下其类声明经过详细注释的源码,如下。
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 dxl, dx2;
int rowBorderType, columnBorderType;
vector<int> borderTab;
int borderElemSize;
vector<uchar> ringBuf;
vector<uchar> srcRow;
vector<uchar> constBorderValue;
vector<uchar> constBorderRow;
int bufStep, startY, startYO, endY, rowCount, dstY;
vector<uchar *> rows;
Ptr<BaseFilter> filter2D;
Ptr<BaseRowFilter> rowFilter;
Ptr<BaseColumnFilter> columnFiIter;
};
3.OpenCV 中 blur函数源码剖析
我 们 可 以 在 OpenCV 的 安 装 路 径 \sources\moduleslimgproclsrc 下 的 smooth.cpp 源文件中找到blur 的源代码,下面一起来看OpenCV 中 blur 函数定 义 的 真 面 目 。
void cv::blur(InputArray src, OutputArray Size ksize, Point anchor, int)
{
// 调用boxFilter 函数进行处理
boxFilter(src, dst, -1, ksize, anchor, true, borderType);
}
可以发现,在blur 函数内部调用了一个boxFilter函数,且第6个参数为 true, 也就是我们上文所说的normalize=true,即均值滤波是均一化后的方框滤波。
6.1.10 OpenCV中 GaussianBlur 函数源码剖析
下面我们看一下OpenCV中 GaussianBlur函数源代码:
void cv::GaussianBlur(InputArray _src, OutputArray _dst, Size ksize,
double sigmal, doublesigma2,
int borderType)
{
// 复制形参Mat 数据到临时变量,用于稍后的操作
Mat src = _src.getMat();
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat();
// 处理边界选项不为BORDER_CONSTANT时的情况
if (borderType != BORDER_CONSTANT)
{
if (src.rows == 1)
ksize.height = 1;
if (src.cols == 1)
ksize.width = 1;
}
// 若ksize 长宽都为1,将源图复制给目标图
if (ksize.width == 1 && ksize.height == 1)
{
src.copyTo(dst);
return;
}
// 若之前有过HAVE_TEGRA_OPTIMIZATION 优化选项的定义,则执行宏体中的tegra 优 化版函数并返回
#ifdef HAVE_TEGRA_OPTIMIZATION
if (sigmal == 0 && sigma2 == 0 && tegra::gaussian(src, dst, ksize, borderType))
return;
#endif
// 如果HAVE_IPP&&(IPP_VERSION_MAJOR >=7为真,则执行宏体中语句
#if defined HAVE_IPP && (IPP_VERSION_MAJOR >= 7)
if (src.type() == CV_32FC1 && sigmal == sigma2 && ksize.width == ksize.height && sigmal != 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)sigmal,
(IppiBorderType)borderType, 0.0, alignPtr(&buf[0], 32)) >= 0)
return;
#endif
// 调用滤波引擎,正式进行高斯滤波操作
Ptr<FilterEngine> f = createGaussianFilter(src.type(), ksize, sigmal, sigma2, borderType);
f->apply(src, dst);
}
}
通过本节的源码解析,相信大家应该对OpenCV中的线性滤波有了比较详细 的认识,已经跃跃欲试,想看这个几个函数用起来可以得出什么效果了。
6.1.11 线性滤波核心API 函 数
本节的内容就是为了大家能快速上手boxFilter 、blur 和 GaussianBlur 这 三 个
函数所准备的。下面进行详细讲解。
1 . 方框滤波:boxFilter 函 数
boxFilter 的函数作用是使用方框滤波 (box filter) 来模糊 一 张图片,由src 输
入 ,dst 输出。
函数原型如下。
void boxFilter(InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1, -1), boolnormalize = true, int borderType = BORDER_DEFAULT)
参数详解如下。
- 第一个参数:InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但 需要注意,待处理的图片深度应该是CV_8U 、CV_ 16U 、CV_ 16S 、CV_32F、
CV_64F之一。 - 第二个参数:OutputArray类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。
- 第三个参数:int 类 型 的ddepth, 输出图像的深度。“-1”代表使用原图深度, 即 src.depth()。
- 第四个参数:Size 类 型 的ksize, 内核的大小。 一般用Size(w,h)的写法来表 示内核的大小(其中,w 为像素宽度,h 为像素高度)。例如,Size(3,3)表 示 3x3 的核大小;Size(5,5) 就 表 示 5x5 的核大小。
- 第五个参数:Point 类 型 的anchor, 表示锚点(即被平滑的那个点),注意 它有默认值 Point(-1,-1) 。 如果这个点坐标是负值的话,就表示取核的中心为 锚 点 , 所 以 默 认 值Point(-1,-1)表 示 这 个 锚 点 在 核 的 中 心 。
- 第六个参数:bool类型的normalize,默认值为true,一个标识符,表示内 核是否被其区域归一化(normalized)了。
- 第七个参数:int类型的borderType,用于推断图像外部像素的某种边界模 式。有默认值BORDER_DEFAULT, 我们一般不去管它。
2.均值滤波:blur函数
blur 的作用是对输入的图像src 进行均值滤波后用dst 输出。 函数原型如下。
void blur(InputArray src, OutputArraydst, Size ksize, Point anchor = Point(-1, -1), int borderType = BORDER_DEFAULT)
参数详解如下。
- 第一个参数:InputArray类型的src, 输入图像,即源图像,填Mat 类的对 象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但 需要注意,待处理的图片深度应该为CV_8U 、CV_ 16U 、CV_ 16S 、CV_32F、
CV_64F 之一。 - 第二个参数:OutputArray 类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。比如可以用 Mat::Clone, 以源图片为模板,来初始化得到 如假包换的目标图。
- 第三个参数:Size类型(对Size类型稍后有讲解)的ksize,内核的大小。 一 般用Size(w,h) 的写法来表示内核的大小(其中w 为像素宽度,h 为像素 高度)。例如,Size(3,3) 表示3x3的 核 大 小 ,Size(5,5) 就表示5x5 的核大小。
- 第四个参数: Point 类 型 的anchor, 表示锚点(即被平滑的那个点),注意 它有默认值Point(-1,-1) 。 如果这个点坐标是负值的话,就表示取核的中心 为锚点,所以默认值Point(-1,-1) 表示这个锚点在核的中心。
- 第五个参数:int类 型 的borderType, 用于推断图像外部像素的某种边界模 式。有默认值BORDER_DEFAULT, 我们一般不去管它。
3 .高斯滤波:GaussianBlur函数
GaussianBlur函数的作用是用高斯滤波器来模糊一张图片,对输入的图像src 进行高斯滤波后用dst 输 出 。
函数原型如下。
void GaussianBlur(InputArray src, OutputArray dst, Size ksize,
double sigmaX, double sigmaY = 0, intborderType = BORDER_DEFAULT)
参数详解如下。
- 第一个参数:InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。它可以是单独的任意通道数的图片。但需要注意,图片深度应该
为CV_8U、CV_16U、CV_16S、CV_32F、CV_64F 之一。 - 第二个参数:OutputArray 类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。比如可以用Mat::Clone, 以源图片为模板,来初始化得到 如假包换的目标图。
- 第三个参数:Size 类型的ksize 高斯内核的大小。其中 ksize.width 和 ksize.height可以不同,但它们都必须为正数和奇数,也可以为零。它们都 是 由sigma 计算而来的。
- 第四个参数:double类型的sigmaX, 表示高斯核函数在X 方向的的标准偏 差。
- 第五个参数:double类型的sigmaY, 表示高斯核函数在Y 方向的的标准偏 差。若sigmaY 为零,就将它设为sigmaX, 如 果sigmaX 和 sigmaY 都是0, 那 么 就 由ksize.width 和 ksize.height 计算出来。为了结果的正确性着想,最 好是把第三个参数Size、第四个参数sigmaX 和第五个参数sigmaY 全部指
定到。 - 第 六 个 参 数 :int 类 型 的borderType, 用于推断图像外部像素的某种边界模 式。注意它有默认值BORDER_DEFAULT。
三种滤波的效果
对同一图像进行三种滤波的效果如下:
void Test24() {
Mat image = imread("image.jpg");
Mat out1, out2, out3;
boxFilter(image, out1, -1, Size(7, 7)); //方框滤波
blur(image, out2, Size(7, 7)); //均值滤波
GaussianBlur(image, out3, Size(7, 7), 0, 0); //高斯滤波
imshow("initial_picture", image);
imshow("blurred1_picture", out1);
imshow("blurred2_picture", out2);
imshow("blurred3_picture", out3);
waitKey(0);
}
原图
方框滤波
均值滤波
高斯滤波
6.1.12 图像线性滤波综合示例
本节我们将学习一个综合性示例程序,把前文介绍的知识点以代码为载体, 展现给大家 。
这个示例程序中可以用滑动条来控制三种线性滤波的核参数值。
Mat g_SrcImage, g_DstImage1, g_DstImage2, g_DstImage3;
//方框滤波、均值滤波、高斯滤波
int g_nBoxFilterValue = 3;
int g_nMeanBlurValue = 3;
int g_nGaussianBlurValue = 3;
//轨迹条的回调函数
static void on_BoxFilter(int, void*);
static void on_MeanBlur(int, void*);
static void on_GaussianBlur(int, void*);
void Test25() {
g_SrcImage = imread("image.jpg");
g_DstImage1 = g_SrcImage.clone();
g_DstImage2 = g_SrcImage.clone();
g_DstImage3 = g_SrcImage.clone();
imshow("initial_picture", g_SrcImage);
//创建窗口
namedWindow("on_BoxFilter");
namedWindow("on_MeanBlur");
namedWindow("on_GaussianBlur");
//创建轨迹条
createTrackbar("kernal:", "on_BoxFilter", &g_nBoxFilterValue, 40, on_BoxFilter);
createTrackbar("kernal:", "on_MeanBlur", &g_nMeanBlurValue, 40, on_MeanBlur);
createTrackbar("kernal:", "on_GaussianBlur", &g_nGaussianBlurValue, 40, on_GaussianBlur);
waitKey(0);
}
static void on_BoxFilter(int, void*) {
boxFilter(g_SrcImage, g_DstImage1, -1, Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1));
imshow("on_BoxFilter", g_DstImage1);
}
static void on_MeanBlur(int, void*) {
blur(g_SrcImage, g_DstImage2, Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1), Point(-1, -1));
imshow("on_MeanBlur", g_DstImage2);
}
static void on_GaussianBlur(int, void*) {
GaussianBlur(g_SrcImage, g_DstImage3, Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1), 0, 0);
imshow("on_GaussianBlur", g_DstImage3);
}
6.2 非 线 性 滤 波 : 中 值 滤 波 、 双 边 滤 波
正如我们在6.1 节中讲到的,线性滤波可以实现很多种不同的图像变换。而 非线性滤波,如中值滤波器和双边滤波器,有时可以达到更好的实现效果。
6.2.1 非 线 性 滤 波 概 述
在6.1节中,我们所考虑的滤波器都是线性的,即两个信号之和的响应和它 们各自响应之和相等。换句话说,每个像素的输出值是一些输入像素的加权和。 线性滤波器易于构造,并且易于从频率响应角度来进行分析。
然而,在很多情况下,使用邻域像素的非线性滤波会得到更好的效果。比如 在噪声是散粒噪声而不是高斯噪声,即图像偶尔会出现很大的值的时候,用高斯 滤波器对图像进行模糊的话,噪声像素是不会被去除的,它们只是转换为更为柔 和但仍然可见的散粒。这就到了中值滤波登场的时候了。
6.2.2 中 值 滤 波
中值滤波(Median filter) 是一种典型的非线性滤波技术,基本思想是用像素 点邻域灰度值的中值来代替该像素点的灰度值,该方法在去除脉冲噪声、椒盐噪 声的同时又能保留图像的边缘细节。
中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,其基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值 的中值代替,让周围的像素值接近真实值,从而消除孤立的噪声点。这对于斑点 噪声(speckle noise) 和椒盐噪声(salt-and-pepper noise) 来说尤其有用,因为它 不依赖于邻域内那些与典型值差别很大的值。中值滤波器在处理连续图像窗函数 时与线性滤波器的工作方式类似,但滤波过程却不再是加权运算。
中值滤波在一定的条件下可以克服常见线性滤波器,如最小均方滤波、方框 滤波器、均值滤波等带来的图像细节模糊,而且对滤除脉冲干扰及图像扫描噪声 非常有效,也常用于保护边缘信息。保存边缘的特性使它在不希望出现边缘模糊 的场合也很有用,是非常经典的平滑噪声处理方法。
中值滤波与均值滤波器比较
- 优势:在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了 噪声的影响。但是在中值滤波器中,由于噪声成分很难选上,所以几乎不会影响 到输出。因此同样用3×3区域进行处理,中值滤波消除的噪声能力更胜一筹。中 值滤波无论是在消除噪声还是保存边缘方面都是一个不错的方法。
- 劣势:中值滤波花费的时间是均值滤波的5倍以上。
顾名思义,中值滤波选择每个像素的邻域像素中的中值作为输出,或者说中值 滤波将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。
例如,取3×3的函数窗,计算以点[i,j] 为中心的函数窗像素中值,具体步骤 如 下 。
(1)按强度值大小排列像素点。
(2)选择排序像素集的中间值作为点[i,j]的新值。
一般采用奇数点的邻域来计算中值,但像素点数为偶数时,中值就取排序像 素中间两点的平均值。
中值滤波在一定条件下,可以克服线性滤波器(如均值滤波等)所带来的图 像细节模糊,对滤除脉冲干扰即图像扫描噪声最为有效,而且在实际运算过程中 并不需要图像的统计特性,也给计算带来不少方便。但是对一些细节(特别是细、 尖顶等)多的图像不太适合。
6.2.3 双 边 滤 波
双边滤波 (Bilateral filter) 是一种非线性的滤波方法,是结合图像的空间邻 近度和像素值相似度的一种折中处理,同时考虑空域信息和灰度相似性,达到保 边去噪的目的,具有简单、非迭代、局部的特点。
双边滤波器的好处是可以做边缘保存(edge preserving)。以往常用维纳滤波 或者高斯滤波去降噪,但二者都会较明显地模糊边缘,对于高频细节的保护效果 并不明显。双边滤波器顾名思义,比高斯滤波多了一个高斯方差sigma-d, 它 是 基于空间分布的高斯滤波函数,所以在边缘附近,离得较远的像素不会对边缘上 的像素值影响太多,这样就保证了边缘附近像素值的保存。但是,由于保存了过 多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净地滤掉,只 能对于低频信息进行较好地滤波。
在双边滤波器中,输出像素的值依赖于邻域像素值的加权值组合,公式如下。
而加权系数w(ij,k,1) 取决于定义域核和值域核的乘积。 其中定义域核表示如下。
值域核表示如下。
定义域滤波和值域滤波如图6.10所示。
两者相乘后,就会产生依赖于数据的双边滤波权重函数,如下。
6.2.4 非 线 性 滤 波 相 关 核 心API 函 数
1 . 中 值 滤 波 :medianBlur 函 数
medianBlur 函数使用中值滤波器来平滑(模糊)处理一张图片,从src 输入, 结果从 dst 输出。对于多通道图片,它对每一个通道都单独进行处理,并且支持 就地操作(In-placeoperation) 。
函数原型如下。
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 ……
2.双边滤波:bilateralFilter函数
此函数的作用是用双边滤波器来模糊处理一张图片,由src 输入图片,结果 于 dst 输 出 。 函 数 原 型 如 下 。
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。
void Test26() {
Mat image = imread("image.jpg");
Mat out1, out2;
medianBlur(image, out1, 7); //中值滤波
bilateralFilter(image, out2, 25, 25 * 2, 25 / 2); //双边滤波
imshow("initial_picture", image);
imshow("medianBlur", out1);
imshow("bilateralFilter", out2);
waitKey(0);
}
原图
中值滤波
双边滤波
6.2.5 OpenCV中的5种图像滤波综合示例
这一小节会将前文介绍的知识点以代码为载体,展现给大家。
在下面这个示例程序中,可以用滑动条来控制我们学习到的各种滤波(方框 滤波、均值滤波、高斯滤波、中值滤波、双边滤波)的参数值,通过滑动滚动条, 就可以控制图像在各种平滑处理下的模糊度,有一定的可玩性。详细注释的完整 代码如下。
void Test27() {
g_Image = imread("food.jpg");
g_Dst1 = g_SrcImage.clone();
g_Dst2 = g_SrcImage.clone();
imshow("initial_picture", g_Image);
//创建窗口
namedWindow("on_MedianBlur");
namedWindow("on_BilateralFilter");
//创建轨迹条
createTrackbar("kernal:", "on_MedianBlur", &g_nMedianBlurValue, 40, on_MedianBlur);
createTrackbar("kernal:", "on_BilateralFilter", &g_nBilateralFilterValue, 40, on_BilateralFilter);
waitKey(0);
}
原图
中值滤波
双边滤波
6.3 形态学滤波(1):腐蚀与膨胀
本小节中,我们将一起探究图像处理中最基本的形态学运算——膨胀与腐蚀。
6.3.1 形 态 学 概 述
形态学(morphology) 一词通常表示生物学的一个分支,该分支主要研究动 植物的形态和结构。而我们图像处理中的形态学,往往指的是数学形态学。下面 一起来了解数学形态学的概念。
数学形态学 (Mathematical morphology)是一门建立在格论和拓扑学基础之 上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二 值腐蚀和膨胀、二值开闭运算、骨架抽取、极限腐蚀、击中击不中变换、形态学 梯度、Top-hat 变换、颗粒分析、流域变换、灰值腐蚀和膨胀、灰值开闭运算、灰 值形态学梯度等。
简单来讲,形态学操作就是基于形状的一系列图像处理操作。OpenCV 为 进 行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有两种,分 别是:膨胀 (dilate) 与腐蚀(erode)。
膨胀与腐蚀能实现多种多样的功能,主要如下。
- 消除噪声;
- 分割(isolate)出独立的图像元素,在图像中连接(join) 相邻的元素;
- 寻找图像中的明显的极大值区域或极小值区域;
- 求出图像的梯度。
在进行腐蚀和膨胀的讲解之前,首先提醒大家注意,腐蚀和膨胀是对白色部 分(高亮部分)而言的,不是黑色部分。膨胀是图像中的高亮部分进行膨胀,类 似于“领域扩张”,效果图拥有比原图更大的高亮区域;腐蚀是原图中的高亮部分 被腐蚀,类似于“领域被蚕食”,效果图拥有比原图更小的高亮区域。
6.3.2 膨胀
膨胀 (dilate) 就是求局部最大值的操作。从数学角度来说,膨胀或者腐蚀操
作就是将图像(或图像的一部分区域,称之为A) 与核(称之为B) 进行卷积。
核可以是任何形状和大小,它拥有一个单独定义出来的参考点,我们称其为 锚点(anchorpoint) 。 多数情况下,核是一个小的,中间带有参考点和实心正方形 或者圆盘。其实,可以把核视为模板或者掩码。
而膨胀就是求局部最大值的操作。核B 与图形卷积,即计算核B 覆盖的区域 的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像 中的高亮区域逐渐增长,如图6.20所示。这就是膨胀操作的初衷。
膨胀的数学表达式如下
6.3.3 腐蚀
大家应该知道,膨胀和腐蚀 (erode) 是相反的一对操作,所以腐蚀就是求局
部最小值的操作。
我们一般都会把腐蚀和膨胀进行对比理解和学习。下文就可以看到,两者的 函数原型也是基本一样的。腐蚀操作示例如图6.23所示。
腐蚀的数学表达式如下。
6.3.4 相 关OpenCV 源码分析溯源
在 … \opencv\sources\modules\imgproc\sre\morph.cpp 路 径 中 , 我 们 可 以 发 现 erode(腐蚀)函数和dilate(膨胀)函数的源码,如下。
void cv::erode(InputArray src, OutputArraydst, InputArray kernel,
Point anchor, int iterations,
int borderType, constScalar &borderValue)
// 调用morphOp 函数,并设定标识符为MORPH_ERODE
morphop(MORPH_ERODE, src, dst, kernel, anchor, iterations,
borderType, borderValue);
void cv::dilate(InputArray src, OutputArray dst, InputArray kernel,
Point anchor, int iterations,
int borderType, constScalar &borderValue)
{
// 调用morphop 函数,并设定标识符为MORPH_DILATE
morphop(MORPH_DILATE, src, dst, kernel, anchor, iterations,
borderType, borderValue);
}
以 发 现 ,erode 和 dilate 这 两 个 函 数 内 部 就 是 调 用 了 一 下morphOp, 只 是 它
们调用morphOp 时,第一个参数标识符不同: 一个为MORPH_ERODE (腐蚀), 一个为MORPH_DILATE (膨胀)。
morphOp 函数的源码在…\opencvlsourceslmoduleslimgproclsrc\morph.cpp 中 的
第1286行,有兴趣的朋友们可以自行研究,这里就不再展开分析了。
6.3.5 相关核心API 函数讲解
1 . 膨胀:dilate 函 数
dilate 函数使用像素邻域内的局部极大运算符来膨胀一张图片,从src 输入, 由dst 输出。支持就地(in-place) 操作。
函数原型如下。
void dilate(
InputArray src, OutputArray dst,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar &borderValue = morphologyDefaultBorderValue());
参数详解如下。
- 第一个参数,InputArray 类 型 的src, 输入图像,即源图像,填Mat 类的对 象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、
CV_ 16S 、CV_32F 或 CV_64F 其中之一。 - 第二个参数,OutputArray 类 型 的dst, 即目标图像,需要和源图片有一样 的尺寸和类型。
- 第三个参数,InputArray类 型 的kernel,膨胀操作的核。当为NULL 时,表 示的是使用参考点位于中心3×3的核。
我 们 一 般 使 用 函 数 getStructuringElement 配 合 这 个 参 数 的 使 用 。 getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。其中, getStructuringElement 函数的第一个参数表示内核的形状,有如下三种形状可以选 择 。 - 矩形:MORPH_RECT;
- 交叉形:MORPH_CROSS;
- 椭圆形:MORPH_ELLIPSE。
而 getStructuringElement 函数的第二和第三个参数分别是内核的尺寸以及锚
点的位置。
一般在调用erode 以 及dilate 函数之前,先定义一个Mat 类型的变量来获得 getStructuringElement 函数的返回值。对于锚点的位置,有默认值Point(-1,-1), 表示锚点位于中心。此外,需要注意,十字形的element 形状唯一依赖于锚点的位
置,而在其他情况下,锚点只是影响了形态学运算结果的偏移。 getStructuringElement 函数相关的调用示例代码如下。
int g_nStructElementSize = 3; // 结构元素(内核矩阵)的尺寸
// 获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1),
Point(g_nStructElementSize, g_nStructElementSize));
调用之后,我们可以在接下来调用erode 或 dilate 函数时,在第三个参数填保 存 了getStructuringElement 返 回 值 的Mat 类型变量。对应于上面的示例,就是 element 变 量 。
- 第 四 个 参 数 ,Point 类 型 的anchor, 锚的位置,其有默认值(-1,-1),表示锚 位于中心。
- 第 五 个 参 数 ,int类型的 iterations,迭 代 使 用erode()函数的次数,默认值为 1。
- 第 六 个 参 数 ,int类 型 的border Type,用于推断图像外部像素的某种边界模 式。注意它有默认值BORDER_DEFAULT。
- 第 七 个 参 数 ,const Scalar&类 型 的borderValue, 当边界为常数时的边界值, 有默认值 morphologyDefaultBorderValue(), 一般不用去管它。需要用到它 时,可以看官方文档中的createMorphologyFilter(函数,以得到更详细的解 释 。
使 用erode 函数, 一般只需要填前面的三个参数,后面的四个参数都有默认 值,而且往往会结合getStructuringElement 一起使用。
void Test28() {
Mat image = imread("word.jpg");
imshow("initial_picture", image);
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15)); //自定义核
Mat out;
dilate(image, out, element); //膨胀操作
imshow("dilate_picture", out);
waitKey(0);
}
原图
膨胀
2 . 腐蚀:erode 函 数
erode 函数使用像素邻域内的局部极小运算符来腐蚀一张图片,从src 输入, 由dst 输出。支持就地(in-place) 操作。
看一下函数原型,如下。
void erode(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar &borderValue = morphologyDefaultBorderValue());
参数详解如下。
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、 CV_ 16S 、CV_32F或 CV_64F 其中之一。
- 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样 的尺寸和类型。
- 第三个参数,InputArray类型的kernel,腐蚀操作的内核。为NULL 时,表 示的是使用参考点位于中心3x3的核。一般使用函数 getStructuringElement 配合这个参数的使用。getStructuringElement 函数会返回指定形状和尺寸的 结构元素(内核矩阵,具体看上文中dilate 函数的第三个参数讲解部分。
- 第四个参数,Point类型的anchor,锚的位置。其有默认值(-1,-1),表示锚 位于单位(element) 的中心,一般不用管它。
- 第 五 个 参 数 ,int类 型 的iterations,迭 代 使 用erode()函数的次数,默认值为1。
- 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模 式。注意它有默认值BORDER_DEFAULT。
- 第 七 个 参 数 ,const Scalar&类 型 的borderValue, 当边界为常数时的边界值, 有默认值 morphologyDefaultBorderValue(),一般不用去管它。需要用到它 时,可以看官方文档中的createMorphologyFilter()函数以得到更详细的解 释。
同 样 的 , 使 用erode 函数, 一 般只需要填前面的三个参数,后面的四个参数 都有默认值。而且往往结合getStructuringElement 一 起 使 用 。
void Test29() {
Mat image = imread("word.jpg");
imshow("initial_picture", image);
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15)); //自定义核
Mat out;
erode(image, out, element); //腐蚀操作
imshow("erode_picture", out);
waitKey(0);
}
原图
腐蚀
6.3.6 综合示例:腐蚀与膨胀
此示例程序中的效果图窗口中有两个滑动条第一个滑动条“腐蚀/膨胀”用于 在腐蚀/膨胀之间进行切换;第二个滚动条”内核尺寸”用于调节形态学操作时的 内核尺寸,以得到效果不同的图像
namespace test30 {
Mat g_srcImage, g_dstImage;
int g_nTrackbarNumber = 0; // 0表示腐蚀,1表示膨胀
int g_nStructElementSize = 3; // 结构元素(内核矩阵)的大小
// 处理不同事件
void Process() {
// 创建结构元素
Mat element = getStructuringElement(MORPH_RECT, Size(g_nStructElementSize * 2 + 1, g_nStructElementSize * 2 + 1),
Point(g_nStructElementSize, g_nStructElementSize));
// 选择腐蚀还是膨胀
if (g_nTrackbarNumber == 1) { // 膨胀
dilate(g_srcImage, g_dstImage, element);
}
else { // 腐蚀
erode(g_srcImage, g_dstImage, element);
}
// 显示处理后的图像
imshow("process_picture", g_dstImage);
}
// 响应滑动条函数 - 改变图像类型
void on_TrackbarNumberChange(int, void*) {
Process(); // 触发图像处理
}
// 响应滑动条函数 - 改变内核大小
void on_TrackbarSizeChange(int, void*) {
Process(); // 触发图像处理
}
}
void Test30() {
// 读取图像
test30::g_srcImage = imread("image.jpg");
// 检查图像是否加载成功
if (test30::g_srcImage.empty()) {
std::cerr << "Image not found!" << std::endl;
return;
}
// 显示原始图像
imshow("initial_picture", test30::g_srcImage);
// 创建窗口
namedWindow("process_picture");
// 创建滑动条
createTrackbar("erode/dilate", "process_picture", &test30::g_nTrackbarNumber, 1, test30::on_TrackbarNumberChange);
createTrackbar("kernal:", "process_picture", &test30::g_nStructElementSize, 21, test30::on_TrackbarSizeChange);
waitKey(0);
}
原图
膨胀/腐蚀
6.4 形态学滤波(2):开运算、闭运算、形态学梯度、顶帽、黑帽
上一节中,我们重点了解了腐蚀和膨胀这两种最基本的形态学操作,而运用 这两个基本操作,可以实现更高级的形态学变换。
所以,本节的主角是OpenCV 中 的morphologyEx 函数,它利用基本的膨胀和 腐蚀技术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、 “黑帽”等。
首先,我们需要知道,形态学的高级形态,往往都是建立在腐蚀和膨胀这两 个基本操作之上的。而关于腐蚀和膨胀,概念和细节以及相关代码请参考上一小 节。对膨胀和腐蚀心中有数了,接下来的高级形态学操作,应该就不难理解。
6.4.1 开运算
开运算(Opening Operation),其实就是先腐蚀后膨胀的过程。其数学表达式 如下:
d
s
t
=
o
p
e
n
(
s
r
c
,
e
l
e
m
e
n
t
)
=
d
i
l
a
t
e
(
e
r
o
d
e
(
s
r
c
,
e
l
e
m
e
n
t
)
)
dst=open(src,element)=dilate(erode(src,element))
dst=open(src,element)=dilate(erode(src,element))
开运算可以用来消除小物体,在纤细点处分离物体,并且在平滑较大物体的 边界的同时不明显改变其面积
6.4.2 闭运算
先膨胀后腐蚀的过程称为闭运算 (Closing Operation),其数学表达式如下:
d
s
t
=
c
l
e
s
e
(
s
r
c
,
e
l
e
m
e
n
t
)
=
e
r
o
d
e
(
d
i
l
a
t
e
(
s
r
c
,
e
l
e
m
e
n
t
)
)
dst=clese(src,element)=erode(dilate(src,element))
dst=clese(src,element)=erode(dilate(src,element))
闭运算能够排除小型黑洞(黑色区域)。
6.4.3 形态学梯度
形态学梯度 (Morphological Gradient) 是膨胀图与腐蚀图之差,数学表达式
如 下 :
d
s
t
=
m
o
r
p
h
−
g
r
a
d
(
s
r
c
,
e
l
e
m
e
n
t
)
=
d
i
l
a
t
e
(
s
r
c
,
e
l
e
m
e
n
t
)
−
e
r
o
d
e
(
s
r
c
,
e
l
e
m
e
n
t
)
dst=morph-grad(src,element)=dilate(src,element)-erode(src,element)
dst=morph−grad(src,element)=dilate(src,element)−erode(src,element)
对二值图像进行这一操作可以将团块 (blob) 的边缘突出出来。我们可以用形态学梯度来保留物体的边缘轮廓
6.4.4 顶帽
顶帽运算(Top Hat) 又常常被译为”礼帽“运算,是原图像与上文刚刚介绍 的“开运算”的结果图之差,数学表达式如下:
d
s
t
=
t
o
p
h
a
t
(
s
r
c
,
e
l
e
m
e
n
t
)
=
s
r
c
−
o
p
e
n
(
s
r
c
,
e
l
e
m
e
n
t
)
dst=tophat(src,element)=src-open(src,element)
dst=tophat(src,element)=src−open(src,element)
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域。因此,从原图 中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域, 且这一操作与选择的核的大小相关。
顶帽运算往往用来分离比邻近点亮一些的斑块。在一幅图像具有大幅的背景,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取
6.4.5 黑帽
黑 帽 (Black Hat)运算是闭运算的结果图与原图像之差。数学表达式为:
d
s
t
=
b
l
a
c
k
h
a
t
(
s
r
c
,
e
l
e
m
e
n
t
)
=
c
l
o
s
e
(
s
r
c
,
e
l
e
m
e
n
t
)
−
s
r
c
dst=blackhat(src,element)=close(src,element)-src
dst=blackhat(src,element)=close(src,element)−src
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作 和选择的核的大小相关。
所以,黑帽运算用来分离比邻近点暗一些的斑块,效果图有着非常完美的轮 廓。
6.4.6 形态学滤波OpenCV 源码分析溯源
本节的主角是OpenCV中的morphologyEx函数,它利用基本的膨胀和腐蚀技 术,来执行更加高级的形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽” 等。这 一 节我们来 一 起看 一 下morphologyEx 函 数 的 源 代 码 。
void ev::morphologyEx(InputArray _src, OutputArray _dst, int op, InputArray kernel, Pointanchor, int iterations, int borderType, constScalar &borderValue)
{
// 复制Mat 数据到临时变量
Mat src = _src.getMat(), temp;
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat();
// 一个大switch, 根据不同的标识符取不同的操作
switch (op)
{
case MORPH_ERODE:
erode(src, dst, kernel, anchor, iterations, borderType, borderValue);
break;
case MORPH_DILATE:
dilate(src, dst, kernel, anchor, iterations, borderType, borderValue);
break;
case MORPH_OPEN:
erode(src, dst, kernel, anchor, iterations, borderType, borderValue);
dilate(dst, dst, kernel, anchor, iterations, borderType, borderValue);
break;
case CV_MOP_CLOSE:
dilate(src, dst, kernel, anchor, iterations, borderType, borderValue);
erode(dst, dst, kernel, anchor, iterations, borderType, borderValue);
break;
case CV_MOP_GRADIENT:
erode(src, temp, kernel, anchor, iterations, borderType, borderValue);
dilate(src, dst, kernel, anchor, iterations, borderType, borderValue);
dst -= temp;
break;
case CV_MOP_TOPHAT:
if (src.data != dst.data)
temp = dst;
erode(src, temp, kernel, anchor, iterations, borderType, borderValue);
dilate(temp, temp, kernel, anchor, iterations, borderType, borderValue);
dst = src - temp;
break;
case CV_MOP_BLACKHAT:
if (src.data != dst.data)
temp = dst;
dilate(src, temp, kernel, anchor, iterations, borderType, borderValue);
erode(temp, temp, kernel, anchor, iterations, borderType, borderValue);
dst = temp - src;
break;
default:
CV_Error(Cv_StsBadArg, "unknown morphological operation");
}
}
看 上 面 的 源 码 可 以 发 现 ,morphologyEx 函 数 其 实 就 是 内 部 的 一 个 大switch 而已,根据不同的标识符取不同的操作。比如开运算MORPH_OPEN, 按我们上文 中讲解的数学表达式,就是先腐蚀后膨胀,即依次调用erode 和dilate 函数,代码 非常简明干净。
6.4.7 核 心API 函 数 :morphologyEx()
上面已经讲到,morphologyEx函数利用基本的膨胀和腐蚀技术,来执行更加 高级形态学变换,如开闭运算、形态学梯度、“顶帽”、“黑帽”等。下面我们来详 细地讲解它的参数意义和使用方法。
void morphologyEx(InputArray src,
OutputArray dst, int op,
InputArraykernel,
Pointanchor = Point(-1, -1), intiterations = 1,
intborderType = BORDER_CONSTANT,
constScalar &borderValue = morphologyDefaultBorderValue());
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat 类的对 象即可。图像位深应该为以下5种之一:CV_8U、CV_16U、CV_16S、CV_32F 和CV_64F。
- 第二个参数,OutputArray 类型的dst,即目标图像,函数的输出参数,需 要和源图片有 一 样的尺寸和类型。
- 第三个参数,int类型的op,表示形态学运算的类型,可以是如表6.2中任 意之 一 的标识符。
- 第四个参数,InputArray类型的kernel,形态学运算的内核。若为NULL,表示的是使用参考点位于中心3×3的核。一般使用函数getStructuringElement 配合这个参数的使用。getStructuringElement 函数会返回指定形状和尺寸的 结构元素(内核矩阵)。关于getStructuringElement 我们之前有讲到过,这 里为了大家参阅方便,再写一遍。
getStructuringElement 函数的第一个参数表示内核的形状,我们可以选择如下 三种形状之一: - 矩形 — —MORPH_RECT
- 交叉形——MORPH_CROSS
- 椭圆形——MORPH_ELLIPSE
而 getStructuringElement 函数的第二和第三个参数分别是内核的尺寸以及锚
点的位置。
一般在调用erode 以 及dilate 函数之前,要先定义一个Mat 类型的变量来获得 getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表 示锚点位于中心。另外需要注意:十字形的element 形状唯一依赖于锚点的位置。 而在其他情况下,锚点只是影响形态学运算结果的偏移。
getStructuringElement 函数相关的调用示例代码如下。
int g_nStructElementSize=3;//结构元素(内核矩阵)的尺寸
//获取自定义核
Mat element =getStructuringElement(MORPH_RECT,
Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1), Point(g_nStructElementSize,g_nStructElementSize ));
之后,便可以在调用erode 、dilate 或 morphologyEx 函数时,由kernel 参数填 保存getStructuringElement 返回值的Mat 类型变量。对应于上面的示例,就是填 element 变量。
- 第五个参数,Point类 型 的anchor, 锚的位置,其有默认值(-1,-1),表示锚 位于中心。
- 第六个参数,int类型的iterations,迭代使用函数的次数,默认值为1。
- 第七个参数,int类型的border Type,用于推断图像外部像素的某种边界模 式。注意它有默认值BORDER_CONSTANT。
- 第八个参数,const Scalar&类型的borderValue, 当边界为常数时的边界值, 有默认值morphologyDefaultBorderValue(), 一般不用去管它。需要用到它 时,可以看官方文档中的createMorphologyFilter (函数得到更详细的解释。 其中的这些操作都可以进行就地(in-place) 操作,且对于多通道图像,每 一个通道都单独进行操作。
6.4.8 各形态学操作使用范例一览
核心函数讲解完毕,下面便开始讲解使用范例。为了方便大家需要的时候随时取用,这里提供了利用morphologyEx 函数实现的几乎全部的形态学操作:开运 算、闭运算、形态学梯度、顶帽、黑帽、腐蚀、膨胀的效果实现简化版完整代码。 其实说白了,这些代码基本上内容一致,就是改一下morphologyEx 里面的第三个 标识符参数而已。核都是选的MORPH_RECT (矩形元素结构)。另外,通过观察 源代码发现,最基本的腐蚀和膨胀操作也可以用morphologyEx 函数来实现,它们 由morphologyEx 函数源码中switch 的前两个case 来实现(虽然在case 体内就是简单地各自调用了一下erode 和 dilation 函数,但还是有写出来的必要)。所以在 这里,我们也用morphologyEx 再重新来实现一遍它们。
void Test31() {
Mat image = imread("word.jpg");
imshow("initial_picture", image);
Mat out1, out2, out3, out4, out5;
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));//自定义内核
//开运算、闭运算、形态学梯度、顶帽、黑帽
morphologyEx(image, out1, MORPH_OPEN, element);
morphologyEx(image, out2, MORPH_CLOSE, element);
morphologyEx(image, out3, MORPH_GRADIENT, element);
morphologyEx(image, out4, MORPH_TOPHAT, element);
morphologyEx(image, out5, MORPH_BLACKHAT, element);
imshow("MORPH_OPEN", out1);
imshow("MORPH_CLOSE", out2);
imshow("MORPH_GRADIENT", out3);
imshow("MORPH_TOPHAT", out4);
imshow("MORPH_BLACKHAT", out5);
waitKey(0);
}
原图
开运算
闭运算
形态学梯度
顶帽
黑帽
6.4.9 综合示例:形态学滤波
这个综合示例程序中, 一共会出现4个显示图像的窗口,包括原始图一个,
开/闭运算为一个,腐蚀/膨胀为一个,顶帽/黑帽运算为一个。它们分别使用滚动 条,来控制得到的形态学效果,且迭代值为10的时候为中间点。另外,还可以通 过键盘按键1、2、3以及空格键来调节成不同的元素结构(矩形、椭圆、十字形)。
namespace test32 {
Mat g_srcImage, g_dstImage;
int g_nElementShape = MORPH_RECT;
int g_MaxIterationNum = 10;
int g_nOpenCloseNum = 0;
int g_nErodeDilateNum = 0;
int g_nTopBlackHatNum = 0;
int g_nGradientNum = 0;
// The trackbar value should be mapped from [-MaxIterationNum, MaxIterationNum]
static void on_OpenClose(int, void*) {
// Map the trackbar value to the range [-MaxIterationNum, MaxIterationNum]
int offset = g_nOpenCloseNum - g_MaxIterationNum; // This will give the range [-Max, Max]
int Absolute_offset = std::abs(offset); // Ensure offset is non-negative
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset));
if (offset < 0) {
morphologyEx(g_srcImage, g_dstImage, MORPH_OPEN, element); // Opening operation
}
else {
morphologyEx(g_srcImage, g_dstImage, MORPH_CLOSE, element); // Closing operation
}
imshow("on_OpenClose", g_dstImage);
}
static void on_ErodeDilate(int, void*) {
// Map the trackbar value to the range [-MaxIterationNum, MaxIterationNum]
int offset = g_nErodeDilateNum - g_MaxIterationNum; // This will give the range [-Max, Max]
int Absolute_offset = std::abs(offset); // Ensure offset is non-negative
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset));
if (offset < 0) {
erode(g_srcImage, g_dstImage, element); // Erosion operation
}
else {
morphologyEx(g_srcImage, g_dstImage, MORPH_DILATE, element); // Dilation operation
}
imshow("on_ErodeDilate", g_dstImage);
}
static void on_TopBlackHat(int, void*) {
// Map the trackbar value to the range [-MaxIterationNum, MaxIterationNum]
int offset = g_nTopBlackHatNum - g_MaxIterationNum; // This will give the range [-Max, Max]
int Absolute_offset = std::abs(offset); // Ensure offset is non-negative
Mat element = getStructuringElement(g_nElementShape, Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset));
if (offset < 0) {
morphologyEx(g_srcImage, g_dstImage, MORPH_TOPHAT, element); // Top-hat operation
}
else {
morphologyEx(g_srcImage, g_dstImage, MORPH_BLACKHAT, element); // Black-hat operation
}
imshow("on_TopBlackHat", g_dstImage);
}
static void on_Gradient(int, void*) {
Mat element = getStructuringElement(g_nElementShape, Size(g_nGradientNum * 2 + 1, g_nGradientNum * 2 + 1),
Point(g_nGradientNum, g_nGradientNum));
morphologyEx(g_srcImage, g_dstImage, MORPH_GRADIENT, element); // gradient operation
imshow("on_Gradient", g_dstImage);
}
}
void Test32() {
test32::g_srcImage = imread("image.jpg");
imshow("initial_picture", test32::g_srcImage);
// Create windows
namedWindow("on_OpenClose");
namedWindow("on_ErodeDilate");
namedWindow("on_TopBlackHat");
namedWindow("on_Gradient");
// Assign values to parameters
test32::g_nOpenCloseNum = 9;
test32::g_nErodeDilateNum = 9;
test32::g_nTopBlackHatNum = 2;
test32::g_nGradientNum = 2;
// Create trackbars with mapping to [-MaxIterationNum, MaxIterationNum]
createTrackbar("Value:", "on_OpenClose", &test32::g_nOpenCloseNum, test32::g_MaxIterationNum * 2 + 1, test32::on_OpenClose);
createTrackbar("Value:", "on_ErodeDilate", &test32::g_nErodeDilateNum, test32::g_MaxIterationNum * 2 + 1, test32::on_ErodeDilate);
createTrackbar("Value:", "on_TopBlackHat", &test32::g_nTopBlackHatNum, test32::g_MaxIterationNum * 2 + 1, test32::on_TopBlackHat);
createTrackbar("Value:", "on_Gradient", &test32::g_nGradientNum, test32::g_MaxIterationNum * 2 + 1, test32::on_Gradient);
while (1) {
int c = waitKey(1); // Use a shorter delay to ensure updates
if ((char)c == 'q' || c == 27) { // Press 'q' or 'esc' to exit
break;
}
// Switch element shape with key presses
if (c == 49) { // Number 1
test32::g_nElementShape = MORPH_ELLIPSE; // Switch to ellipse shape
}
else if (c == 50) { // Number 2
test32::g_nElementShape = MORPH_RECT; // Switch to rectangle shape
}
else if (c == 51) { // Number 3
test32::g_nElementShape = MORPH_CROSS; // Switch to cross shape
}
else if ((char)c == ' ') { // Space key to cycle shapes
test32::g_nElementShape = (test32::g_nElementShape + 1) % 3; // Cycle through shapes
}
// Update morphological operations
test32::on_OpenClose(test32::g_nOpenCloseNum, nullptr);
test32::on_ErodeDilate(test32::g_nErodeDilateNum, nullptr);
test32::on_TopBlackHat(test32::g_nTopBlackHatNum, nullptr);
test32::on_Gradient(test32::g_nGradientNum, nullptr);
}
}
原图:
开运算、闭运算
腐蚀、膨胀
顶帽、黑帽
形态学梯度
6.5 漫水填充
本节我们将一起探讨OpenCV 填充算法中漫水填充算法相关的知识点,并了 解OpenCV 中实现漫水填充算法的两个版本的floodFill 函数的使用方法。
6.5.1 漫水填充的定义
漫水填充法是一种用特定的颜色填充连通区域,通过设置可连通像素的上下 限以及连通方式来达到不同的填充效果的方法。漫水填充经常被用来标记或分离 图像的一部分,以便对其进行进一步处理或分析,也可以用来从输入图像获取掩 码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某 个连续的区域。
6.5.2 漫水填充法的基本思想
所谓漫水填充,简单来说,就是自动选中了和种子点相连的区域,接着将该 区域替换成指定的颜色,这是个非常有用的功能,经常用来标记或者分离图像的 一部分进行处理或分析。漫水填充也可以用来从输入图像获取掩码区域,掩码会 加速处理过程,或者只处理掩码指定的像素点。
以此填充算法为基础,类似PhotoShop 的魔术棒选择工具就很容易实现了。 漫水填充 (FloodFill) 是查找和种子点连通的颜色相同的点,魔术棒选择工具则 是查找和种子点连通的颜色相近的点,把和初始种子像素颜色相近的点压进栈做 为新种子。
在OpenCV 中,漫水填充是填充算法中最通用的方法。且在OpenCV2.X 中 , 使用C++ 重写过的FloodFill 函数有两个版本:一个不带掩膜mask 的版本,和一 个带mask 的版本。这个掩膜mask, 就是用于进一步控制哪些区域将被填充颜色 (比如说当对同一图像进行多次填充时)。这两个版本的FloodFill, 都必须在图像 中选择一个种子点,然后把临近区域所有相似点填充上同样的颜色,不同的是, 不一定将所有的邻近像素点都染上同一颜色,漫水填充操作的结果总是某个连续 的区域。当邻近像素点位于给定的范围(从loDiff 到 upDiff) 内或在原始seedPoint 像素值范围内时,FloodFill 函数就会为这个点涂上颜色。
6.5.3 实现漫水填充算法: floodFill 函 数
在OpenCV 中,漫水填充算法由 floodFill 函数实现,其作用是用我们指定的 颜色从种子点开始填充一个连接域。连通性由像素值的接近程度来衡量。 OpenCV2.X 有两个C++ 重写版本的floodFill, 具体如下。
第一个版本的floodFill:
int floodFil1(InputOutputArray image, Point seedPoint, Scalar newVal,
Rect *rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4)
第二个版本的floodFill:
int floodFill(InputoutputArray image, InputoutputArray mask, Point seedPoint, Scalar newVal, Rect *rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4)
下面是参数详解。这两个版本除了第二个参数外,其他的参数都是共用的。
(1)第一个参数,InputOutputArray 类型的image, 输入/输出1通道或3通道, 8位或浮点图像,具体参数由之后的参数指明。
(2)第二个参数,InputOutputArray 类型的mask, 这是第二个版本的floodFill 独享的参数,表示操作掩模。它应该为单通道,8位,长和宽上都比输入图像image 大两个像素点的图像。第二个版本的floodFill 需要使用以及更新掩膜,所以对于 这个mask 参数,我们一定要将其准备好并填在此处。需要注意的是,漫水填充 不会填充掩膜 mask 的非零像素区域。例如,一个边缘检测算子的输出可以用来 作为掩膜,以防止填充到边缘。同样的,也可以在多次的函数调用中使用同一个 掩膜,以保证填充的区域不会重叠。另外需要注意的是,掩膜 mask 会比需填充 的图像大,所以 mask 中与输入图像(x,y)像素点相对应的点的坐标为(x+1,y+1)。
(3)第三个参数,Point 类型的seedPoint, 漫水填充算法的起始点。
(4)第四个参数,Scalar 类型的newVal, 像素点被染色的值,即在重绘区域 像素的新值。
(5)第五个参数,Rect* 类型的rect, 有默认值0,一个可选的参数,用于设 置floodFill 函数将要重绘区域的最小边界矩形区域。
(6)第六个参数,Scalar 类型的loDiff, 有默认值Scalar(), 表示当前观察像 素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之负差 (lower brightness/color difference) 的最大值。
(7)第七个参数,Scalar 类型的upDiff, 有默认值Scalar(), 表示当前观察像 素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之正差 (lowerbrightness/color difference) 的最大值。
(8)第八个参数,int 类型的flags, 操作标志符,此参数包含三个部分,比较 复杂,我们一起详细看看。
- 低八位(第0~7位)用于控制算法的连通性,可取4(4为默认值)或者8。 如果设为4,表示填充算法只考虑当前像素水平方向和垂直方向的相邻点; 如果设为8,除上述相邻点外,还会包含对角线方向的相邻点。
- 高八位部分(16~23位)可以为0或者如下两种选项标识符的组合。
- FLOODFILL_FIXED_RANGE:如果设置为这个标识符,就会考虑当前 像素与种子像素之间的差,否则就考虑当前像素与其相邻像素的差。也 就是说,这个范围是浮动的。
- FLOODFILL_MASK_ONLY-如果设置为这个标识符,函数不会去填充 改变原始图像(也就是忽略第三个参数 newVal), 而是去填充掩模图像 (mask)。这个标识符只对第二个版本的floodFill有用,因第一个版本里 面压根就没有mask 参数。
- 中间八位部分,上面关于高八位FLOODFILL_MASK_ONLY标识符中已经 说得很明显,需要输入符合要求的掩码。Floodfill 的 flags 参数的中间八位 的值就是用于指定填充掩码图像的值的。但如果flags 中间八位的值为0, 则掩码会用1来填充。
而 所 有flags 可 以 用or 操作符连接起来,即“ | ”。例如,如果想用8邻域填充, 并填充固定像素值范围,填充掩码而不是填充源图像,以及设填充值为38,那么 输入的参数是下面这样:
flags=8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE 上(38<<8)
void Test33() {
Mat src = imread("image.jpg");
imshow("initial_picture", src);
Rect ccomp;
floodFill(src, Point(50, 300), Scalar(100, 50, 50), &ccomp, Scalar(10, 10, 10), Scalar(10, 10, 10)); //漫水填充
imshow("floodFill", src);
waitKey(0);
}
原图:
效果图:
6.5.4 综合示例:漫水填充
本次的综合示例为OpenCV 文档中自带的一个程序。作者对其做了适当的修 改并详细注释,供大家消化理解。
可以看到,此程序有不少按键功能。我们用鼠标对窗口中的图形进行多次 单击,就可以得到类似PhotoShop 中魔棒的效果。当然,就这短短的两百来行 代码写出来的东西,体验还是比不上PS 的魔棒工具的。程序详细注释的源码 如 下 。
void Test34() {
test34::g_srcImage = imread("image.jpg");
test34::g_srcImage.copyTo(test34::g_dstImage);
cvtColor(test34::g_srcImage, test34::g_grayImage, COLOR_BGR2GRAY); //转换三通道的image到灰度图
test34::g_maskImage.create(test34::g_srcImage.rows + 2, test34::g_srcImage.cols + 2, CV_8UC1);//创建掩码
namedWindow("no_threshold");
//创建滚动条
createTrackbar("negtive_Max:", "no_threshold", &test34::g_nLowDifference, 255, 0);
createTrackbar("positive_Max:", "no_threshold", &test34::g_nUpDifference, 255, 0);
//鼠标回调函数
setMouseCallback("no_threshold", test34::onMouse, 0);
while (1) {
imshow("no_threshold", test34::g_bIsColor ? test34::g_dstImage : test34::g_grayImage);
int c = waitKey(0);
if ((c & 255) == 27) {
std::cout << "program exit ..." << std::endl;
break;
}
switch ((char)c) {
case '1':
if (test34::g_bIsColor) {
std::cout << "key 1 pressed! changed to gray mode" << std::endl;
cvtColor(test34::g_srcImage, test34::g_grayImage, COLOR_BGR2GRAY);
test34::g_maskImage = Scalar::all(0);
test34::g_bIsColor = false;
}
else {
std::cout << "key 1 pressed! changed to color mode" << std::endl;
test34::g_srcImage.copyTo(test34::g_dstImage);
test34::g_maskImage = Scalar::all(0);
test34::g_bIsColor = true;
}
break;
case'2':
if (test34::g_bUseMask) {
destroyWindow("threshold");
test34::g_bUseMask = false;
}
else {
namedWindow("threshold",0);
test34::g_maskImage = Scalar::all(0);
test34::g_bUseMask = true;
imshow("threshold", test34::g_maskImage);
}
break;
case '3':
std::cout << "key 3 pressed! reset picture" << std::endl;
test34::g_srcImage.copyTo(test34::g_dstImage);
cvtColor(test34::g_dstImage, test34::g_grayImage, COLOR_BGR2GRAY);
test34::g_maskImage = Scalar::all(0);
break;
case '4':
std::cout << "FillMode 0" << std::endl;
test34::g_nFillMode = 0;
break;
case '5':
std::cout << "FillMode 1" << std::endl;
test34::g_nFillMode = 1;
break;
case '6':
std::cout << "FillMode 2" << std::endl;
test34::g_nFillMode = 2;
break;
case '7':
std::cout << "FillMode 3"<<std::endl;
test34::g_nFillMode = 3;
break;
case '8':
std::cout << "FillMode 4" << std::endl;
test34::g_nFillMode = 4;
break;
}
}
}
灰度图
掩码图
6.6 图 像 金 字 塔 与 图 片 尺 寸 缩 放
本节中,我们将一起探讨图像金字塔的一些基本概念,学习如何使用OpenCV 函 数pyrUp 和 pyrDown 对图像进行向上和向下采样,以及了解专门用于缩放图像 尺寸的resize 函数的用法。
6.6.1 引 言
我们经常会将某种尺寸的图像转换为其他尺寸的图像,如果要放大或者缩小 图片的尺寸,笼统来说,可以使用OpenCV 提供的如下两种方法。
- resize函数。这是最直接的方式
- pyrUp 、pyrDown (函数。即图像金字塔相关的两个函数,对图像进行向上 采样和向下采样的操作。
pyrUp、pyrDown 其实和专门用作放大缩小图像尺寸的resize在功能上差不多, 披着图像金字塔的皮,说白了还是在对图像进行放大和缩小操作。
另外需要指出 的是,pyrUp 、pyrDown 在 OpenCV 的 imgproc 模块中的Image Filtering子模块里, 而 resize 在 imgproc 模 块 的Geometric Image Transformations子模块里。
本节,我们将先介绍图像金字塔的原理,接着介绍resize 函数,然后是pyrUp 和pyrDown 函数,最后是一个综合示例程序。
6.6.2 关于图像金字塔
图像金字塔是图像中多尺度表达的一种,最主要用于图像的分割,是一种以 多分辨率来解释图像的有效但概念简单的结构。
图像金字塔最初用于机器视觉和图像压缩, 一幅图像的金字塔是一系列以金 字塔形状排列的,分辨率逐步降低且来源于同一张原始图的图像集合。其通过梯 次向下采样获得,直到达到某个终止条件才停止采样。
金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。
我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。
一般情况下有两种类型的图像金字塔常常出现在文献和以及实际运用中。它 们分别是:
- 高斯金字塔(Gaussianpyramid)—— 用来向下采样,主要的图像金字塔。
- 拉普拉斯金字塔(Laplacianpyramid)—— 用来从金字塔低层图像重建上层 未采样图像,在数字图像处理中也即是预测残差,可以对图像进行最大程度的还原,配合高斯金字塔一起使用。
两者的简要区别在于:高斯金字塔用来向下降采样图像,而拉普拉斯金字塔 则用来从金字塔底层图像中向上采样,重建一个图像。
要从金字塔第i 层生成第i+1 层(我们将第i+1 层表示为G+1), 我们先要用 高斯核对G₁ 进行卷积,然后删除所有偶数行和偶数列,新得到图像面积会变为源 图像的四分之一。按上述过程对输入图像G₀ 执行操作就可产生出整个金字塔。
当图像向金字塔的上层移动时,尺寸和分辨率会降低。OpenCV 中,从金字 塔中上一级图像生成下一级图像的可以用PryDown, 而通过PryUp 将现有的图像 在每个维度都放大两遍。
图像金字塔中的向上和向下采样分别通过OpenCV 的 函 数pyrUp 和 pyrDown 实现。
概括起来就是:
- 对图像向上采样—pyrUp 函数;
- 对图像向下采样—pyrDown 函数。
这里的向下与向上采样,是针对图像的尺寸而言的(和金字塔的方向相反), 向上就是图像尺寸加倍,向下就是图像尺寸减半。而如果按图6.55和图6.56中演 示的金字塔方向来理解,金字塔向上图像其实在缩小,这样刚好是反过来了。
但需要注意的是,PryUp 和 PryDown 不是互逆的,即PryUp 不是降采样的逆 操作。这种情况下,图像首先在每个维度上扩大为原来的两倍,新增的行(偶数 行)以0填充。然后给指定的滤波器进行卷积(实际上是一个在每个维度都扩大 为原来两倍的过滤器)去估计“丢失”像素的近似值。
PryDown()是一个会丢失信息的函数。为了恢复原来更高的分辨率的图像,我 们要获得由降采样操作丢失的信息,这些数据就和拉普拉斯金字塔有关系了。
6.6.3 高 斯 金 字 塔
高斯金字塔是通过高斯平滑和亚采样获得一些列下采样图像,也就是说第K 层高斯金字塔通过平滑、亚采样就可以获得K+1 层高斯图像。高斯金字塔包含了 一系列低通滤波器,其截止频率从上一层到下一层以因子2逐渐增加,所以高斯 金字塔可以跨越很大的频率范围。金字塔的图像如图6.57所示。
另外,每一层都按从下到上的次序编号,层级
G
i
+
1
G_{i+1}
Gi+1 (表示为
G
i
+
1
G_{i+1}
Gi+1尺寸小于第i 层
G
i
G_i
Gi)。
1.对图像的向下取样
为了获取层级为G+1的金字塔图像,我们采用如下方法:
(1)对图像G₁ 进行高斯内核卷积;
(2)将所有偶数行和列去除。
得到的图像即为
G
i
+
1
G_{i+1}
Gi+1的图像。显而易见,结果图像只有原图的四分之一。通 过对输入图像
G
i
G_i
Gi(原始图像)不停迭代以上步骤就会得到整个金字塔。同时我们 也可以看到,向下取样会逐渐丢失图像的信息。
以上就是对图像的向下取样操作,即缩小图像。
2 .对图像的向上取样
如果想放大图像,则需要通过向上取样操作得到,具体做法如下。
(1)将图像在每个方向扩大为原来的两倍,新增的行和列以0填充。
(2)使用先前同样的内核(乘以4)与放大后的图像卷积,获得“新增像素” 的近似值。
得到的图像即为放大后的图像,但是与原来的图像相比会发觉比较模糊,因 为在缩放的过程中已经丢失了一些信息。如果想在缩小和放大整个过程中减少信 息的丢失,这些数据就形成了拉普拉斯金字塔。
接下来一起看一下拉普拉斯金字塔的概念。
6.6.4 拉普拉斯金字塔
下式是拉普拉斯金字塔第i 层的数学定义:
L
i
=
G
i
−
U
P
(
G
i
+
1
)
⊗
g
5
∗
5
L_i=G_i-UP(G_{i+1})\otimes g_{5*5}
Li=Gi−UP(Gi+1)⊗g5∗5
式中的
G
i
G_i
Gi; 表示第i 层的图像。而UPO 操作是将源图像中位置为(x,y)的像素映射 到目标图像的(2x+1,2y+1)位置,即在进行向上取样。符号
⊗
\otimes
⊗表示卷积,
g
5
∗
5
g_{5*5}
g5∗5 5x5的 高斯内核。
我们下文将要介绍的pryUp, 就是在进行上面这个式子的运算。
因此,我们可以直接用OpenCV 进行拉普拉斯运算:
L
=
G
i
−
P
y
r
U
p
(
G
i
+
1
)
L=G_i-PyrUp(G_{i+1})
L=Gi−PyrUp(Gi+1)
也就是说,拉普拉斯金字塔是通过源图像减去先缩小后再放大的图像的一系 列图像构成的。
整个拉普拉斯金字塔运算过程可以通过图6.58来概括.
所以,我们可以将拉普拉斯金字塔理解为高斯金字塔的逆形式。
另外再提一点,关于图像金字塔非常重要的一个应用就是图像分割。图像分 割的话,先要建立一个图像金字塔,然后对 G i G_i Gi 和 G i + 1 G_{i+1} Gi+1 的像素直接依照对应的关系, 建立起“父与子”关系。而快速初始分割可以先在金字塔高层的低分辨率图像上 完成,然后逐层对分割加以优化。
6.6.5 尺 寸 调 整 :resize ( 函 数
resize() 为 OpenCV 中专门用来调整图像大小的函数。
此函数将源图像精确地转换为指定尺寸的目标图像。如果源图像中设置了 ROI(Region Of Interest,感兴趣区域),那么 resize() 函数会对源图像的ROI 区 域进行调整图像尺寸的操作,来输出到目标图像中。若目标图像中已经设置了ROI 区域,不难理解resize() 将会对源图像进行尺寸调整并填充到目标图像的ROI 中 。
很多时候,我们并不用考虑第二个参数dst 的初始图像尺寸和类型(即直接 定义 一 个Mat 类型,不用对其初始化),因为其尺寸和类型可以由src 、dsize 、fx 和 fy 这几个参数来确定。
看一下它的函数原型:
void resize(InputArray src, OutputArray dst, Size dsize, double fx = 0,
double fy = 0, int interpolation = INTER_LINEAR)
( 1 ) 第 一 个 参 数 ,InputArray 类 型 的src, 输入图像,即源图像,填Mat 类 的 对象即可。
(2)第二个参数,OutputArray 类 型 的dst, 输出图像,当其非零时,有着dsize (第三个参数)的尺寸,或者由src.size() 计算出来。
( 3 ) 第 三 个 参 数 ,Size 类 型 的dsize, 输出图像的大小。如果它等于零,由下 式进行计算:
d
s
i
z
e
=
S
i
z
e
(
r
o
u
n
d
(
f
x
∗
s
r
c
.
c
o
l
s
)
,
r
o
u
n
d
(
f
y
∗
s
r
c
.
r
o
w
s
)
dsize=Size(round(fx*src.cols),round(fy*src.rows)
dsize=Size(round(fx∗src.cols),round(fy∗src.rows)
其中,dsize 、fx 、fy 都不能为0。
( 4 ) 第 四 个 参 数 ,double 类 型 的fx, 沿水平轴的缩放系数,有默认值0,且 当其等于0时,由下式进行计算:
(
d
o
u
b
l
e
)
d
s
i
z
e
.
w
i
d
t
h
/
s
r
c
.
c
o
l
s
(double)dsize.width/src.cols
(double)dsize.width/src.cols
( 5 ) 第 五 个 参 数 ,double 类 型 的fy, 沿垂直轴的缩放系数,有默认值0,且 当其等于0时,由下式进行计算:
(
d
o
u
b
l
e
)
d
s
i
z
e
.
h
e
i
g
h
t
/
s
r
c
.
r
o
w
s
(double)dsize.height/src.rows
(double)dsize.height/src.rows
( 6 ) 第 六 个 参 数 ,int 类 型 的 interpolation, 用 于 指 定 插 值 方 式 , 默 认 为
INTER_LINEAR (线性插值)。
可选的插值方式如下:
- NTER_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.59)。
当进行6次图像缩小接着6次图像放大操作后,两种不同的插值方式得到的 效果图如图6.60、图6.61所示。
效果很明显,第一张全是一个个的像素,非常影响美观。另外一张却有雾化 的朦胧美感,所以插值方式的选择,对经过多次放大缩小的图片最终得到的效果 是有很大影响的。
接着我们来看resize 的调用范例。
void Test35() {
Mat srcImage = imread("image.jpg");
Mat tempImage, dstImage1, dstImage2;
srcImage.copyTo(tempImage);
imshow("initial_picture",srcImage);
//缩小/放大
resize(tempImage, dstImage1, Size(tempImage.cols / 2, tempImage.rows / 2), (0, 0), (0, 0), 3);
resize(tempImage, dstImage2, Size(tempImage.cols * 2, tempImage.rows *2), (0, 0), (0, 0), 3);
imshow("dstImage1", dstImage1);
imshow("dstImage2", dstImage2);
waitKey(0);
}
原图
放大
缩小
6.6.6 图像金字塔相关API函数
图 像 金 字 塔 相 关API 函 数 主 要 是pyrUp 、pyrDown 这 一 对,下面分别对其进
行讲解 。
1 . 向 上 采 样 :pyrUp() 函 数
pyrUpO 函数的作用是向上采样并模糊一张图像,说白了就是放大一张图片。
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.cols2,src.rows2) 来进行计算,且一直需要 满足下列条件:
∣
d
s
t
s
i
z
e
.
w
i
d
t
h
−
s
r
c
.
c
o
l
s
∗
2
∣
≤
(
d
s
t
s
i
z
e
.
w
i
d
t
h
m
o
d
2
)
|dstsize.width-src.cols*2|≤(dstsize.width \ \ mod2)
∣dstsize.width−src.cols∗2∣≤(dstsize.width mod2)
∣
d
s
t
s
i
z
e
.
h
e
i
g
h
t
−
s
r
c
.
r
o
w
s
∗
2
∣
≤
(
d
s
t
s
i
z
e
.
h
e
i
h
t
m
o
d
2
)
|dstsize.height-src.rows*2|≤(dstsize.heiht \ \ mod2)
∣dstsize.height−src.rows∗2∣≤(dstsize.heiht mod2)
- 第 四 个 参 数 ,int类 型 的borderType, 边界模式, 一般不用去管它。
pyrUp 函数执行高斯金字塔的采样操作,其实它也可以用于拉普拉斯金字塔的。 首先,它通过插入可为零的行与列,对源图像进行向上取样操作,然后将结
果 与pyrDown() 乘以4的内核做卷积。
2 . 采样:pyrDown()函数
pyrDown() 函数的作用是向下采样并模糊一张图片,说白了就是缩小一张图片。
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)来进行计算,且 一直需要满足下列条件:
∣ d s t s i z e . w i d t h ∗ 2 − s r c . c o l s ∣ ≤ 2 |dstsize.width*2-src.cols|≤2 ∣dstsize.width∗2−src.cols∣≤2
∣ d s t s i z e . h e i g t h ∗ 2 − s r c . r o w s ∣ ≤ 2 |dstsize.heigth*2-src.rows|≤2 ∣dstsize.heigth∗2−src.rows∣≤2
该pyrDown 函数执行了高斯金字塔建造的向下采样的步骤。首先,它将源图
像与如下内核做卷积运算:
依然是看看完整的示例程序,如下。
void Test36() {
Mat srcImage = imread("image.jpg");
Mat tempImage, dstImage1,dstImage2;
srcImage.copyTo(tempImage);
imshow("initial_picture", srcImage);
pyrUp(tempImage, dstImage1, Size(tempImage.cols * 2, tempImage.rows * 2)); //向上取样操作
pyrDown(tempImage, dstImage2, Size(tempImage.cols / 2, tempImage.rows / 2)); //向下取样操作
imshow("dstImage1", dstImage1);
imshow("dstImage2", dstImage2);
waitKey(0);
}
原图
向上采样
向下采样
6.6.7 综合示例:图像金字塔与图片尺寸缩放
这个示例程序中,分别演示了用resize,pryUp,pryDown 来让源图像进行放 大缩小的操作,分别用键盘按键1、2、3、4、A 、D 、W 、S 来控制图片的放大与 缩小。如图6.67所示。
void Test37() {
Mat g_secImage, g_dstImage, g_tempImage;
g_srcImage = imread("image.jpg");
namedWindow("window", WINDOW_AUTOSIZE);
imshow("window", g_srcImage);
g_dstImage = g_tempImage = g_srcImage;
int key = 0;
while (1) {
key = waitKey(0);
switch (key) {
case 27:
return;
break;
case 'q':
return;
break;
case '1':
pyrUp(g_tempImage, g_dstImage, Size(g_tempImage.cols * 2, g_tempImage.rows * 2)); //向上取样
break;
case '2':
pyrDown(g_tempImage, g_dstImage, Size(g_tempImage.cols / 2, g_tempImage.rows / 2)); //向下取样
break;
case '3':
resize(g_tempImage, g_dstImage, Size(g_tempImage.cols * 2, g_tempImage.rows * 2)); //resize * 2
break;
case '4':
resize(g_tempImage, g_dstImage, Size(g_tempImage.cols / 2, g_tempImage.rows / 2)); //resize / 2
break;
}
imshow("window", g_dstImage);
g_tempImage = g_dstImage;
}
}
多次变换后图片变得模糊
6.7 阈值化
在对各种图形进行处理操作的过程中,我们常常需要对图像中的像素做出取 舍与决策,直接剔除一些低于或者高于一定值的像素。
阈值可以被视作最简单的图像分割方法。比如,从一副图像中利用阈值分割 出我们需要的物体部分(当然这里的物体可以是一部分或者整体)。这样的图像分 割方法基于图像中物体与背景之间的灰度差异,而且此分割属于像素级的分割。 为了从一副图像中提取出我们需要的部分,应该用图像中的每一个像素点的灰度 值与选取的阈值进行比较,并作出相应的判断。注意:阈值的选取依赖于具体的 问题。即物体在不同的图像中有可能会有不同的灰度值。
一旦找到了需要分割的物体的像素点,可以对这些像素点设定一些特定的值来 表示。例如,可以将该物体的像素点的灰度值设定为“0”(黑色),其他的像素点 的灰度值为"255"(白色)。当然像素点的灰度值可以任意,但最好设定的两种颜 色对比度较强,以方便观察结果。
在OpenCV 2.X中 ,Threshold() 函数(基本阈值操作)和adaptiveThreshold() 函数(自适应阈值操作)可以完成这样的要求。它们的基本思想是:给定一个数 组和一个阈值,然后根据数组中的每个元素的值是高于还是低于阈值而进行一些 处理。下面,我们将对这两个函数分别进行剖析。
6.7.1 固定阈值操作:Threshold(函数
函数Threshold() 对单通道数组应用固定阈值操作。该函数的典型应用是对灰 度图像进行阈值操作得到二值图像,(compare() 函数也可以达到此目的)或者是 去掉噪声,例如过滤很小或很大象素值的图像点。
double threshold(InputArray src, OutputArray dst, double thresh,
double maxval, int type)
- 第一个参数,InputArray 类型的src, 输入数组,填单通道,8或32位浮点 类型的Mat 即可。
- 第二个参数,OutputArray 类 型 的dst, 函数调用后的运算结果存在这里, 即这个参数用于存放输出结果,且和第一个参数中的Mat 变量有一样的尺 寸和类型。
- 第三个参数,double 类型的thresh,阈值的具体值。
- 第 四 个 参 数 ,double 类 型 的 maxval, 当第五个参数阈值类型 type 取 CV_THRESH_BINARY 或 CV_THRESH_BINARY_INV 时阈值类型时的 最大值(对应地,OpenCV2 中可以为 CV_THRESH_BINARY 和
CV_THRESH_BINARY_INV)。 - 第五个参数,int类 型 的type,阈值类型。threshold()函数支持的对图像取阈 值的方法由其确定,具体用法如图6.70。
上述标识符依次取值分别为0,1,2,3,4。
而针对上述公式,一一对应的图形化的阈值描述如图6.71 所示 (THRESH_BINARY 对应二进制阈值,依次类推)。
6.7.2 自 适 应 阈 值 操 作 :adaptiveThreshold) 函 数
adaptiveThreshold()函数的作用是对矩阵采用自适应阈值操作,支持就地操 作。函数原型如下。
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
- 第一个参数,InputArray类型的src, 输入图像,即源图像,填Mat 类的对 象即可,且需为8位单通道浮点型图像。
- 第二个参数,OutputArray 类型的dst, 函数调用后的运算结果存在这里, 需和源图片有一样的尺寸和类型。
- 第三个参数,double类型的maxValue, 给像素赋的满足条件的非零值。 具体看下面的讲解。
- 第四个参数,int类型的adaptiveMethod,用于指定要使用的自适应阈值算 法,可取值为ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_ GAUSSIAN_C。
- 第五个参数,int 类型的 thresholdType,阈值类型。取值必须为 THRESH_BINARY、THRESH_BINARY_INV 其中之一。
- 第六个参数,int类型的blockSize,用于计算阈值大小的一个像素的邻域尺 寸,取值为3、5、7等。
- 第七个参数,double 类型的C, 减去平均或加权平均值后的常数值。通常 其为正数,但少数情况下也可以为零或负数。
namespace test38 {
int g_nThresholdValue = 100;
int g_nThresholdType = 3;
Mat g_srcImage, g_grayImage, g_dstImage;
// 滑动条回调函数
void on_Threshold(int, void*) {
threshold(g_grayImage, g_dstImage, g_nThresholdValue, 255, g_nThresholdType);
imshow("window", g_dstImage);
}
}
void Test38() {
test38::g_srcImage = imread("image.jpg");
if (test38::g_srcImage.empty()) {
std::cerr << "Error loading image!" << std::endl;
return;
}
cvtColor(test38::g_srcImage, test38::g_grayImage, COLOR_BGR2GRAY);
namedWindow("window", WINDOW_AUTOSIZE);
createTrackbar("mode:", "window", &test38::g_nThresholdType, 4, test38::on_Threshold);
createTrackbar("value:", "window", &test38::g_nThresholdValue, 255, test38::on_Threshold);
test38::on_Threshold(0, nullptr);
while (true) {
int key = waitKey(0);
if (key == 27) { // 27是ESC键的ASCII码
break;
}
}
}
模式1
模式2
模式3
模式4
模式5
6.8 本章小结
本章中我们学习了各种利用OpenCV 进行图像处理的方法。包括属于线性滤 波的方框滤波、均值滤波与高斯滤波,属于非线性滤波的中值滤波、双边滤波; 两种基本形态学操作——膨胀与腐蚀;5种高级形态学滤波操作——开运算、闭 运算、形态学梯度、顶帽以及黑帽;还有漫水填充算法、图像金字塔、图像缩放、 阈值化。涉及到的内容可谓非常丰富。相信学完此章的内容,你对OpenCV 的了 解已经上了一个层次。