4、图像预处理(下):形态学操作、边缘检测与二值化

好,咱们接着聊图像预处理。上一节我们讲了滤波和直方图,这一节的内容更“暴力”一些——形态学操作、边缘检测和二值化。说实话,这三个东西在工业现场几乎是天天见。我刚开始做视觉项目那会儿,总觉得形态学就是“腐蚀膨胀随便玩玩”,后来吃了不少亏才明白,这里面的门道深着呢。

4.1 形态学操作:腐蚀、膨胀、开闭运算

形态学操作,说白了就是“用一个小结构元素去图像里滚一圈”。这个小结构元素可以是3x3的方形,也可以是十字形、圆形。你想想看,它就像一把刷子,在二值图像上刷来刷去。

4.1.1 腐蚀与膨胀

腐蚀:让白色区域“瘦一圈”。
膨胀:让白色区域“胖一圈”。

我在项目中遇到过这样一个场景:检测电路板上的焊点,结果图像里有一些细小的噪点。腐蚀一下,噪点就没了。但代价是焊点也变小了。嗯,这里要注意——腐蚀和膨胀是一对“冤家”,用的时候得权衡。

核心公式(简单理解)

  • 腐蚀:结构元素内所有像素都是白色,中心才保留白色。
  • 膨胀:结构元素内只要有一个白色,中心就变成白色。
// C++ 示例:腐蚀与膨胀
cv::Mat src = cv::imread("binary.png", cv::IMREAD_GRAYSCALE);
cv::Mat eroded, dilated;

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));

cv::erode(src, eroded, kernel);
cv::dilate(src, dilated, kernel);

cv::imshow("Eroded", eroded);
cv::imshow("Dilated", dilated);
cv::waitKey(0);

我的小技巧:工业现场如果图像噪声是“白点”(白色背景上的黑点),先腐蚀再膨胀。如果是“黑点”(黑色背景上的白点),先膨胀再腐蚀。这个顺序搞反了,效果会差很多。

4.1.2 开运算与闭运算

开运算 = 先腐蚀后膨胀。闭运算 = 先膨胀后腐蚀。

为什么要搞这两个?因为单独用腐蚀或膨胀,图像的整体亮度会变。开运算和闭运算能保持整体亮度不变,只改变细节。

  • 开运算:消除小白点(噪声),断开细小的连接。
  • 闭运算:填充小黑洞,连接断开的区域。

我曾经在一个螺丝检测项目里,螺丝表面有反光造成的“黑洞”。闭运算一上去,黑洞填平了,螺丝轮廓完整了。但要注意——闭运算做多了,会把两个相邻的螺丝连在一起,那就麻烦了。

// Python 示例:开闭运算
import cv2
import numpy as np

img = cv2.imread('screw.png', 0)
kernel = np.ones((5,5), np.uint8)

opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

cv2.imshow('Opening', opening)
cv2.imshow('Closing', closing)

避坑指南:我曾经在一条产线上用开运算去噪,结果把产品上的关键特征也给去掉了。后来才发现——结构元素选太大了。记住:结构元素的大小,不要超过你要保留的最小特征尺寸。

4.2 边缘检测:Canny 与 Sobel

边缘检测,说白了就是找图像中“亮度变化剧烈”的地方。工业视觉里,边缘就是产品的轮廓、缺陷的边界。没有边缘检测,很多后续算法根本没法玩。

4.2.1 Sobel 算子

Sobel 算子是边缘检测的“老前辈”。它用两个3x3的核,一个检测水平边缘,一个检测垂直边缘。

我个人习惯在需要“方向性”边缘的时候用 Sobel。比如检测 PCB 板上的直线走线,Sobel 能告诉你边缘是水平还是垂直的。

// C++ 示例:Sobel 边缘检测
cv::Mat src = cv::imread("pcb.jpg", cv::IMREAD_GRAYSCALE);
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);

cv::imshow("Sobel", grad);

注意:Sobel 对噪声比较敏感。如果图像本身有噪声,建议先做一次高斯滤波。我一般会这样组合:高斯滤波 → Sobel → 阈值处理。

4.2.2 Canny 边缘检测

Canny 是边缘检测里的“王者”。它比 Sobel 强在哪?强在“双阈值”和“非极大值抑制”。

