阅读opencv计算机视觉编程四(形态学运算变换图像)

用形态学运算变换图像
用形态学滤波器腐蚀和膨胀图像;
 用形态学滤波器开启和闭合图像;
 在灰度图像中应用形态学运算;
 用分水岭算法实现图像分割;
 用MSER 算法提取特征区域。

数学形态学是一门20 世纪60 年代发展起来的理论,用于分析和处理离散图像。它定义了一
系列运算,用预先定义的形状元素探测图像,从而实现图像的转换。这个形状元素与像素邻域的
相交方式决定了运算的结果。本文将介绍几种最重要的形态学运算,并探讨用基于形态学运算的
算法进行图像分割和特征检测的问题。

用形态学滤波器腐蚀和膨胀图像:
腐蚀和膨胀是最基本的形态学运算,因此把它们放在第一节介绍。数学形态学中最基本的概
念是结构元素。结构元素可以简单地定义为像素的组合(下图的正方形),在对应的像素上定义
了一个原点(也称锚点)。形态学滤波器的应用过程就包含了用这个结构元素探测图像中每个像
素的操作过程。把某个像素设为结构元素的原点后,结构元素和图像重叠部分的像素集(下图的
九个阴影像素)就是特定形态学运算的应用对象。结构元素原则上可以是任何形状,但通常是一
个简单形状,如正方形、圆形或菱形,并且把中心点作为原点。自定义结构元素可用于强化或消
除特殊形状。
OpenCV 用简单的函数实现了腐蚀和膨胀运算,它们分别是cv:erode 和cv:dilate,用法
也很简单:
// 读取输入图像

cv::Mat image= cv::imread("binary.bmp");
// 腐蚀图像
// 采用默认的3×3 结构元素
cv::Mat eroded; // 目标图像
cv::erode(image,eroded,cv::Mat());
// 膨胀图像
cv::Mat dilated; // 目标图像
cv::dilate(image,dilated,cv::Mat());

腐蚀就是把当前像素替换成所定义像素集合中的最小像素值;膨胀是腐蚀的反运算,
它把当前像素替换成所定义像素集合中的最大像素值。由于输入的二值图像只包含黑色(值为0)
和白色(值为255)像素,因此每个像素都会被替换成白色或黑色像素。
要形象地理解这两种运算的作用,可考虑背景(黑色)和前景(白色)的物体。腐蚀时,如
果结构元素放到某个像素位置时碰到了背景(即交集中有一个像素是黑色的),那么这个像素就
变为背景;膨胀时,如果结构元素放到某个背景像素位置时碰到了前景物体,那么这个像素就被
标为白色。正因如此,图像腐蚀后物体尺寸会缩小(形状被腐蚀),而图像膨胀后物体会扩大。
在腐蚀图像中,有些面积较小的物体(可看作背景中的“噪声”像素)会彻底消失。与之类似,
膨胀后的物体会变大,而物体中一些“空隙”会被填满。OpenCV 默认使用3×3 正方形结构元素。
在调用函数时,参考前面的例子将第三个参数指定为空矩阵(即cv::Mat()),就能得到默认的
结构元素。你也可以通过提供一个矩阵来指定结构元素的大小(以及形状),矩阵中的非零元素
将构成结构元素。下面的例子使用7×7 的结构元素:

// 用更大的结构元素腐蚀图像
// 创建7×7 的mat 变量,其中全部元素都为1
cv::Mat element(7,7,CV_8U,cv::Scalar(1));
// 用这个结构元素腐蚀图像
cv::erode(image,eroded,element);

开启和闭合滤波器的定义只与基本的腐蚀和膨胀运算有关:闭合的定义是对图像先膨胀后腐
蚀,开启的定义是对图像先腐蚀后膨胀。
为了应用较高级别的形态学滤波器,需要用cv::morphologyEx 函数,并传入对应的函数
代码。例如下面的调用方法将适用于闭合运算:
// 闭合图像
cv::Mat element5(5,5,CV_8U,cv::Scalar(1));
cv::Mat closed;
cv::morphologyEx(image,closed, // 输入和输出的图像
cv::MORPH_CLOSE, // 运算符
element5); // 结构元素
注意,为了让滤波器的效果更加明显,这里使用了5×5 的结构元素
cv::Mat opened;
cv::morphologyEx(image, opened, cv::MORPH_OPEN, element5);

