尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

智慧物流车道线分割数据集实战:从U-Net训练到路径规划

智慧物流车道线分割数据集实战:从U-Net训练到路径规划 简介智慧物流车道线分割数据集面向从事自动驾驶、AGV导航与智慧物流路径规划的研究者和开发者提供可直接用于模型训练与验证的标注图像资源。数据集包含500余张覆盖不同照明环境的车道线图像并已按训练集与验证集划分可用于车道线检测、语义分割及定位与路径规划等任务。资源包共1482个文件以888个png图像、298个txt标注文件和296个yaml配置文件为主压缩包约155.86MB目录结构清晰便于按训练与验证模块快速检索使用。目前已有301人学习下载适合需要快速搭建车道线分割实验环境、验证算法效果或进行课程实践的中高级读者参考使用。1. 智慧物流车道线分割数据集500 张图背后的路径规划实战做智慧物流项目时最头疼的往往不是模型结构而是找不到贴合实际场景的车道线数据。公开数据集要么是高速路况要么是城市主干道真正在园区、仓库、厂区内部道路这种低速、多遮挡、光照复杂的场景下能用的标注数据少得可怜。这份智慧物流车道线分割数据集就是冲着这个缺口来的——500 多张标注好的图像覆盖不同照明环境已经划分好训练集和验证集直接能喂给分割网络做定位和路径规划。它适合两类人一类是刚接触语义分割、想拿真实场景练手的新手另一类是在物流 AGV、园区配送车上做感知模块、需要快速验证车道线提取效果的工程师。数据集本身不复杂但怎么把它用对、用出效果里面有不少细节值得拆开讲。2. 车道线分割任务拆解从像素级标注到路径可行驶区域2.1 为什么物流场景的车道线分割和公开数据集不一样公开道路数据集里车道线通常是标准白线或黄线曲率平缓视野开阔。物流场景完全是另一回事仓库地面可能是水泥灰、环氧地坪、甚至带反光车道线可能是黄色油漆、白色胶带、或者干脆就是一条磨损严重的旧线。照明条件也极端有的区域是顶棚均匀灯光有的区域是叉车灯直射造成局部过曝还有的角落光线不足导致车道线和地面灰度几乎一致。这份数据集明确标注了“不同照明环境”说明采集时已经考虑了这些变量这对训练一个鲁棒的分割模型非常关键。从任务定义上看车道线分割属于二分类语义分割每个像素要么是车道线要么是背景。但实际落地时我们往往不满足于只分割出线还要进一步做车道线拟合、消失点估计、可行驶区域推断。所以拿到数据集后第一步不是直接跑训练而是先确认标注格式和类别定义。常见做法是看标注文件是 PNG 掩码图还是 JSON 多边形坐标。如果是掩码图像素值 0 代表背景255 代表车道线这种最省事直接可以构造 Dataset 类。如果是多边形坐标就需要自己写脚本转成掩码转换时注意多边形填充的边界处理别让线宽在转换后变细或断裂。2.2 数据集的目录结构与划分逻辑虽然项目正文没有给出具体的目录树但根据“划分为训练集与验证集”这个描述以及 500 多张图的规模合理的组织方式通常是按 8:2 或 7:3 划分。我一般会按下面的结构来整理方便后续用 PyTorch 的 ImageFolder 或自定义 Dataset 加载lane_dataset/ ├── train/ │ ├── images/ # 训练集原图jpg 或 png │ └── masks/ # 对应的标注掩码png 格式单通道 ├── val/ │ ├── images/ │ └── masks/ └── dataset_info.txt # 记录图像数量、分辨率范围、采集设备等这里有个容易翻车的地方训练集和验证集的划分不能随机打乱。因为同一段道路的连续帧如果被分到训练集和验证集两边验证指标会虚高模型实际上没学到泛化能力。正确做法是按采集批次或路段划分比如前 400 张来自 A 仓库后 100 张来自 B 仓库那就把 A 仓库的图做训练B 仓库的图做验证。如果数据集已经划分好了拿到后先检查一下验证集里有没有和训练集高度相似的帧有的话手动剔除。2.3 标注质量检查三个必须做的可视化步骤拿到任何分割数据集直接开训之前我强制自己走一遍可视化检查。这一步花不了十分钟但能避免后面几天的无效训练。import os import cv2 import numpy as np import matplotlib.pyplot as plt # 检查训练集前 5 张图的标注对齐情况 img_dir lane_dataset/train/images mask_dir lane_dataset/train/masks img_files sorted(os.listdir(img_dir))[:5] fig, axes plt.subplots(5, 3, figsize(12, 20)) for i, fname in enumerate(img_files): img cv2.imread(os.path.join(img_dir, fname)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask_name fname.replace(.jpg, .png) # 根据实际命名规则调整 mask cv2.imread(os.path.join(mask_dir, mask_name), cv2.IMREAD_GRAYSCALE) # 原图 axes[i, 0].imshow(img) axes[i, 0].set_title(fImage: {fname}) # 掩码 axes[i, 1].imshow(mask, cmapgray) axes[i, 1].set_title(Mask) # 叠加 overlay img.copy() overlay[mask 127] [255, 0, 0] axes[i, 2].imshow(overlay) axes[i, 2].set_title(Overlay) for ax in axes.flatten(): ax.axis(off) plt.tight_layout() plt.show()这段代码做三件事显示原图、显示掩码、把掩码叠加到原图上。重点看第三个子图如果红色区域和实际车道线位置有偏移说明标注和原图没对齐可能是采集时相机标定变了或者标注时用了不同的分辨率。另一个要看的点是掩码的线宽是否一致有些标注员画线时粗时细这种噪声会让模型学到一个模糊的边界。如果发现某几张图标注明显有问题直接删掉500 张里少几张不影响训练但留着会拉低整体指标。3. 训练分割模型从 U-Net 到损失函数选型3.1 为什么物流车道线分割首选 U-Net 而不是 DeepLab车道线分割有个特点目标细长、占比小、对边界精度要求高。DeepLab 系列用空洞卷积扩大感受野适合大目标分割但在细长结构上容易丢失细节。U-Net 的跳跃连接能把浅层的高分辨率特征直接传到解码器对细线边界的恢复更友好。500 张图的规模也不算大U-Net 参数量适中从头训练不容易过拟合用预训练权重也能快速收敛。我一般会用一个轻量化的 U-Net 变体编码器用 ResNet34 或 MobileNetV2解码器保持标准结构。如果部署在边缘设备上MobileNetV2 更合适推理速度快精度损失在可接受范围内。下面是一个最小可运行的 U-Net 实现编码器部分用双卷积加最大池化解码器用转置卷积加跳跃连接import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, out_ch1): super().__init__() # 编码器 self.enc1 DoubleConv(in_ch, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) # 瓶颈层 self.bottleneck DoubleConv(512, 1024) # 解码器 self.up4 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, out_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return torch.sigmoid(self.out(d1))参数说明in_ch3对应 RGB 输入out_ch1输出单通道概率图配合sigmoid做二分类。如果显存不够把enc4和bottleneck的通道数减半或者把输入分辨率降到 256×256。跳跃连接里的torch.cat是 U-Net 的核心把编码器的高分辨率特征和解码器的上采样结果拼在一起让边界信息不丢失。3.2 损失函数Dice Loss 和 BCE 怎么配比车道线像素占比通常不到 5%用纯 BCE 会让模型倾向于全预测背景因为这样 loss 也能降到很低。常见做法是 BCE 和 Dice Loss 加权组合BCE 负责稳定梯度Dice 负责优化重叠度。我一般用 0.5:0.5 的权重如果发现模型对细线召回率低就把 Dice 权重提到 0.7。class BCEDiceLoss(nn.Module): def __init__(self, bce_weight0.5): super().__init__() self.bce nn.BCELoss() self.bce_weight bce_weight def forward(self, pred, target): bce_loss self.bce(pred, target) # Dice Loss smooth 1e-6 pred_flat pred.view(-1) target_flat target.view(-1) intersection (pred_flat * target_flat).sum() dice_loss 1 - (2. * intersection smooth) / (pred_flat.sum() target_flat.sum() smooth) return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_loss注意smooth不能省否则当预测和标签都全为 0 时会出现除零。另外pred必须经过sigmoid如果模型输出 logits要在 loss 里先做torch.sigmoid(pred)或者改用BCEWithLogitsLoss并把 sigmoid 从模型里去掉避免数值不稳定。3.3 训练循环与验证指标训练时我习惯每 5 个 epoch 在验证集上算一次 IoU 和 Dice这两个指标比 loss 更直观。IoU 对细线分割很敏感如果 IoU 卡在 0.3 上不去大概率是学习率太大或者数据增强太激进。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for imgs, masks in loader: imgs, masks imgs.to(device), masks.to(device) optimizer.zero_grad() preds model(imgs) loss criterion(preds, masks) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def validate(model, loader, device): model.eval() iou_sum, dice_sum, count 0, 0, 0 with torch.no_grad(): for imgs, masks in loader: imgs, masks imgs.to(device), masks.to(device) preds (model(imgs) 0.5).float() intersection (preds * masks).sum() union preds.sum() masks.sum() - intersection iou (intersection 1e-6) / (union 1e-6) dice (2 * intersection 1e-6) / (preds.sum() masks.sum() 1e-6) iou_sum iou.item() dice_sum dice.item() count 1 return iou_sum / count, dice_sum / count阈值 0.5 不是固定的如果验证时发现漏检多降到 0.4误检多提到 0.6。这个阈值最终要结合路径规划模块的需求来定宁可多检一点也不能漏掉车道线否则 AGV 可能直接冲出可行驶区域。4. 避坑与排查标注、显存、过拟合的五个血泪教训4.1 现象训练 loss 正常下降但验证 IoU 始终低于 0.2原因训练集和验证集的光照分布差异太大。数据集虽然覆盖了不同照明但如果划分时没做分层抽样可能出现训练集全是亮光、验证集全是暗光的情况。模型在暗光下完全失效。解决重新划分数据按亮度直方图做分层。简单做法是算每张图的平均灰度排序后交替分到训练和验证集。或者用数据增强把训练集的亮度范围拉宽加随机 gamma 变换和随机对比度。4.2 现象训练到一半 loss 突然变成 NaN原因学习率太大或者 Dice Loss 的除零保护不够。另外如果用了混合精度训练sigmoid 后的值太小可能导致梯度下溢。解决先把学习率降到 1e-4加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果还不行检查输入图像有没有全黑或全白的异常样本这种图会让 loss 计算出现极端值。4.3 现象显存溢出batch size 只能设到 2原因输入分辨率太高。500 张图的原图分辨率可能很大直接 resize 到 512×512 以上U-Net 的中间层特征图会吃掉大量显存。解决训练时用 256×256 或 320×320推理时再恢复到原分辨率。或者把编码器换成 MobileNetV2参数量减少后显存占用能降一半。另一个技巧是用梯度累积batch size 设 2累积 4 次再更新等效 batch size 为 8。4.4 现象模型在训练集上 IoU 0.95验证集只有 0.4原因过拟合。500 张图对 U-Net 来说不算多如果没加数据增强模型会记住训练集的纹理。解决加随机旋转±10 度、随机裁剪、颜色抖动。注意车道线分割不能做水平翻转因为翻转后车道线的左右关系变了如果后续要做路径规划模型学到的空间先验会出错。垂直翻转也要慎用地面和天空的上下文关系会乱。4.5 现象推理时车道线断断续续不连续原因模型对遮挡区域预测置信度低阈值化后出现断点。物流场景里叉车、货物经常挡住车道线这是常态。解决后处理加形态学闭运算把断点连上。或者用连通域分析保留面积最大的几个区域去掉噪点。如果断点太长考虑在训练时加入模拟遮挡的数据增强随机在图上画黑色矩形让模型学会推断被遮挡的部分。5. 从分割掩码到路径规划车道线拟合与可行驶区域生成分割模型输出的是二值掩码但路径规划模块需要的是车道线的数学表达和可行驶区域边界。这一步我一般用多项式拟合加透视变换来做。先把掩码做逆透视变换转到鸟瞰图视角车道线在鸟瞰图里近似平行拟合稳定性更高。import numpy as np import cv2 def fit_lane_polynomial(mask, img_size): 在鸟瞰图上拟合车道线返回左右车道线的多项式系数 h, w img_size # 逆透视变换矩阵根据实际相机参数调整 src np.float32([[w*0.2, h*0.9], [w*0.8, h*0.9], [w*0.6, h*0.6], [w*0.4, h*0.6]]) dst np.float32([[w*0.2, h], [w*0.8, h], [w*0.8, 0], [w*0.2, 0]]) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(mask, M, (w, h)) # 按列统计非零像素找左右车道线 histogram np.sum(warped[w//2:, :], axis0) midpoint w // 2 left_x np.argmax(histogram[:midpoint]) right_x np.argmax(histogram[midpoint:]) midpoint # 滑动窗口拟合 left_fit, right_fit [], [] for side, x_base in [(left, left_x), (right, right_x)]: nonzero warped.nonzero() ys, xs np.array(nonzero[0]), np.array(nonzero[1]) x_current x_base indices [] for window in range(9): y_low h - (window 1) * (h // 9) y_high h - window * (h // 9) x_low x_current - 50 x_high x_current 50 good ((ys y_low) (ys y_high) (xs x_low) (xs x_high)) indices.append(good) if np.sum(good) 50: x_current int(np.mean(xs[good])) indices np.concatenate(indices) if len(indices) 100: fit np.polyfit(ys[indices], xs[indices], 2) if side left: left_fit fit else: right_fit fit return left_fit, right_fit, M这段代码的逻辑是先做逆透视变换把前视图变成鸟瞰图然后用滑动窗口从底部往上搜索车道线像素最后用二次多项式拟合。参数src和dst需要根据实际相机的内参和外参调整如果拟合出来的线明显偏离先检查这两个矩阵。window9表示把图像高度分成 9 段每段单独搜索适合车道线弯曲的情况。margin50是搜索窗口的半宽如果车道线在鸟瞰图里比较宽可以加到 80。拟合出左右车道线后两条线之间的区域就是可行驶区域。把这个区域投影回前视图叠加到原图上就能给路径规划模块提供明确的边界。实际部署时我还会加一个时序平滑用上一帧的拟合结果约束当前帧避免单帧噪声导致车道线跳动。从那以后我每次拿到新的分割数据集都强制自己先跑一遍可视化检查再拿 10 张图过一遍完整推理链路确认从掩码到拟合到可行驶区域没有断点才敢开正式训练。这个习惯帮我省下了至少三次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表