4. 特征提取与匹配
各位工程师,咱们继续往下走。上一章我们聊了相机模型和标定,算是给机器装上了“眼睛”。但光有眼睛不行,它得能从图像里认出有用的东西,还得能把这些东西在不同视角下对上号。这就是本章要讲的核心——特征提取与匹配。
说白了,特征就是图像里那些“与众不同”的点、线、面。我个人的理解是:特征提取就是帮机器找到图像里的“地标”,特征匹配就是让机器在不同图像里认出同一个“地标”。没有这一步,视觉SLAM和机械臂定位都无从谈起。
4.1 边缘检测:Canny算子
边缘是图像中灰度变化最剧烈的地方。你想想看,物体的轮廓、零件的边界,这些信息对机械臂抓取来说有多重要?Canny算子是我在项目中用得最多的边缘检测方法,没有之一。
Canny的核心思想其实就三步:去噪、找梯度、做非极大值抑制。嗯,这里要注意,它比别的算子多了一步“滞后阈值”处理,这一步很关键。
// C++ OpenCV 示例
cv::Mat src = cv::imread("part_image.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat edges;
cv::Canny(src, edges, 50, 150, 3); // 低阈值50,高阈值150,Sobel核大小3
cv::imshow("Edges", edges);
我曾经在一个PCB板检测项目里,因为光照不均匀,Canny死活检测不出焊盘边缘。后来我加了一步自适应直方图均衡化,问题就解决了。所以记住:Canny不是万能的,预处理往往比调参更重要。
4.2 角点检测:Harris与Shi-Tomasi
边缘是线,角点就是线跟线交叉的地方。角点包含的信息量更大,在视觉SLAM里,我们通常用角点来做特征点跟踪。
Harris角点检测的原理,说白了就是用一个窗口在图像上滑动,看窗口内灰度变化有多大。如果往哪个方向滑灰度都变化很大,那就是角点。Shi-Tomasi是Harris的改进版,它直接取两个特征值中较小的那个作为角点响应,效果更好。
| 方法 | 响应函数 | 特点 | 适用场景 |
|---|---|---|---|
| Harris | R = det(M) - k * trace(M)² | 对旋转、光照变化鲁棒 | 纹理丰富的场景 |
| Shi-Tomasi | R = min(λ₁, λ₂) | 角点响应更稳定,不易误检 | SLAM前端、目标跟踪 |
// Shi-Tomasi 角点检测示例
std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(gray_img, corners, 100, 0.01, 10);
// 参数:最大角点数100,质量等级0.01,最小距离10像素
4.3 特征描述子:SIFT与ORB
找到角点之后,怎么在不同图像里认出同一个角点?这就需要特征描述子了。描述子就是给每个特征点算一个“身份证号”,这个号码要足够独特,还要对旋转、缩放、光照变化不敏感。
SIFT(尺度不变特征变换)是经典中的经典。它用128维向量描述一个特征点,对尺度和旋转都有不变性。但缺点也很明显——慢。在产线上,如果每帧图像要处理几百个特征点,SIFT基本跑不到实时。
ORB(Oriented FAST and Rotated BRIEF)就是冲着实时性来的。它用FAST检测角点,用BRIEF做描述子,再加上方向信息。速度比SIFT快两个数量级,精度在大多数场景下也够用。
| 描述子 | 维度 | 旋转不变性 | 尺度不变性 | 速度 |
|---|---|---|---|---|
| SIFT | 128 | 是 | 是 | 慢 |
| ORB | 32(二进制) | 是 | 否(可近似) | 快 |
// ORB 特征提取示例
cv::Ptr<cv::ORB> orb = cv::ORB::create(500); // 提取500个特征点
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
orb->detectAndCompute(image, cv::noArray(), keypoints, descriptors);
4.4 特征匹配:FLANN与Brute-Force
特征点有了,描述子也有了,接下来就是匹配。匹配说白了就是找两幅图像里“最像”的特征点对。
Brute-Force(暴力匹配)是最简单的方法:拿第一张图的每个特征点,跟第二张图的所有特征点算一遍距离,取最近的那个。优点是精度高,缺点是慢。特征点一多,计算量就爆炸。
FLANN(快速近似最近邻)用了KD-Tree或LSH等数据结构来加速搜索。它不保证找到最近的点,但能在极短时间内找到“足够近”的点。在产线上,我几乎只用FLANN。
// FLANN 匹配示例
cv::FlannBasedMatcher matcher;
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
// 比率测试过滤
std::vector<cv::DMatch> good_matches;
for (size_t i = 0; i < matches.size(); i++) {
if (matches[i].distance < 0.75 * matches[i+1].distance) {
good_matches.push_back(matches[i]);
}
}
嗯,这里还要提一句。匹配完之后,通常还要用RANSAC或者PROSAC做一步几何验证,剔除那些不符合单应性矩阵或基础矩阵的误匹配点。这一步在视觉SLAM里是标配,千万别省。