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 够用
避坑: Canny 之前一定要做滤波。我曾经直接对原始图像跑 Canny,结果边缘图上全是纹理噪声,特征点提取时匹配率不到 20%。加上高斯滤波后,匹配率直接升到 70%。

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);
我的习惯: 先用暴力匹配做一次“基准测试”,如果匹配时间超过 30ms,再切换到 FLANN。另外,匹配后一定要做筛选(比如用 Lowe's ratio test),否则误匹配会很多。

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 本章知识体系

下面这张图总结了本章的核心流程。从原始图像到可匹配的特征点,每一步都环环相扣。

图像处理与特征提取流程 图像处理与特征提取流程 原始图像 图像滤波 边缘检测 (Canny) 特征提取 (SIFT/ORB) 特征匹配 (FLANN/BF) 图像分割 (阈值/聚类) 流程可并行:边缘检测与特征提取可同时进行,最终在匹配阶段汇合

核心要点: 图像处理不是孤立步骤。滤波的质量影响边缘检测,边缘检测的结果影响特征点提取,特征点的质量决定匹配的成败。我在实际部署中,经常需要回头调整滤波参数,才能让特征匹配达到预期。