ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

铁轨裂纹数据集实战:从数据标注到YOLO检测的完整指南

铁轨裂纹数据集实战:从数据标注到YOLO检测的完整指南

简介:面向铁路基础设施维护与计算机视觉研究的铁轨裂纹数据集(第一部分),重点解决轨道裂纹、缺陷的自动识别与定位问题,适合从事目标检测、智能巡检和轨道安全监测的研究者用于模型训练与验证。资源采用VOC2007标准标注格式,所有XML注释文件均由LabelImg工具人工生成,详细标出每个裂纹和缺陷的位置、大小与类别,可直接作为深度学习模型的输入。压缩包共包含2000个文件,以XML标注文件与JPG原图为主,整体约252.69MB,已有4815人学习下载。依托这份数据集,使用者能够系统开展图像预处理、特征提取、模型训练与效果验证等完整流程,还能通过阅读XML了解VOC格式的目录组织方式;后续若结合第二部分数据,可进一步增强模型的泛化能力与鲁棒性,为铁路安全自动化检测提供扎实的数据支撑。

1. 铁轨裂纹数据集:先搞懂这份数据,再谈高精度检测

你花了两周把模型结构调得花团锦簇,一上轨检车实拍图,漏检率直接打回原形。这种翻车十有八九不是模型的问题,而是训练数据本身没吃透。铁轨裂纹数据集(第一部分)就是用来补这门课的:它把轨道表面裂纹的图像、标注和场景分布打包好,让做目标检测的人能在真实光照、真实锈蚀、真实枕木干扰下训练和验证模型,而不是在一套干净得不像话的合成图里自欺欺人。

这份数据适合两类人:一是刚接触轨检视觉的开发者,需要一套带标注的数据跑通检测流程;二是已经在做钢轨伤损识别、但对训练集和验证集划分没有把握的工程师,想用一套结构清晰的数据检验自己的预处理和模型配置。你能从这份数据里得到的,不是「跑起来就完事」的体验,而是把裂纹检测当作一个正经目标检测任务来对待的完整路径:数据怎么组织、标签怎么解析、划分怎么做、训练参数怎么定、哪些坑会导致指标虚高或直接崩掉。本文按这个顺序展开,每一章都可以直接照着操作。

2. 读懂数据组织方式:图像、标注与两类常见格式

拿到数据集的第一步不是训练,是先搞清楚它内部的目录结构和标注格式。铁轨裂纹数据集(第一部分)的常见组织方式是 images 目录存原始图像,labels 目录存标注文件,另外附带一份类别说明。这个结构看着简单,实际用起来有几个容易忽视的细节。

2.1 图像内容与裂纹形态:横裂、纵裂和网裂为什么不能混在一个类别里

轨道表面裂纹按走向和形态大体分三类:横向裂纹(垂直于钢轨走向)、纵向裂纹(沿钢轨走向延伸)、网状裂纹(龟裂或不规则分叉)。如果数据集把所有裂纹统一标成一个 crack 类别,训练出来的模型只能告诉你「这里有异常」,但现场工程师真正需要的是「这是什么类型的伤损」——横裂涉及断轨风险,纵裂可能和材质缺陷有关,网裂往往伴随疲劳累积。所以拿到数据集先看类别清单,确认是否区分了多个裂纹类别,这决定了后续模型是单类检测还是多类检测,评估指标也要跟着分开看。

实际标注中,横向裂纹的框通常窄而扁,纵向裂纹的框又长又细,两者在 aspect ratio(宽高比)上差异极大。如果你用 COCO 预训练权重直接迁移学习,默认的 anchor 配置对极端长宽比目标很不友好,后面会细说。另外轨枕、扣件、锈蚀斑、油污在图像里经常被误标成裂纹,或者反过来把细小裂纹漏标了,这份数据集里同样存在,属于标注噪声的一部分,第 5 章会专门讲怎么处理。

2.2 XML 与 TXT 两种标注格式:各自的字段含义与解析要点

