4、感兴趣区域提取:ROI定义、透视变换原理、逆透视映射
车道线检测,说白了就是在一张图里找到那几条线。但你想啊,一张1080p的图,上面一半是天空、树木、广告牌,这些跟车道线半毛钱关系没有。我刚开始做这个项目时,傻乎乎地把整张图扔进模型,结果模型学了半天,净在那识别云朵了。后来我学乖了——先切掉没用的区域,只留下路面。这就是ROI提取。
4.1 ROI定义:只盯着路面看
ROI,Region of Interest,感兴趣区域。在车道线检测里,ROI通常就是路面那一块梯形区域。为什么是梯形?因为相机视角的关系,远处的路窄,近处的路宽,投影到图像上就是个倒梯形。
核心思路: 保留路面,剔除天空、车头、两侧护栏等干扰区域。
我个人习惯用多边形来定义ROI。比如给一个1280x720的图像,我会这样设置:
import cv2
import numpy as np
def get_roi_mask(img):
height, width = img.shape[:2]
# 定义梯形四个顶点(左下、右下、右上、左上)
roi_vertices = np.array([
[(0, height), # 左下
(width, height), # 右下
(width * 0.6, height * 0.6), # 右上
(width * 0.4, height * 0.6)] # 左上
], dtype=np.int32)
mask = np.zeros_like(img)
cv2.fillPoly(mask, roi_vertices, (255, 255, 255))
masked_img = cv2.bitwise_and(img, mask)
return masked_img
嗯,这里要注意:ROI的顶点坐标不是随便写的。我踩过一个坑——把梯形下底直接拉到图像最底部,结果车头引擎盖上的反光也被包含进来了,导致误检率飙升。后来我习惯把下底往上抬个10-20像素,避开引擎盖。
我的经验: ROI的上下边界不要卡得太死。上边界留点余量,防止车辆颠簸时车道线跑出ROI;下边界稍微抬高,避开车身反光。
4.2 透视变换原理:把路面“掰正”
ROI提取完,你得到的还是“斜着看”的路面。远处的车道线挤在一起,近处的又宽又大。这种透视畸变对后续的曲线拟合非常不友好。怎么办?透视变换。
透视变换,说白了就是把一个平面投影到另一个平面上。在车道线检测里,我们想把相机视角下的梯形路面,变成一张俯视图——也就是鸟瞰图(Bird's Eye View, BEV)。
数学上,透视变换用3x3的矩阵表示:
[ x' ] [ a11 a12 a13 ] [ x ]
[ y' ] = [ a21 a22 a23 ] [ y ]
[ w' ] [ a31 a32 a33 ] [ 1 ]
其中(x, y)是原图坐标,(x'/w', y'/w')是变换后的坐标。这个矩阵有8个自由度(a33通常归一化为1),所以我们需要4组对应点来求解。
为什么是4组?因为透视变换是线性变换加平移,4个点刚好确定一个四边形到另一个四边形的映射。我刚开始学的时候总在想:为什么不是3个点?3个点确定一个仿射变换,但透视变换多了个“近大远小”的缩放,需要第4个点来约束。
关键点: 透视变换是线性变换在齐次坐标下的推广。它保留了直线性——原图中的直线,变换后还是直线。
在OpenCV里实现很简单:
def perspective_transform(img):
height, width = img.shape[:2]
# 源点:原图中的梯形四个顶点
src_points = np.float32([
[width * 0.4, height * 0.6], # 左上
[width * 0.6, height * 0.6], # 右上
[0, height], # 左下
[width, height] # 右下
])
# 目标点:鸟瞰图中的矩形四个顶点
dst_points = np.float32([
[width * 0.2, 0], # 左上
[width * 0.8, 0], # 右上
[width * 0.2, height], # 左下
[width * 0.8, height] # 右下
])
M = cv2.getPerspectiveTransform(src_points, dst_points)
warped = cv2.warpPerspective(img, M, (width, height))
return warped, M
我曾经在高速公路上测试时发现,同样的透视变换参数,在弯道上效果很差。为什么?因为弯道上的车道线不再是直线,透视变换只能把直线映射成直线,弯道会变形。后来我改用分段透视变换——把路面分成近处和远处两段,分别做变换,效果好了不少。
4.3 逆透视映射:从鸟瞰图回到原图
逆透视映射,就是透视变换的逆过程。你想想看,我们在鸟瞰图上检测到了车道线,但最终要在原图上显示结果吧?那就得把鸟瞰图上的点映射回原图。
数学上很简单,就是求透视变换矩阵的逆矩阵:
M_inv = cv2.getPerspectiveTransform(dst_points, src_points)
# 或者直接用
M_inv = np.linalg.inv(M)
然后对检测结果做逆变换:
def inverse_perspective(warped_result, M_inv, original_shape):
unwarped = cv2.warpPerspective(warped_result, M_inv,
(original_shape[1], original_shape[0]))
return unwarped
避坑指南: 我曾经在逆透视映射时踩过一个坑——直接对二值化的车道线掩码做逆变换,结果边缘出现锯齿。后来我改成先对连续值(比如概率图)做逆变换,再二值化,边缘平滑多了。
还有一个细节:逆透视映射后的图像,边缘区域会有黑边。这是因为原图的某些区域在鸟瞰图中被拉伸了,逆变换时那些区域没有对应的像素值。我一般会裁剪掉边缘10-20像素,或者用边界填充。
4.4 全链路流程总结
把上面三步串起来,就是完整的ROI提取+透视变换流程:
- 原图输入:相机采集的RGB图像
- ROI提取:用多边形掩码切出路面区域
- 透视变换:将梯形路面映射为矩形鸟瞰图
- 车道线检测:在鸟瞰图上做检测(后续章节会讲)
- 逆透视映射:将检测结果映射回原图
- 结果叠加:在原图上绘制车道线
这个流程看起来简单,但每个环节都有坑。我建议你在实际项目中,先用标定板把透视变换矩阵标定好,不要用手动选点。手动选点误差太大,换个场景就得重新调。
我的习惯: 在车上贴几个标定点,开车在平直路面上跑一段,用这些点来标定透视变换矩阵。这样得到的矩阵更鲁棒,换车换相机时只需要重新标定一次。
嗯,以上就是ROI提取和透视变换的核心内容。说白了,就是三步:切掉没用的、掰正视角、再映射回去。每一步都有细节,但掌握了这些,你的车道线检测就成功了一半。
一句话总结: ROI提取解决“看哪里”的问题,透视变换解决“怎么看”的问题,逆透视映射解决“怎么画回去”的问题。
可可精品资料库