简介:面向航拍路面病害检测与道路裂缝识别任务,这份VOC+YOLO双格式数据集包含3302张实拍路面图像及对应标注,覆盖Alligator crack、Longitudinal crack、Oblique crack、Pothole、Repair、Transverse crack共7类常见病害,适合目标检测模型训练、算法验证及学术论文实验。压缩包采用7z格式,共2000个文件,主要类型为Pascal VOC格式的xml标注文件,另含一个使用说明txt,整体约166.21MB,解压后能方便地接入YOLO训练管线或转为其他格式。目前已有1197人学习浏览,适用人群包括计算机视觉方向学生、科研人员及道路养护检测开发者。通过该数据集可省去自行采集、筛选与标注路面缺陷的繁琐工作,直接用于裂缝定位、破损分类与模型性能对比;同时7类细粒度病害标注有助于针对性优化检测器在航拍场景下的鲁棒性与泛化能力。
1. 航拍路面病害检测数据集:3302张双格式图,落地裂缝检测的第一块跳板
航拍路面病害检测和车载相机检测完全是两个难度档:无人机视角下的裂缝往往只有几个像素宽,光照不均,道路走向还不固定,很多在普通目标检测上表现不错的yolo模型放上去直接翻车。这套“航拍路面病害检测道路裂缝检测数据集VOC+YOLO格式3302张7类别.7z”把最耗精力的数据环节一次性补齐了——3302张真实航拍路面图、7类病害标注,同时给出VOC和YOLO双目录,从LabelImg这类老工具到最新ultralytics训练脚本都能直接接。对刚入坑的研究生来说,它是跑通基线的第一天素材;对有工程经验的熟手来说,省掉格式转换和类名对齐的杂活,可以把精力直接放在小目标检测和tile切图这类真正决定精度的环节上。动手之前先把解压、验货做了,后面才不会白跑一轮训练。
2. 先验货再上车:数据集结构、七类病害定义与解压检查
2.1 七类病害的实际定义:横向缝、纵向缝、龟裂、块裂、坑槽、修补、车辙
路面病害检测的第一步不是选模型,而是定类别边界。这套数据集的7个类别,从常见道路病害数据集的标注习惯看,应该是横向裂缝、纵向裂缝、网状裂缝(龟裂)、块状裂缝、坑槽、修补区域和车辙,具体以包内classes.txt或data.yaml里的声明为准。横向裂缝是垂直于行车方向的单条裂纹,纵向裂缝平行于行车方向,这两类在航拍俯视角下最容易标反——无人机机头方向和道路走向经常不一致,标注时如果不是按道路方向为参照,横向变纵向、纵向变横向的错标会直接带偏yolo训练。
龟裂和块状裂缝是第二对高频混淆对象。龟裂是网状细碎裂纹,面积大但纹理细密,框选时稍微大一点就框进大量正常路面;块状裂缝是近似矩形的破碎带,边界相对清晰。如果标注者把龟裂中的某一条长裂纹单独标成横向缝,类别比例和位置分布都会被污染。坑槽在航拍图里呈现深色阴影,边缘不规则;修补区域纹理和周围明显不同、形状规则;车辙是轮迹带方向的连续下陷,常出现在沥青路面。验货时需要重点看这三类是否和你的业务场景对上,因为它们内部差异大,模型很容易把坑槽当阴影、把修补当新铺装。
2.2 VOC和YOLO双格式:目录结构、xml与txt的对应关系
VOC格式的核心是三个目录:JPEGImages放jpg原图,Annotations放同名xml,ImageSets/Main放train.txt、val.txt这类划分文件。xml里每个object包含name和bndbox的xmin、ymin、xmax、ymax像素坐标,图片尺寸在size节点。YOLO格式则是一一对应的images和labels两个目录,每个txt文件一行记录一个目标:class_id cx cy w h,坐标全部归一化到0到1。
两种格式的换算关系是检测训练里最基础也最容易出错的公式:cx=(xmin+xmax)/(2width),cy=(ymin+ymax)/(2height),w=(xmax-xmin)/width,h=(ymax-ymin)/height。反过来画框时,x1=int((cx-w/2)*width)。数据包自称双格式,意思是同一套图在Annotations和labels里各有一份,训练时不需要再转换。但实际使用中应该以YOLO格式为训练输入,VOC格式作为人工可读的原始凭证,两边一旦不一致,优先修正YOLO侧,因为训练脚本只认txt。
提示:解压后先对比image文件名和label文件名是否逐张对应。缺一个txt的图在训练时会被跳过但不会报错,属于典型的“看起来正常、实际少数据”的坑。
2.3 解压7z压缩包:Linux命令行与Windows工具的注意点
在Linux服务器上解压,常见做法是用p7zip。命令里的x表示保留目录结构解压,-o指定输出目录,-p后面直接跟密码。密码含特殊字符时建议用单引号包住,避免shell把$、!这类字符吞掉。如果环境里没有7z命令,先装p7zip-full。
# 安装p7zip后解压,-y跳过确认,示例密码仅做占位 sudo apt install p7zip-full -y 7z x 航拍路面病害检测道路裂缝检测数据集VOC+YOLO格式3302张7类别.7z -o./road_data -p'你的密码' -y这段命令里,x是小写,和提取操作对应,很多人会写成大写X导致行为不同;-o后面没有空格,写成-o ./road_data会直接报错。解压完成后先看顶层目录,正常情况下应该有images和labels两个主目录,或者JPEGImages、Annotations、ImageSets三件套。Windows下推荐用7-Zip官网的最新版右键解压,不要用国产压缩软件解7z,遇到高压缩比或分卷时容易出现“数据错误”但文件已生成的情况,这时候目录看似完整,实际图片会缺帧。
2.4 三段式验货:数量统计、类别分布和标注可视化
解压完不要直接开训,先用一个Python脚本把家底盘清楚。第一段验货是统计图片和标签数量是否都是3302,两数对不上说明有缺文件;第二段是统计每个类别的实例数,看分布是否严重倾斜;第三段是抽几张图画框确认标注没有整体偏移。
import os from collections import Counter img_dir = './road_data/images' label_dir = './road_data/labels' imgs = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] labels = [f for f in os.listdir(label_dir) if f.endswith('.txt')] print(f'图片数: {len(imgs)},标签数: {len(labels)}') assert len(imgs) == 3302, '图片数量与标题声明不一致' # 统计每个类别的实例数量 cat_counter = Counter() for lb in sorted(labels): with open(os.path.join(label_dir, lb), 'r') as f: for line in f: parts = line.strip().split() if parts: cat_counter[int(parts[0])] += 1 for cls_id in range(7): print(f'类别{cls_id}: {cat_counter.get(cls_id, 0)} 个实例')脚本逻辑很直白:先列图片文件和txt文件,数量不等立即中断;然后逐行读txt,取每行第一个字段作为类别id累加。类别id从0开始,所以循环0到6就能看到7个类别的分布。如果某个类别实例数接近0,说明要么标注严重倾斜,要么类别定义和你的任务不一致,这种数据直接进训练会拖累yolo损失函数的收敛节奏。
2.5 抽检可视化:把YOLO txt画回jpg,十秒看出标注错位
数量统计只能发现文件缺失,发现不了坐标错位。常见做法是把txt里的归一化坐标还原成像素框,画回原图肉眼检查,重点看框是否贴着物体边界、是否大面积框进背景。
import cv2 img_path = './road_data/images/000001.jpg' label_path = './road_data/labels/000001.txt' img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: cls, cx, cy, bw, bh = map(float, line.strip().split()) 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.LINE_AA) cv2.putText(img, str(int(cls)), (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite('./check_000001.jpg', img) print('标注可视化已输出: check_000001.jpg')这里有个容易忽略的参数:cv2.rectangle用了cv2.LINE_AA抗锯齿,框边缘柔和但不影响检查坐标。putText字号0.8对1280分辨率合适,如果原图是4K航拍图,字号要放到1.5左右。实际验货时随机抽10到20张,覆盖面铺开比单张精细检查更重要,错标比例超过5%建议先修正数据再训练,别指望模型把错的标注“学回来”。
3. 把双格式数据跑进YOLO训练:划分、配置与格式互转
3.1 数据划分别用随机抽样:按航段聚簇才能防泄漏
3302张航拍图不是独立样本,连续帧之间高度相关。如果随机划分train和val,同一段路的相邻帧会同时出现在两边,val指标虚高,部署时一换新路段立刻现原形。常见做法是按文件名里的路段前缀聚簇,每个前缀的所有帧作为一个整体分到train或val,不让同一个航段跨集合。
import os import random from collections import defaultdict random.seed(42) img_dir = './road_data/images' imgs = sorted(os.listdir(img_dir)) # 假设文件名形如 flight01_0001.jpg,取下划线前部分作为路段id groups = defaultdict(list) for name in imgs: prefix = name.split('_')[0] groups[prefix].append(name) groups = list(groups.values()) random.shuffle(groups) train_num = int(len(imgs) * 0.8) val_num = int(len(imgs) * 0.9) train_set = [f for g in groups[:int(len(groups) * 0.8)] for f in g] val_set = [f for g in groups[int(len(groups) * 0.8):int(len(groups) * 0.9)] for f in g] test_set = [f for g in groups[int(len(groups) * 0.9):] for f in g] print(f'train: {len(train_set)}, val: {len(val_set)}, test: {len(test_set)}')随机种子固定为42,是为了让后续对比实验在完全一致的划分下进行。train/val/test按路段比例切而不是按张数切,因为每组帧数不一样,按组数切更均衡。切完把列表写成txt,如果走yolo默认目录结构,就按txt把图片拷到images/train、images/val、images/test三个子目录,labels做同样操作。这一步做完用3.2节的yaml即可直接开训。
3.2 一份能直接改的data.yaml和最小训练命令
yolo训练需要data.yaml指定路径、类别数和类名。类名必须和解压后labels目录里的txt第一列对齐,顺序错了训练不会报错,但mAP曲线和混淆矩阵上的名字会错位,属于最难排查的一种低级错误。
# road.yaml,路径以实际解压位置为准 path: ./road_data train: images/train val: images/val test: images/test nc: 7 names: 0: transverse_crack 1: longitudinal_crack 2: alligator_crack 3: block_crack 4: pothole 5: repair 6: rut训练命令用ultralytics的CLI,模型先选n或m跑基线,不要一上来就上x。imgsz对航拍裂缝非常关键,1280比640在Recall上通常高10个点左右,代价是显存和训练时间翻倍。batch从16起步,显存不够降到8。
# 先跑一版小模型把流程走通,数据没问题再换大模型 yolo detect train data=road.yaml model=yolov8n.pt epochs=150 imgsz=1280 batch=163.3 VOC转YOLO的自用脚本:坐标裁剪与类名映射一步到位
虽然数据包声称双格式,但实际使用中经常需要自己重新生成一份YOLO标签,比如训练脚本需要不同的目录结构,或者你只想挑其中几个类别做实验。这时候有一份可靠的VOC转YOLO脚本比手工改xml快得多。
import xml.etree.ElementTree as ET def voc2yolo(xml_path, out_txt, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) with open(out_txt, 'w') as f: for obj in root.iter('object'): name = obj.find('name').text.strip() cls_id = class_map.get(name, -1) if cls_id < 0: continue # 不认识的类直接跳过 box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 边界裁剪:防止标注越界导致训练时loss异常 x1 = max(0, min(x1, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) x2 = max(0, min(x2, img_w - 1)) y2 = max(0, min(y2, img_h - 1)) 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 f.write(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n')class_map是一个字典,形如{'transverse_crack': 0, 'longitudinal_crack': 1},不认识的名字返回-1直接跳过。边界裁剪这段很多人省略,但xml里确实可能出现xmax大于图片宽度的情况,不裁剪的话归一化坐标超过1,训练时锚框匹配直接错乱。x2<=x1或y2<=y1的判断用于过滤手滑产生的无效框。
3.4 预训练模型的选择:n/m起步还是直接上x
“yolo预训练模型下载”是搜索热词,常见路径是ultralytics在首次运行时自动拉取github release权重,网络不稳定时可以提前手动下载yolov8n.pt、yolov8m.pt放进工作目录。对航拍路面病害这种小目标任务,预训练权重依然有用,但建议冻结backbone前几层再微调,因为航拍图分布和COCO差异很大,底层纹理特征迁移价值有限。先用n把数据链路验通,再换m或x刷精度,是性价比最高的路径。
4. 训练环节的翻车点:小目标尺寸、损失函数、BN崩溃与混淆矩阵
4.1 航拍裂缝为什么成了小目标重灾区:像素宽度与imgsz的取舍
一条裂缝在航拍图里实际宽度可能只有3到5像素,imgsz=640时经过stride=8的下采样,只剩不到1像素的有效特征。这就是为什么很多人用默认配置训练,mAP看起来50多,一画PR曲线发现横向缝和纵向缝的Recall极低。常见做法是把imgsz提到1280,裂缝宽度回到6像素左右,特征图至少保留2到3个有效像素。显存够就上1600,不够就先做tile切图训练。
如果坚持用yolov5系列,还需要关注anchor。yolov5在训练前会通过k-means重新聚类anchor,训练日志里会打印新的anchor尺寸。航拍小目标多,默认shape里的最小anchor可能还是太大,可以手动把anchor缩小到原值的0.7倍再训。yolov8用了anchor-free设计,省掉这一步,但也意味着它更依赖特征金字塔对小目标的表达能力,imgsz不足时提升有限。
4.2 yolo损失函数在类别不均衡下的表现
默认yolo检测头用BCEWithLogitsLoss处理分类,框回归用CIoU。7个类别里龟裂如果占了六成,BCE的梯度会被多数类牵着走,车辙、修补这类样本少的类别经常学不出来。常见做法是给数据加载器加类别采样权重,让每个batch里稀有类别出现的频次更均匀,或者用复制粘贴数据增强——把稀有类别的目标从别的图上贴过来,顺便做水平翻转扩充位置多样性。
focal loss也是一种思路,它对难分类样本给更高权重,但yolov8没有直接暴露switch参数,需要自己改loss或者用社区补丁。对大多数工程场景,先做类别加权采样比换损失函数收益更直接。采样权重按实例数反比计算,实例最少那一类权重设为1,其他类按比例降低。
4.3 训练中BN崩溃:现象、前置条件和恢复办法
“yolo训练中bn崩溃”是真实且高频的灾难。现象是训练到某个epoch后,val loss突然变成nan,或者feature map全部归一化成同一个值,模型输出全为0。原因通常有三个:学习率过大、batch size太小导致BN统计量噪声很大、预训练权重与当前数据分布差异过大。解决路径是先把lr降到1e-4,batch提到16以上,打开warmup,同时检查数据里有没有全黑或全白的坏图。
全黑的航拍图会让BN的running mean猛地偏移,这类图在数据集里哪怕只有一两张也足够引发崩溃。排查时先跑一个只加载数据的脚本,统计每张图的均值和方差,异常图直接剔除。训练日志也要盯前50个iter,loss出现跳变而不是平滑下降,立刻停住查lr和batch。恢复训练时从最近的正常checkpoint续跑,不要从头再来。
4.4 混淆矩阵总和为什么不是100%:yolo混淆矩阵的独特结构
很多人训练完打开confusion_matrix.png,发现每一行加起来不是100%,以为矩阵画错了。“yolo混淆矩阵总合不唯一”是搜索热词,实际原因是yolo的混淆矩阵最右边有一列background,漏检的目标会被计入background预测,所以行总和不再是1。正确读法是看对角线数值,再单独看每个类别有多少比例被漏到background列。
例如横向缝类别,对角线显示0.62,background列显示0.31,说明近三成横向缝没被检出来。只看mAP50会认为模型还行,但这个小目标漏检率在航拍场景是致命的,解决办法仍然是提imgsz或tile切图。混淆矩阵的另一行总和问题来自预测框可能命中多个GT,yolo在算混淆矩阵时做了贪心匹配,一个框只算一次,所以列总和也可能超过或不足实际预测数,这都是正常的,不是bug。
5. 避坑:7z解压、格式转换、训练中的五个高频问题
5.1 7z压缩包提示密码正确却一直报数据错误
现象:用7-Zip解压带密码的压缩包,输入密码后提示“数据错误”,或者解压到一半中断,部分文件已生成但打不开。 原因:第一,压缩包在传输或下载过程中字节损坏,CRC校验不通过;第二,7-Zip版本过老,不支持新的压缩算法;第三,分卷压缩时只下载了第一个分卷就直接解压。 解决:先执行测试命令而不是直接解压,7z t会跑完整CRC校验。
7z t 航拍路面病害检测道路裂缝检测数据集VOC+YOLO格式3302张7类别.7z -p'你的密码'返回“All tests passed”说明压缩包没坏,问题在解压工具或路径;如果某个文件名出现CRC Error,那就是文件损坏,需要重新获取资源。另外注意中文文件名编码问题,有些包内是UTF-8,但旧版工具按GBK处理导致解压后文件名乱码,先解压到纯英文路径再改名,不要直接在中文目录下解压。
5.2 VOC转YOLO后坐标出界导致loss激增
现象:转换后训练,loss一开始就很大,或者检测框全部偏到图像边缘,mAP趋近于零。 原因:xml里某个坐标写错,xmax大于图片宽度,转换时没做裁剪,归一化后cx超过1,锚框匹配完全错乱。 解决:在转换脚本里做边界裁剪,转换完再扫一遍所有txt,凡是存在任何坐标小于0或大于1的行都要单独修。可以写一个快速检查脚本,把异常txt文件名打出来,定位到具体是哪张图的哪个object问题。
5.3 data.yaml中类名和txt第一列对不上
现象:训练正常,mAP也正常,但混淆矩阵和结果图上的类别名字全乱。 原因:yaml里的names顺序和标注时的class_id不一致,最常见的是把0和1的映射搞反,或者漏了一个类别导致后续全部错位。 解决:训练前写个小脚本,打印每个类别id出现次数并对照yaml names。更保险的做法是先用单类别模型在验证集上跑一次,确认类别0对应的确实是横向裂缝再全量训练。这个检查和模型精度无关,纯粹是数据治理习惯。
5.4 训练中途loss变成nan
现象:train loss在某个epoch突然nan,之后一直nan,显存占用正常但val mAP掉到0。 原因:lr太大导致梯度爆炸、BN崩溃、或者数据中有损坏图片在增强时产生了非法像素值。 解决:先检查图片能否正常解码,再降lr,yolov8的lr降到1e-5通常能救回来。另外某些老显卡上Amp混合精度会触发nan,关掉amp再试。不要用减半batch的方式去解,nan和OOM不是一回事,减batch解决不了梯度问题。
5.5 显存不足OOM后的优雅降级方案
现象:imgsz=1280、batch=16直接报CUDA out of memory。 原因:高分辨率下的特征图和gt分配矩阵占用大量显存,航拍大图尤其明显。 解决:batch降到4或8,同时开梯度累积。yolov8的accumulate参数默认为2,可以调到4或8,等效batch不变但显存占用减半。不要先降imgsz,小目标检测对分辨率敏感,降imgsz等于直接放弃精度。如果还是OOM,再考虑换显存更大的卡或走tile切图训练路线。
6. 验货与部署:mAP之外的三个航拍指标和tile切图推理
6.1 逐类PR曲线与Recall比mAP更诚实
训练完别只看mAP50-95。对不平衡的7类病害,mAP会被占多数的龟裂拉高,横向缝和纵向缝这种细裂缝可能只有30出头的Recall。画每类PR曲线时,把预测结果和真实标签先对齐,看看纵向缝的PR曲线是不是贴着右下角。如果某一类的Recall远低于其他类,优先检查是不是小目标漏检,解决办法是提imgsz或者走tile切图。
6.2 tile切图推理:重叠率、坐标还原与NMS合并
航拍推理时,如果整图直接过模型,小目标几乎必丢。常见做法是把原图切成1280或1024的patch,重叠率设25%到50%,推理完把patch里的框坐标换算回原图坐标,再对重叠区做NMS去重。重叠率越高,裂缝跨patch被截断的概率越低,但推理耗时也线性上涨。
def tile_inference(model, img, tile_size=1280, overlap=0.25, conf=0.25): h, w = img.shape[:2] stride = int(tile_size * (1 - overlap)) dets = [] for y in range(0, max(h - tile_size + 1, 1), stride): for x in range(0, max(w - tile_size + 1, 1), stride): patch = img[y:y + tile_size, x:x + tile_size] result = model.predict(patch, conf=conf, verbose=False)[0] for box in result.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf_val = float(box.conf[0]) cls_val = int(box.cls[0]) dets.append([x1 + x, y1 + y, x2 + x, y2 + y, conf_val, cls_val]) # 全图坐标下再做一次NMS,合并重叠区重复框 final = nms(dets, iou_thresh=0.5) return final这段代码把patch坐标加上偏移量x和y换算回原图坐标,这是最容易被漏掉的一步。NMS建议用torchvision.ops.nms,比手写的稳定,iou_thresh在航拍场景用0.5比较合适,设太高会让重叠区的重复框都保留,设太低又会误删真正的相邻病害。反复试验后,25%重叠率加0.5的iou阈值是兼顾速度和精度的组合。
6.3 我现在的习惯
最后说一个我自己的教训。第一次跑这类航拍数据的时候,我没做按航段划分,直接random split,val mAP到了0.67,觉得模型很能打。换到新航拍视频上直接掉到0.4,后来才发现val里全是训练帧的邻居,等于开卷考试。从那以后每条数据集都要先看文件名前缀,划分脚本里强制隔离航段,宁可train少几百张也不让数据泄漏。另一个习惯是每轮训练盯前50个iter的loss走向,BN崩溃和lr过大的苗头在Early Stopping触发之前就能看出来。还有一条:拿到任何标注数据包,先跑可视化抽检再谈训练,标注质量不过关,后面所有调参都是浪费时间。希望帮到你。
本文还有配套的精品资源,点击获取