4. 图像处理基础:图像滤波与增强、边缘检测、轮廓提取、点云预处理
各位同学,欢迎来到第四讲。说实话,这一章的内容是视觉引导抓取的“地基”。你想想看,如果相机拍回来的图像全是噪点,或者边缘模糊得像毛玻璃,那后面的识别和抓取根本无从谈起。我个人习惯把这一章叫做“视觉清洗工坊”——我们要把原始数据里乱七八糟的东西过滤掉,只留下最干净、最有用的信息。
4.1 图像滤波与增强
先聊聊滤波。为什么要滤波?说白了,就是去噪。工业相机在产线上拍到的图像,经常会有传感器噪声、光照不均、甚至灰尘带来的伪影。我最早做项目时,就吃过这个亏——一个螺丝钉的轮廓被噪点淹没了,结果机器人抓了个空。
常用的滤波方法,我列个表给你看:
| 滤波类型 | 核心思想 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 均值滤波 | 邻域像素取平均 | 高斯噪声,速度要求高 | 边缘会变模糊,慎用 |
| 高斯滤波 | 加权平均,中心权重高 | 通用去噪,保留边缘 | 核大小选不对,要么没效果要么太糊 |
| 中值滤波 | 取邻域中值 | 椒盐噪声,极佳 | 计算慢,实时性差 |
| 双边滤波 | 考虑空间+颜色差异 | 保边去噪,深度图预处理 | 参数调起来很头疼 |
嗯,这里要注意:滤波不是越强越好。我在项目中遇到过一位同事,为了去噪把高斯核设到15x15,结果图像直接变成“油画风格”,边缘全没了。后来抓取失败率飙升到30%。所以我的建议是:从3x3开始试,逐步增大,直到噪声刚好消失为止。
图像增强这块,我常用的是直方图均衡化。特别是当光照不均匀时,比如工件一半亮一半暗,直接做阈值分割会死得很惨。直方图均衡化能把对比度拉开,让暗部细节露出来。代码很简单:
import cv2
img = cv2.imread('part.jpg', 0)
equalized = cv2.equalizeHist(img)
但注意,不要对彩色图直接做全局均衡化,否则颜色会失真。我一般先转成HSV,只对V通道做均衡,再合并回去。
4.2 边缘检测
边缘检测,是视觉引导抓取的核心中的核心。你想想看,机器人要抓一个工件,首先得知道它的轮廓在哪。Canny算子是我最常用的,没有之一。
Canny的流程其实不复杂:
- 高斯滤波去噪
- 计算梯度幅值和方向
- 非极大值抑制(把不是边缘的像素干掉)
- 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)
这里有个关键点:双阈值的设定。我刚开始做的时候,总是用固定阈值,结果换一个工件就崩了。后来我学乖了,用自适应阈值——比如取梯度幅值的均值作为低阈值,高阈值设为低阈值的2-3倍。这样鲁棒性好很多。
举个例子,抓取手机中框时,边缘非常细,如果阈值设高了,边缘会断掉;设低了,又会出现一堆假边缘。我曾经花了一整个下午调参数,最后发现用Otsu自动阈值法效果最好。
import cv2
edges = cv2.Canny(img, threshold1=50, threshold2=150)
# 实际项目中,我会用 Otsu 自动计算阈值
ret, thresh = cv2.threshold(grad, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
edges = cv2.Canny(img, thresh * 0.5, thresh)
4.3 轮廓提取
有了边缘,下一步就是提取轮廓。OpenCV里用findContours函数,但这里有个坑——版本差异。OpenCV 3和4的返回值不一样,我当年升级版本时,代码直接报错,排查了半天才发现是返回值顺序变了。
现在的标准写法是:
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
提取轮廓后,我一般会做几件事:
- 过滤小轮廓:面积小于某个阈值的直接扔掉,那些都是噪声
- 多边形逼近:用
approxPolyDP把轮廓简化,方便后续匹配 - 计算矩:用
moments求质心,这个就是抓取点
我记得有一次做圆形工件的抓取,轮廓提取出来总是有毛刺,导致质心偏移。后来我用cv2.minEnclosingCircle拟合最小外接圆,效果立竿见影。说白了,不要直接用原始轮廓的质心,用拟合后的几何中心更稳。
4.4 点云预处理
好了,聊完2D图像,咱们进入3D点云。点云预处理,说白了就是给深度相机拍到的点云“洗个澡”。原始点云数据量巨大,而且充满了离群点和噪声,直接拿去用,算法会崩溃的。
我常用的预处理流程:
- 体素降采样:把点云空间划分成小立方体(体素),每个体素内只保留一个点。这样能把几十万个点压缩到几万个,速度提升10倍以上。
- 统计滤波:计算每个点邻域内的平均距离,如果距离远大于平均值,就认为是离群点,直接删除。
- 半径滤波:如果一个点周围指定半径内没有足够多的邻居,也删掉。
这里我分享一个经验:降采样的体素大小怎么选? 如果工件尺寸是10cm,体素设1mm就太细了,设1cm又太粗。我一般取工件最小特征尺寸的1/5到1/10。比如抓取一个直径5cm的圆盘,体素设5mm左右。
代码示例(PCL库):
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setLeafSize(0.005f, 0.005f, 0.005f); // 5mm体素
sor.filter(*cloud_filtered);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor2;
sor2.setInputCloud(cloud_filtered);
sor2.setMeanK(50);
sor2.setStddevMulThresh(1.0);
sor2.filter(*cloud_clean);
嗯,说到点云,我还想提一个容易被忽略的点——深度图转点云时的坐标系。很多初学者直接拿深度图转,结果点云是反的或者歪的。我建议转之前先标定相机内参,确保每个像素的深度值对应正确的3D坐标。这个坑我踩过两次,一次是抓取时机器人往反方向走,另一次是点云和实际工件对不上。
最后,我画了一张图,把这一章的知识体系串起来:
这张图很直观:原始数据进来后,分两条路走。2D这边做滤波增强、边缘检测、轮廓提取;3D这边做降采样和滤波。两条路最终汇合到抓取特征提取。我个人习惯是先做2D处理,因为2D速度快,能快速定位工件的大致位置,然后再用3D点云做精细的位姿估计。
好了,这一章的内容就这些。记住,预处理做得好,后面识别抓取就成功了一半。别嫌这些基础操作麻烦,我见过太多项目因为预处理没做好,导致整个系统崩溃的例子。稳扎稳打,才是工程师该有的态度。