简介铁路轨道缺陷数据集面向轨道交通视觉检测与深度学习目标检测方向的研究者、算法工程师及学习者可用于裂缝、间隙缺陷的识别与定位。数据集共4278张原始图片配套提供Pascal VOC XML格式的标注信息压缩包内含2000个XML标注文件整体约273.84MB已有1688人学习下载。标注文件采用统一命名规则每个XML记录缺陷目标类别与边界框坐标覆盖多种轨道场景下的真实样本适合用于模型训练、验证与算法对比。通过解析XML可快速构建训练集也可借助脚本转换为COCO、YOLO等格式辅助完成轨道表面裂缝识别、间隙缺陷测量等任务。对需要工业场景真实数据做预训练或基准测试的团队是一套可直接使用的标注资源能有效减少数据采集与标注成本。1. 铁路轨道缺陷数据集的价值4278张原始图片带PASCAL VOC XML标注裂缝与间隙识别从哪入手铁路轨道缺陷数据集的价值从来不只是 4278 张原始图片本身而在于每张图都带一份 PASCAL VOC XML 标注可以把“识别裂缝、间隙两类缺陷”从找数据阶段直接推进到跑模型阶段。铁路轨道的安全巡检长期依赖人工目视裂缝和间隙又小又不规则走一趟天窗点看几十公里漏检并不罕见。目标检测算法能当第二双眼睛但卡住现场工程师的往往是数据网上轨道缺陷图不少可要么没有标注要么格式五花八门想直接丢进 YOLO 训练得先花一两周整理数据。这套数据集把门槛压了下来4278 张原始图统一 XML 标注任务就两类——裂缝与间隙。它不是神秘项目而是一个能直接落地的数据起点用它跑通缺陷检测从数据到模型的完整链路再基于它微调自己的场景数据。适合算法工程师做方案验证也适合巡检团队拿它评估视觉检测在自己线路上的价值。2. PASCAL VOC XML 标注拆解size、object、bndbox 三个节点决定数据能不能用2.1 先核对一件事图片、XML、类别三者对齐市面上叫“轨道缺陷数据集”的资源不少但真正拿到手能直接训练的关键看标注文件是不是规范的 PASCAL VOC XML。先提醒一句标题里的 PASICAL 是笔误标准写法是 PASCAL VOC按后者去检索资料能捞到更多文档。拿到数据后别急着解压看图片4278 张原图和 4278 个 XML 标注文件首先得确认数量对得上。我处理这类数据集时习惯先跑两条命令ls images/*.jpg | wc -l ls annotations/*.xml | wc -l两个数字必须一致。如果图片比 XML 多YOLO 训练工具会直接把没有标注的图片过滤掉全程不报错只会发现验证集 mAP 莫名其妙偏低折腾一圈才找到是数据缺口。如果 XML 比图片多那要警惕冗余标注多半有人在重命名后没删旧文件。数量核对完还要随机抽三五个 XML看里面的 filename 节点和实际图片名是不是同一根名比如 rail_0001.xml 对应 rail_0001.jpg这是 PASCAL VOC 最常见的翻车点手工改过文件名但没同步 XML。2.2 annotation 节点拆解一个典型 XML 里到底写了什么PASCAL VOC 的 XML 结构从 2012 年后就没怎么大变轨道缺陷这种单类别目标检测更是简单。打开一个标注文件结构大约长这样annotation folderrail_images/folder filenamerail_0001.jpg/filename path/data/rail_images/rail_0001.jpg/path source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namecrack/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin534/xmin ymin221/ymin xmax602/xmax ymax384/ymax /bndbox /object /annotation这里有几个节点必须看懂。size 节点记录图片真实宽高和通道数后面把标注转成 YOLO 格式时要做归一化分母就是这个 width 和 height。有些工具生成的 XML 里 size 是 0 或缺项这种文件到了转换阶段会直接报除零错误所以解析脚本里最好对 size 做防护。其次是 object 节点一个 object 代表一个目标框单张图里有几条裂缝就会有几个 object 节点name 是类别名bndbox 里 xmin、ymin、xmax、ymax 是像素级坐标原点在左上角。truncated 表示目标被截断difficult 表示难例这两个字段在轨道缺陷数据里一般用不到但保留它们对下游代码的兼容性更稳。从人工标注角度看这个结构最大的好处是纯文本、可读、可手改。用 LabelImg 打开一张轨道图画个框保存出来就是这样一段 XML。老工程师核对标注时甚至能用文本编辑器直接搜 bndbox 看坐标是否异常这是 COCO JSON 做不到的。2.3 为什么是 PASCAL VOC轨道缺陷数据选格式的三个理由PASCAL VOC 与 COCO、YOLO txt 的差别直接决定了下游训练链路顺不顺。我用一张表把三者放在一起格式标注文件形态坐标是否归一化人工可读性常见配套工具PASCAL VOC XML每张图一个 XML像素坐标高LabelImg、CVAT、labelme 均可导入导出COCO JSON整个数据集一个 JSON像素坐标低labelme 可导出YOLO txt每张图一个 txt中心点加宽高归一化中Ultralytics 官方训练直接消费轨道缺陷数据集选 PASCAL VOC 有三个朴素理由。其一它按图拆文件新增一张缺陷图只需增加一个 XML不用改全局 JSON适合缺陷样本持续积累其二像素坐标看着直观工程师想确认框画歪没有不用先换算百分比其三主流目标检测标注工具都认这种格式换工具不损失标注工作量。不少从现场攒数据的团队最后也把 PASCAL VOC 作为中间格式再往 YOLO 转等于把这份数据集的价值延续到自建样本库上。2.4 标注工具怎么接LabelImg、CVAT 和 labelme 的差异PASCAL VOC 标注虽老但工具生态最成熟。最容易上手的是 LabelImg选择后打开图片目录画框、填类别名、保存为 PASCAL VOC直接产出 XML缺点是单机操作几百张图还行4278 张全人工复核很累。规模大的团队会用 CVAT 做在线标注多人协作、审核流齐全导出时选 PASCAL VOC 1.1 即可。还有一类链路是 labelme默认产出 JSON但提供 VOC 转换脚本如果手头存量标注是用 labelme 画的转成这套轨道数据集的 VOC 格式不难。经验之谈无论 LabelImg 还是 CVAT导出后我都会跑一个小脚本统计所有 XML 里 object 的 name 直方图。这套数据集号称两类缺陷 crack 和 gap但人工标注最容易出错的就是同一目标在不同图上被标成不同名字比如 crack 和 crack_1、gap 和 gap defect这类名字差异会在训练时被当成独立类别直接搞乱分类头。3. 把 VOC XML 转成 YOLO txt4278张标注图的批量转换脚本与归一化参数3.1 为什么要转格式YOLO 训练只认归一化坐标PASCAL VOC 用的是像素坐标YOLO 系列训练时要求每行是“类别ID x_center y_center width height”四个坐标值全部归一化到 0 到 1类别 ID 从 0 开始计数。所以训练前必须把 4278 个 XML 转换成 YOLO 的 txt。我一般先建一个干净目录track_defect_yolo/ ├── images/ # 原图保持与 VOC 相同文件名 └── labels/ # 转换后的 txt根名与图片一致把所有图片复制到 images再跑转换脚本生成 labels后续划分训练集验证集只需移动文件。注意一张图可能包含多个缺陷框转换后 txt 里会有多行如果一个对象都没有txt 会是空文件这类空文件要么剔除要么单独处理不能直接喂给训练器。3.2 转换脚本主体解析 XML 并写出归一化 txtimport os import xml.etree.ElementTree as ET # 类别顺序即类别IDcrack0gap1 CLASSES [crack, gap] def voc_to_yolo(xml_path, label_out): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width float(size.find(width).text) height float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f跳过未知类别 {name} 在 {xml_path}) continue cls_id CLASSES.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 归一化中心点坐标除以图片宽高 x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height w (xmax - xmin) / width h (ymax - ymin) / height # 越界保护避免训练时被忽略 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) base os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(label_out, base), w) as f: f.write(\n.join(lines)) if __name__ __main__: voc_dir annotations label_dir track_defect_yolo/labels os.makedirs(label_dir, exist_okTrue) for f in sorted(os.listdir(voc_dir)): if f.endswith(.xml): voc_to_yolo(os.path.join(voc_dir, f), label_dir) print(转换完成)这段脚本有三个地方值得细说。第一是 CLASSES 的顺序它决定类别 ID一旦确定后续训练集、验证集和预测解释都必须沿用中途改顺序等于推倒重来。第二是归一化写法 x_center(xminxmax)/2.0/width先算像素中心再除以宽高避免整数除法导致精度丢失Python 3 里用 float 兜底更稳。第三是越界保护那四行 min/max 钳制轨道缺陷的标注框偶尔会超出图片边界几个像素比如 xmax 标成 1922 而宽度只有 1920钳制到 1.0 以内可以保证 YOLO 训练不会把这行样本剔除掉。如果图片分散在多个子目录遍历改成 glob 递归匹配即可小规模数据集没必要搞复杂。比较常见的是在循环里先过滤掉空 XML比如文件里没有任何 object 节点转换出来就是空 txt这类文件需要在后续做数据清理时单独列出。3.3 转换完必须做可视化抽查三分钟看出坐标错误转换完不要直接开训先用 OpenCV 把框画回图上抽查一遍。这一步能看到两个问题一是坐标明显偏移框整体跑到铁轨外侧二是归一化比例不对导致框被压扁或拉长。抽查脚本如下import cv2 CLASSES [crack, gap] def draw_boxes(image_path, txt_path): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) x_center float(parts[1]) * w y_center float(parts[2]) * h bw float(parts[3]) * w bh float(parts[4]) * h # 从中心点反算左上角和右下角 x1 int(x_center - bw / 2) y1 int(y_center - bh / 2) x2 int(x_center bw / 2) y2 int(y_center bh / 2) color (0, 0, 255) if cls_id 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, CLASSES[cls_id], (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imwrite(txt_path.replace(.txt, _check.jpg), img) # 抽查前 30 张 if __name__ __main__: for i in range(30): img ftrack_defect_yolo/images/rail_{i:04d}.jpg txt ftrack_defect_yolo/labels/rail_{i:04d}.txt draw_boxes(img, txt)这段脚本把归一化坐标还原成像素坐标时唯一要注意的是先乘后取整。x1、y1 用中心坐标减半框如果出现 x1 为负或 x2 大于图宽说明越界保护没覆盖到位回到源 XML 检查。抽查数量不必多30 张覆盖到不同光线、不同缺陷形状就够重点是看框是否贴合裂缝走向、有没有大面积偏移。这一步是训练前最后一道防线多花十分钟能省下后面几小时的排障时间。4. 数据划分与增强轨道裂缝和间隙样本少先靠对齐保证训练不偏科4.1 为什么要先做类别统计再划分从原始标注直接复制到训练目录容易踩到一枚暗雷gap 的数量远少于 crack如果不做分层划分验证集里甚至可能一个 gap 都没有。目标检测的数据划分不能只按“图”分要按“类别实例”分。第一步是统计每类框的数量import os import xml.etree.ElementTree as ET counts {crack: 0, gap: 0} xml_dir annotations for f in os.listdir(xml_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, f)) for obj in tree.getroot().findall(object): name obj.find(name).text if name in counts: counts[name] 1 print(counts)如果两类数量差在五倍以上要考虑在 loss 层面给少类加权或者对少类样本做复制增强。轨道缺陷里 gap 往往出现在接头、扣件部位裂缝则多见于轨面二者形态差异大单纯加权重不如先保证每个训练 epoch 里两类都出现在足够多的批次里。4.2 按类别分层划分训练集与验证集常见的做法是把 4278 张图按 8:1:1 分成训练、验证、测试三份但不要直接随机打乱文件名。先按包含 gap 的图片集合和只含 crack 的图片集合分别打乱再按比例取出这就是分层划分。脚本如下import os import random import shutil random.seed(42) img_dir track_defect_yolo/images label_dir track_defect_yolo/labels target track_defect_split split_map { train: [0.0, 0.8], val: [0.8, 0.9], test: [0.9, 1.0], } def buckets_by_label(): files_by_class {gap: [], crack_only: []} for txt in os.listdir(label_dir): if not txt.endswith(.txt): continue with open(os.path.join(label_dir, txt)) as f: content f.read().strip() if not content: continue # 按类别ID判断是否包含gap class_ids [int(line.split()[0]) for line in content.splitlines()] if 1 in class_ids: files_by_class[gap].append(txt.replace(.txt, .jpg)) else: files_by_class[crack_only].append(txt.replace(.txt, .jpg)) return files_by_class def assign_split(bucket, split_map): random.shuffle(bucket) n len(bucket) for split_name, (lo, hi) in split_map.items(): start, end int(n * lo), int(n * hi) for img_name in bucket[start:end]: img_src os.path.join(img_dir, img_name) txt_src os.path.join(label_dir, img_name.replace(.jpg, .txt)) os.makedirs(os.path.join(target, split_name, images), exist_okTrue) os.makedirs(os.path.join(target, split_name, labels), exist_okTrue) shutil.copy(img_src, os.path.join(target, split_name, images, img_name)) shutil.copy(txt_src, os.path.join(target, split_name, labels, img_name.replace(.jpg, .txt))) if __name__ __main__: buckets buckets_by_label() assign_split(buckets[gap], split_map) assign_split(buckets[crack_only], split_map)random.seed(42) 是这里最容易被删掉的一行。固定种子有两个实际理由一是可复现换个人跑同样脚本结果一致二是后续若新增缺陷图片固定种子能让你在增量划分时不打散原有验证集。分桶逻辑也值得注意gap 桶和 crack_only 桶分别打乱这样在总比例固定的前提下gap 样本不会因为随机抽样全部落进训练集。4.3 用 albumentations 做增强光照抖动、高斯噪声与 Mosaic不要因为数据集有 4278 张就乐观轨道拍摄的光照环境差异很大。白天逆光、夜间补光、雨后反光都会让检测器在验证时出现误检。我常用的增强方案边界框同步变化import albumentations as A transform A.Compose([ A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.7), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.GaussNoise(var_limit(10.0, 50.0), p0.3), A.RandomScale(scale_limit0.15, p0.3), A.PadIfNeeded(min_height416, min_width416, border_mode0, p0.5), A.Mosaic(p0.2), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels]))这段增强里有两个参数容易调坏。第一个是 RandomScale 的 scale_limit它只缩放不改变图片尺寸放大倍数过大时小缺陷会被撑成大块色斑丢失真实纹理0.15 是相对稳的上限。第二个是 Mosaic 的概率Mosaic 把四张图拼成一张对小目标效果明显但轨道图本身视野很窄四图拼接后裂缝和间隙的上下文会被切断p0.2 已经偏高建议先从 0.1 试起。如果时间允许把增强后的结果画框抽查十张确认这些操作没有把 gap 目标推出图片边界比任何理论都管用。4.4 如果要补充自己的数据标注链路怎么接4278 张不是终点现场团队往往要往数据集里补自己线路的图。补充时保留原有 PASCAL VOC 习惯最省事新图先用 labelme 或 CVAT 画框导出 PASCAL VOC XML再丢进第 3 章的转换脚本。关键约束是 CLASSES 顺序不能变新增图片里的类别名必须严格写成 crack 和 gap差一个字符都会在转 ID 时产生断层。数据标注这一环手工标注的粗心程度在轨道缺陷这类灰度小目标上会被放大宁可复核慢一点也不要为省时间把明显不清晰的框直接交给训练。5. 常见问题与避坑4278张轨道图从数据到训练会踩的五个坎5.1 文件名错位训练时静默丢样本现象数据目录里图片名是 rail_0001.jpg、rail_0002.jpg但某个 XML 的 filename 节点写的是 rail_1.jpg转换脚本按文件系统遍历时没问题一旦按 filename 节点去取图就会拿到空路径。原因数据集由多人标注合并时没有统一重命名XML 内 filename 与磁盘文件名脱节。解决写脚本前先做一次根名一致性校验。最省力的办法是忽略 XML 里的 filename 节点统一以磁盘上的 XML 文件名作为根名去匹配图片校验脚本可以用两个 set 做差集把不一致的文件名全部打印出来人工确认。这个坑通常在训练开始后才会暴露因为缺图不会直接报错只会表现为某类 AP 偏低。5.2 标注框坐标越界或 size 节点为零现象转换后某张图 txt 出现 1.2 或 -0.3 这样的归一化值训练日志里出现 LoadImage 阶段警告或一张图的目标全部被剔除。原因bbox 坐标超出图片边界通常是标注时在图像边缘拖拽鼠标没刹住size 节点为零则可能是生成 XML 时读图失败。解决转换脚本加钳制处理如第 3 章代码所示同时在遍历 XML 时检查 width、height 是否为 0为 0 直接打印文件名并跳过避免除零。肉眼抽检时如果发现大量框贴边建议回标注工具统一调整而不是全部依赖脚本钳制因为钳制只是把越界值拉到边界并不能修正框本身偏移。5.3 类别标签被写成中文或自定义缩写现象训练时 loss 正常下降但验证时模型几乎只输出一类另一类完全消失。打开 XML 一看gap 类在部分样本里被标成了“间隙”或“gap_fine”。原因LabelImg 的类别列表没锁定标注者手动输入了对同一目标的不同叫法。解决转换脚本里对未知类别打印并跳过是兜底真正防线在标注阶段用 LabelImg 或 CVAT 时把 classes.txt 预置好只允许下拉选择不接受手动输入。已经发生的情况下要写一段规范化脚本把相似类别名映射到 crack 和 gap再重新统计类别直方图确认没有残余。5.4 gap 样本太少模型偏科现象训练几十个 epoch 后crack 的 AP 达到 0.85 以上gap 的 AP 还在 0.4 上下。原因4278 张图里包含 gap 的可能不到两成同一 epoch 内 gap 样本见到的次数太少。解决优先做 4.2 节的分层划分保证验证集里至少有 30 个 gap 实例再对少类图做 3 到 5 倍复制增强、水平翻转和 ±15 度旋转复制和翻转不改变语义。如果还不够采用两阶段训练先只训 crack固定骨干后把 gap 加入联合训练避免少类梯度被多类淹没。5.5 增强把裂缝方向语义改歪了现象实测时对竖直方向裂缝检测良好对斜向裂缝几乎漏检。查训练记录发现数据增强里配了大角度旋转和上下翻转。原因旋转增强虽然能增加方向多样性但轨道图像有强方向性把图旋转 180 度后裂缝纹理走向与实际采集方向不一致等于引入伪样本。解决轨道这类强方向性场景旋转角限制在 ±15 度以内翻转只做水平翻转不要上下翻转。裂缝和间隙的语义对方向极度敏感宁可少增强也不要增强出脱离物理现实的样本。这个坑一度让我怀疑增强是玄学实际是没考虑场景语义。6. 训练后的验证技巧盯住误检率而不是只看 mAP6.1 用置信度阈值控制现场误报在验证集上我常用的方法是把置信度阈值从 0.1 到 0.9 各跑一遍记录每个阈值下的召回率和每百张误报数画一条折线再选点。轨道缺陷的特殊性在于漏检代价高但误报会消耗大量人力复核所以一般取召回率在 0.95 附近、误报率最低的阈值而不是模型默认的 0.25。这个阈值会随夜晚、雨雾等环境变化建议每个场景各标定一次。6.2 迁移学习与少样本微调的起点4278 张对 YOLO 这类深度检测器来说仍不算大常规做法是用 COCO 预训练权重做迁移学习冻结前若干层只训练检测头。训练输入分辨率从默认 640 调到 1024 或更高会明显改善小裂缝的召回。调参前先确认验证集 gap 数量不低于 30达不到就回去重新划分模型再强也补不了数据划分的窟窿。6.3 一个固定习惯我现在拿到任何新的缺陷数据集第一件事不是立刻写训练脚本而是先把类别统计、XML 一致性校验、划分种子这三样固定下来。曾经有一版轨道模型在验证集上 mAP 0.91现场一测误报率比训练时高三倍最后查明是训练验证划分时随机泄漏同一张图的相似版本同时进了训练集和验证集。从那以后我坚持用固定种子划分并在训练日志里记录数据指纹比如各类别实例数、图片分辨率分布这个习惯帮我避免了不少返工。裂缝和间隙的识别走到今天数据格式已经不是瓶颈真正花时间的是把数据里看不见的脏东西处理干净。这套 4278 张轨道缺陷数据集提供了一个不错的起点PASCAL VOC 标注规范两类缺陷目标明确按上述步骤把它变成可靠、可复现的训练原料即可。希望帮到你。本文还有配套的精品资源点击获取
