☰
卡车篷布破损检测数据集构建:VOC与YOLO双格式实战
2026/10/18 12:34:15 网站建设 项目流程

上个月在做一个智慧交通相关的边缘检测项目时,我反复往物流园区跑了好几趟。起因其实很简单:园区进站口的值班人员每天要盯着一辆接一辆的大货车,人工判断篷布有没有破损、捆扎有没有松动。这种活计很磨人,尤其是傍晚逆光和夜间,稍一疏忽就会漏掉那些“看起来还能撑一阵”的破损篷布。后来恶劣天气一来,破损处被风扯开,货物散落一地,事情就闹大了。

为了把这件事交给算法自动干,我前后花了大概三周,从选址拍摄、抽帧筛选、人工标注,到格式统一和模型训练,整理出了一套面向卡车货车防水布、篷布、雨布破损检测的数据集:1894张图像,2个类别,同时给出VOC格式的XML标注和YOLO格式的TXT标注。这篇文章不打算只讲数据集本身,而是想把“这套数据是怎么设计出来的、坑是怎么踩过来的、模型训练和部署时又会遇到哪些意外”这些完整链路一次性讲清楚。看完后,无论你是想复现这套数据流程,还是想给自己的检测任务设计类似的数据集,都能直接照着做。

1. 篷布破损检测的需求来源:不是拍脑袋想出来的

1.1 真实业务场景:收费站、物流园和巡查车都在等这个功能

篷布破损检测听起来是个很垂直的细分任务,但它在实际业务里的覆盖面比想象中大得多。

先看高速收费站。很多省份对货车外观查验有明确要求,篷布破损、遮盖不严的车辆在特定路段会被拦截。过去这一道工序全靠收费员肉眼识别,赶上夜间车流高峰,一辆车在窗口前停留的时间就那么十几秒,人工判断既累又不可靠。有的站点试过让保安拿手电筒绕车检查,效率更低,排队能从收费口延伸几百米。

再看物流园区。每天进出园区的重卡数量很大,园区方关心的是货物在运输途中有没有因篷布破损受潮、掉落,这直接关系到货损赔付责任的划分。可园区闸口通常没有足够的人力去一辆辆仔细检查,最多看个大概,拦下来后再补拍照片走人工复核,周转效率输出受限。

还有一类场景是路侧巡查车和移动执法终端。车上装个摄像头,沿着固定线路巡逻,拍到货车后实时判断篷布状态。这个需求一旦跑通,等于把过去“抽查靠运气”的模式变成了“全量自动筛查”,能节约的人力非常可观。

我接触到的项目对应的是综合场景,视频源来自园区出入口的枪机、偶尔人工手持设备补拍的外场照片,以及巡查车的车载相机。目标很简单:区分“篷布状态正常”和“篷布破损”。不需要去计算破损面积,也不需要生成破损位置的轮廓,只要能在画面中把破损目标框出来,先做到“有破损就能告警”,就已经能替代大量人工。

1.2 二分类背后的业务逻辑:先能“断案”,再谈“重判”

很多没有实际业务经验的人看到数据集只有两个类别,第一反应往往是“这也太简陋了”。但站在落地的角度,二分类恰恰是大多数检测项目最早的合理形态。

为什么这么说?因为现场工作人员拿到告警之后,还需要二次确认。如果模型输出的是“轻微破损”和“严重破损”两类,那无数个介于中间状态的样本会让标注人员和模型都非常痛苦——张三觉得这算轻微,李四觉得这已经严重了,标注标签的噪声一大,模型性能反而下降。

把类别压成“正常”和“破损”两个粒度之后,业务规则可以这样定义:只要系统告警,现场人员就去看一眼;确认是严重情况就走破损处置流程,确认是小刮擦就记录后放行。把“判断有没有问题”交给算法,把“问题有多严重”留给人,这样配合起来的可靠度最高。

所以数据集最终设计为2个类别:一类是完好的篷布,另一类是破损篷布。在标注规范里,我对“破损”的范围做了明确约定:包括撕裂、破洞、布面明显老化龟裂、边缘开线脱线,以及被风掀起后明显变形且伴随破损特征的情况。那种纯粹因为没拉紧而轻微起伏、但布料本身完好的,不算破损。这条规则在后续标注环节帮了大忙。

