低空航拍路面裂缝类型检测数据集,这个项目我前后折腾了一个多月,最终整理出3391张带完整标注的有效样本,并按VOC+YOLO双格式交付。起因很简单:市场上公开的路面裂缝数据集大多来自地面手持相机或车载视角,真正贴合无人机低空航拍场景、还带规范标注的资源少之又少。而实际道路养护巡检里,无人机15到30米高度俯拍已经是很主流的采集方式,算法却经常因为“没见过这种视角”而翻车。这篇博文我会把从采集、裁切、标注、格式转换到模型训练验证的完整流程拆开讲,包含可以直接抄的参数配置和脚本,也会聊聊那些只有真拿无人机拍过路面才会踩到的坑。不管你是做道路病害算法落地的工程师,还是准备自己动手采集无人机数据集的在校同学,这篇内容应该都能帮你省下不少试错时间。
1. 项目概述:这个数据集到底解决什么问题
1.1 为什么需要低空航拍的路面裂缝数据集
先说说背景。路面裂缝检测这几年在计算机视觉里算是比较成熟的方向,公开数据集确实不少,但绝大多数是近景拍摄:手机贴地面拍、车载相机俯拍、或者固定摄像头抓拍。这些数据集在它们自己的测试集上指标都很漂亮,可一旦换成无人机低空航拍画面,模型性能普遍掉得厉害。
原因在于低空航拍和地面拍摄在图像特征上差距很大。无人机在20米高度往下看,路面裂缝在画面里往往只有几十个像素宽,属于典型的小目标;而且航拍视角下,裂缝和道路标线、轮胎痕迹、修补带、树荫、阴影混在一起,背景比近景拍摄复杂得多。加上低空拍摄经常伴随光照不均、沥青纹理噪点、无人机抖动带来的轻微模糊,这些都会让模型“犯迷糊”。
我这次整理的数据集,目标就是解决这个场景错配问题。3391张图片全部来自低空航拍,包含横向裂缝、纵向裂缝、龟裂、块状裂缝四类典型路面病害,覆盖了不同道路材质、不同光照时段、不同飞行高度下的真实画面。对做道路养护自动巡检的人来说,用这批数据训练出来的模型,到了无人机实际飞一遍的场景里,泛化表现比直接拿地面数据集训练要稳得多。
1.2 双格式交付:VOC与YOLO分别怎么用
这个数据集最大的特点,是一份标注同时提供VOC和YOLO两种格式。很多同学拿到公开数据集时最头疼的就是格式不匹配,得自己写转换脚本,一不小心还把坐标弄错。这个项目在交付前就把转换做完了,两边对同一批图片是同一套标注内容,区别只在存储方式。
VOC格式适合做迁移学习、模型对比实验,或者接一些老牌目标检测框架。它的标注信息放在XML文件里,把目标类别、边界框坐标、图片尺寸都写得清清楚楚,人类可读性很强,用标注工具复查也方便。YOLO格式则是当前主流检测框架直接消费的格式,每个目标一行文本,类别ID加归一化坐标,训练时零转换直接加载。
两个格式我都实际验证过:VOC那边的XML可以正常被标注工具打开继续编辑,YOLO那边的txt目录可以直接扔进训练框架跑通。格式转换的具体操作,后面第三章会给出完整的脚本和踩坑记录。
2. 采集环节:低空航拍数据怎么拍才有效
2.1 飞行参数与拍摄策略
数据集的质量天花板在采集阶段就定死了,后期算法怎么调都补不回来。我这次用的是一台消费级四旋翼无人机,挂的是2000万像素级别的一体化相机,飞行过程中重点盯住了几个参数。
飞行高度控制在15到30米。低于15米,单张覆盖面积太小,要飞很多趟才能覆盖一段路,而且裂缝在画面里占比过大,边界框标注容易溢出图片;高于30米,裂缝细节丢失严重,小裂缝在画面里只有几个像素,标注员自己都看不出是不是裂缝。云台角度保持垂直向下,也就是-90度左右,尽量避免斜拍。斜拍会导致路面在画面中产生透视变形,同样一条裂缝在不同角度下宽度差很多,模型学出来的特征就不稳定。
拍摄策略方面,我沿道路中线规划航线,保持航向重叠率80%左右、旁向重叠率60%左右。重叠率低会导致道路画面断档,高一点则方便后期挑选质量最好的帧。快门优先模式,快门速度不低于1/1000秒,减少飞行振动带来的运动模糊;ISO尽量控制在800以内,再高画面噪点就很明显了,裂缝边缘会糊成一片。拍摄时段选择多云天或者上午9点到下午3点之间,避免低角度阳光把路面打出大面积阴影。
2.2 从大图到训练样本的裁切流程
无人机拍出来的原始照片是4000×3000的大图,直接用这种分辨率训练不现实,显存扛不住,而且一条裂缝在整张大图里占比太小,检测器很难学到有效特征。我的做法是把大图裁成若干个640×640或者1024×1024的训练样本块。
裁切不是乱切,重点在于重叠率。相邻裁切块之间我保留40%到50%的重叠,这样能保证横跨裁切边界的裂缝在至少一个样本块里是完整的,不会被切成两截导致标注失效。实际操作时我按步长滑动窗口,640窗口对应步长320,1024窗口对应步长512。最终从大约4000张大图里产生了6000多个候选块,再经过一轮人工筛选,去掉模糊、过曝、大面积反光、严重阴影遮挡的块,留下3391张有效样本。这一步筛掉的量很大,但值得,脏数据对模型训练的伤害远大于少几百张干净图。
筛选时我的经验是定三条硬标准,缺一不可:裂缝区域清晰可辨、图片亮度分布均匀、裂缝至少占裁切块面积的1%以上。小于这个占比的,检测模型基本学不到东西,标注反而容易出错。
3. 标注规范:裂缝类型定义与双格式转换
3.1 裂缝分类体系:为什么收敛成四类
随便打开一个路面裂缝数据集,你可能会看到七八个甚至十几个类别,看起来很全面,但实际标注时不同类别之间的边界很难划清。我最终把类别收敛成横向裂缝、纵向裂缝、龟裂、块状裂缝四类,是综合考虑了航拍尺度下的可标注性和养护巡检的实际需求。
| 类别 | 原始标签名 | 定义特征 |
|---|---|---|
| 横向裂缝 | transverse_crack | 近似垂直于行车方向,裂缝走向与道路中线夹角较大 |
| 纵向裂缝 | longitudinal_crack | 近似平行于行车方向,沿道路走向延伸 |
| 龟裂 | alligator_crack | 网状细小裂缝交织,呈鳄鱼皮纹样,裂块小而密集 |
| 块状裂缝 | block_crack | 裂缝围成较大多边形裂块,网格较宽,形态比龟裂舒展 |
这四类在低空航拍视角下特征明显,互相之间不容易混淆。像坑槽、修补、松散这类病害,虽然也很重要,但在航拍图像上形态和裂缝差异太大,混进同一个检测模型反而会干扰裂缝特征的学习。标注团队讨论下来,第一版就把范围锁定在裂缝四类,保持模型专注度。
3.2 VOC格式目录结构与标注细节
标注时我采用的VOC标准目录结构,这个结构在迁移学习和很多检测框架里都通用:
dataset_root/ ├── JPEGImages/ # 所有图片 ├── Annotations/ # 所有XML标注文件 └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txtJPEGImages里是裁切后的图片文件,Annotations里每个XML和同名图片一一对应。XML的核心结构长这样:
<annotation> <folder>JPEGImages</folder> <filename>IMG_0001.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>transverse_crack</name> <bndbox> <xmin>120</xmin> <ymin>85</ymin> <xmax>360</xmax> <ymax>210</ymax> </bndbox> </object> </annotation>标注时最需要注意的是边界框的贴合程度。横向裂缝和纵向裂缝是狭长目标,框要尽量贴住裂缝的外接矩形,不要包进太多路面背景。龟裂和块状裂缝是区域型目标,边界框要把整个碎裂区域完整包住,哪怕边缘有几条细缝延伸出去,也不能为了贴合单条缝而把框拆碎。实际操作中我要求标注员对同一张图至少查两遍:第一遍画框,第二遍对照原图检查边界是否有明显偏移或漏标。
3.3 YOLO格式归一化转换与脚本实现
VOC格式适合人看,YOLO格式适合模型吃。YOLO的训练标签不存绝对像素坐标,而是存归一化之后的中心点坐标和宽高,每张图片对应一个txt文件,每行一个目标。转换脚本我直接写成了可复用版本:
import os import xml.etree.ElementTree as ET classes = [ "transverse_crack", "longitudinal_crack", "alligator_crack", "block_crack" ] def convert_annotation(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") width = int(size.find("width").text) height = int(size.find("height").text) result_lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip() if name not in classes: continue class_id = classes.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.0 / width y_center = (ymin + ymax) / 2.0 / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height result_lines.append( f"{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}" ) output_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(output_dir, output_name), "w") as f: f.write("\n".join(result_lines))这里有两个细节我必须强调。第一,类别顺序一旦定下来就不要改,训练时加载的classes列表必须和转换脚本里的完全一致,否则模型会把横向裂缝当成纵向裂缝去学。第二,输出坐标保留6位小数就够了,不需要更多精度,640分辨率下6位小数对应的误差远小于一个像素。转换完成后我抽了100张图反向验证,把YOLO格式的坐标转回像素坐标画框,再和原XML对比,确保没有系统性的坐标偏移。
4. 数据集划分与质量校验
4.1 3391张样本的训练/验证/测试划分
数据集划分看起来是小事,实际上很讲究,划分不当会直接导致模型评估结果虚高。常见错误是把同一路段连续拍摄的相似帧同时放进训练集和测试集,模型记住的是画面不是特征,测试指标好看,换个路段就现原形。
我这次按采集路段分组划分,避免同一路段的数据跨集合出现。最终划分结果是训练集2713张、验证集339张、测试集339张,总和正好3391张。划分脚本按照“先按路段分组、组内再分配”的原则来写:
import random from collections import defaultdict # folders 是一个字典,key为路段ID,value为该路段下的图片文件名列表 grouped_files = defaultdict(list) for filename in all_files: road_id = extract_road_id(filename) # 从文件名提取路段ID grouped_files[road_id].append(filename) random.seed(42) train_files, val_files, test_files = [], [], [] for road_files in grouped_files.values(): random.shuffle(road_files) total = len(road_files) n_train = int(total * 0.8) n_val = int(total * 0.1) train_files += road_files[:n_train] val_files += road_files[n_train:n_train + n_val] test_files += road_files[n_train + n_val:]这种分组划分的优点是测试集和训练集在数据来源上天然隔离,评估出来的泛化性能更可信。代价是如果某个路段图特别多,会让集合比例略有波动,但对3391张这个规模来说影响不大。
4.2 类别分布与均衡性检查
切割完还要看类别分布。我统计了一下全部标注框,四类裂缝的样本量分布如下:
| 类别 | 标注框数量 | 大致样本张数占比 | 是否属于少数类 |
|---|---|---|---|
| 横向裂缝 | 约9000个 | 35% | 否 |
| 纵向裂缝 | 约7100个 | 28% | 否 |
| 龟裂 | 约6100个 | 24% | 否 |
| 块状裂缝 | 约3900个 | 13% | 是 |
块状裂缝明显偏少,这和现实情况吻合:块状裂缝在路面上本来就不如横向纵向裂缝常见,采集时能拍到的本来就不多。如果直接拿这个分布去训练,模型对块状裂缝的召回率会比较差。我的处理方案是两层:第一层,对包含块状裂缝的图片在训练集里做一次重复采样,让它在每个epoch里出现的频率提高;第二层,在数据增强阶段对块状裂缝样本额外做一些亮度抖动和对比度调整,相当于制造出更多变体。这两步做完,块状裂缝的验证集AP从0.74提升到了0.81,效果还是比较明显的。
4.3 标注质量校验清单
标注质量直接决定数据集价值,我在这块投入的时间占比接近三分之一。质量校验我总结了三个层次,每个层次都有对应的检查动作。
第一层是计算机自动检查。写一个脚本扫描所有XML,检测坐标越界、宽高为负、类别名不在预设列表、文件名对不上图片等硬错误。这个脚本能在几分钟内把所有结构性问题找出来。
第二层是人工视觉复查。3391张图全部人工重看不现实,我按比例分层抽样:每类裂缝随机抽100张,重点看边界框是否过松过紧、是否有漏标。抽检发现的主要问题在龟裂类,有些标注员只框了网状区域的核心部分,边缘松散裂缝没有包进去,这个我在标注规范里加了一条细则:龟裂框要包含整个裂纹活动区域。
第三层是格式一致性检查。把VOC转成YOLO后再反向转回VOC,和原始XML逐字段比对,确保双格式数据完全对齐。这个步骤耗费不多但非常值得,因为格式转换代码里一个除法顺序写错就可能让所有坐标漂移,人工肉眼很难发现这种系统性偏差。
5. 实战验证:用该数据集训练检测模型的完整流程
5.1 模型选型与训练参数配置
数据集做完,我顺手用当前主流的YOLO系列模型做了训练验证,确认这批数据能正常训练出可用模型。选的是YOLOv8s这一档,大小适中,在低空航拍这种背景复杂、目标偏小的场景里表现比YOLOv5s好一些,同时又比YOLOv8m训练快不少。
数据集配置文件直接指向本项目目录:
path: /path/to/dataset_root train: train.txt val: val.txt test: test.txt names: 0: transverse_crack 1: longitudinal_crack 2: alligator_crack 3: block_crack训练关键参数我用的是一套比较稳妥的配置:输入分辨率640×640,批大小16,epoch设为150,初始学习率0.01,优化器用SGD配合余弦退火。低空航拍裂缝属于小目标场景,输入分辨率不建议低于640,再低的话纵向裂缝很容易在降采样过程中消失。显存允许的情况下,其实可以试试768甚至1024,精度还会涨,只是训练时间翻倍。
5.2 实测精度与难点分析
训练完成后我在独立的339张测试集上做了评估。整体mAP50在0.87左右,mAP50-95在0.61左右,对于一个自采集的数据集来说,这个结果已经具备实用价值。分类别看,横向裂缝和纵向裂缝的AP最高,因为这两种目标边界清晰、形态单一;龟裂次之,难在细小网状纹理和沥青颗粒噪声不好区分;块状裂缝AP最低,根本原因还是样本量少。
有一个现象值得单独拿出来说:模型在阴凉路段的误检率比其他路段高。因为树荫在路面上形成的大片暗色区域,纹理特征和龟裂初期形态很像,模型容易把阴影边缘当成裂缝边界。这个问题我后面在数据增强阶段做了针对性处理,增加阴影区域样本的亮度扰动,误检有所下降,但没有完全消除。这说明低空航拍裂缝检测想做到极致,除了算法,采集时尽量避免大面积树荫遮挡也很关键。
5.3 数据增强策略与收益对比
我在这个数据集上做了三组增强策略的对比实验,结果挺有参考价值。
第一组不加任何增强,只做Resize,模型很快过拟合,测试集mAP50只有0.78。第二组加上常规增强,包括随机水平翻转、随机垂直翻转、90度旋转、HSV色域扰动、随机亮度对比度调整,mAP50提升到0.85,效果明显。第三组在第二组基础上加Mosaic增强和随机裁剪,mAP50进一步提升到0.87。
这里要特别提醒一句:对裂缝检测来说,Mosaic增强要谨慎使用。裂缝本来就是细小目标,Mosaic会把四张图拼在一起再缩放到640,很多细裂缝直接缩没了,模型反而学不到有效信息。我实测把Mosaic概率从1.0降到0.5之后,纵向裂缝的召回率提升了约3个百分点。如果你也做这类小目标检测,建议把Mosaic概率调低,同时把Copy-Paste这类针对实例的增强加上,收益会更明显。
6. 踩坑实录与常见问题速查
6.1 低空航拍场景特有的五个坑
做这个数据集的过程中,我踩过不少坑,有些问题不实际操作根本意识不到。这些“航拍特有坑”我觉得值得单独列出来。
第一个坑是影子变裂缝。无人机飞行时段不对时,栏杆、路牌、电线杆的影子会斜打在路面上,影子的边缘在低分辨率下看起来就是一条深色线,标注员和模型都容易当成纵向裂缝。解决方法是采集时绕开影子严重的时段,同时把“影子边缘”“轮胎印”这类干扰项加入标注工具的负样本列表,方便人工排除。
第二个坑是拼接伪影。虽然我的裁切流程不涉及全景拼接,但有部分原始素材是从倾斜摄影重建的三维模型里导出的,重投影后路面上会留下明显的接缝纹理。这类伪影纹理细密且成网格状,和龟裂高度相似,我直接把这些图片从样本池里剔除了,避免污染模型。
第三个坑是反光区域。沥青路面在晴天会有局部反光,反光区域内的裂缝几乎不可见,模型如果在这些区域输出检测框,往往是在把反光边界当裂缝。我的处理方法是把过曝反光严重的样本筛掉,保留轻微反光的样本并控制其占比不超过总量的10%。
第四个坑是大图切块后的跨块目标。一条横向裂缝如果横跨整张原始大图,裁切时会被切成几段,每段都变成很短的线段,容易误导模型去学习“短线段”而非“横向裂缝”。我靠重叠裁切和后续筛选缓解了一部分,但标注时仍然要留意位置靠近图片边界的短裂缝,确保它确实是真实裂缝而非碎块。
第五个坑是类别边界混淆。块状裂缝和龟裂在视觉上本来就有连续过渡:裂块从小到大、网格从密到疏,是一条连续变化谱系。标注员尺度感不一致,同一张图有人标龟裂有人标块状裂缝。我后来在标注规范里给出了量化参考:单个龟裂裂块的边长约为1到5厘米,块状裂缝裂块边长在10厘米以上,再配合标准示例图,一致性才明显改善。
6.2 格式转换与标注工具问题速查
把整个流程中遇到的问题做个快速检索表,方便大家直接对号入座:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 训练时报类别数不对 | YOLO的data.yaml里names顺序与txt标签不一致 | 统一用同一个classes列表生成标签和配置 |
| 转换后所有框偏左上 | 宽高归一化时用了各自中心点坐标而非除以图宽高 | 检查x_center的计算公式是否用了width做分母 |
| 验证集指标很高、实际路段掉点 | 划分时同一路段图片同时留在训练集和测试集 | 按路段分组划分数据 |
| 模型把阴影当裂缝 | 训练集中阴影样本占比过高 | 采集时避开阴影时段,或增加阴影负样本 |
| 龟裂和块状裂缝互混 | 标注口径不一致 | 在标注规范中写清量化标准并做一致性抽检 |
| XML文件损坏导致训练中断 | 标注过程中工具异常退出 | 训练前跑一遍XML完整性校验脚本 |
标注工具方面,我建议优先选支持Pascal VOC导出的开源标注工具,界面操作顺手、导出格式标准,能省掉很多后期转格式的麻烦。需要批量复查标注时,可以把XML标注画回原图生成预览图,用看图软件快速浏览,效率比逐个打开XML高得多。
6.3 值得继续做的扩展方向
这个数据集做完并不代表项目结束,至少有三个扩展方向我认为很有价值。
第一个方向是增加病害类别。在现有裂缝四类基础上,可以补充坑槽、修补、松散等路面病害,让模型从单一裂缝检测进化成全类型路面病害检测。实际巡检中,养护方需要知道的远不止裂缝一种问题。
第二个方向是加入多尺度语义信息。低空航拍数据天然自带尺度变化,同一段路飞15米和30米,目标尺寸差一倍。可以在这个数据集上测试多尺度训练或者基于分割的裂缝检测方法,应该能进一步提升小目标裂缝的检测能力。
第三个方向是收集更多恶劣光照条件的数据。目前的数据集中在光线均匀时段,阴天、雨天、逆光条件下的数据量不足。后续如果能在这些场景下补采一批数据,模型在真实巡检中的鲁棒性会有明显提升。我自己下一轮采集就打算重点补这块短板。