简介:面向铁路轨道缺陷检测任务的数据集,原始图片规模4278张,采用COCO JSON格式标注,可识别轨道裂缝、间隙等缺陷,适合计算机视觉、基础设施巡检方向的研究者与算法工程师开展模型训练及效果验证。压缩包共2000个文件,其中1997张jpg图像和3个JSON标注文件,整体大小约271.95MB;jpg包含原始现场图像及增强样本,JSON标注与COCO格式对齐,可直接导入主流目标检测框架。已有1366人学习下载,资源包内容聚焦,除图片与标注外无冗余脚本,拿到后即可进行数据划分、模型微调与缺陷识别实验。对于需要公开轨道缺陷数据或验证裂缝、间隙检测算法的团队,能有效节省数据采集与人工标注时间。
1. 铁路轨道缺陷数据集:4278张图里藏着裂缝与间隙的标注密码
铁路巡检的缺陷识别,一直是目标检测里比较尴尬的场景:说难,是因为裂缝这类目标细长、对比度低;说容易,是因为场景固定,数据一旦到位,模型效果立竿见影。这份4278张原始图片的铁路轨道缺陷数据集,直接省掉了最磨人的标注环节——COCO JSON格式的标注文件已经把裂缝和间隙两类缺陷框好,图片与标注信息可参考CSDN那篇144853970的说明。适合正在跑轨道缺陷检测、被数据格式卡住的工程师,也适合拿真实工业数据练手的学生团队。
2. 数据长什么样:从RF命名到COCO JSON的逐字段拆解
2.1 图片文件名里的信息量
拿到数据集先看文件名,别急着解压训练。这份数据里的文件名明显是Roboflow导出后重命名的格式,比如aug_prefix_0_161_jpg.rf.d7a356013bac55b6f6365330b178aad3.jpg,去掉.rf.和后面的哈希串,剩下aug_prefix_0_161_jpg,说明这张图来自增强流水线,是第161号样本。再比如IMG_20201211_121541_jpg.rf.28ccb6ff36f5171ae77792d80103b29c.jpg,原始文件名是IMG_20201211_121541,这是2020年12月11日12点15分41秒拍摄的相机原图,文件名自带拍摄时间戳。还有Image-123_jpg.rf.*这种,是人工整理过一轮的样本。
rf后缀是Roboflow导出的标志,不是拍摄信息。Roboflow在导出数据集时会统一重命名,把原始文件名、文件名后缀、哈希串拼接在一起。看到这个后缀,基本可以确定数据经过Roboflow平台处理,这对接下来的格式转换有参考价值——部分标注框坐标可能已经过Roboflow的预处理变换。
文件名的前缀对数据划分举足轻重。IMG_开头的图片代表按时间序列连拍,同一时段内同一个缺陷会被拍到多帧;Image-开头的图片多半是人工挑出来的关键帧;aug_prefix开头的则是增强后的派生样本。这三种来源如果混在一起随机划分,极易造成数据泄漏,具体解决方式放在3.2节。
2.2 COCO JSON的五个核心块
这份标注文件遵循标准COCO格式,包含info、licenses、images、annotations、categories五个核心块。images块记录每张图的id、width、height、file_name四个字段。注意width和height是图片的原始像素尺寸,不是增强后的尺寸。取尺寸时以images块为准,自己用PIL去读图很容易出错,因为Roboflow可能已经对图片做过resize,文件系统里的实际尺寸和标注文件里记录的尺寸不一致。
annotations块是核心中的核心。每一条记录包含image_id、category_id、bbox、segmentation、area、iscrowd六个字段。bbox是[x, y, width, height]绝对像素坐标,x、y是左上角坐标。对裂缝这种细长目标,bbox会被拉得特别长,宽高比夸张是正常现象。segmentation如果是polygon格式,表示用一组点围出缺陷的精确轮廓,如果标注工具导出了RLE格式,还需要额外解码才能转成点坐标。
categories块是类别表。这份数据类别数是2,对应摘要里的裂缝和间隙两个缺陷类别,所以这是一个二分类目标检测任务。三个块的协作逻辑是:images定义像素空间,annotations给目标位置,categories给语义标签。字段关系整理如下:
| 数据块 | 关键字段 | 在本数据集中的作用 |
|---|---|---|
| images | id, width, height, file_name | 建立图像ID与文件路径、尺寸的映射 |
| annotations | image_id, category_id, bbox, segmentation, area | 定位每个缺陷实例的位置与轮廓 |
| categories | id, name | 定义crack和gap两个类别名称 |
表面上是三个块,实际坑藏在area字段。COCO官方要求area是分割区域的真实面积,不是bbox宽乘高。Roboflow导出的JSON有时直接按bbox宽乘高填area,这在用COCO API计算mAP时会导致小目标权重失真。拿到数据后第一件事,是校验area与segmentation多边形实际面积是否一致,不一致就重算。
2.3 五分钟统计数据集分布
训练之前先摸清数据分布,这能帮你在后续配置里做出合理决策。
import json import collections with open('annotations/instances_train.json', 'r', encoding='utf-8') as f: coco = json.load(f) # 类别统计 cat_name = {cat['id']: cat['name'] for cat in coco['categories']} ann_count = collections.Counter() img_sizes = {} for ann in coco['annotations']: cat_id = ann['category_id'] ann_count[cat_name[cat_id]] += 1 img_id = ann['image_id'] if img_id not in img_sizes: # 从images块取宽高,避免重复读文件 for img in coco['images']: if img['id'] == img_id: img_sizes[img_id] = (img['width'], img['height']) break # 输出类别统计 for name, cnt in ann_count.most_common(): print(f"{name}: {cnt} 个实例") # 抽查bbox与area的一致性 ann = coco['annotations'][0] bbox = ann['bbox'] print("bbox: ", bbox, "area字段: ", ann['area']) print("bbox宽乘高: ", bbox[2] * bbox[3])这段脚本分三步:先做类别计数,再用image_id反查图片尺寸,最后抽查一条记录的area字段是否等于bbox宽乘高。cat_name是从categories块建立id到名称的映射;ann_count做无序计数,适合快速看分布;img_sizes字典用来缓存尺寸,避免在循环里反复遍历images列表,4278张图全跑下来也就多几毫秒,但养成缓存习惯对十万级数据集能省下不少时间。
如果输出显示间隙类实例数远少于裂缝类,就是类别不平衡,解决方式在3.3节。如果area和bbox宽乘高差异超过几个像素,说明标注工具流程不统一,后续训练时建议按segmentation重新计算面积,或用bbox面积参与损失计算。统计的价值还在于预估训练时长:总实例数除以图片数能得到平均实例密度,轨道场景大多是低密度,每张图一到两个缺陷,这种数据分布下,batch size调得过大反而容易过拟合。
3. 把COCO JSON接进YOLO训练流程:转换脚本与三个关键参数
3.1 COCO转YOLO的归一化逻辑
YOLO系列训练用的是txt标注,每行一个目标,格式是class cx cy w h,所有值都是相对图片宽高的0到1浮点数。COCO JSON里存的是绝对像素坐标,转换的核心就是做一次归一化。
常见做法是遍历annotations,通过image_id找到对应的图片尺寸,利用bbox计算出中心点和宽高,再除以图片宽高。下面这段脚本是完整的转换过程。
import json import os def coco_to_yolo(coco_json, img_dir, out_dir): with open(coco_json, 'r', encoding='utf-8') as f: coco = json.load(f) # 建立 id -> 图片信息 的映射 img_info = {img['id']: img for img in coco['images']} # 类别编号:按categories的id排序后从0开始 cats = sorted(coco['categories'], key=lambda x: x['id']) cat2yolo = {cat['id']: i for i, cat in enumerate(cats)} # 按图片分组annotations anns_by_img = {} for ann in coco['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) os.makedirs(out_dir, exist_ok=True) for img_id, anns in anns_by_img.items(): info = img_info[img_id] w = info['width'] h = info['height'] base = os.path.splitext(info['file_name'])[0] txt_path = os.path.join(out_dir, base + '.txt') lines = [] for ann in anns: x, y, bw, bh = ann['bbox'] # 中心点加宽高归一化 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h # 防御:归一化结果超出[0,1]时做裁剪 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) nw = min(max(nw, 0.0), 1.0) nh = min(max(nh, 0.0), 1.0) lines.append(f"{cat2yolo[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(txt_path, 'w') as f: f.write('\n'.join(lines)) print(f"转换完成,产出 {len(anns_by_img)} 个txt文件")脚本执行逻辑是这样:先把categories按id排序,建立COCO类别id到YOLO目标编号的映射。因为COCO JSON里的category_id不一定是0、1,不能直接拿原始id当YOLO类别号。再按image_id把annotations分组,确保每个图片只生成一个txt。最后对每个bbox做归一化,并把结果裁剪到[0,1]区间。
参数说明:coco_json是标注文件路径,img_dir在这里其实是备用的,因为宽高取的是JSON里的值,不再从img_dir读图;out_dir是输出的txt目录。cat2yolo决定类别顺序,训练时配置文件的names必须和这里完全一致,否则会出现"裂缝标成间隙"的错位。防御性裁剪那段是血泪经验,Roboflow导出的bbox偶发越界,目标贴边时x+width可能大于图片宽度,归一化后会变成1.0几。YOLO训练遇到超界标注不会报错,但梯度会变得不稳定,表现出来就是loss掉了又涨。
3.2 数据划分不能随机:按拍摄来源分治
文件名前缀不同的图片来源不同,直接随机划分会让验证集失真。轨道缺陷会被连续多帧拍到,这些帧高度相似,假如散落进了train和val两份,验证集精度会高得离谱,现场一测就露馅。推荐按文件名前缀分组,把同一来源的所有图片放进同一个集合。
import os import random img_files = [f for f in os.listdir('images') if f.endswith('.jpg')] # 提取来源前缀:IMG_20201211 / Image-123 等 def source_key(fname): if fname.startswith('IMG_'): parts = fname.split('_') return parts[0] + '_' + parts[1] if fname.startswith('Image'): return fname.split('_')[0] return fname.split('_')[0] groups = {} for f in img_files: key = source_key(f) groups.setdefault(key, []).append(f) all_keys = list(groups.keys()) random.seed(42) random.shuffle(all_keys) val_keys = set(all_keys[: int(len(all_keys) * 0.15)]) train_files, val_files = [], [] for key, files in groups.items(): if key in val_keys: val_files.extend(files) else: train_files.extend(files) print(f"train: {len(train_files)}, val: {len(val_files)}")source_key的规则是关键:IMG_20201211_121541取出前两段得到IMG_20201211,代表一次拍摄批次;Image-123取第一段,代表单张人工样本;aug_prefix开头的保留前缀,因为增强样本是派生的,归为一组避免与原始图串集。划分比例我一般用15%做验证,按这份数据算,train大约3600多张,val大约600多张。
分组划分的代价是训练集的分布偏离了随机划分的分布,但换来的是验证指标可信。如果类别不平衡,按组划分后还要检查val里两个类别的比例是否失调,必要时调整seed或用分组抽样。
3.3 类别不平衡:重复采样还是换损失函数
上一节统计完你会发现,裂缝类的标注框数量通常远超间隙类。轨道上裂缝高频出现,间隙缺陷相对稀有,模型为了把整体loss压下来,会倾向于把所有疑似区域都预测成裂缝。
处理不平衡有两条路,数据层面重复采样,或者损失函数层面调整。数据层面最直接。
import collections # 统计每张图包含的类别 def count_classes(coco): img_anns = collections.defaultdict(list) for ann in coco['annotations']: img_anns[ann['image_id']].append(ann['category_id']) return img_anns img_anns = count_classes(coco) # 找出包含间隙缺陷的图片id gap_cat_ids = [cat['id'] for cat in coco['categories'] if cat['name'] == 'gap'] gap_imgs = [img_id for img_id, cat_ids in img_anns.items() if any(cid in gap_cat_ids for cid in cat_ids)] # 复制这些图片的标注到额外训练列表 extra_weight = 2.0 # 间隙样本权重这段逻辑是找出所有包含间隙缺陷的图片,给它们在训练列表里的权重乘2,相当于每轮epoch里间隙样本被多读一次。extra_weight是经验值,间隙样本本来就少,权重调太高会造成模型对间隙过拟合,反而把裂缝误判成间隙。实操中我习惯从1.5开始试,观察验证集两类的AP曲线,找到两条曲线都不掉的平衡点。
4. YOLOv8与MMDetection怎么选:配置文件与训练启动
4.1 两个框架的真实差异
拿到COCO格式标注后,面临第一个选择:训练框架用YOLOv8还是MMDetection。这个选择直接决定后续调优成本。两个框架的对比整理如下:
| 维度 | YOLOv8 | MMDetection |
|---|---|---|
| 上手成本 | 低,一个yaml加一条命令启动 | 高,需要维护完整config |
| 模型结构 | anchor free,单阶段 | 支持Faster R-CNN、RetinaNet、DETR等 |
| 部署生态 | ONNX、TensorRT导出顺畅 | 需要mmdeploy配合 |
| 自定义能力 | 超参数封装较深 | 每个模块都能改配置 |
轨道巡检这类边界清晰、目标密度低的场景,YOLOv8在默认设计下已经能拿到不错的mAP,部署链路也短。MMDetection的价值在于可复现学术模型、可细调anchor和loss,适合做对比实验。从这份数据的实际需求看,先跑YOLOv8出基线,再用MMDetection复验,是性价比最高的路径。
4.2 YOLOv8的yaml文件要点
YOLOv8用yaml文件定义数据集路径和类别名称,配置极其简洁。
# dataset.yaml path: ./ train: train/images val: val/images # 两个类别,名称与COCO的categories保持一致 names: 0: crack 1: gap三个要点。第一,train和val指向的目录里放的是图片,模型不会自动扫描标注文件,它在相同路径下找同名txt。第二,names里的顺序必须和3.1节转换脚本里cat2yolo的编号一致,这里0对应crack、1对应gap。第三,path字段是根目录,train和val用相对路径,换机器不用改yaml。
训练命令:
yolo detect train data=dataset.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 patience=10epochs先设100,配合patience=10做早停,验证集mAP连续10轮不涨就停。imgsz=640对轨道图片够用,裂缝这类目标在图像里占的像素不多,640分辨率下还能保持足够细节。想让模型更聚焦小目标,可以试imgsz=960,代价是训练时间增加约1.5倍。轨道图像长宽比离散,YOLOv8默认letterbox填充,改imgsz不会丢太多信息。
4.3 MMDetection的config差异与Anchor调整
如果用MMDetection做基线,config的写法是另一个体系。以Faster R-CNN为例,需要定义数据集路径、预处理pipeline、模型结构、训练参数四块。相比YOLOv8的极简yaml,MMDetection确实繁琐,但可调项也更细。
锚框参数在model.roi_head.bbox_head里:
anchor_generator = dict( type='AnchorGenerator', scales=[8, 16, 32], ratios=[0.5, 1.0, 2.0], strides=[4, 8, 16, 32, 64])关键调整点是ratios。裂缝这种细长目标的长宽比经常超过2.0,默认ratios覆盖不到。有经验的做法是把ratios改成[0.2, 0.5, 1.0, 2.0],包含更扁平的锚框。修改后裂缝的召回率通常能上涨几个点。训练启动命令是:
python tools/train.py configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py注意MMDetection的默认config针对COCO 80类,需要修改num_classes=2和data配置。如果只改检测头不改数据集,训练开始后会发现ground truth的类别id超出范围,直接报错。建议改config后做一次模拟训练,跑一个step确认dataloader正常再放长训练。
5. 避坑与常见问题:裂缝检测踩过的五个坑
5.1 细长裂缝丢检:anchor与长宽比不匹配
现象:训练完mAP看起来不错,但实际推理时,细长的、几乎水平的裂缝经常漏检,短粗的缺陷都能检出来。
原因:anchor或采样点没覆盖极端长宽比。裂缝bbox的长宽比可以到5比1甚至10比1,默认锚框最多覆盖2比1,预测出来的框先天无法贴合细长目标。
解决:在MMDetection里把ratios扩到[0.2, 0.5, 1.0, 2.0];用YOLOv8则加大输入尺寸imgsz=960,配合图片分块检测。另一个有效手段是数据增强里的随机旋转,让裂缝出现不同朝向,迫使模型学习方向不变性。
5.2 裂缝和间隙边界混淆:标注规则不统一
现象:模型在裂缝附近同时预测出间隙类,两个框叠在一起,置信度都超过0.5。
原因:裂缝和间隙在视觉上本来就模糊,标注标准不统一时,同一个缺陷有的标识成裂缝、有的标识成间隙,模型学到的类间边界是混乱的。这份数据经过Roboflow导出,标注来自多个批次,出现标准漂移并不奇怪。
解决:训练前人工抽查50张含间隙的图,确认标注标准,必要时合并类别。如果间隙类实在分不清,先合并成一类"缺陷"跑通流程,再细化。我更建议统一标准:把间距超过既有宽度的剥离层归为间隙,剩余细线归为裂缝。
5.3 COCO转YOLO后框偏移:segmentation与bbox不一致
现象:转出的txt在可视化脚本里画出来,框的位置和缺陷实际位置偏移明显,有时框到缺陷旁边。
原因:COCO JSON里segmentation是人工勾勒的多边形,bbox有时是标注工具按外接矩形计算,有时是人工微调的矩形,两者边缘不完全对齐。转换脚本如果优先取segmentation的外接矩形,而手工bbox才是给模型看的标准,就会偏移。
解决:转换时以手工bbox为准,丢弃segmentation生成的外接矩形。如果bbox明显超出segmentation边界,再用polygon的外接矩形覆盖。这个判断用shapely几行代码就能做。
5.4 数据泄漏:同源连拍帧串进两个集合
现象:验证集mAP高达0.9以上,新拍的现场图片mAP掉到0.5以下。
原因:随机划分导致同源连拍帧同时出现在train和val,模型提前见到了答案。排查时按文件名统计才发现IMG_20201211_121541和IMG_20201211_121540在train和val各占一张,这两张图拍的几乎是同一个位置。
解决:按source_key分组划分。检查方法是写脚本扫一遍val集里是否存在和train集文件名前缀相同的图片,出现就直接替换划分结果。
5.5 逆光与阴影下的裂缝漏检
现象:白天顺光图片检测效果很好,阴天或逆光时裂缝漏检率明显上升。
原因:轨道表面的裂缝在阴影或逆光条件下对比度大幅降低,模型学到的边缘特征依赖局部对比度,光照一变特征就失效。这是工业场景的经典问题,不是模型结构能单独解决的。
解决:训练pipeline里加随机亮度、对比度、灰度化增强项。更直接的做法是推理时对输入图像做CLAHE对比度增强,实际效果一般能提升阴影场景3到5个点的召回率。
6. 进阶:验证集阈值扫描与缺陷分级
模型训练完后,直接把默认0.5置信度阈值用于部署,是常见的翻车点。裂缝检测里,漏检和误检的代价不同:漏检意味着巡检失效,误检意味着人工复查成本上升。我把阈值扫描写成了固定动作。
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') # 在验证集上跑低阈值推理,拿到所有候选框 results = model.val(data='dataset.yaml', conf=0.001) # 遍历阈值计算F1,选择最优 best_threshold = 0.5 best_f1 = 0.0 for conf in [0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6]: # 用验证结果重新计算精确率和召回率 precision, recall = evaluate_at_threshold(results, conf) f1 = 2 * precision * recall / (precision + recall) if f1 > best_f1: best_f1 = f1 best_threshold = conf print(f"最佳阈值: {best_threshold}, F1: {best_f1:.3f}")阈值选择的实际逻辑,还要结合业务需求。缺陷数量偏少就选召回率优先,把阈值调低,代价是误检多一点;缺陷数量偏多则提高阈值减少复查量。另一种更有价值的进阶是缺陷分级。轨道缺陷不是"有或没有"就能决策的,往往需要区分轻微和严重。借助这份数据,我一般把单条裂缝的像素长度和宽度提取出来,配合实地标准做对照,用长度阈值自动区分严重程度。不需要重新标注,只把推理结果里的bbox宽高换算成实际尺寸——前提是先标定每个像素对应的物理距离。
从那以后跑轨道缺陷检测,我每次都会把数据分布统计、分组划分、阈值扫描这三件事强制走一遍,再考虑调模型结构。数据决定的上下限,远比模型结构本身大。希望帮到你。
本文还有配套的精品资源,点击获取