2. 数据集规划设计:1894张、2类别、VOC+YOLO双格式的合理性

2.1 为什么是1894张:数据量不是越大越好

很多人以为数据集越大越好,但实际项目里,“够用”比“海量”更重要。这个项目最终定为1894张图像,是经过一轮初步实验后得到的平衡点。

团队最初从监控视频里抽了大概5000多帧,但抽帧不等于有效样本。大量连续帧里的卡车几乎是同一姿态,篷布破损状态也没有变化,属于高度相关的重复数据。放进训练集不但不能提升泛化能力,反而会让模型过拟合到这套固定的机位参数上。

我当时的处理思路是:先把视频按场景切段,每个机位采集连续5分钟以上的素材,然后每隔15帧抽1帧;抽出来的帧先人工粗筛一遍,把模糊、遮挡严重、卡车占比过小的直接删掉;对保留下来样本再按“正常”和“破损”做大致分层。最后留下的1894张图像,基本覆盖了早晨、正午、傍晚、夜间,以及晴朗、阴天、小雨和逆光、顺光、侧光等多种条件,车型也涵盖了半挂、厢式、平板加篷布和部分轻卡。

这里有一个值得参考的数据规划逻辑:先用小样本量跑通一个基线模型,再用“难样本继续补充”的策略去扩充。1894张这个数字并不神秘,它恰好是在基线模型已经能跑出还不错的mAP之后,我主观停下来的一个点。因为再继续补数据,模型指标提升幅度已经不那么明显了,边际收益很低。与其无限囤数据,不如先上线,让误报漏报反哺数据迭代。

2.2 VOC和YOLO双格式的差异与兼容策略

把同一个数据集同时转成VOC格式和YOLO格式,不是为了凑工作量,而是因为两套格式的使用场景完全不同。

VOC格式的核心是XML文件,每个标注对象都包含类名、文件名、图像尺寸以及bounding box的绝对坐标。这种格式最明显的好处是可读性好,任何标注工具都能导入,也方便人眼检查和二次调整。做数据分析、手工核对、多轮修正样本时非常顺手。

YOLO格式则是一行一个目标,每行写“类别ID x_center y_center width height”,全部归一化到0到1之间。训练YOLO系列模型时读取效率高,占空间小,但人眼看起来非常不直观。你打开一个标注文件,看到的是一堆小数,根本想象不出来框到底落在哪。

所以我在数据管理上采用了一个“主备双轨”的策略:以VOC XML作为数据仓库里的主标注格式,任何修改和审核都在XML上完成;训练之前通过脚本批量转出一个YOLO版本的TXT目录,供检测模型直接读取。这个策略的好处是,VOC版本保留着最完整的信息,将来要升级做分割标注或者做做多轮难样本分析,都有回旋余地;而YOLO版本始终是从VOC同步生成的,不会出现两边改着改着就对不上的问题。

2.3 类别定义:清晰的判定标准才是标注质量的源头

标注质量的好坏,一半取决于工具,一半取决于有没有把定义讲清楚。我在这套数据集的标注规范里反复强调过一件事:如果标注员对“破损”的理解不统一,那模型学出来的就是一个拧巴的特征空间。

关于“正常”类别,定义很宽,但要求也很明确:篷布整体覆盖完整,没有明显破洞和撕裂,边缘没有大面积脱线,布面起伏不影响货物遮盖。只要满足这些条件,哪怕篷布有轻微折痕、灰尘、发旧,都可以标为正常。

关于“破损”类别,我列了一个常见形态清单:

  • 撕裂口:布面有明显的裂口,裂口形态可能是直线形或三角形,边缘通常有不规则的毛边;
  • 破洞:布面出现穿透性孔洞,能看到洞内货物或者背景;
  • 边缘脱线:篷布边缘缝合线断掉,导致局部翻卷、下垂;
  • 老化龟裂:布面呈现出密集的裂纹,但还没形成穿透性缺口;
  • 掀起变形伴随破损:被风吹起后形态改变,同时局部能看到裂纹或破洞。

