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提取+透视变换流程:

  1. 原图输入:相机采集的RGB图像
  2. ROI提取:用多边形掩码切出路面区域
  3. 透视变换:将梯形路面映射为矩形鸟瞰图
  4. 车道线检测:在鸟瞰图上做检测(后续章节会讲)
  5. 逆透视映射:将检测结果映射回原图
  6. 结果叠加:在原图上绘制车道线

这个流程看起来简单,但每个环节都有坑。我建议你在实际项目中,先用标定板把透视变换矩阵标定好,不要用手动选点。手动选点误差太大,换个场景就得重新调。

我的习惯: 在车上贴几个标定点,开车在平直路面上跑一段,用这些点来标定透视变换矩阵。这样得到的矩阵更鲁棒,换车换相机时只需要重新标定一次。

ROI提取 + 透视变换 全链路流程图 原图输入 ROI提取 透视变换 车道线检测 逆透视映射 结果叠加 最终输出 核心:ROI剔除无关区域 → 透视变换消除透视畸变 → 逆透视映射还原到原图 💡 提示:透视变换矩阵建议用标定板标定,不要手动选点

嗯,以上就是ROI提取和透视变换的核心内容。说白了,就是三步:切掉没用的、掰正视角、再映射回去。每一步都有细节,但掌握了这些,你的车道线检测就成功了一半。

一句话总结: ROI提取解决“看哪里”的问题,透视变换解决“怎么看”的问题,逆透视映射解决“怎么画回去”的问题。

可可精品资料库