简单说:Canny 会先找出所有可能的边缘,然后用两个阈值筛选——高阈值保留强边缘,低阈值保留弱边缘,但只有与强边缘相连的弱边缘才被保留。

我记得有一次做手机屏幕的划痕检测,Sobel 出来的结果全是噪点,Canny 一上,划痕清清楚楚。为什么?因为 Canny 能抑制那些孤立的噪声点。

# Python 示例:Canny 边缘检测
import cv2

img = cv2.imread('scratch.jpg', 0)
edges = cv2.Canny(img, 50, 150)  # 低阈值50,高阈值150

cv2.imshow('Canny', edges)
cv2.waitKey(0)

参数调优经验

  • 低阈值:一般设为高阈值的 1/2 到 1/3。
  • 高阈值:根据图像对比度来调。对比度高,高阈值可以设大一点。
  • 我常用的套路:先用 50/150 试,如果边缘太多(噪声多),提高高阈值;如果边缘太少(漏检),降低低阈值。

避坑指南:我曾经在一个项目中,Canny 死活检测不到一条很明显的边缘。后来发现——那条边缘的灰度变化是“缓慢渐变”的,不是“突变”。Canny 对这种渐变边缘不敏感。解决办法:先做一次直方图均衡化,增强对比度。

4.3 图像二值化

二值化,就是把图像变成“黑白分明”的样子。工业视觉里,二值化是形态学操作和边缘检测的“前置条件”。

4.3.1 全局阈值二值化

最简单的方法:设定一个阈值 T,大于 T 的变成白色,小于 T 的变成黑色。

// C++ 示例:全局阈值二值化
cv::Mat src = cv::imread("part.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat binary;

cv::threshold(src, binary, 128, 255, cv::THRESH_BINARY);

cv::imshow("Binary", binary);

但问题来了——如果光照不均匀,全局阈值就废了。比如产品左边亮右边暗,同一个阈值不可能同时适用。

4.3.2 自适应阈值二值化

自适应阈值,说白了就是“每个像素用自己周围的局部区域算一个阈值”。这样光照不均匀的问题就解决了。

我个人非常推荐在工业现场用自适应阈值。尤其是那些反光、阴影多的场景。

# Python 示例:自适应阈值二值化
import cv2

img = cv2.imread('uneven_light.jpg', 0)
binary = cv2.adaptiveThreshold(img, 255, 
                               cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                               cv2.THRESH_BINARY, 11, 2)

cv2.imshow('Adaptive Binary', binary)
cv2.waitKey(0)

参数说明

  • blockSize:局部区域的大小,必须是奇数。一般 11 或 15 起步。
  • C:从局部均值中减去的常数。C 越大,二值化结果越“黑”。

4.3.3 Otsu 二值化

Otsu 是一种自动寻找最佳阈值的方法。它假设图像直方图是“双峰”的——一个峰是背景,一个峰是前景。Otsu 会找到两个峰之间的“谷底”作为阈值。

我遇到过很多次这种情况:产品背景和前景的灰度分布很清晰,但手动调阈值太麻烦。Otsu 一上,自动搞定。

// C++ 示例:Otsu 二值化
cv::Mat src = cv::imread("bimodal.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat binary;

cv::threshold(src, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

cv::imshow("Otsu Binary", binary);

避坑指南:Otsu 不是万能的。如果直方图不是双峰(比如三个峰或者单峰),Otsu 的结果会很糟糕。我曾经在一个零件检测项目里用 Otsu,结果把零件和背景完全搞反了。后来一看直方图——三个峰,Otsu 根本找不到正确的谷底。

4.4 实战组合拳

在实际项目中,这几个操作往往是组合使用的。我分享一个常用的流程:

  1. 灰度化:彩色图转灰度。
  2. 高斯滤波:去噪。
  3. 自适应二值化:得到干净的二值图。
  4. 开运算:去掉小白点噪声。
  5. Canny 边缘检测:提取轮廓。

这个流程我用了很多年,在大多数工业场景下都管用。当然,具体参数要根据你的产品来调。没有万能参数,只有不断试错。

最后说一句:图像预处理不是“一步到位”的事。我一般会先跑一遍流程,看看效果,然后回头调参数。调两三次,基本就能稳定了。别指望一次成功,那是不可能的。

好,这一节就到这里。下一节我们讲特征提取——从图像里“挖”出有用的信息。到时候见。