这里特别要提一个容易踩坑的点:不要把“掀起的篷布角”和“破损”画等号。篷布四角没有固定好,风一吹就会飘起来,这种动态形变在单帧画面上很像破损,但布料本身是完好的。如果标注时把这类样本全部归进破损类,模型就会把“形状异常”当作主要判据,大量误报就会随之而来。我后来把这类样本单列了一个模糊文件夹,最终按是否能看到破洞或裂口来定标签,处理起来才稳定下来。

3. 从采集到标注的完整流程:一半是体力活,一半是脑力活

3.1 图像来源:监控抽帧、路侧相机和移动采集的取舍

图像采集这条链路,我没有只用一种方案,而是按场景搭了三个来源。

第一种是固定机位的监控视频。这是数量贡献最大的来源。园区出入口的枪机高度在4到6米左右,俯视角度下能完整看到车顶和侧面上半部分的篷布,破损特征非常清晰。这种机位拍摄的视频抽帧效率高,一天能攒出几百张原始素材。但这套来源有一个隐患:机位固定后,画面里的卡车构图位置基本固定,导致数据多样性偏弱。为了解决这个问题,我特意选了不同出入口、不同方向的枪机,让卡车在画面里的朝向、大小产生差异。

第二种是路侧临时架设的相机。我选了一段路侧停车带,用一个三脚架加微单相机,在安全区域拍摄不同方向驶过的货车。这种拍摄方式的视角更低,更有贴近路面执法的味道,而且能够拍到车头侧面和后部,篷布破损的立体感更强。缺点是天气和光线不可控,拍摄效率低,一上午也就拍出几百张可用素材。

第三种是手持设备粗筛。巡查人员在日常巡逻中看到破损车辆,顺手拍下来作为难样本补充。这类样本构图随意、分辨率参差,但恰恰是模型上线后最容易遇到的真实分布。后来在难样本回注阶段,这类数据发挥了很大作用。

在素材合并环节,我做了一个简单但很关键的处理:使用脚本去掉重复度极高的连续帧,只保留其中一张。判断重复很简单,比较文件名里的时间戳,同一机位在同一时间段内取到的帧,间隔少于一定秒数的全部丢弃。这样能极大降低训练集的“时序相关性”,避免模型把背景当特征。

3.2 标注规则的约束:最值钱的不是画框,而是“边界共识”

标注的像素级细节并不是最难的,最难的是让多个标注人员对同一条规则达成共识。我当时的做法是:开工之前先跑两轮“试标”,把示例图打印出来讨论,标注人员之间充分对齐之后再正式开始。

关于边界框的标注,我定了这么几条:

  • 一个破损区域只标一个框,不拆细;如果破损区域之间相隔很近,视觉上明显连成一片,则合并为一个框;
  • 框要贴合破损区域,但允许适当外扩,外扩比例不超过破损区域平均宽度的1/10;
  • 如果破损区域部分被遮挡,遮挡面积小于30%则正常标注;遮挡超过30%则忽略,不标;整个车辆被栏杆、树木大面积遮挡时,这张图直接弃用;
  • 夜间图像如果无法确认破损细节,宁可标为看不清,也不要硬猜。

这四条看起来很琐碎,但对后续模型表现影响极大。比如“硬猜”这个问题,如果标注的时候只是因为帆布在夜里有反光就标了破损,模型就会在夜间被各种反光干扰,做出来的模型白天勉强能用,晚上一片乱告警。

标注完成后,我还做了一次“重复标注一致性验证”。挑了50张图像,让两位标注员各标两遍,对比同一个目标的框位置和类别。结果类别一致性在95%以上,框坐标的IoU平均在0.8左右,我才觉得这个标注质量可以进入训练环节。

3.3 质量校验:我踩过的漏标和错标

这里必须坦白一个教训:第一次形成的VOC标注文件,实际质量远没有想象中好。我做了三轮质检,发现的主要问题包括漏标、框偏移和类别错误。

