铁轨缺陷检测数据集从采集标注到训练部署的完整实践
做铁轨表面缺陷检测做了快两年最深的感触是模型选型其实都排在后头真正卡脖子的反而是一份靠谱的铁轨缺陷检测数据集怎么来、怎么标注、怎么喂给模型。每一步都有可能让整个项目白干。我见过不止一个团队拿通用目标检测数据集跑通 Demo换上铁轨图像后 mAP 直接掉到跟随机猜测差不多。问题不在模型本身而是对“铁轨缺陷”这类目标的特殊性理解够不够深。这篇文章准备把我们团队整理和使用铁轨缺陷检测数据集的全过程串起来讲一遍。不敢说这套方案是最优解但它在实际落地的项目里跑通了半年以上并且一直在迭代对准备入坑工业视觉检测的工程师以及做深度学习落地的算法同学应该都有点借鉴价值。我会尽量把几个层面讲透数据集为什么难做、采集和标注规格怎么定、长图加载和切块的细节、模型训练和评估的取舍以及现场部署时数据回流的那套闭环。先给一句总纲铁轨缺陷检测数据集的价值不在于“图多”而在于类别边界是否清楚、负样本是否足够、采集条件和现场是否一致。把这三点抓住了后面的训练和部署都会顺很多。1. 铁轨缺陷检测的痛点与专用数据集的价值1.1 人工巡检与图像检测的差距铁路钢轨表面长期承受车轮反复碾压加上温度应力和雨水侵蚀容易出现各种表面损伤。常见的缺陷类型有裂纹、剥离、压溃、磨损、锈蚀、鱼鳞伤还有焊缝区域的各种异常。其中裂纹和剥离是安全优先级最高的两类一旦漏检后续可能迅速发展成断轨事故。传统巡检主要靠工人沿线路步行目测或者坐在轨道检查车上观察取像设备回传的画面。这种方式有两个明显短板。首先是效率低一条线路几百公里全靠人眼盯屏幕或盯钢轨疲劳之后漏检率会快速上升。其次是时间窗口受限铁路运维的天窗时间很短尤其是高铁线路留给检测的时间非常紧张。夜间的检测更是对设备和人力的双重考验。所以在工业现场基于图像的自动检测系统成了刚需。相机器材安装在巡检小车或大型轨道检测设备上以一定速度扫过钢轨表面连续采集图像再通过算法自动圈出疑似缺陷。这套流程要真正跑起来背后必须有大量高质量图像样本支撑也就是专用数据集。没有数据再先进的检测算法也无从谈起。1.2 通用目标检测数据集为什么救不了铁轨我最早踩过的坑就是直接拿通用目标检测数据集的预训练权重来做迁移。看起来用 ImageNet 或 COCO 预训练模型是标准操作但换成铁轨图像后效果非常差尤其是裂纹这种细长目标几乎完全检测不到。原因在于通用数据集里的“目标”和铁轨缺陷在本质上是两类东西。COCO 里的猫、狗、汽车、行人都是结构化物体有清晰的轮廓、独立的语义、相对固定的宽高比。而铁轨缺陷是表面纹理异常边界模糊形态连续性很强。一条裂纹可能延伸几十厘米但在局部图像里只是几条交叉的细线剥离区域和正常磨耗区域之间也没有明确分界。模型在自然图像上学到的纹理特征和尺度先验很难直接迁移到这种场景。另外通用数据集几乎不包含“负样本”的概念。而铁轨检测落地时最头疼的不是漏检是误检。水渍、油污、焊花、锈斑、阴影长得很像缺陷但又不是缺陷。如果数据集里没有这些负样本模型上线后就会疯狂误报人工复核工作量暴增整个系统失去意义。这一点是铁轨缺陷检测数据集和通用数据集最大的差异也是后面所有设计都要围绕的核心约束。2. 数据集的采集规格与标注体系设计2.1 采集设备与原始图像规格这份数据集的原始图像来自轨道巡检设备上的线阵相机。线阵相机和普通面阵相机不一样它只有一行感光元件通过连续扫描拼出整幅图像非常适合钢轨这种沿固定方向连续延伸的对象。采集时线阵相机配合侧向 LED 条形光源保证钢轨顶面和两侧照明均匀。原始图像是一张长条形图片宽约 2048 像素高度根据扫描行数累积单帧经常到几千甚至上万像素。存储上我建议保留为 8 位灰度或 PNG 无损格式不要用 JPEG。缺陷检测中最怕的就是压缩带来的边缘模糊尤其裂纹这种只有两三个像素宽的目标JPEG 的块效应会让标注和训练都变得很困难。这里有一个非常容易忽略的点车速波动。线阵相机的行频如果不和车速严格同步图像纵向会被拉伸或压缩导致同一处缺陷呈现不同的形态。所以采集系统里必须有编码器或速度传感器让行频跟随车速变化。我们当时在数据整理阶段才发现有一部分图像长度明显异常排查后就是因为车速联动没调好。这批图像后来要么剔除要么按比例重采样绝不能让它们混进训练集。2.2 缺陷分类体系与标注边界数据集的核心是标注规范。我们最终把缺陷分为六类每一类都有明确的形态定义和标注规则。分类表如下类别形态特征常见成因标注建议裂纹线状、细长、走向不规则疲劳应力、接触疲劳沿延伸方向取最小外接矩形剥离片状剥落表面粗糙疲劳裂纹扩展覆盖整个剥落区域的外接矩形压溃轨头塑性变形宽度变大轮轨接触力过大圈定明显变形区域磨损大面积磨耗亮带长期滚动摩擦仅标注异常程度的磨损带锈蚀面状锈斑颜色纹理变化长期水浸、腐蚀覆盖锈蚀核心区域鱼鳞伤鱼鳞状浅层剥离滚动接触疲劳包含整片鳞状区域把类别定这么细不是追求学术上的完整而是为了现场处置方便。比如裂纹和剥离的危险等级完全不同运维人员看到告警后需要快速判断优先级。如果模型只输出“表面异常”现场的响应效率就会大打折扣。标注边界是最磨人的环节。以锈蚀和裂纹为例锈蚀区域经常伴随着细小的裂纹两者在局部图像里非常相似。我们的约定是裂纹必须能看到明确的线性主干如果只是锈蚀区里几条细小的纹理不标为裂纹如果确实有连续线状缺陷穿出锈蚀区则单独标注裂纹框允许与锈蚀框重叠。这个约定需要在标注团队里反复强调否则不同人标出来的结果会千差万别。2.3 标注工具与标注一致性控制标注工具我们试过好几款最后用的是支持 YOLO 格式直接导出的标注软件。界面操作简单对没有算法背景的标注人员很友好。不建议直接用 Polygon 多边形标注来做所有缺陷因为多边形标注耗时太长而铁轨缺陷很多是模糊的纹理区域多边形反而容易引入主观误差。矩形框加类别对大多缺陷类型足够。控制标注一致性的办法有两个。一个是建立“标准案例集”挑出各类缺陷的典型图像和边缘案例每个标注员人手一份遇到拿不准的就去对照标准查。另一个是抽检复核我们要求每批次标注结果中至少 30% 由资深成员复标重点看类别是否标错、边界框是否明显过大或过小。刚开始的前两周会把大量时间花在这上面但后面返工率会快速下降这个前期投入绝对值得。3. 从原始长图到训练样本切块与坐标换算3.1 目录结构设计原始图像动辄几千像素高不能直接丢进检测模型训练。我把整个数据集按训练、验证、测试三部分组织同时也保留一份完整标注文件方便做统计和可视化。rail_defect_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ └── val/ ├── annotations/ │ ├── instances_train.json │ ├── instances_val.json │ └── instances_test.json ├── scripts/ │ ├── crop_dataset.py │ ├── visualize_boxes.py │ └── split_data.py └── rail_defect.yamlimages 下放的直接是切块后的训练图像labels 下放对应的 YOLO 格式标注文件。annotations 里是转换成 COCO 格式的完整标注方便用现成工具做数据可视化或评测。scripts 里放数据集处理的脚本这一点容易被忽略但强烈建议保留。因为后面每次新增数据、修复标注都要重新走一遍处理流程有脚本就省事得多。3.2 为什么必须对长图切块很多人一开始图省事把长图直接 Resize 到 640x640 再训练。这种做法对铁轨缺陷基本是灾难。原图可能高 4000 像素一个十几像素宽的裂纹缩放到 640 后几乎消失模型就算再强也学不到特征。正确的做法是滑窗切块。把长图切成一堆 640x640 或 512x512 的小图再训练。切块时要有重叠因为缺陷可能正好横跨两个 patch 的接缝处一旦被切成两半且两边都只剩一半模型很难正确识别。我们的经验是重叠率 20% 比较合适既不会产生重复样本过多的问题又能保证绝大多数目标在一张 patch 里是完整的。切块后还要做一次清洗。比如完全没有目标的纯背景 patch可以按比例保留一部分作为负样本但不要全部保留否则训练数据里负样本比例过高模型会倾向于把一切区域都判为背景。我们当时把无缺陷 patch 的比例控制在整个训练集的 15% 左右。3.3 滑窗切块与坐标换算的实现切块的坐标换算是最容易写错的地方。原始标注坐标基于完整长图切块后必须转换到 patch 坐标系。下面这个脚本是我们一直在用的版本核心是先读 YOLO 格式再用滑窗遍历最后对落在 patch 内的框做裁剪和过滤。import os import cv2 def load_yolo_label(txt_path, img_w, img_h): boxes [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:5]) xmin (cx - bw / 2) * img_w ymin (cy - bh / 2) * img_h xmax (cx bw / 2) * img_w ymax (cy bh / 2) * img_h boxes.append((cls_id, xmin, ymin, xmax, ymax)) return boxes def crop_long_image(image_path, label_path, out_img_dir, out_lbl_dir, patch_size640, overlap0.2): img cv2.imread(image_path) h, w img.shape[:2] boxes load_yolo_label(label_path, w, h) stride int(patch_size * (1 - overlap)) patch_id 0 for y in range(0, h, stride): for x in range(0, w, stride): x_end min(x patch_size, w) y_end min(y patch_size, h) if x_end - x patch_size // 2 or y_end - y patch_size // 2: continue pw x_end - x ph y_end - y out_boxes [] for cls_id, xmin, ymin, xmax, ymax in boxes: ixmin max(xmin, x) iymin max(ymin, y) ixmax min(xmax, x_end) iymax min(ymax, y_end) if ixmax - ixmin 2 or iymax - iymin 2: continue ccx (ixmin ixmax) / 2 - x ccy (iymin iymax) / 2 - y bbw ixmax - ixmin bbh iymax - iymin out_boxes.append( f{cls_id} {ccx / pw:.6f} {ccy / ph:.6f} f{bbw / pw:.6f} {bbh / ph:.6f} ) if not out_boxes: continue patch img[y:y_end, x:x_end] base_name os.path.splitext(os.path.basename(image_path))[0] patch_img_name f{base_name}_p{patch_id:04d}.png patch_lbl_name f{base_name}_p{patch_id:04d}.txt cv2.imwrite(os.path.join(out_img_dir, patch_img_name), patch) with open(os.path.join(out_lbl_dir, patch_lbl_name), w) as f: f.write(\n.join(out_boxes)) patch_id 1代码里有两个细节值得注意。第一个是裁剪框和 patch 的交并集计算直接用 max/min 求重叠区域即可不需要复杂的几何运算。第二个是过滤条件重叠区域小于 2 像素的框直接丢弃。这个阈值不能设得太大否则靠近 patch 边缘的小缺陷会被全部清掉等于人为制造漏检。4. 基线模型选型与训练参数实战4.1 模型选型单阶段还是双阶段拿到一份干净的数据集之后接下来的问题就是用什么模型。我们在项目里前后对比过双阶段的 Faster R-CNN 和单阶段的 YOLO 系列。Faster R-CNN 在部分小目标上确实有优势但训练速度慢推理速度也慢不太适合巡检车这种有实时性要求的部署场景。YOLO 系列单阶段结构推理速度快工程生态完善从训练到导出的工具链很顺所以我们最终把 YOLO 作为项目主基线。具体选 YOLO 的哪个规格取决于部署平台。训练机上跑的是中等规格模型板上验证用轻量规格最后根据现场硬件再确定。有一点要提醒不要一上来就追求最大最重的模型。工业项目里模型吞吐量、显存占用、部署精度都和模型大小强相关。先跑通基线再逐步加精度比一步到位可控得多。4.2 数据划分防止评估虚高的关键数据划分这件事表面看很简单随机按 8:1:1 分成训练、验证、测试就完事。但铁轨检测有一个具体情况同一段钢轨的连续采图相邻帧之间的背景基本一致缺陷形态也高度相似。如果随机划分时同一个场景的图像同时出现在训练集和测试集里测试指标会虚高真实场景下表现马上打回原形。我们的做法是按采集批次或线路段划分而不是按单张图划分。比如一次巡检采集到的若干段图像整体划入同一个集合。这样验证集和测试集看到的场景和训练集不完全重叠评估结果才更能反映真实泛化能力。另外测试集一旦固定就不要再动。不要在调参过程中频繁往测试集里加数据或改标注否则测出来的指标会失真。我们的测试集从第一批数据就冻住了后续所有模型对比都基于同一份测试集这才保证了对比的可信度。4.3 训练参数与评估指标配置训练参数方面我们最终稳定在这样一组配置输入分辨率 640x640batch size 16训练轮数 160 轮优化器用带动量的 SGD初始学习率 0.01配合余弦衰减。增强方面开启标准 Mosaic、随机翻转、颜色抖动但关闭了旋转增强因为旋转会破坏裂纹的方向特征而裂纹的方向在工程判断中有重要信息量。评估指标不能只看一个 mAP。mAP0.5 反映定位和分类的综合表现mAP0.5:0.95 更严格但对小目标非常不友好。铁轨缺陷检测要额外关注召回率因为漏掉一条裂纹的代价远远高于把一片锈蚀误报成裂纹。我习惯同时记录 recall0.5 和 precision0.5最终模型筛选时在保证 recall 不低的条件下尽量提高 precision减少现场误报。下表是我们某个版本模型在测试集上的表现具体数字会随数据更新波动但相对趋势可以参考模型配置mAP0.5mAP0.5:0.95Recall0.5Faster R-CNN 基线78.345.782.5YOLO 中等规格83.851.486.8YOLO 大规格 重叠切块推理85.654.288.3从这张表能看出虽然 Faster R-CNN 初期在小目标上有优势但经过合适的切块和数据增强YOLO 系列的指标是可以反超的而且部署成本低得多。5. 训练中反复翻车的边界案例与处理手段5.1 锈蚀与裂纹的混淆问题第一次拿到模型测试结果时最刺眼的不是漏检而是误报。相当大的误报集中在锈蚀区域模型把一些带放射状纹理的锈蚀区整个框成了裂纹。我后来去翻标注发现原因有两层。第一层是形态确实接近锈蚀区里的裂纹纹理和线性裂纹在局部感受野里很像。第二层是标注不一致有些标注员把锈蚀区内几条细纹理标成了多个小裂纹框模型学到的是“只要纹理杂乱就是裂纹”。处理办法是双管齐下。先重新统一标注规则明确裂纹必须有连续线性主干禁止把锈蚀区内的细小纹理单独标成裂纹。再增加负样本把大量纯净锈蚀、水痕、油污的 patch 单独作为负样本来训练。我们另加了一个小技巧对模型输出的高置信度裂纹框切出局部图送一个轻量级纹理分类器做二次确认专门判断“这是裂纹还是像裂纹但其实是锈蚀”。这个策略上线后误报率肉眼可见地往下掉。5.2 小目标缺陷的漏检裂纹的宽度经常只有两三个像素在 640x640 的输入下整个目标占据的像素面积非常小模型很容易漏掉。针对这个问题我们尝试了几个方向。第一个是重叠切块推理。训练时已经做了切块推理时同样做滑窗切块把每个 patch 的检测结果还原回原图坐标再通过 NMS 合并重复框。这一步让目标至少在一次切块中处于完整状态漏检明显减少。第二个是测试时增强具体来说就是多尺度推理。同一张 patch 分别以 0.8、1.0、1.2 倍尺度输入模型把结果合并。效果确实有提升但推理时间也涨了约一倍现场是否采用要看算力余量。第三个做法是提高输入分辨率到 768。这招对小目标最直接但显存占用和推理时间都上升。实际项目中可以把这个选项做成配置开关算力紧张时退回到 640只保留切块推理也算一个可接受的折中。5.3 样本不均衡的工程化处理同一个数据集里六类缺陷的样本量天然不均衡。螺纹剥离和锈蚀样本很多压溃和鱼鳞伤样本很少。如果直接训练少数类基本学不出来验证集上这几类的 AP 会非常难看。最有效的做法不是硬调 loss而是先做数据层面的重采样。少数类样本可以过采样也就是重复加入训练必要时可以对少数类做针对性增强比如复制粘贴式增广把某张 patch 里的少数类目标抠出来贴到干净的背景 patch 上生成新的训练样本。这种方式对剥离、压溃这类面状缺陷有效对裂纹不太适合因为裂纹的连续性很难通过简单的贴图增强保留。训练层面还可以对少数类提高 loss 权重或者在 loss 里引入 Focal Loss 思想让模型把注意力更多放在难分类样本上。我们实际跑下来数据层面重采样带来的收益比 loss 调整更大所以建议先做数据再调 loss顺序不要反了。6. 工程部署阶段的数据闭环与现场适配6.1 现场图像与数据集的域差异训练集里的图像是在相对标准的光照和干净环境下采集的。现场情况则完全不同雨天反光、油污遮挡、灰尘覆盖、车轮与钢轨摩擦留下的金属亮斑都会让图像分布发生偏移。第一次真正上线时模型在识别现场的“脏”图像上报出一堆训练时从未见过的错误框。处理手段总结下来有两个。第一件事是补充现场数据参与训练。我们拿巡检车在现场跑了几轮收集大量未标注图像先用已有模型做推理挑出高置信度的检测结果生成伪标签人工审核后加入训练集。这个半自动迁移的过程让模型快速适应了现场光照和背景变化。第二件事是图像预处理对齐。训练时我们用了某种归一化方式现场推理也必须用完全一致的预处理流程。很多时候性能下降不是因为模型变差而是前后处理不一致这一点在工程里经常被忽视。6.2 两级筛查与人工复核闭环现场部署不能只靠一个模型输出最终结果。我们在真实系统里做了两级筛查结构。第一级用低置信度阈值高召回的模型做初筛目标是把可能的缺陷全部捞出来绝不当场放过一个。这意味着初筛会输出不少误报没关系二级来处理。第二级对初筛输出的每个高概率框做精细化判断用小模型看局部纹理结合长宽比、面积、方向等手工特征排除明显不是缺陷的框。通过两级结构整体误报率被压到人工可接受范围内同时漏检率没有明显恶化。人工复核是闭环里的关键环节。我们的流程是系统把置信度高于阈值的缺陷图推送到检修终端检修人员在平板上确认或否决确认的结果直接回流到标注队列。每两周做一次增量训练把这段时间内新增的确认样本和误报样本喂回去。6.3 数据集持续迭代的节奏感数据集不是攒一次就完事了它是需要持续增长的资产。我们团队的迭代节奏是月初固定做一次数据统计看本周期的缺陷样本新增了多少、哪个类别的表现下降了、负样本够不够。模型表现最差的那个类别往往就是现场新增样本最多的类别两者是强相关的。还有一个容易被忽略的点数据增强不能解决所有问题。我们曾经试图用各种增强手段来替代采集新样本结果是训练数据多样性确实上来了但真实场景的泛化能力没有明显提升。新采集的真实缺陷样本哪怕只有一两百张对泛化能力的提升也远大于几千张的离线增强。所以我现在的原则是增强保底采集优先。最后说点个人体会。这个项目做下来最大的教训是前期太迷信模型结构后期才发现数据质量直接决定上线效果。铁轨缺陷检测这种场景数据集的规范程度和与现场环境的贴合度才是决定项目成败的隐性成本。上面这套东西不是标准答案但希望我踩过的这些坑能帮你少走几段弯路。如果你也在做类似的数据集整理建议从第一天就把标注规范写清楚、把负样本留足、把处理脚本存好后面你会感谢当时的自己。