
做智慧交通和工地监管这类项目我最大的体感是算法反而是最不花心思的部分真正磨人的永远是数据。就拿卡车运输检测来说业务方很少只满足于“检测出卡车”这个结果他们真正想问的是这辆车现在是满载还是空车车厢里装的是沙土、袋装料还是建材车牌能不能稳定读取这个问题拆开来看就是一个典型的多类别目标检测任务而支撑它的无非是数据集、标注格式和训练策略这三件事。最近我整理了一份“智慧交通卡车运输车辆车厢状态满载空车车牌沙土袋子建筑材料检测数据集”一共696张图、8个类别同时提供VOC和YOLO两种标注格式。这篇文章就把这个数据集从设计思路到格式细节再到训练踩坑完整拆开讲一遍希望能给正在做类似项目的朋友省点时间。1. 这个数据集到底解决什么问题1.1 一句话理解项目用一个场景就能说清楚某施工单位的运输车辆出入口需要自动判断一辆进场的卡车是否合规。合规的定义很具体包括车辆是否空车返场、车厢是否满载、运输物料是否为沙土或袋装材料、车牌是否清晰可读。过去靠人工盯监控一天几百辆车根本看不过来换成检测模型后这些信息可以逐帧自动提取。这个数据集的定位就是给这类场景提供训练底料。696张图像覆盖白天、傍晚、出入口、堆场多个视角标注了8类目标既包含车辆整体与状态也包含车牌这种小目标还包含沙土、袋子、建筑材料这类载货内容。VOC和YOLO两种格式并存意味着不管你是习惯XML标注的老手还是直接用txt训练YOLO系列模型的新手拿到手都能直接开工。1.2 8个类别为什么这样设计很多第一次看到这个数据集的人会问卡车检测为什么要分这么多类直接检测卡车、再做一个分类模型判断状态不行吗理论上可以但在实际项目中单阶段多类别检测更实用因为一次推理就能同时拿到车辆位置、车厢状态、货物种类的信息下游做逻辑判断时更省事。我按业务目标把8个类别拆成三组车辆与状态组truck_loaded满载卡车、truck_empty空车卡车这两个类别解决“车辆在哪里、状态如何”的问题。载货内容组sand_soil沙土、bag袋子/袋装料、building_material建筑材料这三个类别解决“车上拉的是什么”的问题。注意它们不一定只在车厢内标注有些图里卸货区堆放的沙土堆、建材堆也被标了出来这样模型对“物料”这个概念的理解会更全。辅助定位组license_plate车牌、wheel车轮、truck整车外框。车牌是典型小目标单独标注后可以配合识别模块读取号牌车轮和整车外框则用于辅助判断车辆朝向与位置也能在满载/空车分类不确定时作为补充特征。为什么要有独立的truck类别而不是只用truck_loaded和truck_empty因为有少量图像里车辆被遮挡严重只露出车头或车尾一小部分无法可靠判断车厢状态。这时强行标满载或空车反而会污染标签标成truck才是最诚实的做法。这个设计看似细节实际训练时能明显减少误检。1.3 适合谁用、能省多少事如果你是做以下方向的人这份数据值得重点参考智慧交通、工地出入口管理、渣土车监管类项目需要快速验证检测方案想训练一个多类别货车检测模型但自己采集标注成本太高对VOC与YOLO格式转换、小目标检测、类别不均衡处理感兴趣的人。从零采集标注一个696张、8类别的数据集光采集就要跑好几个施工场地标注更是按框计费人工加协调的时间成本基本按周算。这个数据集等于把最脏最累的部分替你做了你拿到的是一份可以直接训练、可以二次增补的干净标注集。2. 数据集细节与标注规范2.1 696张图从哪里来、内容分布如何图像来源不是单一视角这一点很关键。如果所有图都来自同一个固定机位模型会很容易过拟合到那个固定角度换个现场就废。这个数据集的采集场景覆盖了三个方向出入口闸机视角车辆正对或斜对镜头车厢状态和车牌都比较清楚施工场地内部视角车辆侧方停放或正在卸料适合检测沙土堆、袋子、建材堆放区道路行驶视角车辆在移动中存在一定运动模糊用来模拟真实抓拍环境。696张的总量不大但类别分布并不极端失衡。粗略统计truck和truck_loaded的样本占比最高wheel和license_plate次之bag、sand_soil、building_material相对少一些这符合真实场景的频次规律。训练时不能直接拿原始分布硬训否则少样本类别容易欠拟合这一点我放到后面讲。2.2 VOC与YOLO两种格式的标注细节这个数据集同时提供VOC和YOLO两种标注本质是同一批标注导出的两种文件格式而不是两套不同的标注结果。VOC格式是Pascal VOC标准的XML文件结构大概是这样的annotation foldertrain/folder filenameimg_0042.jpg/filename size width1920/width height1080/height depth3/depth /size object nametruck_loaded/name bndbox xmin120/xmin ymin300/ymin xmax1500/xmax ymax820/ymax /bndbox /object object namelicense_plate/name bndbox xmin1260/xmin ymin520/ymin xmax1440/xmax ymax590/ymax /bndbox /object /annotationYOLO格式则是纯文本的txt文件每行一个目标依次是类别ID、归一化中心点x、中心点y、归一化宽度w、归一化高度h全部是0到1之间的小数0 0.421875 0.518519 0.718750 0.481481 2 0.703125 0.513889 0.093750 0.064815这里类别ID必须与类别列表严格对应比如我把类别顺序定为truck_loaded0、truck_empty1、license_plate2、sand_soil3、bag4、building_material5、wheel6、truck7。如果你拿到数据集后自定义了类别顺序记得同步修改所有txt的第一列数字否则训练时会出现“目标类别标签错乱”的诡异现象。2.3 类别映射与坐标对照表类别名语义VOC标签YOLO类别ID典型框大小检测难度truck_loaded满载卡车truck_loaded0较大中truck_empty空车卡车truck_empty1较大中license_plate车牌license_plate2小高sand_soil沙土/土堆sand_soil3中/大中bag袋装物料bag4中低building_material建筑材料building_material5中中wheel车轮wheel6中低truck整车外框truck7大低从表里能看出来车牌是唯一一个“小目标”也是最容易出问题的类别。模型如果输入分辨率不够或者图片里车牌被遮挡很容易漏检。这一点我在第五部分展开讲。3. 从VOC到YOLO转换脚本与格式检查3.1 转换脚本实现虽然数据集已经同时给了两种格式但你自己做数据增补时难免会遇到只有VOC标注的情况所以转换脚本是刚需。第一次接触格式转换的人建议直接用下面的脚本基于Python标准库就能运行不需要额外装包import os import xml.etree.ElementTree as ET class_id_map { truck_loaded: 0, truck_empty: 1, license_plate: 2, sand_soil: 3, bag: 4, building_material: 5, wheel: 6, truck: 7 } def voc_to_yolo(xml_file, out_txt): tree ET.parse(xml_file) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_id_map: continue class_id class_id_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) box_w max(xmax - xmin, 1.0) box_h max(ymax - ymin, 1.0) x_center (xmin box_w / 2.0) / width y_center (ymin box_h / 2.0) / height norm_w box_w / width norm_h box_h / height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines))这段代码有两点需要注意一是坐标全部做归一化转换成0到1范围的小数二是对宽高做了最小值保护防止异常标注中出现“宽为0的框”这类坏数据在训练时会导致损失直接变NaN。3.2 坐标归一化与边界处理很多人栽在YOLO格式的“归一化”上以为直接把像素坐标除以宽高就行。实际还要注意边界值处理如果标注框恰好贴到图片边缘计算出的x_center可能等于0.9999而width可能大到把中心点挤到图片外面。稳妥的做法是在转换后检查每个值是否在合理区间x_center、y_center、norm_w、norm_h都必须大于0且小于等于1如果出现坐标越界大概率是原始标注框本身出了问题要回头检查VOC文件。另外YOLO格式里没有图片宽高的显式信息所以训练时数据加载器必须从图片文件本身读取尺寸。如果图片被预处理程序缩放或裁剪过但txt里的归一化坐标没有同步重新换算模型就会在错位坐标上训练结果自然是灾难性的虚框乱飘。3.3 格式自检三板斧拿到任何数据集包括这份我建议你做的第一件事不是开训练而是先花十分钟验证数据一致性。我每次都会做三个检查第一步图片与标注文件一一对应。遍历所有图片文件确认每张图都有同名的txt或xml没有孤儿标注也没有缺标注的图。第二步锚框可读性抽查。随机挑几张图把标注框画回原图肉眼看一遍类别和位置对不对。这一步最朴素但最有价值——很多标签错误是肉眼一眼就能看出来的。第三步类别与尺寸分布统计。用脚本打印每个类别的框数量、平均宽高、最小最大尺寸。这一步能快速暴露“某些类比价少”“某些框全是全图大小”之类的异常。这三板斧做完再训练能避开至少一半的“训练结果莫名其妙”问题。4. 数据规模这么小怎么训练出可用模型4.1 数据增强与样本均衡696张图放在深度学习这个语境下属于典型的小规模数据集。直接硬训练大模型几乎必定过拟合。我的做法是“增强拉量 抽样均衡”两手同时抓。增强策略上YOLO系列自带mosaic、平移、缩放、翻转、HSV扰动等能力建议打开mosaic和数据扰动。但要注意满载卡车和空车卡车在进行水平翻转时类别不会改变这个没问题不过车牌文字在翻转后虽然还是车牌但后续如果接OCR识别方向就反了所以训练检测模型时翻转可以开做识别时要考虑是否保留原方向信息。样本均衡方面如果直接用原始分布训练bag、sand_soil这类框少的类别会被truck_loaded带跑偏。我建议做类别抽样也就是在dataloader里对少样本类别适当多采样或者干脆离线复制少样本图片生成额外副本。离线复制虽然笨但效果可控谁用谁知道。4.2 模型选型与训练配置小数据集不适合一上来就上超大模型。我的经验是先上一个小型或中型的YOLO系列模型做迁移学习。具体配置参考输入分辨率640×640起步。如果你特别关注车牌识别可以试1280×1280但训练时间和显存会明显上涨。预训练权重用COCO预训练权重做初始化不要随机初始化。COCO里有car、truck这些类和我们的场景分布接近迁移效果最好。训练轮数100到150轮配合早停patience20。Batch size根据显存决定8到16都常见。优化器SGD或AdamW均可小数据集上SGD配warmup更稳。我自己跑的时候第一次尝试用的是nano规模的模型mAP0.5能到70%以上说明这个任务本身不算难难的是把小类别和状态判别做准。4.3 评估指标怎么定不要只看总mAP。这个数据集里每种类别的AP值差异巨大truck和wheel的AP可能轻松到95%license_plate如果只到60%整体mAP也会被拉到很平庸的水平。所以我评估时有三个固定动作单独看license_plate的AP低于80%就说明小目标策略没做好单独看truck_loaded和truck_empty的混淆矩阵这两个类别最容易被互相认错对比误检框里面“把沙土堆识别成建筑材料”这类语义混淆情况。只有这三项都过关模型才算能真正落地。5. 踩坑记录与问题排查5.1 满载和空车总混淆怎么办训练初期最常见的现象truck_empty的框会有一部分被预测成truck_loaded。原因不复杂如果你只标注了车厢状态而车厢内有少量残留沙土或杂物模型很容易被局部纹理带偏。我的解决方案是双保险。第一在标注层面把“车厢内空但底部有少量泥沙”的车明确标成truck_empty不要标成sand_soil避免模型学到错误关联。第二在模型输出后加一个后处理规则如果同一辆卡车的truck_loaded和truck_empty同时出现置信度大于0.3的两个框取置信度高的那个另一个抑制掉。这个后处理虽然简单但能把误判数量降低不少。5.2 车牌这类小目标漏检车牌在1080p图像里通常只有几十像素宽在640×640的推理分辨率下可能只剩二三十像素漏检几乎是必然的。我试过三种办法效果依次递增第一种提高输入分辨率到1280能改善但显存和时间成本翻倍。 第二种使用SAHI这类切片推理工具把大图切成棋盘格每个小块独立推理再合并结果。对车牌这种小目标uP提升非常明显。 第三种单独训练一个车牌专用检测器输入裁切的车头区域。这个方案最适合固定卡口场景因为你本来就知道车头大概在画面哪个区域直接裁出来跑小模型又快又准。三种办法不冲突可以叠加用。实际项目里我通常采用“主模型车牌二次裁切”的组合。5.3 类别不均衡带来的训练漂移bag和building_material这类少样本类别训练时容易在验证集上AP波动巨大。这不是模型坏而是样本太少泛化能力不足。我有两个实战建议一是少样本类别用更保守的数据增强尤其是不要对它做大幅度的随机裁剪因为袋装料和建材堆的形态本来就不固定过度增强反而会让类内差异变得更大。二是加入负样本。如果你的业务场景中经常出现“空地上堆着杂物”却被模型识别成袋装料的情况就要额外收集一些不含这些类别的图放进训练集当负样本让模型有机会学习“没有目标”的情况。这一点很多新手容易忽略。5.4 标注污染与重复框问题最后说一个看起来低级但实际经常出现的问题标注框重复。比如同一辆车在两张连续帧里各自标了truck_loaded模型训练时会把几乎相同的位置学两遍对这种数据很敏感。排查方法很简单写个脚本统计所有标注框的中心坐标和尺寸如果发现高度重合的框人工确认是否属于同一个小目标被重复标注。遇到这种框我的原则是只保留置信度最高的那一个其余删掉。另一个常见污染是把建筑材料的框画得太大把卡车车身也包进去了这会让模型把“卡车建材”误学成一体推离线时屡屡识别错位。6. 一点后续扩展想法数据集是用出来的不是放在硬盘里吃灰的。696张、8类别这个规模用来跑通流程绰绰有余但真要在某一个施工现场长期部署我强烈建议做两周的增量迭代第一天先用手头模型跑一批未标注的新视频帧用高置信度预测做自动预标注再人工修正把有效样本滚雪球一样滚起来很快就超过两千张。这个策略特别适合“数据集不够但场景可获取”的实际情况。我体验过的最舒服节奏是模型初版跑通只花一天之后每天用新数据做一次微调每三天评估一次两周后精度和稳定性都会发生质的提升。最后分享一个小技巧这份数据集的类别定义是围绕“卡车检测状态识别载货分类车牌定位”的完整业务链设计的小伙伴们在拓展时不要只盯着模型训练还可以考虑在检测框基础上接车牌识别模型、载货区域分类模型组成一条完整的智慧交通车辆管理流水线。数据永远是项目的起点把起点打牢后面每一步都轻松得多。