做桥梁检测方向的目标检测项目,最痛苦的往往不是调模型,而是找数据。路面裂缝、桥梁支座、螺栓脱落这些方向多少还有通用数据集可以凑合,但混凝土箱梁、拱桥、斜拉桥塔柱上的腐蚀与锈蚀病害,公开可用的图像简直少得可怜。所以当看到一套2370张、VOC和YOLO双格式、2个类别的桥梁腐蚀锈蚀严重程度检测数据集时,我第一反应是这能解决一批项目的冷启动问题,而不是简单当素材下载下来就完事。
这套数据的价值不在“2370”这个数字本身,而在于它把腐蚀(一般劣化)和锈蚀(钢筋锈胀引发的典型病害)归成两个独立类别,而且同时给了VOC XML和YOLO TXT两种标注格式。这意味着你不需要先花两周时间去洗数据、转格式、看标签是否一致,拿来就可以直接跑训练。本文不打算写成数据集介绍页,而是把我拿到这类数据后做的一系列处理、检查、训练验证尽量完整地记录下来,包括怎么换算坐标、怎么划分训练集/验证集、用什么超参起步、以及从二分类检测往严重程度分级延伸的思路。
1. 桥梁腐蚀锈蚀检测为什么这么缺数据
1.1 桥梁病害检测的真实现场:腐蚀锈蚀的核心地位
桥梁结构的耐久性问题里,混凝土碳化和钢筋锈蚀是两大主角。混凝土表面的腐蚀往往表现为酥松、剥落、泛白结晶,而钢筋锈蚀则会在保护层不足的地方形成锈迹、锈胀裂缝甚至保护层崩落。这两类病害外观差异明显,但在一张无人机拍摄的近距离图像里,经常挨在一起出现:锈水从裂缝渗出,周围混凝土已经酥化,区域边缘模糊,互相遮挡。
以前巡检主要靠人挂安全绳或者坐桥检车,肉眼观察后手写记录。这种做法的精度受主观判断影响很大,同一个锈蚀区域,A巡检员看是中度,B巡检员可能记录成重度。而且高空视角下很多病害只有贴近了才看得清,常规巡检照片要么距离太远、要么光线太差,能用于后续算法训练的高质量样本非常有限。
现在无人机巡检和移动式爬壁机器人已经普遍进入桥梁定检流程,一天能拍上千张高清照片。但设备把照片交给了人,人还是要对着屏幕逐张看,真正省力的环节完全没省下来。病害自动识别、自动分级、自动生成报告,就成了检测单位普遍想要的方案。可这一类任务对数据的要求比通用目标检测苛刻得多——病害边缘模糊、样本小、正负例分布极度不均。
1.2 深度学习进入桥梁巡检后,卡住项目的是数据集
用深度学习做桥梁病害检测,模型本身并不神秘,YOLO、Faster R-CNN、SSD都是成熟方案,难点在于数据端。
通用目标检测数据集里,车辆、行人、猫狗这类目标轮廓清晰、类别差异大,标注者容易达成一致。桥梁腐蚀锈蚀不一样:一个区域从轻微变色到明显锈斑再到保护层剥落,是一个连续谱,不同人标出来的类别很可能不一致。标注尺度也不统一,有人把整片锈迹框成一个大框,有人会把锈迹拆成密集小框。这种情况下,类别定义是否清晰、标签是否保持一致,直接决定模型能学到什么。
更麻烦的是桥梁病害图像不像通用数据集那样随手可得。桥梁一般分布在江河、山区、海岸沿线,能接触到桥面、箱梁内部、支座区域的团队有限;病害区域在整座桥中的面积占比很小,无人机拍10个G的素材,里面能用的病害特写可能只有一两百张。公开数据集稀缺的本质原因就在这里,采集成本高,标注成本更高。所以当有人整理出一套已经按统一标准标注、格式给全的数据集时,对做工程的人来说相当于省掉了整个项目最脏最累的前置阶段。
2. 2370张2类别,够训练一个能落地的模型吗
2.1 先算账再训练:规模、类别、标注质量三方权衡
先说结论:2370张图像在目标检测任务里属于“刚过温饱线”的规模。如果每张图像平均有2-4个病害框,那么两个类别合计大概有5000-9000个实例框,这个量训练一个单阶段检测器做baseline完全够用,但要直接上生产环境、面对完全没见过的桥梁结构,还需要配合迁移学习和数据增强。
我拿到数据集后的习惯是先做一个简单统计,把关键指标列出来:
| 指标 | 判断标准 | 这套数据的预期状态 |
|---|---|---|
| 图像总数 | 目标检测baseline至少1000张起 | 2370张,中等偏上 |
| 类别实例数 | 每类至少500-1000框 | 两类合计充足,但需看是否均衡 |
| 背景多样性 | 不同桥梁、不同部位、不同光照 | 需检查是否集中在某几座桥 |
| 标注一致性 | 框是否贴合病害边界 | 需抽查XML中的bbox分布 |
| 目标尺寸分布 | 小目标占比不能过高 | 无人机特写图像通常中等偏大 |
很多人只看总数就决定要不要用,这是不对的。2370张如果全部来自同一座桥的同一个箱室,模型学到的就是“这座桥的病害长这样”,换一座桥就崩;同样是2370张,如果覆盖了板梁、箱梁、T梁、拱桥吊杆等不同结构,模型的泛化能力会完全不一样。所以拿到数据后的第一件事不是训练,而是看分布。
2.2 这组数据最值钱的地方:场景覆盖与标签一致性
从标题里的“严重程度检测”这五个字看,这套数据的定位不是只做“有没有病害”的二分类,而是要给腐蚀锈蚀框出区域,为后续按严重程度分级提供基础。这比单纯目标检测要求更高,因为框的准确度会直接影响后续的面积统计和等级判定。
实用层面,这类数据往往来自两类采集环境:一是无人机贴近拍,视角偏俯拍,病害区域占画面比例较大;二是人工手持设备在箱梁内部、桥墩附近拍摄,光线偏暗,病害特征容易被阴影干扰。如果数据里两类场景都有,那模型在真实巡检中会更稳。我会在拿到数据后先做一轮场景聚类,确认训练图像里是否以某一类采集方式为主,再用外部少量真实巡检照片做一次快速验证,避免模型在光照变化较大时失效。
标签一致性还有个更隐形的价值:帮助团队统一标注规范。我经常遇到的情况是,外包标注团队对“轻度锈蚀”和“重度锈蚀”的理解跟检测工程师不一致,而有一套已经标注好的参考数据集,就能作为标注规范的锚点,让新加入的标注者先看几十张标好的范例,再开始工作。这个价值经常被人忽略,但对重复采集和持续迭代的项目来说特别重要。
3. VOC和YOLO格式的换算逻辑与预处理要点
3.1 两种格式的本质差异:从XML到TXT的坐标交换
VOC格式和YOLO格式的差异,很多人只记了个大概:VOC用XML,YOLO用TXT。其实关键差别在坐标表达方式上。
VOC XML里记录的是绝对像素坐标,比如<xmin>320</xmin>表示左上角x是320。YOLO TXT里记录的是归一化坐标,每行五个值:类别id、中心点x、中心点y、宽度、高度,四个数值都除以图像宽高,范围在0到1之间。比如一个框在640x480的图像里,左上角是(320, 160),右下角是(480, 320),换算成YOLO格式就是:
类id 0.625 0.5 0.25 0.3333中心点x是(320+480)/2/640=0.625,中心点y是(160+320)/2/480=0.5,宽度是(480-320)/640=0.25,高度是(320-160)/480=0.3333。
为啥YOLO要做归一化?因为模型训练时要对图像做resize,不管原图是800x600还是1280x960,归一化后的坐标比例不变,模型不用关心输入分辨率。VOC格式则更符合人的直觉,方便用可视化脚本直接画框检查。两种格式的差异还体现在类别名称上,VOC允许类别名直接写字符串,YOLO则用整数id对应一个classes清单文件。
如果你拿到的数据同时提供两种格式,最省事的做法是选一种主格式做训练,另一种用来校验。我的建议是训练直接用YOLO格式,因为无论是YOLOv5、YOLOv8还是Ultralytics框架,原生支持的就是TXT;VOC格式可以拿来快速可视化检查。如果只有VOC格式,建议写一个脚本批量转成TXT,下面这个是我常用的转换脚本,依赖OpenCV读取图像尺寸:
import os import cv2 import xml.etree.ElementTree as ET # 类别清单,顺序要和最终训练的classes一致 CLASSES = {"corrosion": 0, "rust": 1} def voc_to_yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = os.path.join(img_dir, img_name) img = cv2.imread(img_path) h, w = img.shape[:2] txt_name = os.path.splitext(img_name)[0] + ".txt" lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in CLASSES: print(f"未知类别: {cls_name}, 文件: {img_name}") continue cls_id = CLASSES[cls_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 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h # 防止归一化值越界 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) box_w = min(box_w, 1) box_h = min(box_h, 1) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines))转换不是跑完就完,必须抽查。尤其要注意两类问题:一是XML里有的框坐标超出了图像边界,可能是标注软件出错或原图被裁剪过,转成归一化坐标后,模型会在不存在的区域找目标,影响收敛;二是同一张图里有没有重复标注、完全重叠的框,这通常说明原始数据合过批但没做去重。
3.2 拿到数据集第一天,先做这几件检查
我的固定流程是四步。
第一步,检查图片和标签文件是否一一对应。很多数据集下载后会出现图片有但TXT缺失的情况,YOLO训练时会把缺失标签的图当成背景图,导致类别被漏学。写个脚本遍历图片目录和标签目录,找出缺失项,然后决定是删除图片还是重新标注。
第二步,可视化抽查。用OpenCV把框画到图上,随机看50到80张,重点看两个点:框是否紧贴病害边界;类别标签是否张冠李戴。腐蚀和锈蚀的边界本身就模糊,如果大量相似区域被标成不同类别,训练时模型会无所适从。
第三步,统计类别数量。用下面的方式快速统计TXT里每类出现次数:
cat labels/*.txt | awk '{print $1}' | sort | uniq -c如果两个类别的实例数差距在5倍以上,训练时就要注意。默认的损失函数会让模型偏向实例多的类别,病害区域小的漏检率会明显偏高。这个看情况决定是否要调整权重。
第四步,看图像尺寸分布。无人机原图可能是5472x3648,也可能已经统一resize到1280x960。YOLO训练会强制把图像缩放到训练尺寸,但缩放比例过大会导致小目标信息丢失,比例过小又会浪费显存。如果图像尺寸参差不齐,我会先统一处理,避免训练时的随机crop把关键区域裁掉。
4. 从0.6到0.85:基于这套数据的实际训练配置与调优记录
4.1 训练yaml组织与迁移学习的正确姿势
我习惯把数据集按下面的目录结构组织:
bridge_corrosion_data/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yamldata.yaml内容大概是:
path: /path/to/bridge_corrosion_data train: images/train val: images/val nc: 2 names: 0: corrosion 1: rust关键点:如果是VOC格式转换来的,要确保names的顺序和你转换脚本CLASSES里的顺序完全一致,否则模型会拿corrosion的标签去学rust的特征。类别名统一用小写英文字母,不要有空格和中文,否则部分框架解析会出问题。
数据集划分上,最容易犯的错误是直接把2370张图按比例随机切分。桥梁病害图像往往是从同一座桥的连拍视频里抽出来的,相邻帧高度相似。如果这些相似图像同时进了训练集和验证集,验证集分数会虚高,模型到新桥梁上会明显变差。
我的做法是按“场景”或“桥段”做分组划分。如果不确定数据来源,就先对图像做特征聚类,比如提取颜色直方图或pHash相似度,把相似度高的图像归为一组,再按组划分。这样能避免数据泄漏,验证集分数也更有参考价值。
迁移学习方面,直接拿COCO预训练权重起步基本是默认操作。重点解释一下为什么要这么做:COCO上的模型已经学会了边缘、纹理、颜色梯度等底层视觉特征,虽然它没见过桥梁病害,但特征提取器的前几层是可以复用的。你的训练只需要在已有特征基础上做微调,而不是从随机的卷积核开始学。在2370张这种规模下,从零训练极容易欠拟合,微调则可以很快达到可用水平。
实际命令我用YOLOv8举例:
yolo detect train \ data=bridge_corrosion_data/data.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ patience=15模型选择上,2370张数据规模用yolov8n或yolov8s起步更稳,yolov8m虽然精度上限高但更容易在小数据上过拟合。除非你有明确的大目标检测需求,否则先跑s,把baseline拿到,再往上加模型复杂度。
4.2 训练策略、增强参数与评估指标解读
训练配置里最容易被忽略的是imgsz。桥梁腐蚀锈蚀检测里病害区域有大有小,很多细小的锈点在640x640下可能只有二三十个像素,检测器很难学到。我一般先试640,跑完看小目标类别的recall,如果偏低,再用imgsz=1280做一次对比实验。分辨率提高带来的精度收益通常很明显,但显存占用几乎翻四倍,要在GPU能承受的范围内权衡。
数据增强是这类小数据集的双刃剑。YOLOv8默认开启Mosaic增强,把四张图拼接成一张,对小目标检测提升明显,但在病害图像上要小心:如果原图已经包含很多密集小锈点,Mosaic拼图会把不同区域混在一起,让原本就不清晰的边界更乱。我的经验是前20轮保持Mosaic开启,让它参与预热,后期如果loss震荡就关掉或用0.5概率。
超参数上我倾向于保守起步:
| 参数 | 建议值 | 原因 |
|---|---|---|
| lr0 | 0.001-0.002 | 微调场景下学习率过大容易破坏预训练特征 |
| batch | 16-32 | 小数据集上大batch更稳,但受显存限制 |
| epochs | 100-150 | 病害检测收敛相对慢,要留足轮次 |
| patience | 15-20 | 早停机制,防止过拟合 |
| mosaic | 1.0前20轮,后降至0.5 | 兼顾小目标学习与稳定性 |
训练完不要只盯mAP。桥梁巡检场景里,漏检比误检危险得多。如果一个重度锈蚀区域没被检出来,直接意味着这个构件没有病害告警,可能造成结构安全隐患。所以我会有意同时看precision和recall两个指标,并倾向让recall高一点,误检可以靠后处理阈值过滤。
评估命令:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=bridge_corrosion_data/data.yaml重点看下图里各类别的mAP50和mAP50-95。如果corrosion的AP高而rust的AP低,大概率是两个类别的特征重叠、标签边界不清,可以考虑合并类别或增加标注区分度。
4.3 训练中的常见坑:小目标、类别不平衡、过拟合
实战中遇到最多的三个坑,在这里集中记录一下。
第一个坑是小目标漏检。无人机一次飞掠拍的病害区域可能只占全图的2%-5%,大量锈斑小于32x32像素。YOLO对这类目标的召回效果并不好。除了提高imgsz,我还会尝试把图像切成两块瓦片分别检测,再合并结果。这个思路在桥梁病害检测里尤其好用,因为原始分辨率动辄好几千万像素,切成patch既解决了小目标问题,又不至于把显存撑爆。
第二个坑是类别不平衡。如果腐蚀(corrosion)框有4000个,锈蚀(rust)框只有1000个,模型会倾向于把所有类似区域都判成corrosion,rust的recall会很难看。处理办法有两类:一是增加rust类别的loss权重,在损失函数层面拉平差距;二是做针对性的过采样,在训练集里复制rust的样本并做轻微几何变换。我的习惯是先试第二种,因为它对整体训练节奏影响更小。
第三个坑是过拟合,在小数据集上几乎一定会出现。最直观的信号是训练集loss持续下降,验证集mAP在某个轮次后开始掉。除了早停,我还会用更强的dropout和正则化。YOLOv8里的weight_decay参数可以从默认0.0005调到0.001,增强对权重过大的惩罚,虽然可能让收敛变慢,但能明显改善验证集稳定性。
另外一个很多人会忽略的细节:标注框的完整性。实际项目中,标注人员经常只标了明显病害,漏掉轻微锈斑,导致模型学到的“锈蚀”总是局部特征,遇到轻微锈蚀时无法识别。处理方式是在后处理阶段适当降低置信度阈值,把模型当作“粗筛工具”,把高于0.2的框全部保留,交给人工或下一级分类模型复核。这套数据如果标注质量足够高,那么0.3以下的置信度就可以安全过滤;如果抽查发现大量轻微病害没标,阈值就得放宽到0.15甚至更低。
5. 从二类框检测到严重程度分级的扩展路线
5.1 为什么2个类别不够用:工程上需要的其实是分级
这套数据集的标题叫“严重程度检测”,但实际类别只有2个。这是工程项目的常态:先做病害“有没有”和“在哪”的检测,再想办法做“严重到什么程度”的分级。因为最终交给养护部门的报告,不能只说这里有一片锈,而要写清楚是轻度、中度还是重度锈蚀,影响面积多大,是否已经威胁到钢筋截面。
按现行桥梁养护习惯,混凝土腐蚀锈蚀一般按区域面积、锈胀裂缝宽度、保护层剥落情况分三到四个等级。这些依据很难直接从一张2D图像里精确计算,但深度学习可以做一个近似:用检测框区域的颜色、纹理、裂缝形态等特征,训练一个分级头。二分类检测可以理解为这个任务的第一阶段,先把病害区域切出来,第二阶段再判级。
5.2 给数据集加权的三个方案
我根据自己的实操经验整理了三个从二分类扩展向分级的路线。
方案A:扩充标注样本,把现有两个类别细分。把“锈蚀”按锈蚀面积比例、颜色深浅、是否有裂缝分为轻度、中度、重度,重新标注一遍。这个方法最直接,但重新标注成本极高,一般只有大型养护项目才会投入。
方案B:双阶段级联。第一阶段用这套二分类数据训练检测器,把病害区域裁剪出来;第二阶段用一个独立的图像分类模型对裁剪区域做三分类。这个方案可以复用现有数据,第二阶段分类模型的训练数据可以从已有检测框里裁剪,不用重新框选,成本低很多。我实践过,分类模型只需要每个等级几百张就能有不错的精度,因为裁剪后目标居中、尺度统一,比从大图里检测简单得多。
方案C:回归分数。不硬分等级,而是让模型输出一个0-1之间的严重程度分数,给每个病害区域打分。分数可以结合像素级分析做校正,比如用分割模型计算锈斑面积比例,再映射到分数区间。这个方案在工程报告里很受用,它给养护人员提供了一个连续量化值,而不是模糊的“中重度”描述,后续做健康状态趋势对比也更方便。
无论走哪个方案,这套2370张的二分类数据都是地基。先把它跑通,确认检测器在验证集上的可靠性,再按业务需求往上叠加分级能力,这是我认为最稳妥的工程路径。
个人经验上,我有两个习惯供参考:第一,做训练前一定先花两三个小时把标签可视化和统计看透,磨刀不误砍柴工,很多训练问题其实都是数据问题;第二,任何数据集到手第一周不碰模型参数,只做数据清洗和验证集设计,这让我避开了不少看似是调参问题、实际是标签或划分问题的“假故障”。这套桥梁腐蚀锈蚀数据集如果按上面这套流程走下来,拿到一个mAP50在0.7到0.8的模型并不难,关键是后续怎么把它用进实际桥梁巡检流程里,让检测结果真正变成养护报告里那一行可靠的结论。