铁轨裂纹数据集常见的标注文件有两种格式:一种是 Pascal VOC 风格的 XML,另一种是 YOLO 风格的 TXT。XML 适合做人眼检查,因为坐标是绝对像素值;TXT 适合直接训练,因为坐标是相对图像的归一化值。两种格式在发布包里有时候会同时提供,有时候只提供一种,建议先确认拿到的是哪种,再决定数据加载脚本怎么接。

XML 格式的核心字段是 object 节点下的 name(类别名)和 bndbox(xmin、ymin、xmax、ymax 四个绝对坐标),另外还有 width、height 记录图像尺寸。解析时要把坐标转成模型需要的格式,而且注意 bndbox 的坐标是 0 基还是 1 基——VOC 规范里 xmin/ymin 从 1 开始,转换到 YOLO 归一化坐标时要先减去 1 再除以宽高,否则边界框会偏移一个像素。这个偏移对粗框影响不大,但对细长裂纹这种目标,一两个像素的偏差就可能让 IoU 掉一截。

TXT 格式每一行对应一个目标,五个数值依次是 class_id、x_center、y_center、width、height,全部归一化到 0~1。解析时最常出的问题有两个:一是某些标注文件里宽度或高度算成了 0,说明标注时误点了两个重合的点;二是坐标值出现负数或大于 1 的情况,多半是标注工具导出时没做裁剪,目标贴边导致框超出图像边界。这两种脏数据如果不提前过滤,训练时轻则 warning 刷屏,重则 loss 变成 NaN。

2.3 解析前的自查脚本:三分钟找出配对缺失和坐标越界

拿到数据集后,别急着开训,先写一个检查脚本把下面的问题过一次。我一般把这个脚本存成 inspect_data.py,每次换数据集都跑一遍。