漏标最常见。夜晚或者逆光条件下,破损区域在画面里对比度极低,标注员很容易漏掉车体侧面下方的破洞。解决方式是用图像增强软件把暗部提亮后再过一遍,大幅减少了漏标。

错标则更多出现在把“完好但形变”当成“破损”的情况。比如篷布边缘被风吹起,又正好带着阴影,标注员一看就觉得“这不就是破损吗”。针对这种情况,我重新强调:只按撕裂纹理判断,不看是否掀起。处理完这些错标之后,模型在后续实验中的误报率明显下降。

质量校验的最后一个环节是“逐图过目”。我用脚本把每张图的标注框画回原图,导出成带框预览图,再人工按帧浏览一遍。这一步很费时间,但对于建立一个还想继续迭代的数据集来说,是完全值得的。毕竟数据集是整个检测系统的地基,地基歪了,上面再怎么调参都没用。

4. VOC与YOLO格式转换:一个脚本解决两套生态的对接

4.1 转换原理:坐标归一化的数学细节

VOC格式保存的是绝对像素坐标,比如xmin=312,xmax=518,ymin=204,ymax=403。YOLO格式需要的是归一化后的中心点坐标、宽度和高度,公式很简单:

x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height bbox_width = (xmax - xmin) / image_width bbox_height = (ymax - ymin) / image_height

听上去很基础,但越基础越容易出错。我见过不少人在转换时把xmin直接除以图像宽度当作x_center,也有人忘了在除法前把整数转换成浮点数,结果在Python 2时代整型除法直接把宽高算成了0。虽然现在用Python 3不太会遇到这个坑,但坐标归一化的细节仍然值得重新确认一遍。

归一化的好处是让标注不依赖具体图像尺寸。同一辆卡车的同一处破损,在1280×720的图像上和1920×1080的图像上,归一化坐标是相近的,这样模型输入图像分辨率变化时,标签并不需要重新生成。

4.2 可直接使用的转换脚本

下面这个脚本是我在实际项目中整理出来的通用版本,适合大多数VOC转YOLO的场景。假设目录结构是:

datasets/ annotations/ xxx.xml images/ xxx.jpg

脚本运行后,会在labels目录里生成和图像同名的.txt文件。

import os import glob import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names, out_dir): tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) x_center = ((xmin + xmax) / 2.0) / img_w y_center = ((ymin + ymax) / 2.0) / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") xml_name = os.path.basename(xml_file) txt_name = os.path.splitext(xml_name)[0] + ".txt" out_path = os.path.join(out_dir, txt_name) with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": class_names = ["intact", "damaged"] xml_files = glob.glob("datasets/annotations/*.xml") os.makedirs("datasets/labels", exist_ok=True) for xml_file in xml_files: voc_to_yolo(xml_file, class_names, "datasets/labels") print(f"转换完成,共处理 {len(xml_files)} 个XML文件")

使用这段脚本时,class_names的顺序是唯一的重点。训练YOLO模型时,数据配置YAML文件里也要写成完全一样的顺序,否则同一个数字会映射到完全不同的类别,训练出来的模型根本无法正确预测。

4.3 转换中容易出错的四个细节

第一个细节,是“size节点里取的是图像原生尺寸而不是你准备resize后尺寸”。有些标注工具会在XML里记录图像的原始分辨率,但如果你提前把图像压缩过、缩放过去再保存XML,输入图像尺寸和原始尺寸就不一致,归一化坐标就全乱套了。所以转换脚本必须放在数据集固定版本上运行,不要边改边转。

第二个细节,是类名匹配的严格性。VOC XML里如果出现类名前后有空格、大小写不一致,比如写成了“Damaged”而不是“damaged”,脚本里直接continue把这个目标跳过了。这样做有两个后果:一个是漏标,另一个是类别索引错位。建议在标注平台上就约定好严格的大小写规范,不让类似的脏数据流到转换环节。

第三个细节,是图像方向信息。部分手机或相机拍摄的JPEG文件会带EXIF旋转信息,图像宽高和像素实际排列方向可能不一致。如果XML里记录的宽高是按原始文件取的,而训练时加载图像库又应用了自动旋转,那么图像实际宽高就和标注不匹配了。换我的操作习惯来说:采集照片后先统一洗一遍,去掉EXIF旋转或者把所有图像转正,再进入标注流程,能省掉后面一大堆奇怪的对不齐问题。

