OpenCV实战(11)——形态学变换详解
- 0. 前言
- 1. 腐蚀和膨胀运算
- 1.1 腐蚀和膨胀基础
- 1.2 使用形态学滤波器执行图像腐蚀和膨胀运算
- 2. 开运算和闭运算
- 2.1 使用形态学滤波器执行图像开运算和闭运算
- 3. 形态学变换应用
- 3.1 使用形态学滤波器检测边缘
- 3.2 使用形态学滤波器检测边缘和角点
- 4. 完整代码
- 小结
- 系列链接
0. 前言
形态学变换( Morphological transformations
)通常是在二值图像上执行、基于图像形状的操作。其具体的操作由核结构元素决定,它决定了操作的性质。膨胀和腐蚀是形态学变换领域的两个基本算子,此外,开运算和闭运算是两个重要的运算,它们可以通过上述两个运算(膨胀和腐蚀)获得。
1. 腐蚀和膨胀运算
1.1 腐蚀和膨胀基础
腐蚀 (Erosion
) 和膨胀 (Dilation
) 是最基本的形态学算子,因此,我们将首先介绍这两个基本算子。数学形态学的基本组成部分是结构元素,结构元素可以简单地理解为定义原点(也称为锚点)的像素配置,如下图中的正方形。应用形态学滤波器需要使用此结构元素检测图像的每个像素,当结构元素的原点与给定像素对齐时,它与图像的交集定义了一组像素,在这些像素(下图中的九个阴影像素)上应用了特定的形态学操作。原则上,结构元素可以是任何形状,但出于效率考虑,通常使用简单的形状,例如正方形、圆形或以原点为中心的菱形。
1.2 使用形态学滤波器执行图像腐蚀和膨胀运算
由于形态学滤波器通常作用于二值图像,因此我们使用通过阈值化创建的二值图像。然而,由于在形态学中通常用白色像素值表示前景对象,用黑色像素值表示背景对象,因此我们需要对原二值图像求补(即用黑色像素值表示前景对象,用白色像素值表示背景对象)。在形态学中,下图可以认为是由阈值化创建的二值图像的补:
与其他形态学滤波器一样,腐蚀和膨胀两个滤波器对由结构元素定义的每个像素周围的像素集(或邻域)进行操作,当应用于给定像素时,结构元素的锚点与该像素位置对齐,并且与结构元素相交的所有像素都包含在当前集合中。腐蚀使用在定义的像素集中找到的最小像素值替换当前像素。膨胀是腐蚀的互补算子,它使用定义的像素集中找到的最大像素值替换当前像素。由于输入二值图像仅包含黑色 (0
) 和白色 (255
) 像素,因此每个像素都被替换为白色或黑色像素。腐蚀和膨胀在 OpenCV
中分别使用 cv::erode
和 cv::dilate
函数实现。
(1) 首先,读取二值图片:
// 读取二值图像
cv::Mat binary;
binary = cv::imread("binary.png", 0);
(2) 应用 cv::erode
函数:
// 腐蚀图像
cv::Mat eroded;
cv::erode(image, eroded, cv::Mat());
应用腐蚀形态学滤波器后,可以得到以下结果:
(3) 使用 cv::dilate
膨胀图像:
// 膨胀图像
cv::Mat dilated;
cv::dilate(image, dilated, cv::Mat());
应用膨胀形态学滤波器后,可以得到以下结果:
接下来,我们从这两个运算符的执行效果进行理解,对于腐蚀算子,如果结构元素置于给定像素位置与背景相接触(即,相交集中的像素之一是黑色),则该像素将被赋值为黑色并归属至背景;在膨胀算子中,如果背景像素上的结构元素接触前景对象,则该像素将被赋值为白色值。
这就解释了为什么在腐蚀图像中物体的尺寸会减小(形状被腐蚀),而一些小物体由于可能被认为是嘈杂的背景像素会被完全消除;而膨胀运算后的物体将会变大,并且物体内部的一些孔洞会被填满。默认情况下,OpenCV
使用 3 x 3
方形结构元素,当调用函数时第三个参数指定为空矩阵(即 cv::Mat()
)时,将使用此默认结构元素,我们可以通过使用非零元素定义结构元素矩阵,指定所需大小(和形状)的结构元素。例如,我们可以使用以下代码定义 7 x 7
结构元素:
cv::Mat element(7, 7, CV_8U, cv::Scalar(1));
cv::erode(image, eroded, element);
在这种情况下,效果更加明显,如下图所示:
我们可以简单的在图像上重复应用相同的结构元素,cv::erode
和 cv::dilate
这两个函数都有一个可选参数来指定重复次数:
// 腐蚀同一图像三次
cv::erode(image, eroded, cv::Mat(), cv::Point(-1, -1), 3);
原点参数 cv::Point(-1,-1)
表示原点位于矩阵的中心(默认);原点也可以定义在结构元素的其他位置。使用以上代码得到的图像与我们使用 7 x 7
结构元素获得的图像相同。实际上,腐蚀图像两次类似于腐蚀结构元素尺寸扩大的图像,这一规律也适用于膨胀算子。
由于背景/前景的概念是相对的,用结构元素腐蚀前景对象可也以看作是图像背景部分的膨胀,这是腐蚀/膨胀算子的基本属性。换句话说:
- 图像的腐蚀等价于互补图像膨胀的补
- 图像的膨胀等价于互补图像腐蚀的补
虽然我们在本节主要介绍了如何将形态学滤波器应用于二值图像,但这些滤波器同样也可以应用于灰度或彩色图像。
OpenCV
形态学函数支持就地处理,即可以使用输入图像作为目标图像:
cv::erode(image,image,cv::Mat());
OpenCV
在函数内部创建所需的临时图像,以使其正常工作。
2. 开运算和闭运算
2.1 使用形态学滤波器执行图像开运算和闭运算
上一节中,我们介绍了两个基本的形态学算子——膨胀和腐蚀。利用这些基本算子,我们可以定义其他运算符,本节将介绍开运算和闭运算。为了应用高级形态学滤波器,需要使用 cv::morphologyEx
函数。
(1) 要创建闭运算或开运算符,必须创建一个 cv::Mat
元素用作开/闭运算核:
cv::Mat element5(5, 5, CV_8U, cv::Scalar(1));
(2) 创建另一个 cv::Mat
存储应用形态学运算符后的结果:
cv::Mat closed;
cv::Mat opened;
(3) 最后,应用闭或开运算符。为了创建闭运算符,我们使用 cv::MORPH_CLOSE
参数调用 cv::morphologyEx
函数:
cv::morphologyEx(image, closed, // 输入和输出图像
cv::MORPH_CLOSE, // 操作算子
element5); // 结构元素
如果我们使用二值图像作为输入,结果如下图所示:
(4) 使用 cv::MORPH_OPEN
作为参数调用 cv::morphologyEx
函数,可以创建开运算:
cv::morphologyEx(image, opened, cv::MORPH_OPEN, element5);
应用形态学开运算可以得到以下图像:
(5) 开和闭滤波器是根据基本腐蚀和膨胀操作定义的,闭运算被定义为膨胀图像后腐蚀,开运算被定义为腐蚀图像后膨胀。因此,可以使用以下代码计算图像的闭:
// 1. 膨胀原始图像
cv::Mat result;
cv::dilate(image, result, element5);
// 2. 腐蚀膨胀后的图像
cv::erode(result, result, element5);
可以通过交换这两个函数的调用顺序得到开滤波器。在使用闭滤波器的结果图像中,可以看到白色前景对象中的小孔被填充,滤波器还会将几个相邻的对象连接在一起。实际上,任何无法完全包含结构元素的过小的孔或间隙都将被滤波器消除。相反的,开滤波器会消除图像中的小物体,所有无法包含结构元素的过小对象都会被消除。
开/闭滤波器通常用于对象检测,闭滤波器可以连接被错误分割成小块的对象,而开滤波器可以去除由图像噪声引入的小斑点。因此,根据具体应用可以按不同顺序使用它们。如果二值图像连续应用闭和开运算,将得到一个仅显示场景中主要对象的图像,如下图所示。如果我们希望优先过滤噪声,也可以在闭滤波器之前应用开滤波器,但这会消除一些碎片化对象:
在图像上多次应用相同的开(或闭)运算符不会产生任何效果。实际上,由于孔已被第一个开滤波器填充,因此附加应用相同的滤波器不会对图像产生其他变化。在数学上,这些运算符被称为幂等运算符。
3. 形态学变换应用
形态学滤波器也可用于检测图像中的特定特征。在本节中,我们将学习如何检测灰度图像中的轮廓和角点。
3.1 使用形态学滤波器检测边缘
(1) 通过使用适当滤波器参数调用 cv::morphologyEx
函数提取要检测的图像的边缘:
// 使用 3x3 结构元素获取梯度图像
cv::Mat result;
cv::morphologyEx(image, result, cv::MORPH_GRADIENT, cv::Mat());
// 使用阈值获取二值图像
int threshold(80);
cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY);
可以得到以下结果图像:
理解形态学算子对灰度图像的影响时,可以将图像视为拓扑浮雕,其中灰度与高程(或海拔)相对应。从这个角度而言,明亮的区域对应山脉,而黑暗的区域对应山谷。此外,由于边缘对应于暗像素和亮像素之间的快速过渡,因此可以将其理解为陡峭的悬崖。如果在这样的地形上应用腐蚀算子,最终将用邻域中的最低值替换每个像素,从而降低其高度。因此,随着山谷的扩大,悬崖将被腐蚀。膨胀具有完全相反的效果;也就是说,悬崖将膨胀而山谷相应的会缩小。但在这两种情况下,高原(即强度恒定的区域)均将保持相对不变。
基于此,我们可以得到一种检测图像边缘(悬崖)的简单方法,即计算膨胀和腐蚀图像之间的差。由于这两种变换后的得到的图像主要在与边缘位置不同,因此相减将得到图像边缘。我们可以使用 cv::MORPH_GRADIENT
参数调用 cv::morphologyEx
函数完成以上操作。显然,结构元素越大,检测到的边缘就越宽,这种边缘检测算子也称为 Beucher
梯度(之后的学习中将更详细地讨论图像梯度的概念)。通过简单地从膨胀图像中减去原始图像或从原始图像中减去图像图像,也可以获得类似的结果,但产生的边缘会更细。
3.2 使用形态学滤波器检测边缘和角点
(1) 为了使用形态学检测角点,我们可以定义 MorphoFeatures
类:
class MorphoFeatures {
private:
// 用于产生二值图像的阈值
int threshold;
// 用于角点检测的结构元素
cv::Mat_<uchar> cross;
cv::Mat_<uchar> diamond;
cv::Mat_<uchar> square;
cv::Mat_<uchar> x;
(2) 使用形态学角点检测角点需要连续应用几个不同的形态学滤波器,为此我们需要使用非方形结构元素,实际上,在构造函数中定义了四个不同的结构元素,形状分别为正方形、菱形、十字形和 X
形,为简单起见,这些结构元素尺寸均为 5 x 5
:
public:
MorphoFeatures() : threshold(-1), cross(5, 5), diamond(5, 5),
square(5, 5), x(5, 5) {
// 创建十字形结构元素
cross << 0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
1, 1, 1, 1, 1,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0;
// 菱形结构元素
diamond << 0, 0, 1, 0, 0,
0, 1, 1, 1, 0,
1, 1, 1, 1, 1,
0, 1, 1, 1, 0,
0, 0, 1, 0, 0;
// 方形结构元素
square << 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1;
// x形结构元素
x << 1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1;
}
(4) 在角点特征检测中,顺序应用这些结构元素以获得结果角点图:
// 角点检测
cv::Mat getCorners(const cv::Mat& image) {
cv::Mat result;
// 膨胀
cv::dilate(image, result, cross);
// 腐蚀
cv::erode(result, result, diamond);
cv::Mat result2;
// 膨胀
cv::dilate(image, result2, x);
// 腐蚀
cv::erode(result2, result2, square);
// 角点
cv::absdiff(result2, result, result);
applyThreshold(result);
return result;
}
(5) 在图像上检测角点:
MorphoFeatures morpho;
morpho.setThreshold(35);
// 角点检测
cv::Mat corners;
corners = morpho.getCorners(gray);
// 在图像中显示角点
morpho.drawOnImage(corners, gray);
cv::namedWindow("Corners on Image");
cv::imshow("Corners on Image", gray);
在图像中,检测到的角点显示为圆圈,如下图所示:
角点检测相较更为复杂,因为它需要使用四种不同的结构元素。在 OpenCV
中并没有直接实现角点检测算子,但我们可以通过定义和组合不同形状的结构元素实现。通过用两种不同的结构元素来膨胀和腐蚀图像实现图像闭运算。使用这些元素令图像中直线保持不变,但不同结构元素也会影响角点处的边。以单个白色方块组成的简单图像为例介绍这种非对称闭操作的效果:
上图中,第一个方块是原始图像。当用十字形结构元素执行膨胀操作时,方块边缘会被扩展,除了十字形不碰到方形的角点,如上图中间的方块所示。然后,膨胀后的图像被菱形结构元素腐蚀,腐蚀操作会令大多数边缘恢复到原来的位置,但由于方形的角没有被膨胀,所以其会被进一步腐蚀,如上图中最右边的正方形所示,可以看到其角已经消失。使用 X
形和方形结构元素重复以上过程,由于 X
形是十字形结构元素的旋转版本,因此可以捕获 45
度方向的角。最后,对两个结果进行差分便可以提取角点特征。
4. 完整代码
头文件 (morphoFeatures.h
) 完整代码如下:
#if !defined MFEATURES
#define MFEATURES
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class MorphoFeatures {
private:
// 用于产生二值图像的阈值
int threshold;
// 用于角点检测的结构元素
cv::Mat_<uchar> cross;
cv::Mat_<uchar> diamond;
cv::Mat_<uchar> square;
cv::Mat_<uchar> x;
public:
MorphoFeatures() : threshold(-1), cross(5, 5), diamond(5, 5),
square(5, 5), x(5, 5) {
// 创建十字形结构元素
cross << 0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
1, 1, 1, 1, 1,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0;
// 菱形结构元素
diamond << 0, 0, 1, 0, 0,
0, 1, 1, 1, 0,
1, 1, 1, 1, 1,
0, 1, 1, 1, 0,
0, 0, 1, 0, 0;
// 方形结构元素
square << 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1;
// x形结构元素
x << 1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1;
}
void setThreshold(int t) {
threshold = t;
}
int getThreshold() {
return threshold;
}
void applyThreshold(cv::Mat& result) {
if (threshold > 0) {
cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY_INV);
}
}
// 直线检测
cv::Mat getEdges(const cv::Mat& image) {
cv::Mat result;
cv::morphologyEx(image, result, cv::MORPH_GRADIENT, cv::Mat());
applyThreshold(result);
return result;
}
// 角点检测
cv::Mat getCorners(const cv::Mat& image) {
cv::Mat result;
// 膨胀
cv::dilate(image, result, cross);
// 腐蚀
cv::erode(result, result, diamond);
cv::Mat result2;
// 膨胀
cv::dilate(image, result2, x);
// 腐蚀
cv::erode(result2, result2, square);
// 角点
cv::absdiff(result2, result, result);
applyThreshold(result);
return result;
}
void drawOnImage(const cv::Mat& binary, cv::Mat& image) {
cv::Mat_<uchar>::const_iterator it = binary.begin<uchar>();
cv::Mat_<uchar>::const_iterator itend = binary.end<uchar>();
for (int i=0; it!=itend; ++it, ++i) {
if (!*it) {
cv::circle(image,
cv::Point(i%image.step, i/image.step),
5,
cv::Scalar(255, 0, 0));
}
}
}
};
#endif
主文件 (morphology.cpp
) 完整代码如下所示:
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
int main() {
// 读取图像
cv::Mat image = cv::imread("binary.png");
if (!image.data) return 0;
cv::namedWindow("Image");
cv::imshow("Image", image);
// 腐蚀图像
cv::Mat eroded;
cv::erode(image, eroded, cv::Mat());
cv::namedWindow("Eroded Image");
cv::imshow("Eroded Image", eroded);
// 膨胀图像
cv::Mat dilated;
cv::dilate(image, dilated, cv::Mat());
cv::namedWindow("Dilated Image");
cv::imshow("Dilated Image",dilated);
// 使用一个较大的结构元素腐蚀图像
cv::Mat element(7, 7, CV_8U, cv::Scalar(1));
cv::erode(image, eroded, element);
cv::namedWindow("Eroded Image (7x7)");
cv::imshow("Eroded Image (7x7)",eroded);
// 腐蚀同一图像三次
cv::erode(image, eroded, cv::Mat(), cv::Point(-1, -1), 3);
cv::namedWindow("Eroded Image (3 times)");
cv::imshow("Eroded Image (3 times)",eroded);
// 图像闭运算
cv::Mat element5(5, 5, CV_8U, cv::Scalar(1));
cv::Mat closed;
cv::morphologyEx(image, closed, // 输入和输出图像
cv::MORPH_CLOSE, // 操作算子
element5); // 结构元素
cv::namedWindow("Closed Image");
cv::imshow("Closed Image",closed);
// 图像开运算
cv::Mat opened;
cv::morphologyEx(image, opened, cv::MORPH_OPEN, element5);
cv::namedWindow("Opened Image");
cv::imshow("Opened Image",opened);
// 闭运算分解
// 1. 膨胀原始图像
cv::Mat result;
cv::dilate(image, result, element5);
// 2. 腐蚀膨胀后的图像
cv::erode(result, result, element5);
cv::namedWindow("Closed Image (2)");
cv::imshow("Closed Image (2)", result);
// 闭-开
cv::morphologyEx(image, image, cv::MORPH_CLOSE, element5);
cv::morphologyEx(image, image, cv::MORPH_OPEN, element5);
cv::namedWindow("Closed|Opened Image");
cv::imshow("Closed|Opened Image", image);
cv::imwrite("binaryGroup.png", image);
// 开-闭
image = cv::imread("binary.png");
cv::morphologyEx(image, image, cv::MORPH_OPEN, element5);
cv::morphologyEx(image, image, cv::MORPH_CLOSE, element5);
cv::namedWindow("Opened|Closed Image");
cv::imshow("Opened|Closed Image",image);
// 读取输入图像
image = cv::imread("1.png", 0);
if (!image.data) return 0;
// 使用 3x3 结构元素获取梯度图像
cv::morphologyEx(image, result, cv::MORPH_GRADIENT, cv::Mat());
cv::namedWindow("Edge Image");
cv::imshow("Edge Image", 255 - result);
// 使用阈值获取二值图像
int threshold(80);
cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY);
cv::namedWindow("Thresholded Edge Image");
cv::imshow("Thresholded Edge Image", result);
// 使用 3x3 结构元素获取梯度图像
cv::morphologyEx(image, result, cv::MORPH_GRADIENT, cv::Mat());
// 读取输入图像
image = cv::imread("10.png", 0);
if (!image.data) return 0;
cv::transpose(image, image);
cv::flip(image, image, 0);
// 使用 7x7 结构元素应用黑色顶帽变换
cv::Mat element7(7, 7, CV_8U, cv::Scalar(1));
cv::morphologyEx(image, result, cv::MORPH_BLACKHAT, element7);
cv::namedWindow("7x7 Black Top-hat Image");
cv::imshow("7x7 Black Top-hat Image", 255-result);
cv::waitKey();
return 0;
}
主文件 (cornersDetection.cpp
) 完整代码如下所示:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "morphoFeatures.h"
int main() {
cv::Mat image = cv::imread("3.png");
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::namedWindow("Image");
cv::imshow("Image", gray);
MorphoFeatures morpho;
morpho.setThreshold(35);
// 角点检测
cv::Mat corners;
corners = morpho.getCorners(gray);
// 在图像中显示角点
morpho.drawOnImage(corners, gray);
cv::namedWindow("Corners on Image");
cv::imshow("Corners on Image", gray);
cv::waitKey();
return 0;
}
小结
形态学变换( Morphological transformations
)通常是在二值图像上执行、基于图像形状的操作。本节,首先介绍了基本形态学算子,腐蚀与膨胀,并介绍了根据基本算子组合得到的开运算与闭运算,最后,利用形态学算子实现了经典的边缘/角点检测图像处理应用。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解