
简介这份资源面向自动驾驶车道线检测方向的初学者与进阶开发者提供一套基于U-Net模型在TuSimple数据集上完成训练与预测的完整工程。包内共15个文件以7个Python脚本为核心涵盖模型定义、数据集加载、标签处理、训练与视频推理等环节另附2个txt说明、2个md文档及avi、mp4演示视频压缩包约7.89MB结构紧凑便于快速上手。已有583人学习下载说明该方案在车道线分割任务中具备一定参考价值。读者可借此理解U-Net收缩路径与扩展路径的对称设计、跳跃连接对边界定位的作用掌握TuSimple数据预处理、交叉熵损失与Adam优化器配置、IoU与Precision等指标评估方法并通过实线、虚线及路面有水等场景的预测视频直观对比模型在弯曲车道、遮挡与光照变化下的表现为后续数据增强、注意力机制引入与超参数调优提供可复现的基线。1. 车道线分割这套 UNet 方案为什么我建议先跑通再谈改进如果你手头正好有一份 TuSimple 数据集想快速验证车道线分割能不能落地这套UNet-lanedetection-pytorch-main是我见过结构最干净、上手成本最低的起点之一。它把训练、推理、视频测试、标签处理拆成了独立脚本train.py、predict.py、test_onvideo.py、process_label.py各管一摊model.py里就是标准 UNet没有花哨的注意力模块也没有一堆需要额外编译的算子。压缩包里还附了实线、虚线、路面有水三种场景的测试视频等于把「训练完到底能不能用」这个问题直接摆在你面前。我见过太多人一上来就想着换 backbone、加注意力、上 Transformer结果连数据管线都没跑通。这套代码的价值恰恰在于它让你先用最小代价看到车道线从原图变成二值掩码的完整链路再决定往哪个方向优化。适合刚接触车道线检测的算法工程师、需要快速出 demo 的自动驾驶感知方向学生以及想拿 UNet 做分割练手但不想被工程细节劝退的开发者。下面我按「数据怎么进、模型怎么出、坑在哪」的顺序拆一遍。2. 数据管线拆解TuSimple 标签怎么变成 UNet 能吃的掩码2.1 为什么车道线检测不能直接拿原图硬训TuSimple 原始标注是 JSON 格式每张图给的是车道线在若干横坐标上的纵坐标点本质是一串离散点不是像素级掩码。UNet 做的是逐像素二分类所以中间必须有一个「把点连成线、再把线画成掩码」的转换步骤。process_label.py干的就是这件事读 JSON、按行采样、用多项式或直线拟合把点串起来、在空白图上画线、保存成灰度图。这一步做不对后面 loss 再调也是白搭。常见做法是固定图像高度为 720、宽度 1280把 y 从 160 到 710 按等间隔取点每个点对应一个 x。如果某条车道线在该 y 处没有标注就跳过。拟合时用二次多项式比直线更稳因为 TuSimple 里有不少弯道。画线宽度一般给 5 到 8 像素太细了正样本太少太粗了相邻车道线会粘连。2.2 标签转换脚本的关键参数下面这段是我从process_label.py里抽出来的核心逻辑改成了更直白的写法方便你对照自己的数据调import json import cv2 import numpy as np # TuSimple 标注里 h_samples 是固定的纵坐标列表 H_SAMPLES list(range(160, 711, 10)) IMG_H, IMG_W 720, 1280 LINE_WIDTH 5 # 画线宽度太细正样本不足太粗会粘连 def polyfit_lane(lane_x, lane_y): 对单条车道线的离散点做二次拟合返回拟合系数 if len(lane_x) 3: return None # 注意x 是自变量还是 y 是自变量取决于你的坐标系 # TuSimple 里 y 是固定的x 随 y 变化所以用 y 拟合 x coeff np.polyfit(lane_y, lane_x, 2) return coeff def draw_lane_mask(json_path, save_path): with open(json_path, r) as f: data json.load(f) for item in data: mask np.zeros((IMG_H, IMG_W), dtypenp.uint8) for lane in item[lanes]: xs, ys [], [] for x, y in zip(lane, H_SAMPLES): if x ! -2: # -2 表示该点无标注 xs.append(x) ys.append(y) coeff polyfit_lane(xs, ys) if coeff is None: continue # 在 y 范围内逐像素画点再连成线 for y in range(min(ys), max(ys) 1): x int(np.polyval(coeff, y)) if 0 x IMG_W: cv2.circle(mask, (x, y), LINE_WIDTH, 255, -1) cv2.imwrite(save_path, mask)逻辑说明H_SAMPLES是 TuSimple 官方给的纵坐标采样点间隔 10 像素覆盖 160 到 710。-2是无效标注的占位符必须过滤掉否则拟合出来的线会飞到图像外面。用np.polyfit时把 y 当自变量、x 当因变量是因为车道线在竖直方向上延伸同一个 y 只对应一个 x反过来会有多值问题。cv2.circle的半径参数控制线宽我一般从 5 开始试如果验证集上 IoU 偏低且预测断断续续就加到 8。2.3 训练集和验证集怎么切TuSimple 官方给了 train_set 和 val_set 的划分但很多人直接混在一起重新切。我的建议是如果你要跟论文比指标就用官方划分如果只是自己验证效果按 8:2 随机切也行但要注意同一段连续视频的帧不能同时出现在训练和验证里否则验证指标会虚高。dataset.py里一般会继承torch.utils.data.Dataset在__getitem__里同时返回原图和掩码原图做归一化掩码转成 0/1 的 float 张量。提示掩码保存成 PNG 时像素值 255 表示车道线0 表示背景。读进来后记得除以 255否则 BCE loss 会直接爆炸。3. UNet 模型搭建与训练从 model.py 到 train.py 的完整链路3.1 UNet 的收缩路径和扩展路径到底在干什么model.py里的 UNet 是经典的四层下采样加四层上采样。收缩路径每层两个 3x3 卷积、BN、ReLU然后 2x2 最大池化通道数从 64 翻到 128、256、512。扩展路径每层先上采样再和收缩路径对应层的特征做通道拼接然后又是两个 3x3 卷积。最后 1x1 卷积把通道压到 1输出和输入同尺寸的 logits。这套结构对车道线的优势在于跳跃连接浅层特征保留了边缘和纹理深层特征保留了语义拼接后既能定位细线又能区分车道线和路面标线。但缺点也明显四层下采样后特征图只有原图的 1/16细车道线在深层几乎消失所以 UNet 对虚线、远距离车道线的召回率通常不如带空洞卷积的 DeepLab 系列。这也是为什么后面要讲数据增强和损失函数调优。3.2 训练脚本里必须改的几个参数train.py和config.py配合使用config.py里放超参train.py里跑循环。下面是我一般会动的几个配置项# config.py 里我常改的参数 BATCH_SIZE 4 # 显存 8G 以下别超过 4UNet 输入 720x1280 很吃显存 LR 1e-4 # Adam 初始学习率太大 loss 会震荡太小收敛慢 EPOCHS 50 # TuSimple 大概 30 轮后验证 IoU 就趋于平稳 WEIGHT_DECAY 1e-5 # 防止过拟合但别给太大否则欠拟合 POS_WEIGHT 10.0 # 正样本权重车道线像素占比不到 5%必须加权POS_WEIGHT是这套代码里最容易被忽略但影响最大的参数。车道线像素在整张图里占比极低如果 BCE loss 不加权模型会倾向于全预测背景准确率看着很高但 IoU 接近 0。常见做法是用torch.nn.BCEWithLogitsLoss(pos_weighttorch.tensor([POS_WEIGHT]))或者换成 Dice Loss、Focal Loss。我一般先用加权 BCE 跑一版如果虚线断裂严重再换 Dice。3.3 训练循环里看什么指标train.py一般每轮打印训练 loss每隔几轮在验证集上算一次 IoU。这里有个坑验证时不要用model.eval()就完事还要把输出经过 sigmoid 再二值化阈值通常取 0.5但车道线任务里 0.3 到 0.4 往往召回更好。我习惯在验证脚本里同时算 Precision、Recall、F1 和 IoU只看 IoU 容易被高准确率误导。# 验证阶段计算 IoU 的简化写法 model.eval() with torch.no_grad(): for img, mask in val_loader: img, mask img.cuda(), mask.cuda() logits model(img) pred (torch.sigmoid(logits) 0.4).float() # 阈值 0.4 偏召回 inter (pred * mask).sum() union pred.sum() mask.sum() - inter iou inter / (union 1e-6)逻辑说明阈值从 0.5 降到 0.4 是为了让更多弱响应像素被判为正减少虚线断裂。1e-6防止除零。如果你发现 Precision 掉得厉害但 Recall 涨了说明模型在乱猜这时候要回去检查正样本权重是不是给太大了。3.4 数据增强在车道线任务里的边界翻转、裁剪、颜色抖动是常规操作但车道线有它的特殊性。水平翻转会把左车道线变右车道线语义上没问题可以用。垂直翻转绝对不能用因为车道线永远在图像下半部分。随机裁剪要小心裁到只剩天空的区域掩码全黑这种样本多了模型会学偏。颜色抖动里亮度变化可以大一点因为 TuSimple 有隧道和强光场景但色相变化要小否则车道线颜色会失真。4. 推理与视频测试predict.py 和 test_onvideo.py 怎么用4.1 单张图推理的输出后处理predict.py加载 checkpoint 后对单张图前向传播输出 logits 经过 sigmoid 和阈值化得到二值掩码。但直接输出的掩码往往有噪点和小连通域我一般会加一步形态学开运算去掉孤立像素再用cv2.findContours找连通域面积小于 100 像素的直接丢掉。如果要做车道线拟合还可以对每个连通域按行取中心点再多项式拟合出平滑曲线。import cv2 import numpy as np def postprocess(mask, min_area100): 去掉小噪点保留主要车道线区域 kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) clean np.zeros_like(mask) for cnt in contours: if cv2.contourArea(cnt) min_area: cv2.drawContours(clean, [cnt], -1, 255, -1) return clean逻辑说明开运算先腐蚀后膨胀能去掉比结构元小的亮斑。min_area根据你的分辨率调720p 下 100 是个保守值如果车道线本身很细可以降到 50。drawContours的最后一个参数 -1 表示填充整个轮廓这样输出还是实心线。4.2 视频测试脚本的帧间稳定性test_onvideo.py逐帧读视频、推理、写输出视频。这里最大的问题是帧间闪烁同一车道线在相邻帧的掩码可能时有时无。常见做法是加一个简单的跟踪或平滑比如用上一帧的掩码和当前帧做加权平均或者对拟合出的车道线参数做卡尔曼滤波。压缩包里的实线.avi、虚线.avi、虚线_路面有水.mp4正好覆盖了三种典型场景你可以直接拿它们对比后处理前后的效果。注意视频写出的帧率要和原视频一致否则播放速度会变。用cv2.VideoWriter时四个参数分别是输出路径、编码器、帧率、分辨率编码器在 Linux 下常用cv2.VideoWriter_fourcc(*mp4v)。4.3 三种测试视频分别暴露什么问题实线.avi主要看连续性和边缘贴合度如果实线中间出现断裂说明正样本权重不够或者阈值太高。虚线.avi看召回虚线本身有间隔模型容易把间隔也判成背景导致虚线变短。虚线_路面有水.mp4是最难的水面反光会产生大量高亮区域和车道线特征相似模型容易误检。我一般会针对水面场景单独做一轮亮度归一化或者加一些带反光的合成数据做增强。5. 避坑与排查这套代码跑不起来时先看这几条5.1 现象训练 loss 一直不降验证 IoU 始终为 0原因最常见的是掩码读取时没有归一化像素值还是 0 和 255BCE loss 计算时 logits 和 target 量级不匹配。另一个可能是pos_weight没设模型全预测背景。解决在dataset.py的__getitem__里确认mask mask / 255.0并且mask mask.unsqueeze(0)变成单通道。然后在train.py里打印一下正样本占比如果低于 2%必须加pos_weight。5.2 现象显存溢出batch size 降到 1 还是 OOM原因UNet 输入 720x1280 时第一层特征图就是 720x1280x64显存占用很大。如果还在用 32 位浮点8G 显存基本跑不动 batch 2。解决把输入 resize 到 360x640 再训或者用混合精度torch.cuda.amp。我一般先用 360x640 快速验证管线确认能跑通再上全分辨率。另外检查model.py里有没有不必要的中间变量没释放。5.3 现象验证集 IoU 很高但视频测试效果很差原因训练集和验证集如果来自同一段连续视频帧间高度相似验证指标会虚高。另外如果验证时用了 0.5 阈值而视频测试用了 0.3结果也会不一致。解决按视频片段切分数据集确保验证集里的场景在训练集里没出现过。视频测试时把阈值、后处理参数和验证阶段对齐最好写一个统一的inference.py供两边调用。5.4 现象虚线预测断断续续实线边缘毛刺多原因虚线断裂通常是正样本权重不够或阈值太高实线毛刺多是上采样时棋盘效应或者掩码画线太细导致边缘像素不稳定。解决虚线场景把pos_weight从 10 加到 20阈值降到 0.35。实线毛刺可以在后处理里加高斯模糊再二值化或者把画线宽度从 5 加到 7让边缘更平滑。5.5 现象换自己的数据集后 loss 变成 NaN原因自己的标注里可能有空掩码全黑或者全白掩码计算 loss 时出现 log(0)。另外如果图像路径里有中文或空格cv2.imread会返回 None后续操作直接崩。解决在dataset.py里加过滤跳过掩码全黑或全白的样本。图像读取统一用cv2.imdecode(np.fromfile(path, dtypenp.uint8), -1)处理中文路径。loss 计算前加torch.clamp防止极端值。6. 进阶技巧用日志和 checkpoint 做快速消融实验logs和checkpoints这两个目录是这套代码里最值得深挖的部分。checkpoints里一般保存best_model.pth和每轮的epoch_xx.pth我习惯在文件名里带上验证 IoU比如unet_epoch30_iou0.62.pth这样后面做模型融合时不用一个个加载去测。logs里如果用的是 TensorBoard可以同时开多个实验对比 loss 曲线和验证指标比在终端里翻打印快得多。做消融实验时我一般固定随机种子只改一个变量跑 10 轮看验证 IoU 的走势。比如对比pos_weight10和pos_weight20如果 10 轮后前者 IoU 高 2 个点那就没必要再试 30。另一个技巧是冻结收缩路径的前两层只训后面和扩展路径在小数据集上能防止过拟合训练速度也快一倍。# 启动 TensorBoard 对比多个实验 tensorboard --logdirlogs --port6006 # 训练时指定不同的实验名 python train.py --exp_nameunet_pos10 --pos_weight10 python train.py --exp_nameunet_pos20 --pos_weight20逻辑说明--exp_name会在logs下建不同子目录TensorBoard 会自动把多条曲线画在一起。--pos_weight通过argparse传进config.py避免每次手动改文件。这套流程跑顺之后你换 backbone、加注意力模块、换损失函数都能用同样的方式快速验证不会陷入「改了不知道有没有用」的玄学循环。从那以后我每次拿到新的分割代码都强制先跑一遍原始配置把 baseline 的 IoU、Precision、Recall 和视频效果记下来再动任何一行模型代码。希望帮到你。本文还有配套的精品资源点击获取