第四个细节,是转换后一定要做“反向验证”。我习惯是把生成的YOLO txt读出来,画回原图,再和VOC画框预览图对比。看起来没问题才算转换完成。我自己就在这个验证环节发现过一次问题:一批夜间图像在缩放处理时被误替换了尺寸,导致坐标整体偏移了一个方向,如果没画回图验证,训练出来的模型会出现定位系统性漂移。

5. 用这套数据集训练检测模型的实战记录

5.1 训练集划分与调参起点

数据集准备好后,我按约8:1:1把1894张图像划分成训练集、验证集和测试集。关键原则是保证同一条视频抽出来的帧不会同时散落在训练集和测试集里,否则验证指标虚高得离谱。做法很简单:先按视频片段分组,再整组划分,而不是每帧独立洗牌。

训练起点我选的是YOLO系的成熟框架,骨干网络选择了中小型规模的预训练权重。输入分辨率开到640×640,batch size设为16,初始学习率0.01,训练轮数先设为100。这些参数没有太多花哨的东西,属于经验值起步。篷布破损属于中大型目标,640分辨率足够承载细节,再往上提分辨率对中低端算力设备就不友好了。

训练过程中我关了大部分在线增强,比如随机透视和旋转。原因很直接:卡车篷布是刚性覆盖物,在真实道路上不会出现大幅度旋转扭曲。过度增强会引入一些现实中不存在的形态,反而干扰模型。保留的增强是轻度随机的HSV扰动、随机平移和轻微缩放,以及水平翻转。这些操作和真实拍摄条件比较吻合,对破损纹理的多样性也有帮助。

5.2 训练中的异常现象:类别不平衡和漏检偏向

第一轮训练结果出来,整体mAP还可以,但我一拆开看各类别AP就发现问题了:“normal(完好篷布)”的AP非常高,而“damaged(破损)”的AP明显落后。这在直觉上说得通,但也暴露了类别不平衡问题——原始数据里正常篷布的图像数量明显多过破损图像,模型开始学会偷懒,倾向于把不确定的目标判成完好类。

处理这个问题的第一步,是重新调整训练样本的类别比例,把每类图像数量控制到接近1:1。我通过难样本筛选保留了更多破损样本,最终破损伤样本占到总样本的45%左右,情况有了明显改善。

第二步,加入一些针对破损特征的局部裁剪样本。破损区域往往只占画面的10%都不到,原始尺寸下模型容易忽略。我把包含破损区域的小块按一定比例裁剪出来,单独作为训练样本,让模型在近距离视角下反复见过破洞、撕裂的纹理特征。这一步对AP提升效果非常显著。

第三步,适当调整了损失函数中正负样本的权重,同时把置信度阈值稍微调低了一点,让模型更愿意输出破损告警。这种调法的代价是误报放宽,但业务上误报可以被后台复核兜住,漏报才是真正致命的,所以这个方向值得以牺牲少量精确率为代价换取更高的召回率。

5.3 效果评估:哪些指标真正反映了业务能力

训练阶段我重点盯这几个指标:

  • mAP@0.5:衡量整体检测水平,第一版能到0.85以上才算合格;
  • 各类别AP:用于发现类别不平衡问题,不能只看总体;
  • F1分数:告警场景非常依赖精确率和召回率的平衡点;
  • 针对Dark环境的单独统计:因为夜间才是这个业务真正的硬仗。

我在验证集上看到最好的一个版本,正常类的AP是0.91,破损类的AP是0.88,整体mAP@0.5到了0.9左右。看起来不错,但真正让我对它建立信心的是在约100张夜间测试图上的评测结果:破损召回率在0.84到0.88之间浮动,基本能支撑现场使用。

我还做了一件很朴素的事情:把模型输出画成预览图,一帧一帧地用肉眼扫。指标只能告诉你系统“还行”,但肉眼能告诉你模型到底记住了什么特征。我发现模型在识别“撕裂口的锯齿状边缘”和“破洞内透出的背景纹理”这两类特征上非常敏锐,这正是标注数据里最核心的视觉线索。

