OpenCV 工业视觉学习笔记(三):腐蚀、膨胀与形态学操作
前言
在上一篇文章中,学习了固定阈值、OTSU 自动阈值以及自适应阈值。
经过阈值分割后,通常会得到一张二值图像。但是二值化结果往往并不完美,可能存在以下问题:
- 目标内部存在小孔洞;
- 目标边缘不连续;
- 图像中存在孤立噪点;
- 相邻区域没有连接;
- 多个目标发生轻微粘连;
- 细小缺陷被断开;
- 目标轮廓不够平滑。
这时就需要使用形态学操作对二值区域进行处理。
形态学操作的核心思想是:
使用一个指定形状和大小的结构元素,在图像上进行滑动,并根据结构元素覆盖范围内的像素情况修改当前像素。
本篇主要学习以下内容:
- 什么是形态学操作;
- 什么是结构元素;
- 腐蚀操作;
- 膨胀操作;
- 开运算;
- 闭运算;
- 形态学梯度;
- 顶帽运算;
- 黑帽运算;
- 结构元素形状和尺寸的影响;
- 形态学操作在工业视觉中的使用方法。
一、什么是形态学操作
形态学操作最初是针对二值图像提出的一类图像处理方法。
在二值图中,通常认为:
- 白色像素,也就是非零像素,是前景或目标;
- 黑色像素,也就是零值像素,是背景。
形态学操作主要研究目标区域的形状变化,例如:
- 让白色区域变大;
- 让白色区域变小;
- 去除小白点;
- 填补小黑洞;
- 连接断开的区域;
- 分离轻微粘连的区域;
- 提取目标边界;
- 提取局部亮区域或暗区域。
需要特别注意:
腐蚀和膨胀描述的是白色前景区域的变化。
因此,在使用形态学操作前,最好先确定目标区域是白色还是黑色。
如果目标是黑色,而背景是白色,可以先通过反向二值化让目标变成白色:
threshold(
gray,
binary,
100,
255,
THRESH_BINARY_INV
);
二、什么是结构元素
形态学操作不是单独观察一个像素,而是使用一个小矩阵在图像上滑动。
这个小矩阵称为:
结构元素
英文为:
Structuring Element
也经常称为:
形态学核
例如,一个 3×3 的矩形结构元素可以表示为:
1 1 1
1 1 1
1 1 1
结构元素移动到某个像素位置后,会观察该像素周围 3×3 范围内的像素。
OpenCV 使用 getStructuringElement() 创建结构元素:
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(3, 3)
);
这里:
MORPH_RECT表示矩形结构元素;Size(3, 3)表示结构元素大小为3×3。
三、常见结构元素形状
OpenCV 提供了三种常见结构元素。
1. 矩形结构元素
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(5, 5)
);
对应:
MORPH_RECT
矩形结构元素中的所有位置都会参与计算。
它的作用范围比较完整,膨胀和腐蚀效果通常比较明显。
矩形核适合:
- 填补矩形缺口;
- 连接水平或垂直区域;
- 处理规则形状目标;
- 强度较大的形态学处理。
2. 十字形结构元素
Mat kernel = getStructuringElement(
MORPH_CROSS,
Size(5, 5)
);
对应:
MORPH_CROSS
一个 5×5 的十字形结构元素大致可以表示为:
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
十字形主要作用于水平和垂直方向,对斜方向的影响相对较小。
适合处理:
- 水平线;
- 垂直线;
- 十字形连接关系;
- 不希望过度扩张对角线方向的情况。
3. 椭圆形结构元素
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
对应:
MORPH_ELLIPSE
椭圆形结构元素更接近圆形。
它对目标边缘的处理通常比矩形核更加平滑,适合:
- 圆形目标;
- 孔洞;
- 焊盘;
- 颗粒;
- 圆点;
- 希望减少矩形棱角的区域。
四、打印结构元素
可以直接输出结构元素,观察不同形状之间的区别:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat rectKernel = getStructuringElement(
MORPH_RECT,
Size(5, 5)
);
Mat crossKernel = getStructuringElement(
MORPH_CROSS,
Size(5, 5)
);
Mat ellipseKernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
cout << "矩形核:" << endl;
cout << rectKernel << endl;
cout << "十字核:" << endl;
cout << crossKernel << endl;
cout << "椭圆核:" << endl;
cout << ellipseKernel << endl;
return 0;
}
输出结构元素有助于理解:
- 哪些位置会参与计算;
- 为什么不同形状会产生不同效果;
- 为什么矩形核对角线方向的影响更明显。
五、腐蚀操作
OpenCV 使用 erode() 进行腐蚀。
基本代码:
Mat eroded;
erode(
binary,
eroded,
kernel
);
腐蚀对白色前景区域的主要作用是:
使白色区域缩小。
腐蚀后可能出现以下效果:
- 白色区域边缘向内收缩;
- 细小白色噪点消失;
- 细线被削弱或断开;
- 相邻目标可能被分离;
- 小目标可能完全消失;
- 白色区域中的黑色孔洞变大。
六、如何理解腐蚀
对于二值图像,可以将腐蚀简单理解为:
结构元素覆盖范围内的像素必须全部满足条件,中心像素才能保留为白色。
以 3×3 矩形结构元素为例。
如果当前白色像素位于目标内部,并且周围都是白色,那么腐蚀后它仍然是白色。
如果当前像素位于目标边缘,周围包含黑色背景,那么腐蚀后它可能会变成黑色。
因此,白色目标的边缘会向内部收缩。
七、腐蚀完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread(
"D:/images/binary.png",
IMREAD_GRAYSCALE
);
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat binary;
threshold(
source,
binary,
127,
255,
THRESH_BINARY
);
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(3, 3)
);
Mat eroded;
erode(
binary,
eroded,
kernel
);
imshow("原始二值图", binary);
imshow("腐蚀结果", eroded);
waitKey(0);
destroyAllWindows();
return 0;
}
八、腐蚀的常见用途
1. 去除小白点
如果二值图中存在一些很小的白色噪点,腐蚀可以使这些小区域缩小甚至消失。
erode(
binary,
result,
kernel
);
但是腐蚀也会让正常目标缩小,因此不能只看噪点是否被去除,还需要观察目标尺寸是否受到过大影响。
2. 分离轻微粘连的目标
如果两个白色目标之间只有一条很窄的连接区域,腐蚀可能会将连接区域去掉,从而把两个目标分开。
例如:
目标A──细连接──目标B
腐蚀后可能变成:
目标A 目标B
但是,如果连接区域较粗,仅使用小尺寸腐蚀可能无法分开。
3. 缩小检测区域
有时为了排除目标边缘的不稳定区域,可以先腐蚀 ROI,只分析目标内部。
例如:
- 排除产品边缘反光;
- 排除焊盘边缘;
- 排除字符轮廓附近的干扰;
- 只检测中心区域缺陷。
九、膨胀操作
OpenCV 使用 dilate() 进行膨胀。
基本代码:
Mat dilated;
dilate(
binary,
dilated,
kernel
);
膨胀对白色前景区域的主要作用是:
使白色区域扩大。
膨胀后可能出现以下效果:
- 白色目标边缘向外扩张;
- 细小裂缝被连接;
- 断开的线段被连通;
- 白色区域中的小黑孔被缩小;
- 相邻目标可能粘连;
- 小白点会变大。
十、如何理解膨胀
对于二值图像,可以简单理解为:
只要结构元素覆盖范围内存在白色像素,中心位置就可能变成白色。
当结构元素移动到白色区域边缘时,原本位于目标外部的黑色像素也可能被设置为白色。
因此,白色区域会向外扩张。
十一、膨胀完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread(
"D:/images/binary.png",
IMREAD_GRAYSCALE
);
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat binary;
threshold(
source,
binary,
127,
255,
THRESH_BINARY
);
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(3, 3)
);
Mat dilated;
dilate(
binary,
dilated,
kernel
);
imshow("原始二值图", binary);
imshow("膨胀结果", dilated);
waitKey(0);
destroyAllWindows();
return 0;
}
十二、膨胀的常见用途
1. 连接断开的区域
如果缺陷区域或线条中存在较小间隙,可以通过膨胀将其连接。
例如,一条线段中间断开两个像素:
██████ ██████
经过适当膨胀后可能变成:
██████████████
2. 填补小孔洞
白色目标内部如果存在很小的黑色孔洞,膨胀会让周围白色区域扩张,从而缩小孔洞。
但是膨胀不能保证完全填满所有孔洞。
如果需要完整填充封闭区域中的孔洞,还可能需要:
- 闭运算;
- 漫水填充;
- 轮廓填充;
- 连通域处理。
3. 扩大检测区域
在工业检测中,有时需要将某个区域向外扩展一定范围。
例如:
- 焊盘区域向外扩展;
- 孔区域向外扩展;
- 缺陷附近设置屏蔽区;
- 对定位结果增加容差;
- 将断开的轮廓连接起来。
十三、腐蚀与膨胀的方向
对于白色前景区域:
腐蚀:白色区域缩小
膨胀:白色区域扩大
对于黑色背景区域,表现正好相反:
腐蚀白色区域,相当于扩大黑色区域
膨胀白色区域,相当于缩小黑色区域
因此,不能只记住“腐蚀就是缩小、膨胀就是扩大”,还需要明确缩小和扩大的是哪一种区域。
OpenCV 默认是对较亮区域,也就是局部最大值进行膨胀,对局部最小值进行腐蚀。
在常见二值图中,就表现为对白色区域进行扩张或收缩。
十四、腐蚀和膨胀的函数参数
erode() 和 dilate() 的参数形式基本一致。
以 erode() 为例:
erode(
src,
dst,
kernel,
anchor,
iterations,
borderType,
borderValue
);
常用参数如下。
1. src
输入图像。
可以是二值图,也可以是灰度图或彩色图。
不过最容易理解和最常用的情况是二值图。
2. dst
输出图像。
3. kernel
结构元素。
例如:
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
如果传入空矩阵:
Mat()
OpenCV 会使用默认的 3×3 矩形结构元素。
例如:
erode(
binary,
result,
Mat()
);
4. anchor
锚点表示结构元素的参考位置。
通常使用默认值:
Point(-1, -1)
表示锚点位于结构元素中心。
例如:
erode(
binary,
result,
kernel,
Point(-1, -1)
);
大部分项目不需要手动修改锚点。
5. iterations
表示重复执行次数。
例如:
erode(
binary,
result,
kernel,
Point(-1, -1),
2
);
表示连续腐蚀两次。
次数越多:
- 腐蚀越强;
- 膨胀越强;
- 对目标尺寸影响越大。
6. borderType
用于设置图像边界外部像素的处理方式。
通常可以使用默认值:
BORDER_CONSTANT
一般学习和常规工业处理时,不需要单独设置。
十五、结构元素尺寸的影响
结构元素尺寸越大,形态学处理效果通常越强。
例如:
Size(3, 3)
Size(5, 5)
Size(9, 9)
Size(21, 21)
产生的结果会明显不同。
小尺寸结构元素
例如:
Size(3, 3)
特点:
- 对图像修改较小;
- 能够保留更多细节;
- 适合去除极小噪点;
- 适合连接很小的断口;
- 不容易严重改变目标尺寸。
大尺寸结构元素
例如:
Size(15, 15)
特点:
- 处理效果明显;
- 可以去除较大噪声;
- 可以连接较大间隙;
- 容易让相邻目标粘连;
- 容易使小目标消失;
- 容易改变目标真实尺寸。
在工业测量中需要特别注意:
形态学操作会改变目标边界,因此不能在经过大幅腐蚀或膨胀的区域上直接进行高精度尺寸测量。
十六、重复次数与大尺寸结构元素是否相同
下面两种操作可能产生类似效果:
erode(
binary,
result,
smallKernel,
Point(-1, -1),
3
);
和:
erode(
binary,
result,
largeKernel
);
但是它们并不一定完全等价。
原因包括:
- 结构元素形状不同;
- 连续多次操作会逐步改变目标形状;
- 图像边缘和细节处理方式可能不同;
- 非矩形结构元素重复操作后的实际作用形状会发生变化。
因此,不应该简单认为:
3次3×3腐蚀 = 1次9×9腐蚀
实际效果需要通过图像验证。
十七、开运算
开运算的执行顺序是:
先腐蚀,再膨胀
OpenCV 中使用:
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
也可以手动写成:
Mat temp;
Mat opened;
erode(binary, temp, kernel);
dilate(temp, opened, kernel);
开运算的主要作用是:
- 去除小白点;
- 去除细小突出部分;
- 断开狭窄连接;
- 分离轻微粘连目标;
- 在一定程度上保持较大目标的整体尺寸。
十八、如何理解开运算
第一步腐蚀:
- 小白点被去除;
- 大目标缩小;
- 细连接可能被断开。
第二步膨胀:
- 剩余的大目标重新扩大;
- 已经被完全腐蚀掉的小噪点不会恢复;
- 已经断开的细连接可能不会重新连接。
因此,开运算可以理解为:
在尽量保留主要目标的同时,去除比结构元素更小的白色结构。
十九、开运算完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat binary = imread(
"D:/images/binary_noise.png",
IMREAD_GRAYSCALE
);
if (binary.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
Mat opened;
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
imshow("原图", binary);
imshow("开运算", opened);
waitKey(0);
destroyAllWindows();
return 0;
}
二十、闭运算
闭运算的执行顺序是:
先膨胀,再腐蚀
OpenCV 中使用:
morphologyEx(
binary,
closed,
MORPH_CLOSE,
kernel
);
也可以手动写成:
Mat temp;
Mat closed;
dilate(binary, temp, kernel);
erode(temp, closed, kernel);
闭运算的主要作用是:
- 填补小黑洞;
- 连接相邻白色区域;
- 补全断裂轮廓;
- 填补狭窄裂缝;
- 平滑目标内部的小缺口。
二十一、如何理解闭运算
第一步膨胀:
- 白色目标扩大;
- 小黑孔变小;
- 相邻区域可能连接;
- 小断口被填补。
第二步腐蚀:
- 扩大的目标重新收缩;
- 已经连接的区域可能保持连接;
- 已经填掉的小孔可能不会重新出现。
因此,闭运算可以理解为:
在尽量保持主要目标尺寸的同时,填补比结构元素更小的黑色间隙。
二十二、闭运算完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat binary = imread(
"D:/images/binary_hole.png",
IMREAD_GRAYSCALE
);
if (binary.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(7, 7)
);
Mat closed;
morphologyEx(
binary,
closed,
MORPH_CLOSE,
kernel
);
imshow("原图", binary);
imshow("闭运算", closed);
waitKey(0);
destroyAllWindows();
return 0;
}
二十三、开运算与闭运算的区别
可以简单记忆为:
开运算:先腐蚀后膨胀,主要去除小白点
闭运算:先膨胀后腐蚀,主要填补小黑洞
对比如下:
| 操作 | 执行顺序 | 主要作用 |
|---|---|---|
| 开运算 | 腐蚀后膨胀 | 去除小白点、断开细连接 |
| 闭运算 | 膨胀后腐蚀 | 填补小黑洞、连接小间隙 |
需要注意,白点和黑洞是相对于白色前景而言的。
如果目标区域是黑色,则需要重新考虑黑白方向,或者先将二值图反转。
二十四、先开后闭与先闭后开的区别
在实际项目中,有时会连续使用开运算和闭运算。
先开后闭
Mat opened;
Mat result;
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
morphologyEx(
opened,
result,
MORPH_CLOSE,
kernel
);
适合:
- 先去除白色噪点;
- 再填补目标内部小孔;
- 图像中同时存在小白点和小黑洞。
先闭后开
Mat closed;
Mat result;
morphologyEx(
binary,
closed,
MORPH_CLOSE,
kernel
);
morphologyEx(
closed,
result,
MORPH_OPEN,
kernel
);
适合:
- 先连接断开的目标;
- 再去除连接后残留的小突出部分;
- 优先保证目标连通性。
两种顺序的结果通常不同。
因此,不能把开运算和闭运算当成可以随意交换顺序的操作。
二十五、形态学梯度
形态学梯度可以理解为:
膨胀图 - 腐蚀图
OpenCV 使用:
morphologyEx(
binary,
gradient,
MORPH_GRADIENT,
kernel
);
它会提取目标边缘附近的区域。
对于二值图像:
- 膨胀图比原目标大;
- 腐蚀图比原目标小;
- 两者相减后,剩下的主要是目标边缘。
二十六、形态学梯度完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat binary = imread(
"D:/images/binary.png",
IMREAD_GRAYSCALE
);
if (binary.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat kernel = getStructuringElement(
MORPH_RECT,
Size(3, 3)
);
Mat gradient;
morphologyEx(
binary,
gradient,
MORPH_GRADIENT,
kernel
);
imshow("原始图像", binary);
imshow("形态学梯度", gradient);
waitKey(0);
destroyAllWindows();
return 0;
}
形态学梯度可以用于:
- 提取目标轮廓;
- 检测区域边界;
- 观察孔洞边缘;
- 获取字符外轮廓;
- 分析目标边缘宽度;
- 生成后续轮廓检测的输入图。
不过形态学梯度得到的边缘宽度与结构元素尺寸有关。
结构元素越大,边缘区域通常越宽。
二十七、顶帽运算
顶帽运算也称为白帽运算。
其计算方式可以理解为:
原图 - 开运算结果
OpenCV 使用:
morphologyEx(
gray,
topHat,
MORPH_TOPHAT,
kernel
);
开运算会去除比结构元素小的亮区域。
原图减去开运算结果后,保留下来的主要是:
比周围背景更亮,并且尺寸小于结构元素的区域。
因此,顶帽运算常用于提取:
- 局部亮点;
- 亮色小缺陷;
- 反光点;
- 亮颗粒;
- 亮线;
- 光照不均匀背景上的亮目标。
二十八、顶帽运算完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat gray = imread(
"D:/images/bright_defect.png",
IMREAD_GRAYSCALE
);
if (gray.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(21, 21)
);
Mat topHat;
morphologyEx(
gray,
topHat,
MORPH_TOPHAT,
kernel
);
Mat binary;
threshold(
topHat,
binary,
30,
255,
THRESH_BINARY
);
imshow("原始灰度图", gray);
imshow("顶帽结果", topHat);
imshow("亮缺陷二值图", binary);
waitKey(0);
destroyAllWindows();
return 0;
}
在这个流程中:
灰度图
↓
顶帽运算
↓
突出局部亮区域
↓
阈值分割
↓
得到亮缺陷区域
二十九、黑帽运算
黑帽运算的计算方式可以理解为:
闭运算结果 - 原图
OpenCV 使用:
morphologyEx(
gray,
blackHat,
MORPH_BLACKHAT,
kernel
);
闭运算会填补比结构元素小的暗区域。
闭运算结果减去原图后,保留下来的主要是:
比周围背景更暗,并且尺寸小于结构元素的区域。
因此,黑帽运算常用于提取:
- 局部黑点;
- 暗色缺陷;
- 小孔;
- 裂纹;
- 划痕;
- 暗线;
- 光照不均匀背景上的暗目标。
三十、黑帽运算完整示例
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat gray = imread(
"D:/images/dark_defect.png",
IMREAD_GRAYSCALE
);
if (gray.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(21, 21)
);
Mat blackHat;
morphologyEx(
gray,
blackHat,
MORPH_BLACKHAT,
kernel
);
Mat binary;
threshold(
blackHat,
binary,
30,
255,
THRESH_BINARY
);
imshow("原始灰度图", gray);
imshow("黑帽结果", blackHat);
imshow("暗缺陷二值图", binary);
waitKey(0);
destroyAllWindows();
return 0;
}
三十一、顶帽和黑帽中的结构元素尺寸
顶帽和黑帽对结构元素尺寸非常敏感。
假设需要检测的缺陷宽度约为 5 个像素。
可以尝试:
Size(11, 11)
Size(15, 15)
Size(21, 21)
结构元素通常要比目标尺寸更大,才能在开运算或闭运算时把目标作为局部小结构去除。
如果结构元素太小:
- 缺陷可能没有被去除;
- 顶帽或黑帽结果不明显;
- 只能提取缺陷中的一部分。
如果结构元素太大:
- 会带入较大范围的背景变化;
- 可能提取出产品整体结构;
- 计算量增加;
- 边缘区域可能产生较强响应。
因此,可以把结构元素尺寸理解为:
需要关注的局部尺度。
三十二、morphologyEx 函数
OpenCV 使用 morphologyEx() 完成多种组合形态学操作。
基本形式:
morphologyEx(
src,
dst,
operation,
kernel
);
常见操作类型包括:
MORPH_OPEN
MORPH_CLOSE
MORPH_GRADIENT
MORPH_TOPHAT
MORPH_BLACKHAT
对应关系如下:
| 操作类型 | 含义 |
|---|---|
MORPH_OPEN |
开运算 |
MORPH_CLOSE |
闭运算 |
MORPH_GRADIENT |
形态学梯度 |
MORPH_TOPHAT |
顶帽运算 |
MORPH_BLACKHAT |
黑帽运算 |
三十三、腐蚀和膨胀也可以处理灰度图
腐蚀和膨胀并不只能处理二值图像。
对于灰度图像:
灰度膨胀
膨胀会在结构元素范围内选择较大的灰度值。
可以简单理解为:
局部最大值滤波
效果通常表现为:
- 亮区域扩张;
- 暗区域缩小;
- 小暗点被压缩;
- 图像整体局部变亮。
灰度腐蚀
腐蚀会在结构元素范围内选择较小的灰度值。
可以简单理解为:
局部最小值滤波
效果通常表现为:
- 暗区域扩张;
- 亮区域缩小;
- 小亮点被压缩;
- 图像整体局部变暗。
示例:
Mat grayErode;
Mat grayDilate;
erode(
gray,
grayErode,
kernel
);
dilate(
gray,
grayDilate,
kernel
);
顶帽和黑帽正是基于灰度形态学操作实现的。
三十四、使用不同方向的结构元素
结构元素不一定必须是正方形。
例如,可以创建水平结构元素:
Mat horizontalKernel = getStructuringElement(
MORPH_RECT,
Size(15, 1)
);
也可以创建垂直结构元素:
Mat verticalKernel = getStructuringElement(
MORPH_RECT,
Size(1, 15)
);
水平结构元素
Size(15, 1)
主要作用于水平方向,可以用于:
- 连接水平断线;
- 提取水平线;
- 去除较短的垂直干扰;
- 修复水平裂缝。
垂直结构元素
Size(1, 15)
主要作用于垂直方向,可以用于:
- 连接垂直断线;
- 提取垂直线;
- 去除较短的水平干扰;
- 修复垂直裂缝。
这种具有方向性的结构元素在工业视觉中非常实用。
三十五、提取水平线示例
可以使用水平结构元素进行开运算,保留较长的水平结构。
Mat horizontalKernel = getStructuringElement(
MORPH_RECT,
Size(30, 1)
);
Mat horizontalLines;
morphologyEx(
binary,
horizontalLines,
MORPH_OPEN,
horizontalKernel
);
其原理是:
- 水平长线能够在水平腐蚀后保留下来;
- 较短区域或其他方向结构可能被去除;
- 再经过膨胀恢复水平线的宽度。
三十六、提取垂直线示例
Mat verticalKernel = getStructuringElement(
MORPH_RECT,
Size(1, 30)
);
Mat verticalLines;
morphologyEx(
binary,
verticalLines,
MORPH_OPEN,
verticalKernel
);
这种方法可以用于:
- 表格线提取;
- 网格检测;
- 水平和垂直边框提取;
- 工件规则线条分析;
- 字符区域预处理。
三十七、形态学操作的组合示例
假设阈值分割后:
- 存在较小白色噪点;
- 目标内部存在小黑孔;
- 目标边缘存在小间隙。
可以先进行开运算,再进行闭运算:
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
Mat opened;
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
Mat result;
morphologyEx(
opened,
result,
MORPH_CLOSE,
kernel
);
处理流程为:
原始二值图
↓
开运算去除小白点
↓
闭运算填补小孔和断口
↓
获得较完整目标区域
但是,每增加一次形态学操作,都会对目标边界产生一定影响。
因此,形态学操作并不是越多越好。
三十八、工业视觉中的参数选择思路
形态学参数不能只根据图像分辨率随意设置,而应该结合实际目标尺寸。
例如,一个噪点直径大约为 3 个像素,而正常缺陷最小直径为 10 个像素。
可以尝试使用:
Size(5, 5)
的开运算:
- 3 像素噪点可能被去除;
- 10 像素缺陷有机会保留下来。
如果直接使用:
Size(15, 15)
可能连正常缺陷也会被去除。
因此,结构元素尺寸应该根据以下因素确定:
- 噪点尺寸;
- 缺陷尺寸;
- 目标之间的距离;
- 断口宽度;
- 孔洞尺寸;
- 图像分辨率;
- 实际物理尺寸;
- 相机标定比例。
如果已知图像比例是:
每像素 0.02 mm
希望连接宽度小于:
0.1 mm
那么对应像素数量约为:
0.1 ÷ 0.02 = 5 像素
此时可以围绕 5 像素设计结构元素,而不是完全凭感觉选择参数。
三十九、一个暗缺陷检测流程示例
假设产品表面较亮,缺陷较暗,并且缺陷中存在断裂和噪点。
可以使用以下流程:
灰度化
↓
高斯滤波
↓
反向阈值分割
↓
开运算去除小白点
↓
闭运算连接缺陷
↓
连通域或轮廓分析
代码如下:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread(
"D:/images/defect.jpg"
);
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(
source,
gray,
COLOR_BGR2GRAY
);
Mat blurred;
GaussianBlur(
gray,
blurred,
Size(5, 5),
0
);
Mat binary;
threshold(
blurred,
binary,
0,
255,
THRESH_BINARY_INV | THRESH_OTSU
);
Mat openKernel = getStructuringElement(
MORPH_ELLIPSE,
Size(3, 3)
);
Mat opened;
morphologyEx(
binary,
opened,
MORPH_OPEN,
openKernel
);
Mat closeKernel = getStructuringElement(
MORPH_ELLIPSE,
Size(7, 7)
);
Mat result;
morphologyEx(
opened,
result,
MORPH_CLOSE,
closeKernel
);
imshow("原图", source);
imshow("灰度图", gray);
imshow("二值图", binary);
imshow("开运算", opened);
imshow("最终结果", result);
waitKey(0);
destroyAllWindows();
return 0;
}
这里分别使用了两个不同尺寸的结构元素:
- 较小的开运算核用于去除小噪点;
- 较大的闭运算核用于连接缺陷内部断口。
这比所有操作都使用同一个结构元素更加灵活。
四十、常见问题
1. 为什么腐蚀后目标反而变大了
通常是因为目标区域是黑色,而不是白色。
腐蚀会让白色区域缩小,因此黑色区域看起来会扩大。
可以使用:
bitwise_not(
binary,
binaryInverse
);
反转图像后再观察。
也可以在阈值分割时使用:
THRESH_BINARY_INV
让目标变成白色。
2. 为什么膨胀后两个目标粘在一起了
因为两个目标之间的距离小于膨胀的作用范围。
可以尝试:
- 减小结构元素尺寸;
- 减少膨胀次数;
- 改用十字形或方向性结构元素;
- 对不同区域分别处理;
- 使用腐蚀或距离变换进行分离。
3. 为什么腐蚀后小缺陷消失了
因为缺陷尺寸小于或接近结构元素尺寸。
腐蚀会持续缩小白色目标,小目标可能被完全去除。
可以尝试:
- 减小结构元素;
- 减少迭代次数;
- 改用开运算;
- 在形态学处理前先保存原始缺陷信息;
- 使用连通域面积筛选代替强腐蚀。
4. 为什么闭运算没有完全填满孔洞
闭运算只能填补相对于结构元素较小的孔洞和间隙。
如果孔洞比结构元素大,闭运算可能只能缩小孔洞,而不能完全填满。
可以:
- 增大结构元素;
- 增加迭代次数;
- 使用轮廓填充;
- 使用漫水填充;
- 使用专门的孔洞填充方法。
5. 为什么开运算后目标尺寸仍然发生了变化
开运算虽然会在腐蚀后再次膨胀,但不一定能完全恢复原始形状。
尤其是:
- 尖角;
- 细线;
- 狭窄突出区域;
- 小目标;
- 不规则轮廓。
这些结构在腐蚀阶段被去除后,膨胀无法恢复。
因此,开运算并不是无损操作。
6. 腐蚀和膨胀应该先用哪一个
取决于需要解决的问题:
- 去小白点:先腐蚀,也就是开运算;
- 填小黑洞:先膨胀,也就是闭运算;
- 连接断口:优先膨胀或闭运算;
- 分离粘连:优先腐蚀或开运算;
- 提取边缘:形态学梯度;
- 提取亮缺陷:顶帽;
- 提取暗缺陷:黑帽。
7. 结构元素应该使用矩形还是椭圆形
需要根据目标形状和处理方向选择。
矩形核:
- 作用更强;
- 对水平和垂直方向明显;
- 容易产生棱角;
- 适合规则区域。
椭圆核:
- 处理更加平滑;
- 更适合圆形目标;
- 对角线方向过渡更加自然;
- 常用于颗粒、孔洞和焊盘。
十字核:
- 主要作用于水平和垂直方向;
- 对对角线方向影响较小;
- 适合方向性连接。
8. 可以直接对彩色图像做腐蚀和膨胀吗
可以。
OpenCV 会分别处理各个通道。
但是彩色图像进行形态学处理后,颜色关系可能发生变化,结果不一定容易解释。
工业检测中更常见的方式是:
彩色图像
↓
选择合适颜色通道或转换颜色空间
↓
得到单通道图
↓
阈值分割
↓
形态学处理
四十一、完整对比代码
下面将腐蚀、膨胀、开运算、闭运算和形态学梯度放在同一个程序中进行对比。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat source = imread(
"D:/images/test.jpg"
);
if (source.empty())
{
cerr << "图像读取失败" << endl;
return -1;
}
Mat gray;
cvtColor(
source,
gray,
COLOR_BGR2GRAY
);
Mat binary;
threshold(
gray,
binary,
0,
255,
THRESH_BINARY_INV | THRESH_OTSU
);
Mat kernel = getStructuringElement(
MORPH_ELLIPSE,
Size(5, 5)
);
Mat eroded;
erode(
binary,
eroded,
kernel
);
Mat dilated;
dilate(
binary,
dilated,
kernel
);
Mat opened;
morphologyEx(
binary,
opened,
MORPH_OPEN,
kernel
);
Mat closed;
morphologyEx(
binary,
closed,
MORPH_CLOSE,
kernel
);
Mat gradient;
morphologyEx(
binary,
gradient,
MORPH_GRADIENT,
kernel
);
imshow("原始图像", source);
imshow("灰度图", gray);
imshow("二值图", binary);
imshow("腐蚀", eroded);
imshow("膨胀", dilated);
imshow("开运算", opened);
imshow("闭运算", closed);
imshow("形态学梯度", gradient);
waitKey(0);
destroyAllWindows();
return 0;
}
运行代码后,可以重点观察:
- 腐蚀后白色目标缩小了多少;
- 膨胀后白色目标扩大了多少;
- 开运算去除了哪些小区域;
- 闭运算连接了哪些断口;
- 形态学梯度提取出的边缘有多宽;
- 不同大小和形状的结构元素对结果有什么影响。
四十二、形态学操作在工业视觉中的使用原则
1. 先明确前景颜色
在进行操作前,要先明确目标是白色还是黑色。
最好统一让待分析目标为白色。
2. 结构元素尺寸要有实际依据
不要只尝试:
3×3、5×5、7×7
而不知道为什么。
应结合:
- 缺陷像素尺寸;
- 噪点尺寸;
- 目标间距;
- 孔洞尺寸;
- 标定比例。
3. 尽量减少不必要的形态学处理
每次腐蚀、膨胀、开运算和闭运算都会改变目标形状。
如果可以通过连通域面积筛选去除噪点,就不一定需要使用很强的腐蚀。
4. 不要使用处理后的区域直接做精密测量
经过腐蚀或膨胀后,目标边界已经发生变化。
形态学结果可以用于:
- 目标定位;
- 区域筛选;
- 噪点去除;
- 生成掩膜。
真正测量尺寸时,可以回到原始灰度图或原始二值图,在定位到的区域中重新提取精确边缘。
5. 应使用多张图像验证
同一组形态学参数需要在以下图像上进行测试:
- 正常产品;
- 不同大小缺陷;
- 不同位置缺陷;
- 临界缺陷;
- 不同曝光;
- 不同批次;
- 边缘区域;
- 轻微位置偏差。
在单张图像上效果很好,并不代表可以直接用于生产环境。
四十三、本篇总结
本篇主要学习了 OpenCV 中的腐蚀、膨胀和常见形态学操作。
需要重点掌握以下内容:
- 形态学操作通常以白色区域作为前景;
- 腐蚀会使白色区域缩小;
- 膨胀会使白色区域扩大;
- 开运算是先腐蚀后膨胀;
- 开运算常用于去除小白点和细小突出区域;
- 闭运算是先膨胀后腐蚀;
- 闭运算常用于填补小黑洞和连接小间隙;
- 形态学梯度可以提取目标边缘;
- 顶帽运算适合提取局部亮目标;
- 黑帽运算适合提取局部暗目标;
- 结构元素可以是矩形、十字形或椭圆形;
- 结构元素的大小决定了处理的空间尺度;
- 水平和垂直结构元素可以提取特定方向的线条;
- 形态学操作会改变目标边界;
- 工业检测中需要根据目标实际尺寸选择参数;
- 形态学处理结果更适合作为掩膜或定位依据,不一定适合直接进行精密测量。
形态学操作本身并不复杂,但真正困难的是:
根据实际目标、噪声、孔洞和间隙的尺寸,选择合适的结构元素形状和大小。
浙公网安备 33010602011771号