第二章 数据预处理进阶:图像去畸变、透视变换与ROI区域动态选择策略

2.1 图像去畸变:别让镜头骗了你

做车道线检测,第一个坑往往不是算法不行,而是摄像头本身在撒谎。

你想想看,市面上绝大多数车载摄像头用的都是广角镜头或鱼眼镜头。这类镜头为了看到更宽的路面,会引入明显的径向畸变和切向畸变。说白了,就是画面边缘的直线会变弯。我刚开始做这个项目时,拿了一个鱼眼摄像头对着笔直的车道线一拍——好家伙,边缘的车道线弯成了S形。如果不做去畸变,后面的霍夫变换或者曲线拟合根本没法看。

去畸变的数学原理其实不复杂。我们通常用张正友标定法,先拿到相机的内参矩阵和畸变系数。核心公式就两个:

  • 径向畸变:\( x_{corrected} = x(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \)
  • 切向畸变:\( x_{corrected} = x + [2p_1 xy + p_2 (r^2 + 2x^2)] \)

嗯,公式看着唬人,但OpenCV已经帮我们封装好了。我个人习惯用 cv2.undistort() 一步到位。不过要注意,去畸变后的图像边缘会被裁剪掉一部分,如果你需要保留完整的视野,可以用 cv2.getOptimalNewCameraMatrix() 调整缩放比例。

核心要点:去畸变不是可选项,而是必选项。尤其是使用广角摄像头时,畸变会导致远处车道线的位置偏差达到几十个像素,这对后续的透视变换是致命的。

我的小技巧:标定棋盘格时,尽量让棋盘格占满画面的不同区域。我曾在项目中偷懒只拍了中心区域,结果边缘畸变校正效果很差,后来重新采集数据才搞定。

2.2 透视变换:把路面“拍平”

去畸变之后,我们得到的是前视视角的图像。但车道线检测真正需要的是鸟瞰图——也就是从正上方往下看的样子。为什么?因为鸟瞰图下车道线是平行的,便于我们做直线拟合或曲线拟合。

透视变换的本质,就是找一个单应性矩阵 \( H \),把前视图中的四个点映射到鸟瞰图中的四个点。我一般这样选点:

  • 源点:前视图中车道线的四个角点(比如左右车道线的起点和终点)
  • 目标点:鸟瞰图中对应的矩形四个角点

代码实现很简单,OpenCV提供了 cv2.getPerspectiveTransform() 和 cv2.warpPerspective()。但这里有个坑——源点的选择直接影响变换效果。我记得有一次,我随便选了四个点,结果鸟瞰图里的车道线歪歪扭扭,完全没法用。

避坑指南:我曾经因为源点选得太靠近车辆,导致鸟瞰图中远处车道线被严重拉伸,分辨率极低。建议源点覆盖从车头前5米到50米的范围,这样远近兼顾。

另外,透视变换后的图像分辨率会发生变化。如果你发现鸟瞰图模糊,可以适当增大输出图像的尺寸。我一般设为源图宽度的1.5倍。

2.3 ROI区域动态选择:别让背景干扰你

做完透视变换,你是不是觉得万事大吉了?别急,还有一个关键问题——ROI区域的选择。

传统的做法是固定一个梯形或矩形ROI,只保留车道线所在的区域。但实际路况千变万化:上坡时车道线会往上移,下坡时会往下移,弯道时车道线会偏向一侧。固定ROI很容易把车道线切掉,或者引入大量无关背景。

我个人建议采用动态ROI策略。核心思路是根据上一帧检测到的车道线位置,自适应调整当前帧的ROI。具体做法:

  1. 上一帧检测到左右车道线的拟合曲线
  2. 计算车道线在图像中的上下边界和左右偏移量
  3. 在当前帧中,以这些边界为中心,向外扩展一定像素(比如左右各50像素,上下各30像素)
  4. 用这个动态ROI裁剪图像,再进行车道线检测

这样做的好处很明显:

  • 减少计算量,只处理关键区域
  • 提高鲁棒性,避免背景干扰
  • 适应弯道和坡度变化

注意:动态ROI需要配合车道线跟踪使用。如果连续几帧都检测不到车道线,建议回退到固定ROI,防止ROI漂移到错误区域。

2.4 整体流程与代码示例

把上面三步串起来,就是一套完整的数据预处理流程。我画了一张流程图,帮你理清思路:

原始图像 图像去畸变 cv2.undistort() 透视变换 getPerspectiveTransform 动态ROI选择 基于上一帧位置 车道线检测 边缘检测 + 拟合 反馈更新ROI位置

下面是一个精简的代码示例,展示了核心步骤:

import cv2
import numpy as np

# 1. 去畸变
def undistort_image(img, mtx, dist):
    h, w = img.shape[:2]
    new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
    undistorted = cv2.undistort(img, mtx, dist, None, new_mtx)
    return undistorted

# 2. 透视变换
def perspective_transform(img):
    src_points = np.float32([[200, 720], [600, 450], [700, 450], [1100, 720]])
    dst_points = np.float32([[300, 720], [300, 0], [900, 0], [900, 720]])
    M = cv2.getPerspectiveTransform(src_points, dst_points)
    warped = cv2.warpPerspective(img, M, (1200, 720))
    return warped, M

# 3. 动态ROI(基于上一帧车道线位置)
def dynamic_roi(img, left_fit, right_fit, margin=50):
    h, w = img.shape[:2]
    # 根据拟合曲线计算ROI边界
    y_top = int(min(left_fit[0]*h**2 + left_fit[1]*h + left_fit[2],
                    right_fit[0]*h**2 + right_fit[1]*h + right_fit[2]))
    y_bottom = h
    x_left = max(0, int(left_fit[0]*h**2 + left_fit[1]*h + left_fit[2]) - margin)
    x_right = min(w, int(right_fit[0]*h**2 + right_fit[1]*h + right_fit[2]) + margin)
    roi = img[y_top:y_bottom, x_left:x_right]
    return roi

2.5 参数调优建议

数据预处理这块,参数调优直接决定后续检测效果。我整理了一张表,列出关键参数和我的经验值:

参数 推荐范围 我的经验
去畸变缩放因子 alpha 0.0 ~ 1.0 0.5 左右,保留大部分视野又不引入过多黑边
透视变换源点纵向范围 图像底部到中部 底部取图像高度90%,顶部取40%
动态ROI扩展像素 margin 30 ~ 80 像素 直道用40,弯道用60
鸟瞰图输出宽度 原图宽度的1.0 ~ 2.0倍 1.5倍,分辨率与计算量平衡

避坑指南:我曾经在高速场景下把动态ROI的margin设得太小(20像素),结果车辆一颠簸,车道线就跑到ROI外面去了。后来改成60像素,稳如老狗。

好了,数据预处理这块就聊到这儿。去畸变、透视变换、动态ROI,这三板斧用好了,你的车道线检测就成功了一半。剩下的,就是特征提取和拟合的事了。

可可精品资料库