
简介道路病害检测数据集压缩包面向计算机视觉与深度学习开发者适用于道路病害识别模型的数据准备与工程落地核心价值在于解决标注数据获取难的痛点。压缩包内共两千个文件其中一千九百九十八个为XML格式的标注文件每个标注文件对应一张道路图像详细记录病害的位置、类别与属性信息另附一份说明文档与一个Python脚本可用于快速理解数据组织方式、完成类别统计或格式转换压缩包整体约815.67MB大小标注数据可直接接入YOLO、Faster R-CNN等主流目标检测框架进行训练与验证。目前已有二百四十二人学习下载。该数据集覆盖城市街道、乡村道路、高速公路等多种道路场景标注内容由人工使用labelimg工具精细化完成免去了自行收集、挑选与标注图像的时间成本有助于使用者将精力集中于模型调优、检测效果对比以及后续工程化部署。1. 道路病害数据集怎么选先看这套23k张XML标注包值不值得用做道路病害检测的工程最耗时间的三件事拍图、挑图、画框。这份道路病害数据集就是冲着这三件事来的把23,000张带XML标注的道路图像直接打包成一个压缩包JPEGImages管图Annotations管标注解压即可进入训练流程。它覆盖城市街道、乡村道路、高速公路和住宅区等常见场景适合训练YOLO、Faster R-CNN这类目标检测模型也适合先拿来做预训练再迁移到监控杆、巡检车等实际场景。如果你正卡在“没有图片”或“有图但没标”这个阶段这篇笔记能帮你把这份数据集的边界、用法和常见坑一次性看清楚。2. 数据集结构拆解JPEGImages与Annotations的目录约定、XML字段与统计脚本2.1 目录与命名先弄清VOC结构解压后第一件事不是建模型而是把目录结构过一遍。JPEGImages和Annotations是典型的Pascal VOC排布方式图片放在JPEGImages同名XML标注放在AnnotationsYOLO、Faster R-CNN这类框架都能直接消费这种目录约定。压缩包根目录还放着leishu1.py和说明.txt前者从命名看是作者留的辅助脚本后者多半承载着字段口径说明这两个文件建议先读一遍再动手。我习惯先用简单命令确认规模和目录是否干净。ls JPEGImages | wc -l ls Annotations | wc -l tree -L 2 -d第一行统计图片数量第二行统计XML数量第三行看目录层级是否有多余嵌套。如果两个数字差很多或者发现目录里混进了备份文件先记下来这往往是后面翻车的源头。我还会抽看三组文件名图片是jpg后缀标注是xml后缀前缀必须完全一致框架的Dataset类才能把两者配对。这套数据集的XML命名带有明显批次风格大量文件名以编号开头说明它多半是多个采集路段合并后的结果。作者在合并时已经做过一轮统一命名但“统一”不等于“完整”后续仍然需要做一致性检查。我自己在复刻这种结构时也会把自家采集数据按同样方式摆放这样一套训练代码可以同时吃多份数据不用每个项目改一次路径逻辑。提示JPEGImages与Annotations必须保持同级目录后续所有脚本的路径都以此为基准。如果压缩包解压出来把它们套在子目录里建议先挪出来。2.2 XML字段bndbox、name与difficult的真实含义一份标准的VOC风格XML标注长这样annotation folderJPEGImages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecrack/name difficult0/difficult bndbox xmin100/xmin ymin120/ymin xmax350/xmax ymax220/ymax /bndbox /object /annotationfilename字段是用来关联JPEGImages下实际图片的桥梁size里的width和height是原图真实宽高YOLO做坐标归一化时离不开这两个数。object节点下name是病害类别bndbox里的四个值依次是框左上角x、左上角y、右下角x、右下角y单位是像素。difficult标记难例在VOC标准里它表示“这张标注是否被纳入评测”很多人在转换YOLO标签时默认过滤掉它。在写转换脚本前建议先把字段和用途一一对应清楚。下面这张是我整理速查时的固定表格XML字段含义转换时用途filename图片文件名与JPEGImages路径拼接size/width图片宽YOLO归一化分母size/height图片高YOLO归一化分母object/name病害类别名映射为类别IDobject/difficult是否难例可选过滤条件object/bndbox四个像素角点计算中心点与宽高说明.txt如果写得清楚通常会把这套字段的边界条件也交代掉比如是否允许越界、是否有类别别名。从这份资源的XML文件列表看主干字段完整没有缺胳膊少腿意味着后面转格式时可以直接按标准流程处理不需要额外修补字段结构。这也是我说它“适合直接工程化”的一个重要前提。2.3 统计脚本先跑一遍类别分布和残缺检查leishu1.py从命名推断是作者用来辅助数据检查的脚本但不同压缩包里的辅助脚本输入输出路径经常不一致我不建议一上来就硬跑别人的代码。常见做法是先在自己的目录结构下写一个等价小脚本把数量、缺图、类别分布一次跑清楚既快又可控。下面这段代码直接放在解压后的根目录运行import os import xml.etree.ElementTree as ET ann_dir Annotations img_dir JPEGImages xml_files [f for f in os.listdir(ann_dir) if f.endswith(.xml)] cls_counter {} missing [] for xf in xml_files: tree ET.parse(os.path.join(ann_dir, xf)) root tree.getroot() filename root.findtext(filename).strip() if not os.path.exists(os.path.join(img_dir, filename)): missing.append((xf, filename)) for obj in root.iter(object): cls obj.findtext(name).strip() cls_counter[cls] cls_counter.get(cls, 0) 1 print(XML总数:, len(xml_files)) print(缺失图片数量:, len(missing), 示例:, missing[:5]) print(类别统计:) for k, v in sorted(cls_counter.items(), keylambda x: -x[1]): print(f {k}: {v})逻辑说明这段脚本遍历Annotations目录下的全部XML逐个解析filename和object/namemissing数组用来记录“XML里写了、但JPEGImages里实际找不到”的图片这类样本一旦进入训练会在读图阶段直接打断流程。类别统计结果会决定后续转换脚本里的classes列表怎么写优先级最高。参数说明ann_dir和img_dir按实际解压路径填写脚本不区分大小写但系统文件是大小写敏感的。如果missing数量很大先检查图片扩展名是jpg还是jpeg以及是否存在文件名前后空格这类问题用肉眼很难发现写脚本统计是最快的。如果只是临时想做人工判断不写代码也行用一条命令看类别分布grep -h name Annotations/*.xml | sort | uniq -c | sort -rn这里grep把每个XML里的 行抽出来sort让相同类名排在一起uniq -c按连续重复计数sort -rn按数量倒序。它适合在拿到压缩包后一分钟内回答“这套数据到底有哪些病害类别、每类大概多少框”。缺点是不检查filename也不合并大小写差异只能做概览不能替代上面的Python脚本。统计跑完先不要急着把几十个类全部丢给模型。道路病害项目通常只关心裂缝、坑槽、修补带等核心类别跟业务无关的类直接在转换阶段过滤掉训练稳定性和收敛速度都会好不少。如果发现某个有效类只有几十个框后面第3章的增广步骤就是为这种情况准备的。3. 把XML变成可训练样本划分数据集、VOC转YOLO与病害小目标增广3.1 训练/验证划分固定随机种子按8:2分流23k张图直接全量训练连验证集都不留等于裸奔。常见做法是按8:2划分训练和验证但道路场景数据有个特殊性同一路段的画面高度相似纯随机划分会把相似片段同时塞进训练集和验证集评估结果虚高换一段新路就现原形。更稳妥的做法是如果文件名里带路段前缀就按前缀分组再划分如果前缀信息不完整退回随机划分但必须固定随机种子。import os import random random.seed(42) src_xml Annotations src_img JPEGImages train_txt train.txt val_txt val.txt names [f[:-4] for f in os.listdir(src_xml) if f.endswith(.xml)] random.shuffle(names) val_n int(len(names) * 0.2) val_set set(names[:val_n]) with open(train_txt, w) as tr, open(val_txt, w) as va: for n in names: line f{os.path.join(src_img, n .jpg)}\n if n in val_set: va.write(line) else: tr.write(line) print(train:, len(names) - val_n, val:, val_n)逻辑说明脚本把全部XML的basename收集起来打乱前20%放进验证集剩下80%写进train.txttxt里每行是一个图片路径。框架读train.txt时会根据这个路径前缀去同一级Annotations下找同名XML所以这里只写图片路径就够。参数说明val_n改为int(len(names) * 0.15)就是85/15的划分random.seed(42)必须保留否则每次运行划分结果都不同后续做实验对比时无法复现。如果发现验证集里某类病害完全没出现把随机种子换一个再跑一次或者直接按文件名前缀分层采样。3.2 从XML到YOLO标签归一化坐标与越界钳制YOLO系列训练需要的是txt标签每行格式是“类别ID 中心点x 中心点y 宽 高”并且全部相对原图尺寸归一化。XML里存的是像素绝对值所以转换脚本的核心任务就是一次坐标系换算。import os import xml.etree.ElementTree as ET ann_dir Annotations img_dir JPEGImages out_dir labels classes [crack, pothole, longitudinal_crack, transverse_crack] # 实际类别以上一步统计结果为准列表顺序就是类别ID os.makedirs(out_dir, exist_okTrue) for xf in os.listdir(ann_dir): if not xf.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xf)) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) lines [] for obj in root.iter(object): name obj.findtext(name).strip() diff obj.findtext(difficult) if name not in classes: continue if diff 1: continue box obj.find(bndbox) x1 float(box.findtext(xmin)) y1 float(box.findtext(ymin)) x2 float(box.findtext(xmax)) y2 float(box.findtext(ymax)) x1 min(max(x1, 0), img_w) x2 min(max(x2, 0), img_w) y1 min(max(y1, 0), img_h) y2 min(max(y2, 0), img_h) if x2 x1 or y2 y1: continue cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{classes.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: base os.path.splitext(xf)[0] with open(os.path.join(out_dir, base .txt), w) as f: f.write(\n.join(lines))逻辑说明对每个object算出归一化中心点和宽高写出一行标签同一个XML里多个病害就写多行。输出的txt文件与图片同名这是YOLO默认的标签配对规则编码器会在训练时按图片路径去labels目录找同名txt。参数说明difficult字段的处理看项目需求我认为难样本如果占比不大就保留只在训练配置里降低权重越界钳制那几行建议保留labelimg拖拽时偶尔会留下超出图像范围的框不处理轻则训练不稳重则直接loss变成NaN。classes顺序就是模型输出的类别ID后续训练配置和验证脚本必须沿用同一份顺序否则类别错位了还不会报错。3.3 病害小目标增广水平翻转、裁剪与Mosaic选择道路病害里裂缝这类目标在1920*1080大图里经常只占几十个像素直接把整张图缩到640训练会丢掉大部分细节。常见做法是先把大图切成若干个patch再在每个patch上做增强和训练。基础版增广里水平翻转是最安全的操作因为路面病害没有左右语义翻转后类别含义不变只需要同步修改每个目标的中心点x坐标。import os import cv2 def flip_horizontal(img_path, label_path, dst_img, dst_label): img cv2.imread(img_path) with open(label_path) as f: lines [l for l in f.read().splitlines() if l.strip()] if not lines: return False img_f cv2.flip(img, 1) out_lines [] for line in lines: c, cx, cy, bw, bh line.split() cx 1 - float(cx) out_lines.append(f{c} {cx:.6f} {cy} {bw} {bh}) cv2.imwrite(dst_img, img_f) with open(dst_label, w) as f: f.write(\n.join(out_lines)) return True逻辑说明cv2.flip第二个参数为1表示绕Y轴水平翻转图像宽度方向镜像目标中心点x从cx变为1-cx中心点y、宽高都不变。如果病害本身是横向裂缝翻转后仍然是横向裂缝不影响语义所以这个增广可以无脑用。参数说明这个函数一次只处理一张图工程里套一个os.walk批量调用。垂直翻转我不建议做道路影像的上下方向往往与相机姿态相关翻转后病害形态变得不自然。Mosaic增强能提升小目标召回但注意一定要先做随机裁剪再拼直接缩放整张大图会让小裂缝磨成一条细线等于帮倒忙。到这里原始XML已经被加工成了模型输入所需的txt标签。新手在这个环节最容易犯的错是把转换脚本里的classes顺序写错导致训练时类别ID错位而且训练过程根本不会报错。下一章把这类坑系统梳理出来。4. 使用避坑指南XML缺失、类名混乱、越界框与重叠样本这一章的每一条都是拆这种大规模标注数据时容易翻车的地方。我会按“现象、原因、解决”的顺序写方便你直接对号入座。4.1 XML缺图训练到一半突然读不到文件现象统计脚本的missing列表不为空训练跑到某个epoch突然报FileNotFoundError进程中断。 原因这类数据集经历过批次合并文件名统一过一次但XML里的filename字段写于合并之前可能与实际文件存在大小写、扩展名或前缀差异。 解决先做一次宽松匹配把XML的filename和JPEGImages里的实际文件名都转小写再比较只要能去掉批次前缀后对上就用实际文件名重写XML字段。重命名的批量处理我用一个几行的Python循环做掉避免手工改几百个XML。4.2 类名大小写和空格统计出来两个“crack”现象类别统计里出现语义相同、写法不同的多个类比如crack和Crack或者pothole多一个尾随空格。转格式后类别数量比真实病害多出好几倍。 原因labelimg多人标注时缺少统一类别表不同标注员对同一病害的命名习惯不同。 解决在转换脚本里做名字清洗。先把name统一转小写再用strip去掉首尾空白最后维护一张别名映射表把同义词全部合并到唯一类别ID。注意在定义classes时不要包含“未映射”的残留类否则模型会多学一个纯噪声输出头。4.3 越界框训练时loss出现NaN现象训练初期box_loss正常下降某个epoch开始数值抖动随后直接变成nan或者验证集mAP突然归零。 原因标注框的xmax超出了图像width或ymin小于0。归一化后这些框会产生负宽高或大于1的坐标损失计算时梯度异常。 解决转换脚本必须保留坐标钳制逻辑把xmin、xmax钳制到[0, width]ymin、ymax钳制到[0, height]。钳制后如果x2x1或y2y1说明这个框已经失去有效面积直接跳过这一行。不要修改原XML只对转换出的txt动手保留原始标注以便回溯。4.4 重叠框同一条裂缝被标成两三个框现象一张图上同一段裂缝被标了相邻的两个框训练后模型对同一目标输出多个检测结果NMS过滤后边界不稳定。 原因人工标注时画框习惯不同有的标注员倾向沿裂缝分段标有的倾向一个大框覆盖整体小目标和细长目标尤其容易重叠。 解决先用画框脚本抽查确认再用IoU合并策略处理。IoU大于0.7且类别相同的框取它们的外接矩形合并成新标签。训练框架自带NMS也能缓解一部分问题但标签层面的合并能让损失函数更干净。4.5 difficult难例过滤太狠把小目标全丢了现象转换脚本里difficult1被过滤后验证集的召回率明显偏低大量模糊裂缝没被模型检出来。 原因部分标注员会把看不清的模糊裂缝标成difficult这一类恰好是模型最需要学习的困难样本一刀切过滤等于把训练难度降低了泛化能力上不去。 解决先统计difficult样本占比占比超过5%就保留训练时给这些样本单独设低权重占比很小则过滤也无所谓。这个配置放在训练脚本里不要放在转换脚本里方便随时切换。5. 拿到手先这样验证自检脚本、画框抽查与一轮loss曲线5.1 一份收敛检查清单在真正训练之前我把这份数据集的验证流程固定成这样一张表每次换新资源都照这个顺序跑检查项方法合格标准XML与图片数量目录统计差值为0缺图统计脚本missing缺失为0类别名类别统计无同义类越界框转换脚本无空框样本划分比例查看train.txt约8:2这张表的前三项复用第2章脚本后两项复用第3章脚本。半小时内能跑完省下的调试时间远比这点投入多。5.2 画框验证把归一化标签还原到像素转换完不画框直接训练等于把标签当黑匣子。我习惯随机抽十张图把YOLO标签画回原图肉眼看贴合度。import cv2 def draw_yolo(img_path, label_path, classes, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f.read().splitlines(): c, cx, cy, bw, bh line.split() cx, cy, bw, bh map(float, (cx, cy, bw, bh)) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[int(c)], (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(out_path, img)逻辑说明从txt逐行读入归一化坐标再乘以原图宽高还原为像素框用OpenCV绘制绿框并在框左上角写出类别名。如果框明显没有贴合病害先回头查转换脚本而不是怀疑模型。参数说明画框颜色用BGR顺序(0,255,0)是绿色字体缩放到0.5配合1080p图片如果原图更大可以调到0.7。out_path建议用png避免jpg压缩后边缘发虚影响判断。5.3 先训一轮看趋势loss收敛与mAP分开看一轮训练不代表最终效果但足够暴露配置错误。过程里看box_loss是否稳定下降结束看验证集的mAP0.5和mAP0.5:0.95。道路病害以裂缝为主目标又细又长普通IoU计算对这类形状的敏感度跟矩形物体不一样所以还要单独关注小目标AP。整体mAP合格但小目标AP很低时优先做Mosaic增强和随机裁剪而不是加大网络深度。从那以后我每次接到新数据集都会先跑一遍自检脚本、抽十张图画框再碰训练配置。这次拆这套道路病害XML包也是先把这套流程走完才放心送进训练管线。如果你正好需要一份省去收集和标注时间的道路病害数据集建议按这个流程验证一遍再投入训练能少踩不少坑。希望帮到你。本文还有配套的精品资源点击获取