3. 缺陷检测算法基础

做工业视觉这几年,我越来越觉得:算法不在多,在于用得对。很多刚入行的朋友喜欢追新模型,但真正产线上跑得稳的,往往是那些最基础的算法。今天咱们就把边缘检测、阈值分割和形态学操作这三板斧彻底聊透。

核心观点: 缺陷检测的底层逻辑就三步——找边界、分区域、修形状。边缘检测找缺陷的轮廓,阈值分割把缺陷区域抠出来,形态学操作把抠出来的区域修干净。这三步走顺了,80%的缺陷场景都能搞定。
缺陷检测算法基础 · 知识体系 边缘检测 阈值分割 形态学操作 Sobel 算子 Canny 边缘检测 双阈值 + 滞后追踪 OTSU 大津法 自适应阈值 局部窗口 + 均值/高斯 膨胀 / 腐蚀 开运算 / 闭运算 结构元素选择策略 产线缺陷检测流水线:预处理 → 检测 → 后处理 💡 组合使用效果更佳 边缘+阈值+形态学

一、边缘检测:找到缺陷的轮廓

边缘检测说白了就是找图像里灰度变化剧烈的地方。缺陷的边缘往往就是灰度突变的位置,比如划痕、裂纹、脏污的边界。

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 次,才把这个问题解决。记住:结构元素的大小不能超过你关心的最小缺陷尺寸。

四、实战组合:一个完整的缺陷检测流程

光讲理论没意思,咱们看一个真实案例。假设要检测手机屏幕上的划痕:

  1. 预处理: 高斯滤波去噪,核大小 5×5
  2. 边缘检测: Canny 提取划痕边缘,阈值 30:90
  3. 阈值分割: 自适应阈值(块大小 15,C=3)得到二值图
  4. 形态学后处理: 开运算(3×3 矩形核)去除噪点,闭运算(3×3 矩形核)连接断裂划痕
  5. 缺陷判定: 计算连通域面积,面积大于 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% 的缺陷检测场景都能用这三板斧解决。关键在于理解每个算法的适用条件,以及参数调优的工程经验。别小看这些基础算法,产线上跑得稳的,往往就是这些「老家伙」。