开启和闭合滤波器的定义只与基本的腐蚀和膨胀运算有关:闭合的定义是对图像先膨胀后腐
蚀,开启的定义是对图像先腐蚀后膨胀。
因此可以用以下方法对图像做闭合运算:
// 膨胀原图像
cv::dilate(image, result, cv::Mat());
// 就地腐蚀膨胀后的图像
cv::erode(result, result, cv::Mat());
调换这两个函数的调用次序,就能得到开启滤波器。
查看闭合滤波器的结果,可看到白色的前景物体中的小空隙已经被填满。闭合滤波器也会把
邻近的物体连接起来。基本上,所有小到不能容纳完整结构元素的空隙或间隙都会被闭合滤波器
消除。
与闭合滤波器相反,开启滤波器消除了背景中的几个小物体。所有小到不能容纳完整结构元
素的物体都会被移除。

这些滤波器常用于目标检测。
闭合滤波器可把错误分裂成小碎片的物体连接起来,而开启滤
波器可以移除因图像噪声产生的斑点。因此最好按一定的顺序调用这些滤波器。如果优先考虑过
滤噪声,可以先开启后闭合,但这样做的坏处是会消除掉部分物体碎片。
先使用开启滤波器,再使用闭合滤波器,会得到如下结果。
11
注意,对一幅图像进行多次同样的开启运算是没有作用的(闭合运算也一样)。事实上,因
为第一次使用开启滤波器时已经填充了空隙,再使用同一个滤波器将不会使图像产生变化。用数
学术语讲,这些运算是幂等(idempotent)的。

形态学梯度运算可以提取出图像的边缘,具体方法为使用cv::morphologyEx 函数,代码
如下所示:
// 用3×3 结构元素得到梯度图像

cv::Mat result;
cv::morphologyEx(image, result,
cv::MORPH_GRADIENT, cv::Mat());

得到图像中物体的轮廓(为方便观察,对图像做了反色处理)。

另一种很实用的形态学运算是顶帽(hat-top)变换,它可以从图像中提取出局部的小型前景
物体。为了说明该运算的效果,我们用本书中一页的照片做试验。由图可知,页面的光照并不均
匀。通过使用cv::morphologyEx 函数并采用正确的参数,可以调用黑帽变换提取出页面上的
文字(作为前景物体):
// 使用7×7 结构元素做黑帽变换

cv::Mat element7(7, 7, CV_8U, cv::Scalar(1));
cv::morphologyEx(image, result, cv::MORPH_BLACKHAT, element7);

运行结果如下图所示,它可以从图像中提取出大部分文字。

理解形态学运算在灰度图像上的效果有一个好办法,就是把图像看作是一个拓扑地貌,不同
的灰度级别代表不同的高度(或海拔)。基于这种观点,明亮的区域代表高山,黑暗的区域代表
深谷;边缘相当于黑暗和明亮像素之间的快速过渡,因此可以比作陡峭的悬崖。腐蚀这种地形的
最终结果是:每个像素被替换成特定邻域内的最小值,从而降低它的高度。结果是悬崖“缩小”,
山谷“扩大”。膨胀的效果刚好相反,即悬崖“扩大”,山谷“缩小”。但不管哪种情况,平地(即
强度值固定的区域)都会相对保持不变。
根据这个结论,可以得到一种检测图像边缘(或悬崖)的简单方法,即通过计算膨胀后的图
像与腐蚀后的图像之间的的差距得到边缘。因为这两种转换后图像的差别主要在边缘地带,所以
相减后会突出边缘。在cv::morphologyEx 函数中输入cv::MORPH_GRADIENT 参数,即可实
现此功能。显然,结构元素越大,检测到的边缘就越宽。这种边缘检测运算称为Beucher 梯度
(下一章将详细讨论图像梯度的概念)。注意还有两种简单的方法能得到类似结果,即用膨胀后的
图像减去原始图像,或者用原始图像减去腐蚀后的图像,那样得到的边缘会更窄。
顶帽运算也基于图像比对,它使用了开启和闭合运算。因为灰度图像进行形态学开启运算时
会先对图像进行腐蚀,局部的尖锐部分会被消除,其他部分则将保留下来。因此,原始图像和经
过开启运算的图像的比对结果就是局部的尖锐部分。这些尖锐部分就是我们需要提取的前景物
体。对于本书的照片来说,前景物体就是页面上的文字。因为书本为白底黑字,所以我们采用它
的互补运算,即黑帽算法。它将对图像做闭合运算,然后从得到的结果中减去原始图像。这里采
用7×7 的结构元素,它足够大了,能确保移除文字。

