第四章:视觉预处理技术——图像去畸变、增强与特征提取

各位同学,欢迎来到第四章。这一章我们聊聊视觉预处理。说白了,就是摄像头拍回来的原始图像,不能直接用。为什么?因为镜头有畸变,光线可能不均匀,图像可能太暗或太亮。你想想看,如果给跟踪算法喂一张歪歪扭扭的图,它怎么可能找到正确的目标?

我个人习惯把视觉预处理比作「给图像做体检和矫正」。这一步做不好,后面融合跟踪的精度直接打折扣。我在项目中遇到过好几次,明明算法模型没问题,但跟踪效果就是差,最后查来查去,发现是图像去畸变的参数没标定准。嗯,这种坑踩过一次就记住了。

4.1 图像去畸变

先讲去畸变。摄像头镜头,尤其是广角镜头,拍出来的图像边缘会有明显的桶形畸变或枕形畸变。说白了,直线变弯了。这对激光雷达和视觉的融合来说是个大问题——你没法把像素坐标和激光点云准确对应起来。

去畸变的原理其实不复杂。我们用一个数学模型来描述畸变:

// 径向畸变模型
x_distorted = x * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)
y_distorted = y * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)

// 切向畸变模型
x_distorted = x + [2 * p1 * x * y + p2 * (r^2 + 2 * x^2)]
y_distorted = y + [p1 * (r^2 + 2 * y^2) + 2 * p2 * x * y]

其中 k1、k2、k3 是径向畸变系数,p1、p2 是切向畸变系数。这些参数需要通过标定得到。

我的经验:标定板最好用棋盘格,角点数量在 7x9 或 8x10 左右。我建议至少拍 20 张不同角度的照片,覆盖图像各个区域。标定结果出来后,记得看一眼重投影误差,一般小于 0.5 像素才算合格。

去畸变的实际流程分三步:

  1. 内参标定:用标定板计算相机内参矩阵和畸变系数
  2. 生成映射表:根据畸变模型,计算每个像素去畸变后的位置
  3. 重采样:用双线性插值生成去畸变后的图像

代码实现上,OpenCV 提供了现成的函数:

import cv2
import numpy as np

# 假设已经标定得到内参矩阵 mtx 和畸变系数 dist
# mtx = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
# dist = [k1, k2, p1, p2, k3]

# 方法一:直接去畸变
undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 方法二:先计算映射表,再重映射(适合视频流)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx, (w, h), cv2.CV_32FC1)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
注意:我曾经在项目中直接用 cv2.undistort 处理视频流,结果每帧都重新计算映射表,性能直接崩了。正确的做法是:先算好 mapx 和 mapy,然后每帧只用 remap。这个坑我替你们踩过了。

4.2 图像增强——直方图均衡化

图像去畸变之后,接下来要处理的是光照问题。自动驾驶场景中,光线变化太剧烈了——隧道里突然变暗,树荫下斑驳的光影,逆光时一片白茫茫。直方图均衡化就是用来解决这个问题的。

直方图均衡化的核心思想很简单:把图像的灰度分布拉开,让暗的地方变亮,亮的地方变暗,整体对比度提升。说白了,就是把原来集中在某个区间的灰度值,均匀地映射到整个 0-255 范围。

数学上,它基于累积分布函数(CDF)做映射:

// 原始直方图 H(r) 表示灰度级 r 的像素数
// 累积分布函数 CDF(r) = sum_{i=0}^{r} H(i)
// 映射函数 T(r) = round( (L-1) * CDF(r) / (M*N) )
// 其中 L=256 是灰度级数,M*N 是图像总像素数

OpenCV 实现起来就一行代码:

import cv2

# 全局直方图均衡化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
equalized = cv2.equalizeHist(gray)

# 自适应直方图均衡化(CLAHE)——我推荐用这个
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_result = clahe.apply(gray)

为什么我推荐 CLAHE?

全局直方图均衡化有个问题:它会放大噪声,尤其是图像中原本比较均匀的区域。CLAHE(对比度受限的自适应直方图均衡化)把图像分成小块,每块单独做均衡化,然后用双线性插值消除块之间的边界。clipLimit 参数控制对比度放大的上限,我一般设 2.0-3.0。

我在项目中做过对比测试:用全局均衡化后,图像噪声增加了约 15%,而 CLAHE 只增加了 3% 左右。对于后续的特征提取来说,这个差异很关键。

4.3 特征提取——SIFT、ORB 与深度学习特征

图像预处理完了,接下来要提取特征。特征是什么?就是图像中那些「与众不同」的点——角点、边缘、纹理丰富的区域。这些特征点要满足两个条件:可重复性和独特性。说白了,换个角度、换个光照,同一个点还能被找到,而且它跟周围点长得不一样。

4.3.1 SIFT 特征

SIFT(尺度不变特征变换)是经典中的经典。它最大的优点是尺度不变性和旋转不变性。什么意思?就是你把图像放大缩小、旋转一下,SIFT 特征点还能匹配上。

