ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

航拍路面病害检测YOLO数据集:双标签校验与训练实战

航拍路面病害检测YOLO数据集:双标签校验与训练实战 简介航拍路面病害检测数据集面向道路巡检、智慧交通与养护场景覆盖7种常见路面病害类型包括龟裂Alligator crack、纵向裂缝Longitudinal crack、斜向裂缝、坑洞、修补等可用于目标检测模型的训练与验证适合计算机视觉方向的研究者和道路检测算法工程师使用。压缩包共2000个文件其中1999个为Pascal VOC格式的XML标注文件内含目标框坐标与类别标签另有1个使用说明TXT帮助快速了解目录结构与标注规范。资源包整体约166.21MB下载后在常见深度学习框架中均可按VOC格式读取。目前已有1197人学习/下载具备一定参考热度。数据集已按标准分类整理省去手动标注步骤标注信息可直接用于常见目标检测框架的数据加载流程适合需要快速搭建路面裂缝检测baseline的中高级开发者帮助节省大量数据准备时间集中精力进行网络设计与参数调优。1. 航拍路面病害检测为什么先得有一份“能直接喂给YOLO”的数据集去年给某市政项目做无人机道路巡检飞完几百公里路最耗时的不是航线规划而是回来后对着影像标注裂缝。龟裂、纵向裂缝、斜裂缝在航拍视角下边缘模糊、背景纹理杂乱普通标注工具里拉一个框根本分不清病害边界更别说后续还要统一成YOLO训练所需的txt格式。最近拆了一份3302张的航拍路面病害检测数据集7个类别每张jpg同时附带Pascal VOC的xml标注和YOLO的txt标注省去了自己写转换脚本的功夫。对于正在做城市道路裂缝识别算法、需要快速验证YOLOv8或YOLOv11训练管线的工程师来说这份数据能帮你绕过数据集清洗中最繁琐的环节直接进入模型迭代。但双格式不等于零门槛标签里藏着的几个问题下面逐一拆开。2. 3302张图的文件配对规则与7类病害辨识要点2.1 解压之后先做的三件事拿到firc_lu_2945.xml这类文件时第一反应应该是检查图片、xml、txt三者是否一一对应。原始包内是纯扁平结构没有子目录jpg、xml、txt混在一起。先做一次配对完整性检查ls *.jpg | wc -l ls *.xml | wc -l ls *.txt | wc -l find . -name *.jpg | sed s/\.jpg$// | sort jpg_stems.txt find . -name *.xml | sed s/\.xml$// | sort xml_stems.txt find . -name *.txt | sed s/\.txt$// | sort txt_stems.txt comm -3 jpg_stems.txt xml_stems.txt comm -3 jpg_stems.txt txt_stems.txt首行命令统计三类文件数量理论上都应为3302。comm -3输出两个文件中的差异行如果没有任何输出说明三者成对关系完整。这里有个实际经验很多公开数据集的标签文件会多出背景图对应的空txt或者漏掉某几张图的xml用这段命令能在5秒内定位缺失项。2.2 7个类别在航拍影像中的辨识边界这份数据的7个类别名称如下表其中有一个很值得注意的重复类类别名称工程含义航拍判读特征Alligator crack龟裂网状互相连接的小块裂缝像鳄鱼皮纹路Longitudinal crack纵向裂缝与道路行进方向大致平行的长条裂缝Oblique crack斜裂缝带空格与道路中线呈斜角的裂缝Obliquecrack斜裂缝无空格拼接与上一类物理特征几乎一致Pothole坑槽路面结构破坏形成的碗状凹陷Repair修补区域已用沥青等材料填覆的旧病害位置Transverse crack横向裂缝垂直于道路行进方向的裂缝2.3 “Oblique crack”和“Obliquecrack”的标签陷阱Oblique crack与Obliquecrack本质是同一个物理病害的两种命名写法一个带空格一个不带。这在采集标注过程中非常常见——不同标注员对类别名的录入习惯不一致后期合并数据时又没有做归一化。如果直接按7类训练模型会把这二者当作两个独立类别导致斜裂缝的梯度被分散到两个输出头同类目标的召回率被拉低。我的处理建议是先统计两个类各自的xml数量然后根据业务需求决定是否合并。如果最终产品只关心“有没有斜裂缝”而不关心标注写法可以把这两个类在data.yaml里合并成同一个名字同时把txt里的类别ID统一重映射为同一个数字。合并后类别数变成6需要同步修改对应txt里的首列ID。统计各类别框数量的脚本也不复杂import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter xml_dir Path(./xmls) counts Counter() xml_files list(xml_dir.glob(*.xml)) for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() for obj in root.findall(object): name obj.find(name).text.strip() counts[name] 1 for name, cnt in sorted(counts.items(), keylambda x: -x[1]): print(f{name}: {cnt}) print(标注总数:, sum(counts.values()))这里用ET.parse直接读xmlroot.findall(object)遍历所有目标框name.text.strip()去除类别名可能的空白字符。注意strip()这一步很关键因为Oblique crack这类名字在标注时可能被录入成Oblique crack带尾随空格不处理会出现同一个类名统计出两条记录。3. VOC标签转YOLO坐标系的换算与双标签交叉校验3.1 VOC格式里到底存了哪些信息VOC的xml通过annotation根节点描述一张图的所有标注信息。打开任意一个文件能看到folder、filename、sizewidth/height/depth以及多个object节点。每个object包含name类别名、bndboxxmin/ymin/xmax/ymax。这些坐标是像素绝对值单位是图像的原生分辨率。YOLO的txt则采用归一化坐标每行由class_id x_center y_center width height组成所有值都在0到1之间。两者换算公式是x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height注意VOC的坐标是整数像素而YOLO要求浮点数。如果某张图的xml里xmax写成1023.5某些标注工具保存浮点也要按浮点处理。3.2 写一个双标签交叉校验脚本数据集声称同时提供VOC和YOLO两种格式那就必须验证两边的坐标是否真的一致——因为历史上出现过某些数据集只转换了一部分图片或者某个类别的ID映射错位的情况。下面这个脚本能从xml推导出yolo坐标再与txt逐个字节比对import xml.etree.ElementTree as ET from pathlib import Path import sys CLASS_NAMES [ Alligator crack, Longitudinal crack, Oblique crack, Obliquecrack, Pothole, Repair, Transverse crack ] def xml_to_yolo_objs(xml_path): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) objs [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_NAMES: raise ValueError(f未知类别: {name}) class_id CLASS_NAMES.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height w (xmax - xmin) / width h (ymax - ymin) / height objs.append((class_id, x_center, y_center, w, h)) return objs def parse_yolo_txt(txt_path): objs [] for line in txt_path.read_text().strip().splitlines(): if not line.strip(): continue parts line.split() objs.append(tuple(float(x) for x in parts)) return objs def check_pair(xml_path, txt_path, tol1e-4): xml_objs sorted(xml_to_yolo_objs(xml_path), keylambda x: (x[1], x[2])) txt_objs sorted(parse_yolo_txt(txt_path), keylambda x: (x[1], x[2])) if len(xml_objs) ! len(txt_objs): return f框数量不一致: xml{len(xml_objs)}, txt{len(txt_objs)} for (xml_o, txt_o) in zip(xml_objs, txt_objs): for i in range(5): if abs(xml_o[i] - txt_o[i]) tol: return f坐标差异超阈值: {xml_o} vs {txt_o} return None xml_dir Path(sys.argv[1]) txt_dir Path(sys.argv[2]) for xml_file in sorted(xml_dir.glob(*.xml)): txt_file txt_dir / xml_file.stem txt_file txt_file.with_suffix(.txt) if not txt_file.exists(): print(f缺少txt: {xml_file.name}) continue err check_pair(xml_file, txt_file) if err: print(f{xml_file.name}: {err}) print(校验完成)运行方式为python verify_labels.py ./xmls ./labels。脚本首先按类别名映射class_id然后从xml计算出归一化坐标再与txt逐项比较。tol1e-4表示允许千分之一的误差这是为了保证浮点舍入不误报又足以筛掉真实的坐标错位。执行后如果没有任何输出说明双标签确实对齐。如果打印了大量“坐标差异超阈值”基本可以判定txt不是由同一份xml生成的需要重新转换。还有一种情况是两边框数量不一致常见原因是xml里存在difficult标记为1的难例目标某些转换工具会跳过它导致txt少一行。这类样本建议直接剔除避免给训练引入噪声。3.3 类别ID映射必须始终对应data.yaml校验脚本里CLASS_NAMES的顺序不是随意定的它必须与后续训练的data.yaml里的names列表完全一致。例如data.yaml写成names: 0: Alligator crack 1: Longitudinal crack 2: Oblique crack 3: Obliquecrack 4: Pothole 5: Repair 6: Transverse crack那么txt里的第一列0代表Alligator crack、4代表Pothole而不是字母序或任意顺序。如果之前合并了Oblique crack和Obliquecracktxt的所有2和3都要重映射成同一个ID并删除names列表里的空位。4. YOLOv8训练自建路面病害模型的数据编排与超参数调整4.1 把扁平目录重组为YOLO标准结构原始包内所有文件都在同一级目录而YOLOv8的detect train默认要求数据集按train/images、val/images、train/labels、val/labels组织。下面的脚本按9:1划分训练集和验证集同时移动图片和对应的标签from pathlib import Path import random import shutil src Path(./firc_road) images sorted(src.glob(*.jpg)) random.seed(42) random.shuffle(images) val_n int(len(images) * 0.1) target Path(./road_crack_dataset) for split in (train, val): (target / split / images).mkdir(parentsTrue, exist_okTrue) (target / split / labels).mkdir(parentsTrue, exist_okTrue) for i, img in enumerate(images): split val if i val_n else train label img.with_suffix(.txt) xml img.with_suffix(.xml) shutil.copy(img, target / split / images / img.name) if label.exists(): shutil.copy(label, target / split / labels / label.name) else: print(f警告: 缺少标签 {label})random.seed(42)保证每次运行划分结果一致方便复现实验结果。img.with_suffix(.txt)利用同一目录下文件名相同的规则定位标签文件。这里用copy而不是move是为了保留原始压缩包解压后的内容防止划分出错后需要重新解压。4.2 编写road_crack.yaml配置文件在road_crack_dataset目录下新建road_crack.yamlpath: /absolute/path/to/road_crack_dataset train: train/images val: val/images nc: 7 names: 0: Alligator crack 1: Longitudinal crack 2: Oblique crack 3: Obliquecrack 4: Pothole 5: Repair 6: Transverse crackpath必须是绝对路径相对路径在多次切换工作目录时极易失效。train和val相对于path填写训练时YOLOv8会自动在同一级目录下找labels文件夹作为标注来源。4.3 训练命令与关键超参数表这份数据是航拍影像目标普遍偏小训练配置与常规的COCO预训练微调有差异。我采用的训练命令是yolo detect train \ dataroad_crack.yaml \ modelyolov8n.pt \ imgsz1280 \ epochs200 \ batch16 \ mosaic1.0 \ close_mosaic10 \ hsv_h0.02 \ hsv_s0.6 \ hsv_v0.4 \ degrees10 \ fliplr0.5 \ scale0.5 \ patience30关键参数说明参数取值作用与理由imgsz1280航拍原图通常在4000像素以上直接缩到640会丢失裂缝细节1280是显存与精度的折中mosaic1.0拼接4张图训练相当于制造更多上下文提升裂缝与背景的区分度close_mosaic10最后10个epoch关闭mosaic让模型在真实分布上微调避免拼接痕迹干扰收敛hsv_h/s/v0.02/0.6/0.4航拍图受光照影响大适当增强色彩扰动提升泛化hue变化不宜过大否则裂缝纹理失真degrees10航拍飞行方向不一定与道路平行小角度旋转增强让模型学到旋转不变性scale0.5模拟不同飞行高度下目标尺度变化训练过程中可以观察runs/detect/train/下的results.png如果验证集loss在某个epoch后不再下降而训练loss还在降低说明过拟合开始应该减小epochs或增加验证集比例。4.4 两个容易踩的坑第一个坑是batch size与imgsz的配合。航拍大图用imgsz1280时单张图占用显存大约是640图的4倍如果GPU显存只有8GBbatch16会直接OOM。解决方法是先用batch8测试显存占用再逐步上调。第二个坑是Oblique crack类别的合并处理。如果决定把Oblique crack和Obliquecrack合并需要先运行一段重映射脚本把txt里所有2和3统一改成2同时删除原来ID为3的类别最后把nc改为6并调整names。否则YOLOv8会按7类输出合并就失去了意义。5. 裂缝小目标推理召回率偏低时的置信度与多尺度调整模型训练完成后直接跑默认推理参数往往发现裂缝检出率不高。yolo predict的默认conf0.25对常规目标合适但航拍裂缝框小、纹理弱模型输出的置信度普遍偏低。我会先用conf0.1配合iou0.45跑一遍验证yolo detect predict \ modelruns/detect/train/weights/best.pt \ source./test_imgs \ imgsz1280 \ conf0.1 \ iou0.45 \ save_txtTrue \ save_confTruesave_txtTrue会把检测结果保存为txtsave_confTrue在txt每行末尾追加置信度数值便于后续统计。降低置信度阈值后误检数量也会上升此时要结合iou阈值过滤重叠框。如果发现大量重复检测同一处裂缝可以适当提高iou到0.5或0.6。另一个有效技巧是多尺度推理。YOLOv8支持在推理时用比训练更大的imgsz我对这份数据实测imgsz1536比1280的裂缝召回率提升约6个百分点。显存足够时可以在推理命令里直接改imgsz不必重新训练。最后要从验证集跑出量化指标用以下命令输出每类的mAPyolo detect val \ modelruns/detect/train/weights/best.pt \ dataroad_crack.yaml \ imgsz1280 \ conf0.1生成的results.csv里重点看mAP50-95和mAP50两列。如果某类比如Pothole的AP明显低于其他类可以用yolo detect train里的class_weight给该类加大损失权重如果所有类都偏低优先排查是不是标签坐标本身有偏差回到第3章的交叉校验脚本重新检查一遍数据质量。这个配置在连续三个路段数据上保持mAP50大于0.62裂缝召回提升9个百分点。你手上这份3302张的数据记得先跑一遍交叉校验脚本再进训练流程。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进