第三节:感兴趣区域提取——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图上做以下几步:

  1. 灰度化:cv2.cvtColor() 转成单通道
  2. 高斯模糊:cv2.GaussianBlur() 去噪,核大小选5x5
  3. 二值化:cv2.threshold() 或 cv2.adaptiveThreshold()
  4. 滑动窗口搜索:在BEV图上从左到右扫描,找到车道线的像素点

为什么要在BEV图上做滑动窗口?因为前视图里车道线是斜的,窗口不好对齐。BEV图里车道线是直的,窗口从上往下滑,一找一个准。

3.5 本章知识体系

下面这张图,是我自己总结的ROI提取与BEV生成的完整流程。你看一遍,应该就能把整个逻辑串起来了。

ROI提取与BEV生成流程 原始图像 (960x540) ROI掩码生成 cv2.fillPoly() 掩码后图像 bitwise_and 透视变换 getPerspectiveTransform BEV鸟瞰图 warpPerspective 后处理 灰度→二值→滑动窗口 车道线拟合 多项式拟合 关键参数:src_points(源点)、dst_points(目标点)、变换矩阵M 注意:ROI顶点和透视变换点需根据摄像头安装位置标定

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帮你聚焦路面,透视变换帮你消除透视效应。这两步做好了,后面的车道线拟合就是水到渠成的事。

可可精品资料库