4. 图像处理基础:图像滤波、边缘检测、特征提取(SIFT、SURF、ORB)

各位同学,欢迎来到第四章。说实话,这一章的内容,是咱们整个感知算法的“地基”。你想想看,摄像头采集回来的原始图像,就像一块没切过的牛排——信息量很大,但直接吃可不行。我们需要先处理一下,才能让算法“消化”掉。

我个人习惯把图像处理分成三步走:去噪 → 找轮廓 → 抓特征。今天我们就按这个顺序,把滤波、边缘检测和特征提取这三板斧讲透。

4.1 图像滤波:给图像“磨个皮”

为什么需要滤波?因为传感器有噪声。我在项目中遇到过,一个晴朗的下午,摄像头拍出来的图像上全是椒盐噪声,像撒了一把盐。如果不处理,后面的边缘检测会把这些噪点当成“假边缘”,结果就是一团糟。

常用的滤波方法,我列个表给你看:

滤波方法 核心思想 适用场景
均值滤波 用邻域像素的平均值代替中心像素 简单平滑,但会模糊边缘
高斯滤波 按高斯权重加权平均,中心权重最大 去除高斯噪声,保留边缘信息
中值滤波 用邻域像素的中位数代替中心像素 去除椒盐噪声,效果极佳
双边滤波 同时考虑空间距离和像素值差异 保边去噪,但计算量大

嗯,这里要注意:没有万能的滤波器。我刚开始做项目时,总想找一个“最优”的滤波参数,后来发现这是徒劳的。你得根据实际场景来调。比如在高速公路上,高斯滤波就够用了;但在城市道路,行人多、光线变化大,我建议用双边滤波。

我的小技巧: 调试滤波参数时,先看滤波后的图像是否还保留着车道线的清晰边缘。如果边缘都模糊了,说明滤波强度太大了。

4.2 边缘检测:找到图像的“骨架”

滤波之后,我们就要找边缘了。边缘是什么?说白了,就是图像中像素值发生剧烈变化的地方。比如车道线的边界、行人的轮廓、路沿的棱角。

最经典的边缘检测算法是 Canny 边缘检测。我个人觉得,Canny 是“祖师爷”级别的算法,到现在很多自动驾驶项目还在用。它的流程是这样的:

  1. 高斯滤波:先降噪,避免把噪声当成边缘。
  2. 计算梯度:用 Sobel 算子算出每个像素的梯度强度和方向。
  3. 非极大值抑制:只保留梯度方向上的局部最大值,把“胖”边缘变“瘦”。
  4. 双阈值检测:设定高低两个阈值,高阈值确定强边缘,低阈值连接弱边缘。

我曾经在项目中踩过一个坑:双阈值设得太高,结果车道线断成了好几截;设得太低,又全是杂乱的边缘。后来我总结了一个经验:高阈值设为低阈值的 2-3 倍,一般效果都不错。

避坑指南: 我曾经在雨夜场景中测试 Canny,发现边缘检测结果惨不忍睹。后来才意识到,雨滴在镜头上的反光会产生大量“伪边缘”。解决方案是:先做一次中值滤波,再用 Canny。效果立竿见影。

4.3 特征提取:SIFT、SURF、ORB

边缘检测找到了轮廓,但还不够。我们需要找到图像中那些“独一无二”的点——也就是特征点。这些特征点要具备尺度不变性旋转不变性,这样不管摄像头怎么动,都能匹配上。

这里我重点讲三个算法:SIFT、SURF、ORB。它们各有千秋,我直接上对比表:

算法 特点 速度 精度 专利问题
SIFT 尺度不变性最强,旋转不变性也好 有专利,商用需授权
SURF SIFT的加速版,用积分图优化 较快 较高 有专利
ORB 基于FAST角点 + BRIEF描述子 极快 中等 无专利,开源友好

你可能会问:“那我该用哪个?” 我的建议是:

  • 如果你做的是高精度地图构建视觉SLAM,SIFT 依然是首选,虽然慢,但稳。
  • 如果你做的是实时性要求高的应用,比如车道保持、行人检测,ORB 是更好的选择。我在一个嵌入式项目里用过 ORB,在 30fps 的摄像头数据流上跑得飞起。
  • SURF 嘛,说实话,现在用得越来越少了。它比 SIFT 快,但不如 ORB 快,处于一个“不上不下”的位置。
注意: 如果你在商用项目中使用 SIFT 或 SURF,一定要确认专利授权问题。我见过有团队因为用了 SIFT 被发律师函的。ORB 就没有这个烦恼。

4.4 实战:用 OpenCV 实现特征匹配

光说不练假把式。我带你写一段代码,用 ORB 做特征匹配。这是我在一个视觉定位项目中用过的简化版:

import cv2
import numpy as np

# 读取两张图像
img1 = cv2.imread('image1.jpg', 0)
img2 = cv2.imread('image2.jpg', 0)

# 初始化 ORB 检测器
orb = cv2.ORB_create(nfeatures=500)

# 检测特征点和计算描述子
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)

# 使用 BFMatcher 进行匹配
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2)

# 按距离排序,取前 50 个最佳匹配
matches = sorted(matches, key=lambda x: x.distance)[:50]

# 绘制匹配结果
img_matches = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=2)

cv2.imshow('ORB Matches', img_matches)
cv2.waitKey(0)
cv2.destroyAllWindows()

这段代码很简单,但有几个关键点我要提醒你:

  • nfeatures=500 表示最多提取 500 个特征点。太少可能匹配不上,太多会拖慢速度。我一般从 500 开始调。
  • NORM_HAMMING 是 ORB 专用的距离度量,因为 ORB 的描述子是二进制的。
  • crossCheck=True 可以过滤掉“一厢情愿”的匹配,提高准确率。

运行这段代码,你会看到两张图像之间连满了线。如果大部分线都是平行的、方向一致的,说明匹配效果不错。如果线乱成一团,嗯,那就要检查一下图像质量或者调整参数了。

我的经验: 在实际项目中,我很少直接用原始图像做特征匹配。我会先做直方图均衡化,增强对比度,这样特征点更容易被检测到。尤其是夜间场景,这个预处理步骤能救命。

4.5 小结

这一章我们讲了图像处理的三个核心步骤:滤波、边缘检测、特征提取。说白了,就是从“像素”到“语义”的桥梁。你想想看,如果没有这些基础操作,后面的目标检测、车道线识别根本无从谈起。

下一章,我们会进入更高级的话题——相机标定与畸变校正。到时候我会分享一个我在标定板上踩过的坑,保证让你印象深刻。

好了,今天就到这里。去动手试试 ORB 匹配吧,代码我都给你了,别偷懒。