第四章:经典车道线检测算法——霍夫变换实战
各位同学,欢迎来到车道线检测的核心环节。今天我们要聊的,是计算机视觉里一个经典到不能再经典的工具——霍夫变换。说实话,我入行那会儿,深度学习还没这么火,大家做车道线检测,十有八九靠的就是它。现在虽然有了更高级的方法,但霍夫变换的思想,依然是理解图像特征的基石。
本章核心脉络:从直线检测的数学原理出发,理解标准霍夫变换与概率霍夫变换的区别,再结合ROI提取,最终完成车道线的拟合与绘制。说白了,就是教会计算机怎么从一堆像素点里,找到那条代表车道的直线。
4.1 霍夫变换原理:直线检测的数学之美
先问大家一个问题:给你一张图片上的一堆白色像素点,你怎么判断它们是不是在一条直线上?
人眼一看就知道,但计算机不行。霍夫变换的思路很巧妙——它把图像空间里的点,映射到参数空间里去投票。具体来说,一条直线在笛卡尔坐标系里是 y = kx + b,但垂直线的斜率 k 是无穷大,不好处理。所以实际工程中,我们用极坐标表示:ρ = x·cosθ + y·sinθ。
这里的 ρ 是原点到直线的距离,θ 是直线法线与 x 轴的夹角。每个图像上的边缘点 (x, y),对应参数空间 (ρ, θ) 里的一条正弦曲线。多个点如果共线,它们的正弦曲线就会在参数空间交于一点。这个交点,就是我们要找的直线参数。
我的经验:刚开始做这个的时候,我总纠结于参数空间的离散化精度。θ 步长设 1 度还是 0.5 度?ρ 步长设 1 像素还是 0.5 像素?后来我发现,对于车道线这种比较粗的线条,θ 步长 1 度、ρ 步长 1 像素完全够用。精度太高反而容易检测出很多噪声线段。
4.2 标准霍夫变换 vs 概率霍夫变换
标准霍夫变换(Standard Hough Transform)会把所有边缘点都扔进参数空间去投票。你想想看,一张 1280x720 的图片,边缘点可能有几万个,每个点都要计算所有 θ 对应的 ρ,这计算量可不小。我在早期项目里用 OpenCV 的 HoughLines(),处理一帧图像要 50 多毫秒,实时性根本没法保证。
后来我换成了概率霍夫变换(Probabilistic Hough Transform),效果立竿见影。它的核心思想是:只随机选取一部分边缘点进行投票。只要采样足够随机,统计结果和全量投票几乎一样。
| 对比项 | 标准霍夫变换 | 概率霍夫变换 |
|---|---|---|
| 算法原理 | 所有边缘点参与投票 | 随机采样部分边缘点 |
| 输出结果 | 直线参数 (ρ, θ) | 线段端点 (x1,y1,x2,y2) |
| 计算速度 | 较慢 | 快 3-5 倍 |
| 适用场景 | 离线分析、精度优先 | 实时系统、车道线检测 |
OpenCV 里对应的函数是 HoughLinesP()。我个人习惯用这个函数,因为它直接返回线段的端点坐标,省去了后续从 (ρ, θ) 反算端点的步骤。
import cv2
import numpy as np
# 假设 edges 是 Canny 边缘检测的结果
lines = cv2.HoughLinesP(
edges,
rho=1, # ρ 步长,1像素
theta=np.pi/180, # θ 步长,1度
threshold=50, # 投票阈值,低于此值的直线被忽略
minLineLength=40, # 最小线段长度,过滤短线段
maxLineGap=10 # 同一直线上两点最大间隔,用于连接断开的线段
)
# lines 的格式:[[x1, y1, x2, y2], ...]
避坑指南:我曾经在一条弯道上栽过跟头。概率霍夫变换对弯曲的车道线检测效果很差,因为它本质上是找直线段。弯道上的线段会被切成很多小段,而且方向不一致。后来我加了一个线段聚类和曲线拟合的后处理,才勉强搞定。如果你做的是高速公路这种大曲率弯道,建议考虑滑动窗口或贝塞尔曲线拟合。
4.3 ROI 感兴趣区域提取
为什么要做 ROI 提取?说白了,就是告诉计算机:别瞎看,只盯着路面那一块。
车载摄像头拍到的画面里,有天空、有树木、有对面来车,这些都不是我们关心的。车道线只出现在车辆前方的路面区域。手动设定一个多边形区域,把图像裁剪成梯形或三角形,能大幅减少计算量,同时避免误检。
我一般这样设置 ROI:
def region_of_interest(img, vertices):
"""
对图像应用多边形掩膜
vertices: 多边形的顶点坐标列表,格式 [(x1,y1), (x2,y2), ...]
"""
mask = np.zeros_like(img)
# 填充多边形为白色
if len(img.shape) > 2:
channel_count = img.shape[2]
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
cv2.fillPoly(mask, vertices, ignore_mask_color)
# 与操作,保留 ROI 内的像素
masked_image = cv2.bitwise_and(img, mask)
return masked_image
# 示例:对于 720x1280 的图像,定义梯形 ROI
height, width = 720, 1280
roi_vertices = np.array([
[(200, height), # 左下
(550, 450), # 左上
(730, 450), # 右上
(1100, height) # 右下
]], dtype=np.int32)
roi_edges = region_of_interest(edges, roi_vertices)
这里有个细节:ROI 的顶点坐标需要根据摄像头安装位置和视角来调整。我记得第一次做实车测试时,发现车道线总是偏左,后来发现是 ROI 的梯形没对准车道消失点。调整了上边两个点的 y 坐标后,效果立马好了。
小技巧:ROI 的梯形上边不要画得太窄。留一点余量,给车辆转弯时车道线偏移的空间。我一般让上边宽度占图像宽度的 40%-50%。
4.4 车道线拟合与绘制
霍夫变换检测出来的是一堆散乱的线段。我们需要把它们归类成左车道线和右车道线,然后拟合成完整的直线。
我的做法分三步:
- 按斜率分组:计算每条线段的斜率,斜率为正(从左下到右上)的归为左车道线候选,斜率为负的归为右车道线候选。
- 线段合并:对同一组的线段,计算它们的平均斜率和截距。可以用 RANSAC 算法剔除离群点,效果更好。
- 绘制车道线:根据拟合出的直线方程,从图像底部画到 ROI 顶部。
def draw_lane_lines(img, lines, color=[0, 255, 0], thickness=3):
"""
在图像上绘制车道线
lines: 霍夫变换检测到的线段列表
"""
left_lines = []
right_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
# 计算斜率和截距
if x2 - x1 == 0: # 垂直线,跳过
continue
slope = (y2 - y1) / (x2 - x1)
intercept = y1 - slope * x1
# 根据斜率分类
if slope > 0.3: # 左车道线(正斜率)
left_lines.append((slope, intercept))
elif slope < -0.3: # 右车道线(负斜率)
right_lines.append((slope, intercept))
# 拟合平均直线
def average_line(lines_group):
if len(lines_group) == 0:
return None
avg_slope = np.mean([l[0] for l in lines_group])
avg_intercept = np.mean([l[1] for l in lines_group])
return avg_slope, avg_intercept
left_avg = average_line(left_lines)
right_avg = average_line(right_lines)
# 绘制
y1 = img.shape[0] # 底部
y2 = int(img.shape[0] * 0.6) # ROI 顶部
line_img = np.zeros_like(img)
if left_avg:
slope, intercept = left_avg
x1 = int((y1 - intercept) / slope)
x2 = int((y2 - intercept) / slope)
cv2.line(line_img, (x1, y1), (x2, y2), color, thickness)
if right_avg:
slope, intercept = right_avg
x1 = int((y1 - intercept) / slope)
x2 = int((y2 - intercept) / slope)
cv2.line(line_img, (x1, y1), (x2, y2), color, thickness)
return line_img
这段代码看起来简单,但实际项目中坑不少。比如车辆颠簸时,检测到的线段会突然消失或出现异常斜率。我一般会加一个帧间平滑:用前几帧的拟合结果做加权平均,避免车道线在画面里跳来跳去。
核心要点回顾:
- 霍夫变换的本质是空间映射与投票统计
- 概率霍夫变换是实时系统的首选
- ROI 提取能过滤 70% 以上的无关区域
- 车道线拟合需要分组、合并、平滑三步走
好了,这一章的内容就到这里。霍夫变换虽然经典,但也不是万能的。下一章我们会聊聊更鲁棒的滑动窗口法,专门应对弯道和阴影干扰。不过在那之前,建议你把今天讲的代码跑一遍,亲手调调参数,感受一下不同阈值对检测结果的影响。