6. 部署落地阶段遇到的问题和后续扩展方向

6.1 边缘设备上的推理速度与精度平衡

数据集和模型只是第一步,真正难的是把模型塞进现场设备。园区出入口的枪机连接着边缘计算盒子,算力有限,模型不能挥霍。

我在部署时首先做了模型压缩。先用半精度浮点方式导出,推理速度提升明显,显存占用减半,精度损失几乎可以忽略。接着用推理优化引擎重新转换模型结构,把一些算子合并,同时修整了后处理解码部分。实际跑下来,单帧推理耗时在边缘设备上大约30毫秒左右,按25帧每秒的码流抽帧检测,完全能跟上。

这个过程中发现一个容易忽略的问题:边缘设备上使用的图像解码库和训练时不太一样,会自动把图像旋转正,但有些老版本对EXIF处理方式不一致。我在部署代码里对图像管道做了统一,在解码后强制检查尺寸并标准化到模型输入大小。否则训练时模型看到的图和部署时输入的图方向不一致,那测试精度再高也没用。

6.2 光照、遮挡、多角度下的实际表现

真正部署到现场,我记录了不少指标以外的现象。

首先是夜间低照度下的表现。靠可见光摄像头,当篷布和背景都接近黑灰色时,模型漏检率直线上升。后来我在园区几个关键点位补装了带红外补光的摄像头,在近红外波段,篷布材料的反射特性和背景差别更明显,夜间的检测能力才恢复正常。

其次是遮挡问题。卡车通过园区出入口时,篷布很容易被立柱、车头侧面、栏杆挡住一部分。如果破损区域恰好被遮挡到无法辨认,模型就会漏报。这里我没有只依赖单帧判断,而是增加了一个简单的时序确认模块:一辆车连续出现多帧,只要其中两帧都能检测到破损,就触发告警。这样做有效滤掉了单帧遮挡带来的漏报波动,也降低了误报频率。

再一个是从侧面拍摄的构图问题。车载相机由于视角低,只能拍到车牌照附近的侧下沿,篷布破损如果出现在车厢中上部,就完全拍不到。这个问题不是模型能解决的,而是必须提醒现场人员:检测设备安装角度要和目标特征出现高度匹配,不然数据做得再好也白搭。

6.3 下一阶段:破损程度分级和实例分割路线

完成二分类检测之后,我一直在琢磨两件事。

第一件是破损程度的分级。现在只在检测层“有”还是“没有”,但现场处置时,破损面积很小的旧化龟裂和已经撕开大口的严重破损,处理优先级完全不同。二分类的下一版可以考虑做回归式破损程度评分,或者在原有检测框基础上升级成三分类:完好、轻度破损、重度破损。“破损面积占比”是最稳妥的量化指标,结合分割掩码计算会更稳定。

第二件是把检测目标从矩形框升级成实例分割掩码。破损区域的形状通常非常不规则,矩形框会把多余背景也包进去,后续无论做面积统计还是做形态分析,精度都不够。用分割模型输出破损轮廓之后,面积估算会准得多,同时对撕裂口边缘形态的分析也能进一步支撑破损类型识别。

如果继续沿着这些方向扩展,还需要重点补充两类数据:一是同一车辆在不同光线、不同角度下重复出现的配对样本,用来做车底、侧围、顶部的分面完整判断;二是部分合成渲染样本,用于覆盖极端破损形态,避免真实采集数据难以覆盖所有布面材质的局限。

在做这个项目的时候,我最大的体会是:数据集的整理工作,永远比模型调参更能决定项目上限。1894张、2类别、VOC+YOLO双格式,这几个数字看起来平平无奇,但背后每一项设计都是在和真实业务反复碰撞后沉淀出来的。如果让我给正在做类似检测场景的人提一个最直接的建议,那就是从一开始就保存好难样本和模糊样本的原始图,别等模型上线被现实摩擦之后再回头补数据,那时候的代价要大得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询