做铁轨表面缺陷检测做了快两年,最深的感触是:模型选型其实都排在后头,真正卡脖子的反而是一份靠谱的铁轨缺陷检测数据集怎么来、怎么标注、怎么喂给模型。每一步都有可能让整个项目白干。我见过不止一个团队拿通用目标检测数据集跑通 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", encoding="utf-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_size=640, overlap=0.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 训练参数与评估指标配置
训练参数方面,我们最终稳定在这样一组配置:输入分辨率 640x640,batch size 16,训练轮数 160 轮,优化器用带动量的 SGD,初始学习率 0.01,配合余弦衰减。增强方面开启标准 Mosaic、随机翻转、颜色抖动,但关闭了旋转增强,因为旋转会破坏裂纹的方向特征,而裂纹的方向在工程判断中有重要信息量。
评估指标不能只看一个 mAP。mAP@0.5 反映定位和分类的综合表现,mAP@0.5:0.95 更严格,但对小目标非常不友好。铁轨缺陷检测要额外关注召回率,因为漏掉一条裂纹的代价远远高于把一片锈蚀误报成裂纹。我习惯同时记录 recall@0.5 和 precision@0.5,最终模型筛选时,在保证 recall 不低的条件下,尽量提高 precision,减少现场误报。
下表是我们某个版本模型在测试集上的表现,具体数字会随数据更新波动,但相对趋势可以参考:
| 模型配置 | mAP@0.5 | mAP@0.5:0.95 | Recall@0.5 |
|---|---|---|---|
| Faster R-CNN 基线 | 78.3 | 45.7 | 82.5 |
| YOLO 中等规格 | 83.8 | 51.4 | 86.8 |
| YOLO 大规格 + 重叠切块推理 | 85.6 | 54.2 | 88.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 数据集持续迭代的节奏感
数据集不是攒一次就完事了,它是需要持续增长的资产。我们团队的迭代节奏是月初固定做一次数据统计,看本周期的缺陷样本新增了多少、哪个类别的表现下降了、负样本够不够。模型表现最差的那个类别,往往就是现场新增样本最多的类别,两者是强相关的。
还有一个容易被忽略的点:数据增强不能解决所有问题。我们曾经试图用各种增强手段来替代采集新样本,结果是训练数据多样性确实上来了,但真实场景的泛化能力没有明显提升。新采集的真实缺陷样本,哪怕只有一两百张,对泛化能力的提升也远大于几千张的离线增强。所以我现在的原则是:增强保底,采集优先。
最后说点个人体会。这个项目做下来,最大的教训是前期太迷信模型结构,后期才发现数据质量直接决定上线效果。铁轨缺陷检测这种场景,数据集的规范程度和与现场环境的贴合度,才是决定项目成败的隐性成本。上面这套东西不是标准答案,但希望我踩过的这些坑,能帮你少走几段弯路。如果你也在做类似的数据集整理,建议从第一天就把标注规范写清楚、把负样本留足、把处理脚本存好,后面你会感谢当时的自己。