4. 图像处理与特征提取:从像素到可匹配的“视觉路标”
图像处理与特征提取,说白了,就是让机器人看懂世界的第一步。摄像头采集到的原始图像,在计算机眼里只是一堆数字矩阵。我们要做的,就是把这些数字变成有意义的“路标”,供后续的定位和机械臂抓取使用。
我个人习惯把这一章称为“视觉前端”。它处理得好不好,直接决定了后面SLAM建图和机械臂控制的精度。嗯,这里要注意,很多新手容易忽略图像预处理,直接上特征提取,结果就是匹配得一塌糊涂。
4.1 图像滤波:去噪是第一步
传感器总有噪声,这是物理定律。我见过不少项目,因为没做好滤波,导致边缘检测出一堆假轮廓。滤波的目的很简单:保留有用信息,抑制无用噪声。
4.1.1 高斯滤波
高斯滤波是最常用的线性平滑滤波器。它用高斯核与图像做卷积,每个像素的灰度值由邻域内像素的加权平均决定,权重由高斯分布给出。离中心越近,权重越大。
我曾经在一个室内定位项目里,因为光照变化剧烈,直接用均值滤波把边缘都模糊掉了。换成高斯滤波后,效果好了很多。它的核心参数是核大小和标准差σ。核越大,图像越模糊;σ越大,平滑效果越强。
// C++ OpenCV 示例
cv::Mat src, dst;
cv::GaussianBlur(src, dst, cv::Size(5, 5), 1.5);
4.1.2 中值滤波
中值滤波是一种非线性滤波。它用邻域内所有像素的中值代替中心像素的值。对付椒盐噪声(图像上的黑白点),它比高斯滤波强得多。
你想想看,高斯滤波会把噪声点“抹开”,但中值滤波直接把它“剔除”了。我调试机械臂视觉抓取时,如果工件表面有反光斑点,中值滤波几乎是必用的。
4.2 边缘检测:Canny 算子
边缘是图像中灰度变化剧烈的地方。Canny 边缘检测是业界标杆,它做了三件事:降噪、找梯度、非极大值抑制加双阈值筛选。
我记得第一次用 Canny 时,调阈值调了半天。阈值设低了,全是假边缘;设高了,关键轮廓断了。后来我总结了一个经验:先用直方图看看图像的灰度分布,再定高低阈值。
cv::Mat edges;
cv::Canny(src, edges, 50, 150); // 低阈值50,高阈值150
| 参数 | 作用 | 我的建议 |
|---|---|---|
| 低阈值 | 低于此值的边缘被丢弃 | 设为高阈值的 1/3 到 1/2 |
| 高阈值 | 高于此值的边缘被保留 | 根据图像对比度动态调整 |
| Sobel 核大小 | 梯度计算的窗口 | 默认 3x3 够用 |
4.3 特征点提取:SIFT 与 ORB
特征点就是图像中的“关键地标”。一个好的特征点,应该在不同视角、光照下都能被找到。SIFT 和 ORB 是两种最常用的方法。
4.3.1 SIFT(尺度不变特征变换)
SIFT 是经典中的经典。它在尺度空间上检测极值点,并为每个点生成 128 维的描述子。尺度不变、旋转不变、光照鲁棒,这些都是它的优点。
但缺点也很明显:慢。我做过一个测试,一张 640x480 的图像,SIFT 提取 1000 个特征点需要 200ms 左右。在实时性要求高的机械臂控制中,这个速度有点吃力。
4.3.2 ORB(定向 FAST 和旋转 BRIEF)
ORB 是 SIFT 的轻量级替代。它用 FAST 角点检测 + BRIEF 描述子,并加入了方向信息。速度比 SIFT 快一个数量级,而且开源免费(SIFT 有专利问题)。
我个人在嵌入式平台(如 Jetson Nano)上做视觉定位时,几乎只用 ORB。它能在 10ms 内完成特征提取,足够满足 30fps 的实时要求。
| 特性 | SIFT | ORB |
|---|---|---|
| 尺度不变性 | 强 | 弱(需图像金字塔辅助) |
| 旋转不变性 | 强 | 中等 |
| 计算速度 | 慢 | 快(约 10 倍) |
| 专利限制 | 有 | 无 |
| 适用场景 | 高精度离线建图 | 实时定位与跟踪 |
4.4 特征匹配:FLANN 与暴力匹配
提取完特征点,下一步就是匹配。我们要在两幅图像中找到同一个物理点。匹配方法主要有两种:暴力匹配和 FLANN 匹配。
4.4.1 暴力匹配(Brute-Force)
暴力匹配就是“穷举法”。对图像 A 的每个特征点,计算它与图像 B 中所有特征点的距离,取最近的那个作为匹配点。简单、直接,但计算量大。
如果特征点数量少(比如少于 500 个),暴力匹配完全够用。我一般在调试阶段先用暴力匹配,看看匹配效果,再决定是否优化。
4.4.2 FLANN 匹配
FLANN(快速最近邻搜索库)用 KD-Tree 或 K-Means 树来加速搜索。当特征点数量超过 1000 时,FLANN 比暴力匹配快 10 倍以上。
但 FLANN 也有坑。它需要调整参数,比如树的棵数、搜索步数。我遇到过 FLANN 匹配结果不如暴力匹配的情况,原因是参数没调好。
// FLANN 匹配示例
cv::FlannBasedMatcher matcher;
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
// 暴力匹配示例
cv::BFMatcher matcher(cv::NORM_HAMMING);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
4.5 图像分割:阈值与聚类
图像分割是把图像分成若干有意义的区域。在机械臂抓取中,分割用来分离目标物体和背景。
4.5.1 阈值分割
阈值分割是最简单的方法。设定一个灰度阈值,大于阈值的设为白色,小于的设为黑色。适合光照均匀、目标与背景对比度高的场景。
我做过一个零件分拣项目,工件是黑色的,传送带是白色的。用 Otsu 自动阈值法,几乎零代码就完成了分割。但一旦光照变化,阈值就不灵了。
4.5.2 聚类分割(K-Means)
K-Means 把像素按颜色或空间位置聚类。它不依赖固定阈值,对光照变化有一定鲁棒性。但需要预先指定聚类数量 K。
举个例子,如果场景中有红色工件和蓝色背景,K=2 就能把它们分开。我曾在抓取实验中用 K-Means 分割多色工件,效果比阈值法稳定得多。
// K-Means 分割示例(OpenCV)
cv::Mat data, labels, centers;
data = src.reshape(1, src.total());
data.convertTo(data, CV_32F);
cv::kmeans(data, 2, labels,
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 10, 1.0),
3, cv::KMEANS_PP_CENTERS, centers);
4.6 本章知识体系
下面这张图总结了本章的核心流程。从原始图像到可匹配的特征点,每一步都环环相扣。
核心要点: 图像处理不是孤立步骤。滤波的质量影响边缘检测,边缘检测的结果影响特征点提取,特征点的质量决定匹配的成败。我在实际部署中,经常需要回头调整滤波参数,才能让特征匹配达到预期。