4、感兴趣区域(ROI)提取:为什么要做ROI?、基于坐标的掩码提取、透视变换(鸟瞰图)的原理与实现。
4.1 为什么要做ROI?—— 别让无关信息干扰你的模型
做车道线检测,第一件事不是调模型,而是先问问自己:“哪些像素是我真正关心的?”
你想想看,一张车载摄像头拍到的画面里,有天空、有路边的树、有对面来车的车头、甚至还有仪表盘的倒影。这些信息对车道线检测有用吗?基本没用。它们只会增加计算量,甚至引入噪声。
我个人习惯,拿到一张图后,第一件事就是切掉上半部分。为什么?因为车道线只出现在路面,而路面永远在图像的下半部分。天空、远处的建筑,这些区域不仅没有车道线,还会让模型去学习一些奇怪的背景特征。
我曾经在一个项目中,模型在测试集上表现很好,但一上路就频繁误检。排查了很久,发现是训练数据里路边的护栏和车道线长得有点像,模型把护栏当成了车道线。后来我加了一个严格的ROI掩码,把护栏区域直接屏蔽掉,问题就解决了。
所以,ROI提取的核心目的就两个:
- 减少计算量:只处理关键区域,帧率能提升20%-30%
- 排除干扰:让模型专注于路面,避免学到无关的上下文
核心原则:ROI不是越精确越好,而是“够用就行”。稍微多留一点边缘区域,比切得太狠导致漏检要好。
4.2 基于坐标的掩码提取 —— 用多边形“切”出路面
最直接的ROI方法,就是手动定义一个多边形区域。说白了,就是告诉程序:“我只想看这个梯形里面的内容,外面全部涂黑。”
为什么是梯形?因为车道在图像中呈现近大远小的透视效果,路面区域在图像里就是一个倒梯形。
代码实现很简单,我一般用OpenCV的fillPoly配合bitwise_and来做:
import cv2
import numpy as np
def apply_roi_mask(image):
# 图像尺寸
height, width = image.shape[:2]
# 定义多边形顶点(梯形)
# 左下、右下、右上、左上
polygons = np.array([
[(0, height), # 左下
(width, height), # 右下
(int(width*0.6), int(height*0.6)), # 右上
(int(width*0.4), int(height*0.6))] # 左上
])
# 创建全黑掩码
mask = np.zeros_like(image)
# 填充多边形为白色
cv2.fillPoly(mask, polygons, (255, 255, 255))
# 与操作,保留ROI区域
masked_image = cv2.bitwise_and(image, mask)
return masked_image
我的经验:坐标值不要写死。不同摄像头的安装角度、焦距都不一样。我一般会在配置文件里用百分比来定义ROI,这样换摄像头时只需要改几个参数,不用改代码。
这里有个细节要注意:多边形的顶点顺序必须是顺时针或逆时针,否则填充出来的形状会乱掉。嗯,我刚开始用的时候也踩过这个坑,画出来的掩码像个扭曲的蝴蝶结。
4.3 透视变换(鸟瞰图)的原理与实现
ROI掩码只是第一步。真正让车道线检测变得简单的方法,是透视变换,也就是把图像转换成鸟瞰图。
为什么会需要这个?你想想看,在原始图像里,车道线是斜的、会相交于消失点。这种透视效果对计算车道曲率、车辆偏离角度非常不友好。但在鸟瞰图里,车道线是平行的,距离关系是线性的,计算起来就舒服多了。
4.3.1 原理:从透视到正交
透视变换的本质,就是找到一个变换矩阵,把图像中的一个四边形映射到另一个四边形。具体来说:
- 源点:原始图像中路面区域的四个角点(比如一个梯形)
- 目标点:鸟瞰图中对应的四个角点(一个矩形)
变换矩阵是一个3x3的矩阵,OpenCV的getPerspectiveTransform可以直接算出来。然后调用warpPerspective就能得到鸟瞰图。
数学本质:透视变换是线性变换在齐次坐标下的推广。它不仅能做旋转缩放,还能模拟“视角变化”。
4.3.2 实现:手把手教你做鸟瞰图
我直接上代码,这是我在实际项目中用过的版本:
import cv2
import numpy as np
def perspective_transform(image):
height, width = image.shape[:2]
# 源点:原始图像中的梯形区域
# 这些值需要根据你的摄像头标定结果来调整
src_points = np.float32([
[int(width*0.15), height], # 左下
[int(width*0.85), height], # 右下
[int(width*0.55), int(height*0.6)], # 右上
[int(width*0.45), int(height*0.6)] # 左上
])
# 目标点:鸟瞰图中的矩形区域
dst_points = np.float32([
[int(width*0.2), height], # 左下
[int(width*0.8), height], # 右下
[int(width*0.8), 0], # 右上
[int(width*0.2), 0] # 左上
])
# 计算变换矩阵
M = cv2.getPerspectiveTransform(src_points, dst_points)
# 逆变换矩阵(用于将结果映射回原图)
M_inv = cv2.getPerspectiveTransform(dst_points, src_points)
# 执行透视变换
warped = cv2.warpPerspective(image, M, (width, height),
flags=cv2.INTER_LINEAR)
return warped, M, M_inv
注意:源点和目标点的顺序必须一一对应。比如源点的左下对应目标点的左下,不能乱。我曾经因为顺序搞反了,结果鸟瞰图里的车道线是反着跑的,排查了半小时才发现。
4.3.3 参数调优:别指望一次成功
透视变换的参数(四个源点坐标)没有标准答案。它取决于:
| 因素 | 影响 | 我的建议 |
|---|---|---|
| 摄像头安装高度 | 越高,路面区域越小 | 适当缩小梯形高度 |
| 摄像头俯仰角 | 角度越大,消失点越靠下 | 调整梯形上边位置 |
| 路面坡度 | 上坡/下坡会影响透视效果 | 动态调整源点,或做坡度补偿 |
我个人习惯的做法是:先拍一张笔直道路的图片,手动标出车道线的位置,然后调整源点直到鸟瞰图中的车道线看起来是平行的。这个过程可能需要反复试几次,但一旦调好,后面就一劳永逸了。
4.4 总结:ROI + 透视变换 = 车道线检测的基石
这两个操作组合起来,效果非常明显:
- ROI掩码:剔除天空、树木等无关区域,减少计算量
- 透视变换:将路面拉直,让车道线变成平行线,简化后续处理
做完这两步,你的输入数据就干净多了。后续的阈值分割、滑动窗口检测,都是在鸟瞰图上进行的。说白了,前面工作做得越扎实,后面模型就越省心。
避坑指南:透视变换后的鸟瞰图,分辨率会不均匀。靠近车头的区域被拉伸,远处被压缩。如果你要做精确的距离测量,记得做像素到实际距离的标定。我曾经忽略了这个,算出来的车道曲率偏差了15%。
下一章,我们会聊如何在鸟瞰图上做二值化处理和车道线像素提取。到时候你会看到,有了鸟瞰图,很多问题都变得简单了。