ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

车道线检测高效方案:12行×80网格的行分类模型详解

车道线检测高效方案:12行×80网格的行分类模型详解 简介一套基于Python实现的车道线检测模型源代码及使用说明包面向自动驾驶、智能交通方向的开发者也适合希望掌握视觉检测流程的中高级学习者。模型把图像下部车道线区域划分成十二行、每行八十个网格将车道线识别转化为逐行网格分类任务可同时输出左侧两条与右侧两条车道线的位置这种设计在保留可接受精度的同时明显提高了计算与部署效率。压缩包内共有十一个文件包括模型训练与评估脚本、训练评估配置文件、依赖清单、中英文说明文档和示例图片整体仅451KB目录结构简洁清晰便于直接阅读、复现与二次改造。目前已有100人学习下载。通过源码和配套说明使用者能够理解车道线的网格化建模思路掌握从环境配置、数据加载到模型运行和结果解读的完整流程并可进一步针对光照变化、车道磨损等真实场景开展优化实验。1. 车道线检测为什么被做成了“12行×80网格”的分类跑过真实道路视频的人应该有同感车道线检测在嵌入式设备上的瓶颈从来不是精度指标而是帧率。像素级分割网络在 GPU 上能跑到几十毫秒换到车载边缘设备上就要翻几倍高速场景下根本来不及给控制模块留出反应时间。这套基于 Python 的源代码走了一条更省的路——不逐像素分割而是把图像下部可能存在车道线的区域固定划分为 12 行、每行 80 个网格让模型学习“这一行的车道线落在哪个网格里”。检测问题被简化成分类问题左侧两条、右侧两条车道线每条线在每一行输出一个 0 到 79 的类别。结果是前向计算量大幅下降精度损失却控制在可接受范围内。适合那些想在 Jetson、树莓派这类低功耗硬件上跑车道线检测的开发者也适合拿来快速验证行分类思路的算法工程师。2. 模型结构拆解网格划分、骨干网络与四线分类头2.1 为什么分割网络不值得优先考虑传统的车道线分割方案输出的是像素级 mask每个像素都要预测“是否属于车道线”然后还要后处理做透视变换、聚类才能得到完整线的坐标。模型参数量大推理耗时就压在卷积层和高分辨率特征图上。而“行分类”思路把问题降了一个维度车道线在水平方向上是连续的垂直方向没必要每个像素都预测。只要在每行选一个网格再把 12 个点连起来就足够还原车道线的走向了。代价是曲线特别剧烈的弯道会损失细节但对绝大多数高速公路和城市道路场景12 行采样已经够用。2.2 检测区域只保留图像下部的感兴趣区模型不处理整张图只处理图像下半部分。理由很简单车道线不可能出现在天空区域上半部分对检测任务几乎没有贡献。代码里通过裁剪把 ROI 截出来再 resize 到固定尺寸。下面的配置示例是常见做法配合示例图 examples.jpg 能直观看到裁剪前后的差异import cv2 def extract_roi(image, h_ratio0.5): # 保留图像下半部分h_ratio控制截取比例 h, w image.shape[:2] roi image[int(h * (1 - h_ratio)):, :] return roih_ratio0.5表示截取下半 50%这个值不是固定的。如果摄像头安装角度高、视野远可以放大到 0.6如果距离车头很近、只关心近处车道线就缩小到 0.4。截取之后再输入网络一方面减少计算量另一方面也避免了背景像素对分类的干扰。2.3 骨干网络选型轻量 CNN 提取共享特征整个模型由两部分组成共享的骨干网络和四组分类头。骨干网络负责提取纹理、边缘、颜色变化等基础特征分类头在共享特征上分别预测四条车道线的位置。常见的做法是直接用 ResNet18 或更轻的 MobileNet 系列去掉最后的全连接层把输出特征图保持在一个适中的分辨率。项目中的 models.py 在这个环节的可读性做得比较好结构大致如下import torch import torch.nn as nn import torchvision.models as models class LaneClassifier(nn.Module): def __init__(self, num_row12, num_col80, num_lane4): super().__init__() backbone models.resnet18(pretrainedTrue) # 去掉最后的池化和全连接层 self.features nn.Sequential(*list(backbone.children())[:-2]) self.classifiers nn.ModuleList([ nn.Linear(512, num_col) for _ in range(num_lane * num_row) ]) def forward(self, x): feat self.features(x) # (B, 512, H, W) feat feat.mean(dim[2, 3]) # 全局平均池化 - (B, 512) out [] for cls in self.classifiers: out.append(cls(feat)) # 整理成 (B, num_lane, num_row, num_col) out torch.stack(out, dim1).view( x.size(0), 4, 12, 80) return out这里把四个车道线的每个行位置都当作独立分类任务nn.ModuleList里共有 4×1248 个线性分类器每个分类器输出 80 类的 logits。feat.mean(dim[2,3])把特征图全局平均池化成一维向量再分别送入各分类器。这样的写法优点是结构透明训练时哪条线哪一行收敛有问题可以直接定位缺点是分类器之间互不共享参数如果训练数据少容易出现某一行拟合不足。2.4 输出结构与推理时的 argmax 操作模型输出形状是(B, 4, 12, 80)含义分别是 batch 大小、车道线编号、行号、网格类别。推理时对最后一维做 argmax得到的就是每条线在每个行位置上的网格索引。下面把输出结构整理成了一张表维度大小含义batchB一次处理的图像数lane4左1、左2、右1、右2row12从上到下 12 个行位置col80每行 80 个网格的类别推理代码只需要一行pred torch.argmax(logits, dim-1)得到的(B, 4, 12)张量就是每条线在每个行位置的网格坐标。后续可视化或计算误差都用这个张量。注意这里没有用 softmax因为 argmax 不依赖 softmax分类数大了之后 softmax 反而会引入不必要的数值开销。3. 从零跑通训练配置、数据集加载与训练入口3.1 环境准备与依赖安装模型基于 PyTorch先确认本机 Python 环境。推荐用 Python 3.8 以上版本安装完 Python 后直接用 pip 安装依赖即可。项目根目录下的 requirements.txt 已经把关键库列好了pip install -r requirements.txt典型依赖包括torch、torchvision、opencv-python、pyyaml、numpy。前两个负责模型和训练opencv 用来做图像读取和可视化pyyaml 用于解析配置文件。如果机器有 NVIDIA GPU建议提前装好 CUDA 版本的 PyTorch没有 GPU 也能训只是慢一些用 CPU 跑 50 个 epoch 可能需要几小时。装库遇到版本冲突时优先保证 torch 和 torchvision 版本匹配即可。3.2 train.yaml 配置逐项说明configs/train.yaml 是训练入口的配置中心模型怎么建、数据怎么读、训练怎么迭代全在这里控制。下面是一个符合模型结构的最小配置示例data_root: ./data input_width: 800 input_height: 320 num_row: 12 num_col: 80 num_lane: 4 backbone: resnet18 pretrained: true batch_size: 16 epochs: 50 learning_rate: 0.001 lr_decay_step: 20 lr_decay_gamma: 0.1 model_path: ./checkpointsinput_width和input_height是网络输入的宽高num_row、num_col、num_lane对应前文提到网格参数。这几个值改任何一个模型输出形状都会跟着变。pretrained: true表示加载 ImageNet 预训练权重用中小数据集训练时能明显加快收敛。lr_decay_step和lr_decay_gamma控制学习率每多少轮衰减多少配合models.py里的分类头参数一起调整。3.3 dataset.py 中的数据加载与标签生成训练数据需要图像和标签配对。图像是摄像头拍到的道路照片标签是每条车道线在每个行位置上的网格索引。原始标注通常是 x 坐标dataset.py 中的核心工作就是把 x 坐标转换成网格编号。一个可用的实现思路如下import numpy as np def xy_to_grid(x, y, roi_top, img_h, num_row, num_col, lane_width, grid_width): # 先判断该行是否有车道线 if x 0 or y roi_top: return -1 # -1表示该行无车道线 # 计算落在哪个行、哪个网格 row_idx int((y - roi_top) / ((img_h - roi_top) / num_row)) col_idx int(x / grid_width) return row_idx, col_idx注意这里的-1很关键。车道线不是每条线 12 行都有值近处可能两条线同时存在远处可能其中一条已经超出 ROI 边界。标签里用 -1 表示缺失训练时对应位置不参与损失计算。项目提供的使用说明里建议先跑一个可视化脚本检查标签是否对齐这是非常必要的坐标换算出错很容易直接导致训练不收敛。3.4 训练入口 train.py 的核心逻辑train.py 里训练循环本身不复杂复杂的是损失计算。由于每行是独立分类常见的做法是对每个有效位置计算交叉熵再求平均import torch import torch.nn.functional as F def lane_loss(pred, label): # pred: (B, 4, 12, 80), label: (B, 4, 12) total_loss 0.0 valid_cnt 0 for b in range(pred.size(0)): for l in range(4): for r in range(12): if label[b, l, r] 0: total_loss F.cross_entropy( pred[b, l, r].unsqueeze(0), label[b, l, r].unsqueeze(0) ) valid_cnt 1 return total_loss / max(valid_cnt, 1)三层循环写起来直观实际跑的时候建议改成向量化实现。优化器一般选 Adam 或 SGDSGD 配合 momentum 在车道线这种分类任务上收敛更稳optimizer torch.optim.SGD( model.parameters(), lr0.001, momentum0.9, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.StepLR( optimizer, step_size20, gamma0.1 )每 20 个 epoch 学习率乘以 0.1前 20 个 epoch 大步探索后面 30 个 epoch 精细收敛。训练到后期如果 loss 波动明显多半是学习率退得太快把lr_decay_gamma改成 0.3 试试。4. 训练调优行数、网格数与损失函数的边界4.1 网格超参数对精度和速度的权衡num_row12、num_col80只是初始选择不是最优解。行数决定垂直采样密度网格数决定水平定位精度。行数太多会显著增加分类器数量从 12 行增到 24 行模型参数几乎翻倍网格数太多会让某些网格在数据集中出现频率过低导致分类器偏向多数类。下面是不同组合同等条件下的大致表现行数网格数直线场景误差弯道场景误差推理耗时1280约 0.5 网格约 1.2 网格约 8 ms12160约 0.3 网格约 0.8 网格约 9 ms2480约 0.4 网格约 0.9 网格约 12 ms24160约 0.2 网格约 0.6 网格约 14 ms从表格能看出来直接翻倍网格数比翻倍行数性价比更高因为行数翻倍会同时增加分类器和采样点。弯道场景下 12 行确实会漏掉部分转弯信息如果测试集里弯道占比高优先把行数加到 18 而不是 24能在速度和精度之间找到更好的平衡点。4.2 样本不平衡缺失行的处理训练过程中最常见的问题不是车道线检测不出来而是某一行的分类器学偏了。比如摄像头视野中右侧第二条车道线在近距离经常被前车挡住导致靠下的几个行位置大量出现 -1 标签。分类器会把所有样本都预测成“无车道线”来降低损失因为这样做几乎不会受到惩罚。解决思路是给有效样本加权或者干脆限制 -1 样本在 batch 中的比例class_weight torch.ones(num_col) # 80类 class_weight[0] 0.1 # 假设类别0表示无车道线 criterion nn.CrossEntropyLoss(weightclass_weight)把无车道线类别的权重调到 0.1 之后分类器不再偷懒必须从剩余 79 个类别中找出真正的网格位置。如果数据严重倾斜权重可以压到 0.05但不要直接设成 0否则推理时无车道线的行会无意义地输出一个随机网格。4.3 损失函数变体相邻行连续性约束独立分类 12 个行位置的另一个问题是预测结果可能出现行间抖动上一行预测网格 20下一行跳到网格 35形成不自然的折线。对车道线来说相邻行之间的水平位移是有限的可以加一个平滑正则项。常见的做法是把行间预测差值的绝对值累加进损失def smoothness_loss(pred_row_idx): # pred_row_idx: (B, 4, 12)非-1的位置才有意义 diff pred_row_idx[:, :, 1:] - pred_row_idx[:, :, :-1] return torch.abs(diff).mean()这条正则项会让模型倾向输出水平连续的车道线弯道处也不会出现折线跳变。需要说明的是这个损失直接作用在 argmax 结果上不可导工程上更稳妥的做法是作用在概率分布上比如计算相邻行概率分布的 KL 散度。项目原版代码里没有内置这个损失需要自己加到 train.py 的total_loss中。4.4 过拟合与欠拟合的判据中小规模数据集训练时过拟合很容易出现。判断标准看验证集准确率和训练集准确率的差距当训练集准确率超过 95% 而验证集只有 70% 时优先尝试降低 backbone 的参数量把 ResNet18 换成 MobileNet。欠拟合则表现为训练和验证准确率都偏低且接近说明模型容量不够或学习率太低先调大学习率到 0.003再看 loss 曲线是否下降。还有一种特殊情况是数据本身有问题即训练图像和标签错位这时 loss 会长期在某个高位震荡不降先用可视化脚本逐张检查标注是最快的排查手段。5. 推理与验证eval.py 结果解析、误差统计与可视化5.1 eval.py 与 eval.yaml 的使用训练完成后用 configs/eval.yaml 指定模型权重和测试集路径运行 eval.py 会逐张图输出预测结果和误差指标。eval.yaml 关键字段如下weight_path: ./checkpoints/best.pth test_root: ./data/test save_vis: true vis_dir: ./outputs执行命令是python eval.py --config configs/eval.yaml评估指标除了分类准确率更重要的是平均像素误差即预测网格中心点与真实标注 x 坐标的欧氏距离。网格索引转换为像素坐标后误差小于 5 个像素视为合格。eval.py 会统计每条车道线的平均误差、最大误差和整体合格率输出到终端。如果某条线的平均误差明显高于其余三条先看是不是该类别的训练样本太少或者是 ROI 截取范围让它总是超出图像边界。5.2 把网格索引还原成可视化车道线原始输出是 12 个离散点直接画在图像上会显得很碎。常见做法是先把网格索引换算成像素坐标再用多项式拟合连成一条平滑曲线。下面是一段可直接运行的绘制代码import cv2 import numpy as np def draw_lanes(img, pred_idx, num_row12, num_col80): h, w img.shape[:2] roi_h int(h * 0.5) grid_width w / num_col row_step roi_h / num_row canvas img.copy() for lane_id in range(4): pts [] for r in range(num_row): c pred_idx[lane_id, r] if c 0 or c num_col: continue x int(c * grid_width grid_width / 2) y int(roi_h r * row_step row_step / 2) pts.append((x, y)) if len(pts) 2: continue pts np.array(pts, dtypenp.int32) # 对x坐标做二次多项式拟合平滑 fit np.polyfit(pts[:, 1], pts[:, 0], 2) xs np.polyval(fit, pts[:, 1]).astype(np.int32) for i in range(len(pts) - 1): cv2.line(canvas, (xs[i], pts[i, 1]), (xs[i1], pts[i1, 1]), (0, 0, 255), 3) return canvas多项式拟合用的是np.polyfit(pts[:, 1], pts[:, 0], 2)因为车道线在图像坐标系中是随 y 变化而变化的曲线所以以 y 为自变量、x 为因变量拟合。二次多项式对直线和缓弯都够用如果遇到高架匝道这种急弯改成三次多项式np.polyfit(..., 3)能减少拟合误差。绘制时把坐标换算回原图坐标系注意 ROI 裁剪时的偏移量要加回去否则画出来的车道线会整体上移半个图像高度。5.3 排错预测结果异常时先检查哪里eval.py 跑出来结果不对时不要直接怀疑模型结构按优先级排查第一步看输入图像是否被正确 resize宽高比拉伸严重会让车道线水平位置整体偏移第二步看网格标签是否和实际分辨率匹配输入宽度改过而标签文件没重新生成误差会恒定偏大第三步看num_row和num_col是否与训练时一致推理时如果配置文件里这两个值被改动输出的网格索引含义就完全变了。在 eval.py 里加一个断点观察pred_idx的值域如果大多数预测都是 79 或 0那基本可以判断是配置不一致或标签换算错误而不是模型没训练好。本文还有配套的精品资源点击获取
返回列表