第三节:感兴趣区域提取——ROI掩码、透视变换与BEV生成
车道线检测,说白了就是让车“看懂”路。但摄像头拍到的画面,信息量太大了——天空、树木、对面来车,这些对车道线检测来说全是干扰。我刚开始做这个项目时,就犯过傻:直接把整张图扔进模型,结果模型学了半天,净在那识别云朵了。
所以,我们得先告诉程序:“别看别处,就看路!” 这就是感兴趣区域提取要做的事。
3.1 为什么要提取ROI?
你想想看,车道线永远只出现在路面那一块区域。上半部分是天空,左右两边是护栏或建筑物。如果我们把整张图都处理,不仅计算量大,还容易引入噪声。
我个人习惯的做法是:先定义一个多边形,把车道区域框出来。这个多边形就是ROI掩码。掩码之外的像素,全部置为黑色。这样,后续的所有处理都只聚焦在路面上。
核心思想: 不是所有像素都值得处理。只关注“路”,忽略“非路”。
3.2 基于OpenCV的ROI掩码生成
OpenCV里实现这个很简单。我们用 cv2.fillPoly() 画一个白色多边形,再用 cv2.bitwise_and() 把原图和掩码做“与”操作。
嗯,这里要注意:多边形的顶点坐标,得根据你的摄像头安装位置来调。我曾在不同车型上试过,轿车和SUV的视角差异很大,顶点位置要重新标定。
import cv2
import numpy as np
def region_of_interest(img, vertices):
"""
生成ROI掩码并提取感兴趣区域
:param img: 输入图像
:param vertices: 多边形顶点列表
:return: 掩码后的图像
"""
mask = np.zeros_like(img)
# 填充多边形为白色
cv2.fillPoly(mask, vertices, 255)
# 与操作,保留ROI区域
masked_img = cv2.bitwise_and(img, mask)
return masked_img
# 示例:定义梯形ROI(假设图像尺寸为 960x540)
height, width = 540, 960
roi_vertices = np.array([[
(200, height), # 左下
(550, 320), # 左上
(750, 320), # 右上
(width-100, height) # 右下
]], dtype=np.int32)
roi_result = region_of_interest(image, roi_vertices)
我的经验: 多边形不要画得太“紧”。留一点余量,防止弯道时车道线跑出ROI区域。我曾经因为ROI画得太窄,导致大弯道时车道线直接消失,差点出事故。
3.3 透视变换:从“前视图”到“鸟瞰图”
ROI提取之后,我们得到的是前视图——近处的车道线宽,远处的窄。这种透视效果对后续的曲线拟合很不友好。你想想看,同样的车道宽度,在图像底部占100像素,在顶部只占20像素,这怎么拟合?
所以,我们需要做透视变换,把前视图转换成鸟瞰图(BEV)。说白了,就是模拟从天上往下看的效果。这样车道线就变成平行线了,拟合起来轻松很多。
透视变换的原理很简单:在原图上选4个点,在目标图上对应4个点,计算一个3x3的变换矩阵。OpenCV提供了 cv2.getPerspectiveTransform() 和 cv2.warpPerspective()。
def perspective_transform(img, src_points, dst_points):
"""
透视变换:前视图 -> 鸟瞰图
:param img: 输入图像
:param src_points: 源图像中的4个点
:param dst_points: 目标图像中的4个点
:return: 变换后的鸟瞰图
"""
# 计算变换矩阵
M = cv2.getPerspectiveTransform(src_points, dst_points)
# 应用变换
warped = cv2.warpPerspective(img, M, (img.shape[1], img.shape[0]))
return warped
# 定义源点和目标点
src = np.float32([
[580, 460], # 左上
[700, 460], # 右上
[1050, 680], # 右下
[200, 680] # 左下
])
dst = np.float32([
[300, 0],
[900, 0],
[900, 720],
[300, 720]
])
bev_image = perspective_transform(roi_result, src, dst)
避坑指南: 我曾经在高速上测试时,发现变换后的BEV图总是歪的。排查了半天,原来是src_points选在了车道线边缘,而不是车道中心。记住:源点一定要选在车道中心线上,且4个点要构成一个矩形(在物理世界中)。
3.4 鸟瞰图(BEV)生成后的处理
BEV图生成后,车道线就变成了竖直或接近竖直的线条。这时候再做二值化、边缘检测,效果会好很多。我个人习惯在BEV图上做以下几步:
- 灰度化:
cv2.cvtColor()转成单通道 - 高斯模糊:
cv2.GaussianBlur()去噪,核大小选5x5 - 二值化:
cv2.threshold()或cv2.adaptiveThreshold() - 滑动窗口搜索:在BEV图上从左到右扫描,找到车道线的像素点
为什么要在BEV图上做滑动窗口?因为前视图里车道线是斜的,窗口不好对齐。BEV图里车道线是直的,窗口从上往下滑,一找一个准。
3.5 本章知识体系
下面这张图,是我自己总结的ROI提取与BEV生成的完整流程。你看一遍,应该就能把整个逻辑串起来了。
3.6 参数调优建议
这部分我踩过不少坑,直接给你列个表,省得你再去试错。
| 参数 | 推荐值 | 我的经验 |
|---|---|---|
| ROI多边形顶点 | 梯形,底部占满宽度,顶部在水平线下方 | 轿车:顶部y坐标在图像高度的60%处;SUV:在55%处 |
| 透视变换源点 | 选在车道中心线上,构成矩形 | 我习惯选在距离车头5米和30米处的车道中心点 |
| BEV图尺寸 | 保持与原图相同或略小 | 960x540就够用,太大反而增加计算量 |
| 高斯模糊核大小 | 5x5 或 7x7 | 雨天或路面湿滑时,建议用7x7,去噪效果更好 |
小技巧: 如果你发现BEV图里的车道线还是有点弯,可以微调dst_points的y坐标。让目标点稍微靠拢一点,能补偿镜头畸变。我一般会在dst_points的y方向上加减5-10个像素,反复试几次就能找到最佳值。
好了,ROI提取和BEV生成这部分就讲完了。记住:ROI帮你聚焦路面,透视变换帮你消除透视效应。这两步做好了,后面的车道线拟合就是水到渠成的事。