SIFT 的流程分四步:

  1. 尺度空间极值检测:用高斯差分金字塔找潜在特征点
  2. 关键点定位:去掉低对比度和边缘响应点
  3. 方向分配:根据梯度方向给每个关键点分配主方向
  4. 描述子生成:在关键点周围 16x16 区域计算梯度直方图,生成 128 维向量
import cv2

# SIFT 特征提取
sift = cv2.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(gray, None)

# 绘制特征点
img_sift = cv2.drawKeypoints(gray, keypoints, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
我的经验:SIFT 的专利在 2020 年过期了,现在 OpenCV 里可以直接用。但 SIFT 计算量比较大,在嵌入式平台上跑实时跟踪有点吃力。我一般在离线标定或建图阶段用 SIFT,在线跟踪用 ORB 或深度学习特征。

4.3.2 ORB 特征

ORB(Oriented FAST and Rotated BRIEF)是 SIFT 的轻量级替代方案。它结合了 FAST 角点检测和 BRIEF 描述子,并加入了旋转不变性。说白了,就是又快又够用。

ORB 的核心改进有两个:

  • 方向计算:用灰度质心法给 FAST 角点分配方向
  • 旋转不变描述子:根据方向旋转 BRIEF 的采样模式
import cv2

# ORB 特征提取
orb = cv2.ORB_create(nfeatures=1000, scaleFactor=1.2, nlevels=8)
keypoints, descriptors = orb.detectAndCompute(gray, None)

# 描述子是二进制的,可以用汉明距离匹配
# 匹配示例
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(descriptors1, descriptors2)

ORB 的速度比 SIFT 快一个数量级。我在 Jetson Nano 上测试过,640x480 的图像,ORB 提取 1000 个特征点大约 15ms,SIFT 要 80ms 左右。对于实时跟踪来说,这个差距很关键。

注意:ORB 的尺度不变性不如 SIFT。如果你在跟踪中目标尺度变化很大(比如车辆从远处开到近处),ORB 的匹配效果会下降。我建议在尺度变化剧烈的场景中,适当增加 nlevels 参数。

4.3.3 深度学习特征

近几年,深度学习特征越来越流行。相比手工设计的 SIFT 和 ORB,深度学习特征有几个明显优势:

  • 端到端学习:特征提取和匹配可以联合优化
  • 更强的判别力:在高难度场景(弱纹理、重复纹理)下表现更好
  • 可定制性:可以根据任务需求设计网络结构

常用的深度学习特征提取方法包括:

方法 特点 适用场景
SuperPoint 自监督训练,同时检测关键点和描述子 视觉SLAM、特征匹配
D2-Net 在特征图上同时做检测和描述 图像检索、三维重建
R2D2 可重复性和可靠性联合预测 大视角变化场景
LoFTR Transformer结构,无检测器匹配 弱纹理、重复纹理场景

以 SuperPoint 为例,它的网络结构是一个编码器-解码器架构:

# 伪代码示意
class SuperPoint(nn.Module):
    def __init__(self):
        # 共享编码器:VGG-like 结构
        self.encoder = VGGStyleEncoder()
        # 关键点头:输出每个像素是特征点的概率
        self.detector_head = DetectorHead()
        # 描述子头:输出每个像素的 256 维描述子
        self.descriptor_head = DescriptorHead()
    
    def forward(self, img):
        features = self.encoder(img)
        scores = self.detector_head(features)
        descriptors = self.descriptor_head(features)
        # 对描述子做 L2 归一化
        descriptors = F.normalize(descriptors, p=2, dim=1)
        return scores, descriptors

我的建议:

在实际项目中,我一般这样选:

  • 算力充足(有 GPU):用 SuperPoint + SuperGlue,匹配效果最好
  • 算力有限(嵌入式平台):用 ORB,速度快,够用
  • 需要高精度离线处理:用 SIFT,稳定可靠

我曾经在一个项目中,用 SuperPoint 替换了 ORB,特征匹配的召回率从 72% 提升到了 91%,但推理时间从 15ms 增加到了 45ms。这个 trade-off 需要根据实际场景权衡。

本章知识体系

下面这张图总结了视觉预处理的核心流程和关键技术:

视觉预处理技术体系 原始图像 第一步:图像去畸变 第二步:图像增强(直方图均衡化) 第三步:特征提取 SIFT(高精度,慢) 尺度不变、旋转不变 ORB(快速,够用) FAST + BRIEF 改进 深度学习特征 SuperPoint / LoFTR

这张图把本章的知识结构串起来了。从原始图像开始,经过去畸变、图像增强,最后到特征提取。每一步都有多种技术可选,具体选哪个,取决于你的算力、精度要求和实时性需求。

好了,这一章的内容就到这里。视觉预处理是融合跟踪的基石,基础打牢了,后面做激光雷达和视觉的融合才能得心应手。下一章我们会讲激光雷达的预处理技术,到时候见。


可可精品资料库