import os from glob import glob from PIL import Image img_dir = "images" label_dir = "labels" ext_map = {".jpg": ".txt", ".jpeg": ".txt", ".png": ".txt"} img_files = glob(os.path.join(img_dir, "*")) label_files = glob(os.path.join(label_dir, "*.txt")) img_stems = {os.path.splitext(os.path.basename(f))[0] for f in img_files} label_stems = {os.path.splitext(os.path.basename(f))[0] for f in label_files} missing = img_stems - label_stems orphan = label_stems - img_stems print("missing labels:", sorted(missing)[:10]) print("orphan labels:", sorted(orphan)[:10]) for tf in label_files: with open(tf, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: print("bad line in", tf, ":", line) else: x, y, w, h = map(float, parts[1:]) if w <= 0 or h <= 0 or x < 0 or y < 0 or x > 1 or y > 1 or w > 1 or h > 1: print("out-of-range in", tf, ":", line)

这段脚本做了三件事:第一,把图像文件和标签文件的文件名做差集比对,找出有图没标、有标没图的文件;第二,检查每一行标注是否恰好五个数值;第三,检查归一化坐标是否在合理区间内。跑完如果有输出,先把这些问题文件隔离到单独目录,不要直接删,因为你还需要它们来反向排查标注工具的导出配置。常见做法是先修数据再训练,而不是让模型去「学会」容忍脏标注。

3. 训练集与验证集的划分策略:为什么不能随机拆分

很多人在拿到数据集后直接train_test_split,按 8:2 随机拆,训练出来的模型在验证集上指标不错,一到现场就露馅。这不是玄学,而是铁轨数据的天然特性导致的——同一根钢轨的连续图像在光照、角度、背景上高度相似,随机拆分会把同一段钢轨的图像同时分进训练集和验证集,模型等于提前看到了答案。

3.1 按场景维度划分:同一段钢轨不要同时出现在训练集和验证集

铁轨裂纹数据的拍摄通常来自轨检车或手持设备的一次连续走行,一次采集会得到大量连续帧。这些帧里裂纹的形态变化不大,但拍摄距离、亮度、曝光在短时间内是渐变的。如果随机划分,验证集里会出现和训练集几乎一模一样的帧,最后的评估分数会虚高几个点,这不代表真实泛化能力。

正确做法是先按场景分组,再在场景级别上划分。比如一次采集是一个 scene,一个 scene 里的所有图像要么全进训练集,要么全进验证集。这样才能模拟「模型见到没走过的钢轨段」的真实推理条件。铁轨裂纹数据集通常会在发布时说明图像来自哪些采集批次,如果没有说明,可以通过文件名前缀或拍摄时间戳来推断分组依据。

3.2 光照与干湿状态分层:不控制分布,验证分数就是自欺欺人

除了场景维度,光照条件也强烈影响裂纹的视觉表现。白天强光下,裂纹阴影清晰;阴天漫射光下,裂纹对比度低;夜间闪光灯下,金属表面反光可能把裂纹完全淹没。如果训练集里大量是晴天白图,验证集里也大量是晴天白图,训练过程会学到一个取巧的 shortcut——看到高亮金属表面就倾向于预测裂纹。这个现象在视觉任务里很常见,模型没有真正学会区分裂纹亮度模式,只学会了区分图像整体亮度。

分层划分的思路是把图像先按拍摄条件打上标签(强光/阴天/夜间/雨雾),然后在每个条件内部按比例划分,保证训练集和验证集里的光照分布一致。铁轨裂纹数据集(第一部分)如果自带这些 meta 信息,直接利用;如果没有,可以用图像的亮度直方图做一个粗略的区分作为分层的替代方案。

3.3 分层分割脚本:按场景ID和光照标签的划分实现

下面这段脚本展示了按场景分组之后再按光照条件做分层采样的划分逻辑。关键函数是train_test_split的stratify参数,它保证每个光照类别在训练集和验证集中的比例一致。

import os import random import pandas as pd from sklearn.model_selection import train_test_split image_dir = "images" records = [] for fname in os.listdir(image_dir): if not fname.lower().endswith((".jpg", ".png", ".jpeg")): continue scene_id = fname.split("_")[0] # 假设文件名前缀是场景ID img_path = os.path.join(image_dir, fname) records.append({"fname": fname, "scene_id": scene_id}) df = pd.DataFrame(records) # 按场景分组:取每个场景第一条记录的scene_id作为分组特征 scene_df = df.groupby("scene_id").agg( count=("fname", "count"), scene_id=("scene_id", "first") ).reset_index() # 在场景级别上做划分,防止同一场景的帧同时出现在训练和验证集 train_scenes, val_scenes = train_test_split( scene_df["scene_id"], test_size=0.2, random_state=42, stratify=scene_df["scene_id"].apply(lambda x: scene_light_level(x)) ) train_df = df[df["scene_id"].isin(train_scenes)] val_df = df[df["scene_id"].isin(val_scenes)] print("train frames:", len(train_df)) print("val frames:", len(val_df))

这段代码的核心思想是先按场景ID聚合,再在场景上进行划分,而不是直接对图像做随机拆分。stratify参数用来保证不同光照条件下的场景在训练集和验证集中占比一致。这里的scene_light_level函数在实际使用时要替换成你的光照分级逻辑:可以用拍摄时间戳、可以用文件名里的光照标记、也可以用图像亮度均值聚类。划分完后把 train_df 和 val_df 写出成两个 txt 文件,每行一个图像路径,后面训练时直接按 list 加载。

注意:random_state固定为固定值是为了可复现。如果你做了数据增广,增广随机性不受它控制,但数据集划分本身每次运行结果一致,方便排查问题。

4. 最小训练配置:用 YOLO 系列工程跑通裂纹检测

划分好数据之后就可以开始训练了。以目标检测领域最常用的 YOLO 系列代码库为例,把铁轨裂纹数据集接进去的步骤是:准备 yaml 配置文件、调整图像尺寸、改类别数、然后启动训练。

4.1 为什么 YOLO 系列适合起步:单阶段检测对长条小目标的平衡

裂纹检测有两种主流路线:两阶段检测器(Faster R-CNN 系)精度有优势,但训练慢、部署重;单阶段检测器(YOLO 系)速度快、生态成熟、预训练权重好找,对细长目标的召回率通过调 anchor 也能提到可用水平。铁轨裂纹检测的实际场景大部分是轨检车离线分析或者手持设备的实时提示,前者对速度不敏感,后者要求实时。如果你只有一次训练资源,先跑 YOLO 系拿到一个能用的 baseline,再根据漏检形态决定是否上两阶段模型。

YOLO 系代码库通常自带数据加载、增广、评估脚本,对新手最友好的一点是训练过程中的 loss 曲线和 PR 曲线都直接可视化。你需要改的主要是三个地方:数据配置 yaml、模型配置 yaml、训练超参数。下面按这三个文件逐一说明。

4.2 数据配置与模型配置:类别数、图像尺寸和 anchor

数据配置里最重要的字段是nc(类别数量)和names(类别名列表)。铁轨裂纹数据集(第一部分)如果是单类,就写nc: 1;如果区分横裂、纵裂、网裂等多类,必须和标注文件里的 class_id 一一对应。类别名写错会导致训练正常但推理时标签错位,这种低级错误在项目交付时很容易被现场人员骂。

# rail_crack.yaml path: ./rail_crack_dataset train: train_list.txt val: val_list.txt nc: 3 names: 0: transverse_crack 1: longitudinal_crack 2: network_crack

这个 yaml 文件告诉训练工程三件事:数据路径、训练集和验证集的文件列表、类别对应关系。train_list.txt里面每一行是一个图像的绝对或相对路径,对应的标注文件路径由代码库根据扩展名自动替换。注意:不同 YOLO 代码库对train字段的写法可能不同,有的是目录,有的是 txt 列表,看官方示例确认一下,不要套错。

图像尺寸是另一个关键参数。裂纹属于小目标,原图如果是 1920×1080 级别的大图,直接缩放到 640×640 会把细裂纹缩得只剩几个像素,模型学不到有效纹理。常见做法是先用 1280×1280 训练,推理时再用滑窗或原图缩放。代价是显存占用变大,batch size 要相应调小。如果你的卡只有 8GB 显存,1280 分辨率下 batch size 可能只能开到 4,这时候宁可把 batch size 调小也不要降分辨率,分辨率对裂纹检测的影响远大于 batch size。anchor 方面,细长裂纹的宽高比经常超过 1:5,甚至 1:10,默认 anchor 设定基本匹配不上。YOLO 系列工程一般提供 k-means 聚类 anchor 的工具,拿训练集的标注框跑一遍,把聚类得到的 anchor 替代默认值,召回率会有肉眼可见的回升。

4.3 训练启动命令与三个必调超参数

启动训练前还需要关注三个超参数:epoch、学习率和 mosaic 增广。epoch 从 100 起步,观察验证集 PR 曲线在 60~80 epoch 之间是否还在上升,如果上升明显就继续加,如果已经震荡就提前停止。学习率用代码库默认的 warmup + cosine 策略即可,但不要随手开很大的初始学习率,裂纹检测的标注噪声一般比通用数据集高,学习率太大容易让模型过拟合到噪声框上。mosaic 增广在拼接四张图时会把裂纹截断成两半,对细长目标反而有害,我通常在训练后半段关闭 mosaic,只保留基本的翻转和颜色抖动。

python train.py --data rail_crack.yaml --weights yolov5s.pt --img 1280 --batch 8 --epochs 100 --cache

这条命令的核心参数含义拆开看:--data指定 4.2 节里写好的配置;--weights用 COCO 预训练权重做迁移学习,不要从随机初始化开始,裂纹数据量不足以支撑从零训练;--img 1280是输入分辨率,前面提过这是小目标任务的关键选择;--batch 8是显存允许范围内的最大值;--cache把图像缓存到内存里,加速数据读取,第一次跑可以不开,后面调参时再开。训练结束后在验证集上跑val.py,得到每个类别的 mAP、precision、recall,以及一张 PR 曲线图。PR 曲线右下角面积大说明模型在低置信度下也能保持高召回,这对裂纹检测很重要——漏检的代价远高于误报,现场宁可多看几个误报也不愿放过一条裂纹。

5. 铁轨裂纹数据集的 5 个常见坑:每个都是血泪经验

这部分把我在实际使用类似数据时遇到最多的问题整理成清单,每条按现象、原因、解决来写。你会发现大部分坑不在模型,而在数据和评测流程。

5.1 验证集出现漏标注框,指标集体翻车

现象:训练 loss 正常下降,验证集 mAP 卡在 0.5 上不去,PR 曲线在低 recall 区域出现明显掉点。

原因:数据集里存在漏标注——图像里明明有裂纹,标注文件里没有对应框。模型有时能检测到这些「未标注的裂纹」,但评测脚本把它算作 false positive,precision 被拉低;同时模型学到这些区域的纹理特征,导致对训练集里同类区域产生误检。

解决:先做一轮「模型辅助标注」清洗。用训练好的模型对训练集和验证集做推理,挑出模型置信度高但和现有标注框 IoU 小于 0.1 的预测,把这些区域提出来人工确认。如果确认是漏标,补上标注然后再训练。这一步很费人力,但对质量不高的数据集是必须的投入。某个模拟项目X里,我仅靠补漏标就把验证集 mAP 从 0.52 提到了 0.63。

5.2 细长裂纹匹配不到 anchor,召回率低到怀疑人生

现象:训练过程正常,但验证集 recall 只有 0.3 左右,查看检测结果发现很多裂纹被识别成很短的碎片框,或者完全没有输出。

原因:默认 anchor 是针对 COCO 数据集聚类出来的,COCO 里绝大多数目标宽高比在 1:1 到 1:3 之间。裂纹宽高比动辄 1:8 甚至 1:15,默认 anchor 和 ground truth 的 IoU 低于匹配阈值,导致这些目标在训练时被标记为「无匹配」而被忽略。

解决:用训练集标注重新聚类 anchor。YOLO 系工程里一般有anchors.py之类的聚类脚本,输入训练集 label 文件,输出新的 anchor 值,替换到模型配置文件里。聚类数量可以先用默认的 9 个,观察匹配率;如果匹配率仍低,增加到 12 个,重点保证长条形 anchor 有覆盖。聚类时过滤掉异常宽高比(比如宽大于图像宽 0.9 的标注,多半是标错了)。

5.3 暗光样本占比失衡,模型学到的是亮度而不是裂纹

现象:验证集整体 mAP 不错,但按光照分层统计时,暗光图像那一组的 mAP 只有正常组的 1/3。

原因:训练集中暗光图像占比太低(比如只有 5%),模型没有见过足够多的暗光正样本,只能通过亮度特征猜测——亮图倾向于输出低置信度,暗图倾向于输出高置信度或直接漏检。

解决:按第 3 章的光照分层先检查和调整分布。如果暗光图像实在很少,优先做色彩增强类的增广:随机降低亮度、提高对比度、加高斯噪声,让模型看到更多「伪暗光」样本。不要用循环生成对抗网络之类的生成式方案,成本高且容易引入伪影,传统光度增广对这个任务够用。另外可以在推理时对输入图像做自适应的直方图均衡化(CLAHE),把暗部纹理拉出来再进模型,这在很多轨检实拍场景下比换模型结构收益更大。

5.4 类别不平衡:横裂只有几十个框,网裂有上千个框

现象:多类训练时总 mAP 尚可,但按类别看,样本少的类别 mAP 几乎为 0,PR 曲线不稳定。

原因:横裂这类高危伤损在实际数据里本来就稀有,而网裂分布广、数量多。模型在训练时被大量网裂样本主导,对横裂的梯度信号被稀释。

解决:第一,按类别统计框数量,确认不平衡程度。第二,对样本量少的类别做过采样,思路是训练时每轮多加载几张包含该类别的图像,而不是复制粘贴同一张图。第三,调整 loss 的类别权重,给稀有类别更高的权重系数。最直接的做法是把稀有类别的图像复制几份到训练集里,简单粗暴但有效,注意不要导致数据泄漏——同一图像的所有副本只能出现在训练集。某些代码库支持class_weight参数,直接用也不错。

5.5 训练到第 1 个 epoch loss 变成 NaN,标注坐标惹的祸

现象:模型启动训练后 loss 在几十个 iteration 内变成 NaN,训练进程直接崩掉或开始输出无意义的权重。

原因:最常见的是标注文件里有class_id超出了nc的范围,或者坐标值出现极端的负数和大于 1 的数。另一个原因是某些代码库在归一化坐标时除以了图像的宽高,如果宽高是 0(损坏的图像文件),结果就是无穷大。

解决:用 2.3 节的检查脚本先全量过滤。对于class_id越界的情况,回头查数据集附带的类别说明文档,确认 0 基还是 1 基索引;有时发布方自己写混了,把类别 ID 从 1 开始标,而训练工程默认从 0 开始,需要统一偏移。对于损坏图像,直接排除并记录文件名,不要修,修复的元数据可能带来更多不可控问题。如果检查完仍然 NaN,再看学习率是不是设得过高,把初始学习率除以 10 再试。这类问题调试时间占比最大的是定位过程——先脚本检查,再挨个排查,别一上来就改网络结构。

6. 最后一招:在大图上滑窗推理,把细裂纹找出来再切成小图

训练完的模型默认是 640×640 或 1280×1280 输入,但轨检相机拍的原始图经常是 4096×4096 甚至更高。直接把大图缩放到模型输入尺寸,细裂纹在缩放过程中就消失了。滑窗推理是应对这个问题的最可靠方案:把大图切成有重叠的若干小块,逐块送入模型,再把检测框映射回原图坐标。

滑窗的核心参数是窗口大小和重叠率。窗口大小一般取模型训练时的分辨率,不要大于 1280,否则显存不够;重叠率取 20%~50%,重叠的目的是让跨越窗口边界的裂纹至少在一个窗口内是完整的,避免裂纹被切成两段导致漏检。推理时保留每个窗口的原始坐标偏移,把检测框换算回完整图像坐标系,最后用 NMS(非极大值抑制)合并重叠窗口产生的重复检测框。NMS 的 IoU 阈值设 0.5 比较合适,太高会保留重复框,太低会把相邻的横裂和纵裂合并成一个误检框。

滑窗推理的代价是速度变慢,一张 4096×4096 的图用 1280 窗口加 50% 重叠,大约要跑 25 个窗口,在普通办公卡上大概花 2~4 秒。如果对实时性有要求,可以先把大图缩小到 2048 再滑窗,速度提升 4 倍左右,代价是小裂纹的召回率会轻微下降。实际项目中我一般是离线分析用全分辨率滑窗,现场实时检测用整图缩放加更高的置信度阈值,两条线并行。

伪标签清洗是一个值得做的收尾动作。把训练好的模型在无标注的大图上跑一遍,筛选出置信度大于 0.9 的检测框,回填成新的标注数据进行二次训练,能显著提升模型在地域变化后的数据集上的表现。注意只回填高置信度结果,且只做半次迭代——用伪标签训练出的模型再生成伪标签再训练,噪声会逐轮积累,一般两轮后就停了。

我自己的习惯是:每拿到一份新数据,先花一个下午做检查和划分,再花一个晚上跑 baseline,永远不在没有看过具体 bad case 的情况下调模型结构。裂纹检测是一个检测精度与工程成本强相关的领域,你多花一天在数据检查上,后面调试模型的效率可能省出一周。这份数据集的价值恰恰在于它暴露了足够多的真实场景噪声,值得你从头到尾按上面的流程走一遍。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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