缺陷检测算法基础:边缘检测、阈值分割与形态学操作
做工业视觉这几年,我越来越觉得:算法不在多,在于用得对。很多刚入行的朋友喜欢追新模型,但真正产线上跑得稳的,往往是那些最基础的算法。今天咱们就把边缘检测、阈值分割和形态学操作这三板斧彻底聊透。
核心观点: 缺陷检测的底层逻辑就三步——找边界、分区域、修形状。边缘检测找缺陷的轮廓,阈值分割把缺陷区域抠出来,形态学操作把抠出来的区域修干净。这三步走顺了,80%的缺陷场景都能搞定。
一、边缘检测:找到缺陷的轮廓
边缘检测说白了就是找图像里灰度变化剧烈的地方。缺陷的边缘往往就是灰度突变的位置,比如划痕、裂纹、脏污的边界。
1.1 Sobel 算子
Sobel 是最基础的边缘检测算子。它用两个 3×3 的卷积核分别计算水平和垂直方向的梯度。
// Sobel 核心算子
// 水平方向 Gx 垂直方向 Gy
[-1, 0, 1] [-1, -2, -1]
[-2, 0, 2] [ 0, 0, 0]
[-1, 0, 1] [ 1, 2, 1]
// OpenCV 调用示例
cv::Mat grad_x, grad_y, grad;
cv::Sobel(src, grad_x, CV_16S, 1, 0, 3); // 水平梯度
cv::Sobel(src, grad_y, CV_16S, 0, 1, 3); // 垂直梯度
cv::convertScaleAbs(grad_x, grad_x);
cv::convertScaleAbs(grad_y, grad_y);
cv::addWeighted(grad_x, 0.5, grad_y, 0.5, 0, grad);
我的经验: Sobel 对噪声敏感,产线上如果图像 SNR 较低,建议先做一次高斯滤波。我遇到过一条产线,直接用 Sobel 检测出来的全是噪点,加了个 3×3 高斯核之后,缺陷边缘一下就清晰了。
1.2 Canny 边缘检测
Canny 是目前工业界用得最多的边缘检测算法。它比 Sobel 多做了两步:非极大值抑制和双阈值滞后追踪。这两步很关键——非极大值抑制让边缘变细,双阈值解决边缘断裂问题。
// Canny 标准流程
cv::Mat edges;
cv::Canny(src, edges, 50, 150, 3); // low_threshold=50, high_threshold=150
// 参数调优建议
// low_threshold : high_threshold = 1:2 到 1:3 之间
// 核大小一般用 3,图像噪声大时用 5
避坑指南: 我曾经在一条金属表面检测线上,Canny 的低阈值设得太低,结果把纹理都当成边缘了。后来我改成先算梯度直方图,取梯度幅值的前 30% 作为高阈值参考,效果才稳定下来。记住:阈值不是拍脑袋定的,要看梯度分布。
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Sobel | 计算快,方向信息完整 | 边缘粗,噪声敏感 | 实时性要求高、图像质量好的场景 |
| Canny | 边缘细且连续,抗噪好 | 计算量大,参数多 | 精密缺陷检测、需要完整轮廓的场景 |
二、阈值分割:把缺陷区域抠出来
阈值分割的目标是把目标和背景分开。工业场景里,缺陷区域通常比背景暗或亮,选对阈值方法就成功了一半。
2.1 OTSU 大津法
OTSU 自动计算最佳阈值,原理是让前景和背景的类间方差最大。说白了,它帮你找到那个「分得最开」的灰度值。
// OTSU 自动阈值
double otsu_thresh = cv::threshold(src, binary, 0, 255,
cv::THRESH_BINARY | cv::THRESH_OTSU);
// 返回的 otsu_thresh 就是算法算出来的最佳阈值
注意: OTSU 假设图像直方图是双峰分布。如果缺陷面积太小(比如只占全图的 1% 以下),OTSU 会失效——因为它会把大背景当成一个峰,小缺陷被忽略。这种情况我建议用自适应阈值。
2.2 自适应阈值
自适应阈值针对光照不均的场景。它把图像分成小块,每块单独算阈值。产线上打光不均匀是常态,自适应阈值比全局阈值靠谱得多。
// 自适应阈值
cv::adaptiveThreshold(src, binary, 255,
cv::ADAPTIVE_THRESH_MEAN_C, // 均值法
cv::THRESH_BINARY,
11, // 块大小,必须为奇数
2); // 常数 C,从均值中减去的值
// 另一种:高斯加权
cv::adaptiveThreshold(src, binary, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C, // 高斯加权
cv::THRESH_BINARY,
11, 2);
参数调优口诀: 块大小要能覆盖缺陷尺寸的 2-3 倍;常数 C 控制分割的敏感度,C 越小越容易分出细节,但也容易引入噪声。我一般从 C=2 开始试,上下调 1-2 个值就能找到最佳点。
三、形态学操作:修整分割结果
阈值分割出来的二值图往往有毛刺、空洞、孤立噪点。形态学操作就是用来修这些问题的。你想想看,产线上如果直接拿有毛刺的二值图去算缺陷面积,那结果肯定不准。
3.1 膨胀与腐蚀
膨胀让白色区域变大,腐蚀让白色区域变小。这两个操作是形态学的基础。
// 定义结构元素
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 膨胀
cv::dilate(binary, dilated, kernel);
// 腐蚀
cv::erode(binary, eroded, kernel);
| 操作 | 效果 | 工业用途 |
|---|---|---|
| 膨胀 | 扩大前景区域,填充小空洞 | 连接断裂的缺陷区域 |
| 腐蚀 | 缩小前景区域,去除小噪点 | 消除孤立噪点、分离粘连目标 |
3.2 开运算与闭运算
开运算 = 先腐蚀后膨胀,用来去除小噪点。闭运算 = 先膨胀后腐蚀,用来填充小空洞。这两个组合操作比单独用膨胀腐蚀更稳定。
// 开运算:去除小噪点
cv::morphologyEx(binary, opened, cv::MORPH_OPEN, kernel);
// 闭运算:填充小空洞
cv::morphologyEx(binary, closed, cv::MORPH_CLOSE, kernel);
我曾经踩过的坑: 有一次做 PCB 焊点检测,闭运算把两个相邻焊点之间的微小间隙也给填上了,导致短路缺陷漏检。后来我把结构元素从 5×5 改成 3×3,迭代次数从 2 次改成 1 次,才把这个问题解决。记住:结构元素的大小不能超过你关心的最小缺陷尺寸。
四、实战组合:一个完整的缺陷检测流程
光讲理论没意思,咱们看一个真实案例。假设要检测手机屏幕上的划痕:
- 预处理: 高斯滤波去噪,核大小 5×5
- 边缘检测: Canny 提取划痕边缘,阈值 30:90
- 阈值分割: 自适应阈值(块大小 15,C=3)得到二值图
- 形态学后处理: 开运算(3×3 矩形核)去除噪点,闭运算(3×3 矩形核)连接断裂划痕
- 缺陷判定: 计算连通域面积,面积大于 50 像素的标记为缺陷
// 完整流程代码片段
cv::Mat gray, blurred, edges, binary, cleaned;
// 1. 灰度化 + 高斯滤波
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
// 2. Canny 边缘检测
cv::Canny(blurred, edges, 30, 90, 3);
// 3. 自适应阈值
cv::adaptiveThreshold(blurred, binary, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY, 15, 3);
// 4. 形态学操作
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(binary, cleaned, cv::MORPH_OPEN, kernel);
cv::morphologyEx(cleaned, cleaned, cv::MORPH_CLOSE, kernel);
// 5. 连通域分析
std::vector<std::vector<cv::Point>> contours;
cv::findContours(cleaned, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
for (auto& c : contours) {
if (cv::contourArea(c) > 50) {
// 标记为缺陷
}
}
总结一下: 边缘检测、阈值分割、形态学操作这三块,是工业视觉缺陷检测的基石。我做了这么多年项目,90% 的缺陷检测场景都能用这三板斧解决。关键在于理解每个算法的适用条件,以及参数调优的工程经验。别小看这些基础算法,产线上跑得稳的,往往就是这些「老家伙」。