用分水岭算法实现图像分割
分水岭变换是一种流行的图像处理算法,用于快速将图像分割成多个同质区域。它基于这样
的思想:如果把图像看作一个拓扑地貌,那么同类区域就相当于陡峭边缘内相对平坦的盆地。分
水岭算法通过逐步增高水位,把地貌分割成多个部分。因为算法很简单,它的原始版本会过度分
割图像,产生很多小的区域。因此OpenCV 提出了该算法的改进版本,使用一系列预定义标记来
引导图像分割的定义方式。
使用分水岭分割法需要调用cv::watershed 函数。该函数的输入对象是一个标记图像,图
像的像素值为32 位有符号整数,每个非零像素代表一个标签。它的原理是对图像中部分像素做
标记,表明它们的所属区域是已知的。分水岭算法可根据这个初始标签确定其他像素所属的区域。
本节将先建立一个标记图像作为灰度图像,然后将其转换成整型图像。我们把这个步骤封装进
WatershedSegmenter 类,它包括指定标记图像和计算分水岭的方法:

class WatershedSegmenter {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// 转换成整数型图像
markerImage.convertTo(markers,CV_32S);
}
cv::Mat process(const cv::Mat &image) {
// 应用分水岭
cv::watershed(image,markers);
return markers;
}

不同应用程序获得标记的方式各不相同。例如,可在预处理过程中识别出一些属于某个感兴
趣物体的像素。然后,根据初始检测结果,使用分水岭算法划出整个物体的边缘。本节将利用本
章一直使用的二值图像,识别出对应原始图像中的动物。因此,我们需要
从二值图像中识别出属于前景(动物)的像素以及属于背景(主要是草地)的像素。这里把前景
像素标记为255,把背景像素标记为128(该数字是随意选择的,任何不等于255 的数字都可以)。
其他像素的标签是未知的,标记为0。
现在,这个二值图像包含了属于图像不同部分的白色像素,因此要对图像做深度腐蚀运算,
只保留明显属于前景物体的像素:
// 消除噪声和细小物体

cv::Mat fg;
cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),4);
注意,仍然有少量属于背景(森林)的像素保留了下来,不用管它们,可将它们看作感兴趣
物体。与之类似,我们可以通过对原二值图像做一次大幅度的膨胀运算来选中一些背景像素:
// 标识不含物体的图像像素
cv::Mat bg;
cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),4);
cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);
得到的黑色像素对应背景像素。因此在膨胀后,要立即通过阈值化运算把它们赋值为128。
得到的图像如下所示。

合并这两幅图像,得到标记图像,代码为:
// 创建标记图像
cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
markers= fg+bg;

注意这里是如何用重载运算符+来合并图像的。下面的图像将被输入分水岭算法。
毫无疑问,在这个输入图像中,白色区域属于前景物体,灰色区域属于背景,而黑色区域带
有未知标签。分水岭算法的作用就是明确地划分前景和背景,并对黑色区域的像素做出标记(属
于前景还是背景)。可用下面的方法来分割图像:

// 创建分水岭分割类的对象
WatershedSegmenter segmenter;
// 设置标记图像,然后执行分割过程
segmenter.setMarkers(markers);
segmenter.process(image);

上面的代码会修改标记图像,每个值为0 的像素都会被赋予一个输入标签,而边缘处的像素
被赋值为-1,得到的标签图像如左图所示,边缘图像如右图所示。

用MSER 算法提取特征区域
最大稳定外部区域
(MSER)算法也用相同的水淹类比,以便从图像中提取有意义的区域。创建这些区域时也使用
逐步提高水位的方法,但是这次我们关注的是在水淹过程中的某段时间内,保持相对稳定的盆地。
可以发现,这些区域对应着图像中某些物体的特殊部分。
计算图像MSER 的基础类是cv::MSER。它是一个抽象接口,继承自cv::Feature2D 类。
事实上,OpenCV 中的所有特征检测类都是从这个类继承的。cv::MSER 类的实例可以通过
create 方法创建。我们在初始化时指定被检测区域的最小和最大尺寸,以便限制被检测特征的
数量,调用方式如下:
// 基本的MSER 检测器

cv::Ptr<cv::MSER> ptrMSER=
cv::MSER::create(5, // 局部检测时使用的增量值
200, // 允许的最小面积
2000); // 允许的最大面积
现在可以通过调用detectRegions 方法来获得MSER,指定输入图像和一个相关的输出数
据结构,代码如下所示:
// 点集的容器
std::vector<std::vector<cv::Point> > points;
// 矩形的容器
std::vector<cv::Rect> rects;
// 检测MSER 特征
ptrMSER->detectRegions(image, points, rects);
检测结果放在两个容器中。第一个是区域的容器,每个区域用组成它的像素点表示;第二个
是矩形的容器,每个矩形包围一个区域。为了呈现结果,创建一个空白图像,在图像上用不同的
颜色显示检测到的区域(颜色是随机选择的)。用以下代码实现:
// 创建白色图像
cv::Mat output(image.size(),CV_8UC3);
output= cv::Scalar(255,255,255);

检测结果放在两个容器中。第一个是区域的容器,每个区域用组成它的像素点表示;第二个
是矩形的容器,每个矩形包围一个区域。为了呈现结果,创建一个空白图像,在图像上用不同的
颜色显示检测到的区域(颜色是随机选择的)。用以下代码实现:
// 创建白色图像

cv::Mat output(image.size(),CV_8UC3);
output= cv::Scalar(255,255,255);

注意,MSER 会形成层叠区域。为了显示全部区域,如果一个较大区域内包含了较小的区域,
就不能覆盖它。可以从下图中检测出MSER。
MSER 的原理与分水岭算法相同,即高度为0~255,逐渐淹没图像。在图像处理技术中,通
常把高于某个阈值的像素集合称为高度集。随着水位的升高,颜色较黑并且边界陡峭的区域会形
成盆地 ,并且在一段时间内有相对稳定的形状(用水位表示颜色,水位高低代表了像素值的强
度)。这些稳定的盆地就是MSER。检测它们的方法是,观察每个水位连通的区域(即盆地)并
测量它们的稳定性。测量稳定性的方法是:计算区域的当前面积以及该区域原先的面积(比当前
水位低一个特定值的时候),并比较这两个面积。如果相对变化达到局部最小值,就认为这个区
域是MSER。增量值将作为cv::MSER 类构造函数的第一个参数,用以测量相对稳定性,默认值
为5。另外要注意,区域面积必须在预定义的范围内。构造函数中后面两个参数就是允许的最小
和最大区域尺寸。另外必须确保MSER 是稳定的(第四个参数),即形状的相对变化必须足够小。
一个稳定区域可以属于另一个更大的区域(称为父区域)。
为了确保有效性,一个父MSER 和它的子区域必须有足够大的差别,即差异限度,由
cv::MSER 类构造函数的第五个参数指定。在前面的例子中,最后两个参数都使用了默认值。
(MSER 允许的最大相对变化的默认值为0.25,父MSER 与子区域的最小差别的默认值为0.2。)
可见,要检测MSER,必须对参数进行规范化,否则难以应对不同环境。
MSER 检测器首先输出一个包含像素集的容器,每个像素集构成一个区域。因为我们需要找
出整个区域的位置,而不是里面的单个像素,所以通常用包含了被检测区域的几何形状表示一个
MSER。检测过程中输出的第二项是一系列矩形,画出所有矩形就能表示检测的结果。但是这样
会画出许多矩形,使结果很不直观(区域之间还会互相包含,结果更加混乱)。这个例子主要想
检测出大楼中的窗户,因此要提取出所有包含垂直矩形的区域。实现方法是将每个矩形的面积与
检测到的对应区域进行比较,如果两者一致(这里用的判断标准是两者比例超过0.6),那么它就
是一个MSER。测试代码如下所示:
// 提取并显示矩形的MSER

std::vector<cv::Rect>::iterator itr = rects.begin();
std::vector<std::vector<cv::Point> >::iterator itp = points.begin();
for (; itr != rects.end(); ++itr, ++itp) {
// 检查两者比例
if (static_cast<double>(itp->size())/itr->area() > 0.6)
cv::rectangle(image, *itr, cv::Scalar(255), 2);

在其他应用程序中,也可以采用别的判断标准和显示方法。下面代码的判断依据是检测到的
区域不能太细长(将封闭的矩形旋转,计算其宽高比),然后用未旋转的封闭椭圆表示它们。
// 提取并显示椭圆形的MSER

  • [ ]
for (std::vector<std::vector<cv::Point> >::iterator it =
       points.begin(); it != points.end(); ++it) { // 遍历MSER 集合中的每个点 for
       (std::vector<cv::Point>::iterator itPts = it->begin(); itPts !=
       it->end(); ++itPts) { // 提取封闭的矩形 cv::RotatedRect rr =
       cv::minAreaRect(*it); // 检查椭圆的长宽比 if (rr.size.height /
       rr.size.height > 0.6 || rr.size.height / rr.size.height < 1.6)
       cv::ellipse(image, rr, cv::Scalar(255), 2); } } }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值