资讯动态

火车轨道检测数据集:3900张VOC标注与YOLO训练实战

发布时间:2026/9/28 14:25:18 来源:尧图企业网站定制
简介火车轨道检测数据集面向铁路智能巡检与计算机视觉目标检测场景可供开发者用于轨道和障碍物识别模型的训练与评估。标注信息符合VOC规范覆盖不同光照、天气和视角下的轨道场景每个XML文件记录了轨道、障碍物等目标的类别标签与边界框坐标基于该数据集训练的检测模型在轨道与障碍物识别任务上准确率可达93.7%。资源包共473.77MB包含2000个XML标注文件文件命名与原始图片对应方便与图片集配合使用也便于二次整理为Pascal VOC标准目录结构。目前已有3608人学习下载适合具备深度学习基础的算法工程师、铁路安全监测项目团队可用于模型训练、数据增广、格式转换及算法调优也可用于评估检测算法在真实铁路环境下的泛化能力为轨道占用和异物入侵检测落地提供可靠数据支撑。1. 火车轨道检测数据集3900张VOC标注背后93.7%是怎么来的一条铁轨在高清监控画面里往往只有十几个像素宽远处一个矿泉水瓶大小的障碍物甚至不到十个像素。要做火车轨道检测数据集的人通常不是想验证某个SOTA模型而是想解决这一类比例极端的目标检测问题轨道是细长目标障碍物是离散小目标两者同时出现还要扛住93.7%这种高指标。这个数据集就是用Pascal VOC格式标记了3900张原始图片目标只有两个类别火车轨道与障碍物。它解决的是铁路巡检、道口占用和异物侵限的自动告警问题替代人工盯屏把“看见轨道”和“看见不该出现在轨道上的东西”一次做到。适合谁呢一类是正在用YOLO族模型做工业视觉检测的工程师想找一个带真实场景分布、类别不平衡明显的干净数据集来调参另一类是自己手握大量现场图片、不知道该怎么标注和训练的团队可以把这套3900张VOC标注的流程当模板。下文会直接从VOC目录结构、VOC转YOLO脚本、训练参数和部署阈值逐层讲最后把坑都列出来。2. VOC格式为什么是轨道检测的主力标记方式目录、类别与两个预设2.1 从目录结构读懂VOC标注JPEGImages、Annotations、ImageSetsPascal VOC的目录结构几乎是自解释的拿到手先看三层就够了。JPEGImages放原始图片Annotations放同名的XML标注文件ImageSets/Main放train.txt、val.txt这类图片文件名清单。一个标准的VOC XML里关键节点是object/name和object/bndboxbndbox给出xmin、ymin、xmax、ymax四个整数坐标。检测任务里最常见的VOC转YOLO脚本本质就是把这四个值换算成归一化的中心点坐标和宽高。目录存放内容在训练中的作用JPEGImages原始监控帧或航拍图模型输入图片来源Annotations同名XML标注文件解析出类别与坐标框ImageSets/Main图片名清单txt划分训练集、验证集、测试集为什么工业项目偏爱VOC而不是COCO因为COCO的标注JSON层级复杂80个预设类别对轨道检测这种垂直场景来说绝大多数是废的。VOC只有一个平铺的XML结构用labelImg就能上手标注改类别名也方便。3900张原始图片不是大数但对VOC标注工作量来说已经相当可观一个熟练标注员每天处理干净、边界明确的轨道图片约200张3900张大约要两周人工标注时间。这个成本决定了数据集的类别不可能多轨道和障碍物各占一类的做法是务实的选择不会因类别过多而稀释每一类的样本数。2.2 轨道和障碍物不是同一类检测任务长条目标与离散目标的处理轨道和障碍物在检测模型眼里是两个极端。轨道是连续细长目标一张1920×1080的图中单轨宽度可能只有20像素长度却横跨整幅画面障碍物是离散小目标落石、倒树、行人可能只在画面里占据一小块。把这两类放进同一个names列表里最容易犯的错误是让模型学“轨道优先”因为轨道目标大、背景简单AP很容易顶到95以上而障碍物AP只有40不到整体mAP被拉高论文里的93.7%看似漂亮实际偏科严重。我一般的处理办法是标注阶段就把铁轨切成多段框每段长度控制在100到300像素之间避免一个细长框把整条轨道框进去。这样做的原因有两个。第一细长框在训练时会被resize得极小多段框能把轨道的局部纹理保留下来第二mosaic数据增强会把四张图拼接缩放整段轨道的框会缩成一条线模型根本学不到“这是一个轨道段”的语义。障碍物则按最小外接矩形标注不要为了追求贴边把框缩到和障碍物严丝合缝留2到3像素的余量对小目标的学习更友好。这里还要提一个数据集质量分诊的习惯。拿到3900张图的标注后我通常会先统计两类目标各自的bbox数量、宽高比分布和中位数面积。如果障碍物bbox的中位面积小于全图面积的1%就要在训练配置里针对小目标做增强否则默认的mosaic与scale增强只会让障碍物越学越模糊。统计脚本本身很简单读取XML的bndbox后除以图片宽高得到归一化宽高再直接看四分位即可这段脚本在第四章会给出可复用的版本。2.3 3900张原始图片的训练边界体量、增强分配与早停3900张图片放在当前视觉模型面前只是一个中小规模数据集。用它去微调一个从头初始化的模型效果一定差用它微调一个在COCO或ImageNet上预训练的模型则是常见且可靠的路线。我一般优先选yolov8s或者yolov8n做底座原因很朴素数据量只有小几千张大模型会迅速把训练集背下来验证指标虚高换一条线路就全线崩盘。训练轮次上我倾向设在120到150之间同时开patience20的早停。3900张的小数据集在60轮左右就能看到验证集指标收敛之后多跑的都是增强带来的抖动。如果到第80轮验证集指标还在明显上升那说明数据增强的强度过大模型还没把真实分布吃透这时应降低mosaic或mixup比例而不是继续加轮次。批次大小按显存来16到32都常见但要注意如果imgsz提到960以上batch记得缩小一半否则显存溢出之后黑匣子式的收敛异常很难排查。还有一个容易忽略的边界ImageSets/Main里的划分文件往往在数据打包时就定死了新人拿到手直接训练不知道划分比例。先用split脚本复核一遍确认验证图片和训练图片来自不同路段或时间段再进训练流程这一步能省掉很多玄学调参。3. 复现93.7%的训练流程VOC转YOLO格式与最短命令路径3.1 用Python脚本把VOC转YOLO格式坐标归一化与损坏XML过滤VOC标注不能直接喂给YOLO系训练脚本第一步永远是转换格式。YOLO的标签格式是每行一个目标类别id 中心x 中心y 宽度 高度四个坐标值均归一化到0到1。转换脚本的逻辑非常简单但坑全在细节处理上比如越界坐标、空标注、未知类别名。下面这段是我拿3900张VOC图片做训练时用的脚本加了三条保护性过滤避免后续训练在某个脏数据上崩掉。import xml.etree.ElementTree as ET import random import shutil from pathlib import Path VOC_ROOT Path(datasets/track_voc) CLASSES [track, obstacle] TRAIN_RATIO 0.8 def convert_one(xml_path: Path, label_dir: Path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: # 未知类别直接跳过避免类别id越界 print(f[skip] unknown class: {name} in {xml_path.name}) continue cls_id CLASSES.index(name) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 坐标倒置的坏框在VOC标注中并不罕见必须过滤 if x2 x1 or y2 y1 or x2 0 or y2 0: print(f[skip] bad box: {xml_path.name}: {x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}) continue x_c (x1 x2) / 2.0 / img_w y_c (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) if not lines: print(f[warn] no valid object in {xml_path.name}) return False label_dir.mkdir(parentsTrue, exist_okTrue) (label_dir / (xml_path.stem .txt)).write_text(\n.join(lines), encodingutf-8) return True image_src VOC_ROOT / JPEGImages annotation_src VOC_ROOT / Annotations for split_name, split_xmls in [(train, []), (val, [])]: all_xmls list(annotation_src.glob(*.xml)) random.seed(42) random.shuffle(all_xmls) split_xmls all_xmls[: int(len(all_xmls) * TRAIN_RATIO)] val_xmls all_xmls[int(len(all_xmls) * TRAIN_RATIO):] for split, xml_list in [(train, split_xmls), (val, val_xmls)]: image_dst Path(fdatasets/track_yolo/images/{split}) label_dst Path(fdatasets/track_yolo/labels/{split}) for xml_path in xml_list: if not convert_one(xml_path, label_dst): continue img_file image_src / (xml_path.stem .jpg) if img_file.exists(): shutil.copy(img_file, image_dst / img_file.name)这个脚本的核心逻辑是三步读XML、校验坐标、写YOLO标签。校验部分是我特别加上的因为实际标注过程中标注员偶尔会把xmin和xmax写反或者画框后没有调整就保存这些脏数据直接进训练会让loss曲线在前几十个epoch一直抖动。脚本运行后建议再在labels/train目录里抽查三个txt文件人工比对一下框是否落在图片上。转换脚本本身不改变图片尺寸所以同一个归一化坐标在训练时配合任意imgsz都能正确映射这一点比直接存像素坐标要稳得多。3.2 一份可复用的dataset.yaml和训练命令imgsz与epochs怎么设转换完成后ultralytics的YOLO训练需要一个数据配置文件指向刚才生成的图片和标签目录。文件内容如下path: ./datasets/track_yolo train: images/train val: images/val names: 0: track 1: obstacle这段YAML的写法很直接path是项目根目录train和val是相对路径。要注意ultralytics会自动检查labels/{train}是否存在如果图片目录叫images而标签目录叫labels它默认能对齐如果换成自定义目录名还需要在YAML里显式加train_labels字段否则训练时会报“found no labels”。这类路径问题在我见过的新手报错里占比很高错误信息通常只有一句“No labels found in ...”初次遇到十有八九会往代码方向查实际上就是目录结构不匹配。然后是训练命令yolo detect train \ modelyolov8s.pt \ datatracks.yaml \ epochs120 \ imgsz960 \ batch16 \ device0 \ patience20 \ seed42这里逐一说明关键参数。modelyolov8s.pt加载COCO预训练权重不加载的话小数据集基本练不动。imgsz960是针对轨道细长目标特意拉高的输入分辨率COCO默认的640会把轨道宽度压到很小检测精度直接受损。patience20是早停耐心值验证指标连续20个epoch没有提升就停止对3900张的小数据集来说能省掉大量无效训练时间。seed42固定随机种子让同一条命令重复跑出的mAP可重复这个参数在对比实验时尤其重要否则增强顺序不同导致的结果波动很可能超过调参本身的收益。第一次跑不需要完整跑完120轮。先把epochs临时设成50看验证集mAP的上升趋势和loss曲线的下降形态。如果50轮时mAP已经在85附近震荡再续跑完整轮次如果50轮还在60以下徘徊先不要加轮次回去检查标签质量和类别比例。3.3 93.7%是哪种“准确率”验证集口径与mAP校验标题里的93.7%是一个吸引人的数字但在目标检测里“准确率”有至少三种口径mAP0.5、mAP0.5:0.95、某个置信度阈值下的精确率。同一个模型mAP0.5到0.93很容易mAP0.5:0.95可能只有0.62把置信度阈值调低检出率上去了但每帧的误报框也会暴增。所以我拿到任何声称“准确率93.7%”的检测数据集时第一反应不是高兴而是去看它统计口径。我建议的验证做法是训练完成后再单独跑一次验证集输出mAP0.5和mAP0.5:0.95两个指标同时生成每类的AP表。如果track的AP是0.96而obstacle的AP只有0.71那93.7%这个整体数字只能说明轨道类拉高了均值障碍物识别远没有达到可用水平。这时候要做的不是调阈值而是回去看障碍物样本量——在3900张图里如果含障碍物的只有六七百张模型天然偏向轨道。常见补救做法是在datasets/track_yolo里对障碍物图片做横向翻转、随机亮度和小角度旋转离线扩到2000张以上让两类样本量接近再重新训练对比AP曲线。4. 让准确率站得住的三个调参动作宽高比分析、Mosaic策略与置信度阈值4.1 先统计bbox宽高比再决定锚框或标签增强策略很多人拿到轨道检测数据集后第一件事就是把训练命令跑起来等结果不好才开始查原因。我习惯先花十分钟统计一下标签的宽高比分布因为这直接决定了后面一系列配置的走向。下面的脚本读取转换后的YOLO标签计算所有框的宽高比并统计极端比例目标占比。from pathlib import Path import numpy as np label_dir Path(datasets/track_yolo/labels/train) ratios [] for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: continue w float(parts[3]) h float(parts[4]) if w 1e-6 and h 1e-6: ratios.append(w / h) ratios np.array(ratios) extreme ((ratios 0.2) | (ratios 5.0)).mean() print(ftotal boxes: {len(ratios)}) print(fextreme aspect ratio (0.2 or 5): {extreme:.1%})如果极端比例占比超过5%说明轨道很可能以整段细长框的形式存在。这时候有两个方向可以走一是回头把轨道标注切成多段这是治本的做法二是换用支持长宽比感知更强的检测头。YOLOv8本身是anchor-free理论上能拟合任意比例但训练时的mosaic增强会把细长框进一步压缩导致回归头学习困难。我在这个数据集上实践下来的结论是把轨道标注切段后同样的模型和同轮数训练mAP0.5能少用30轮就达到同等水平。所以宽高比统计的用途不是让你去调anchor尺寸而是帮你说服标注团队返工切框。4.2 Mosaic增强与close_mosaic小障碍物和长轨道的两难mosaic把四张图拼接成一张训练图是小数据集提升泛化能力的经典手段但在轨道检测里它是一把双刃剑。四张图拼接后再resize到960分辨率每张子图的有效分辨率只剩480左右铁轨这种细长目标直接被压到几个像素宽模型等于在看一条虚线学习轨道特征。可靠的做法是保留mosaic1.0但同时把训练后段的close_mosaic打开。close_mosaic的作用是在最后10轮自动关闭mosaic让模型在接近真实分布的单图上微调。这样既吃到前中期mosaic带来的样本多样性又避免最终模型对拼接图产生依赖。在ultralytics中这一组配置通常写在训练YAML里mosaic: 1.0 close_mosaic: 10 mixup: 0.2 copy_paste: 0.3 scale: 0.5copy_paste对障碍物这种离散小目标很友好它能把一个障碍物实例复制粘贴到同一张图中的其它位置等于免费扩充了样本mixup不要设太高0.2左右即可轨道和背景高度相似两图叠加后轨道边缘容易糊成一团。训练时观察前30轮和最后10轮的loss下降幅度如果close_mosaic生效后验证mAP不升反降说明模型已经过度依赖拼接图的上下文应减小copy_paste比例并重新训练。4.3 置信度阈值与NMS93.7%是指标还是能用的检出率训练指标和部署指标是两套数字。验证时默认conf0.001把所有低置信度框都纳入mAP计算所以只要模型对轨道有一点模糊响应mAP就会被拉高93.7%就是这么“好看”的。真正部署到视频流时每帧几十上百个低置信度框会直接把告警系统淹没。我一般按两个阶段设阈值。第一阶段是调优期用conf0.1跑验证集目标是看分类能力上限第二阶段是上线期按误报容忍度把conf提到0.35到0.5之间同时设iou0.5做NMS合并。轨道检测对误报的容忍度很低因为轨道区域本身纹理单一任何光影变化都可能造成假阳性。如果把置信度阈值从0.1提到0.4mAP会掉几个点但实际视频帧的误报数可能从每10帧5次降到每100帧1次这个交换在工业场景里完全值得。做部署验证时还要记得给轨道类和障碍物类分别画PR曲线如果两类的最佳置信度阈值差异很大就得考虑对两类设置不同的检测头输出阈值。这个在ultralytics里没有直接参数支持常见做法是先统一0.4跑一晚日志统计两类各自误报占比再决定是否值得用一个后处理分类器做二次过滤。5. 轨道检测避坑指南5个最常见的翻车点与排查方式5.1 训练时loss为nan且集中在前10个epoch现象loss曲线正常下降几轮后突然变成nan然后一直nan到训练结束日志里没有明显报错。原因绝大部分情况是标签文件里有非法值。VOC转换脚本没有过滤xmax小于xmin的坏框或者某个txt里出现了负数坐标另一类是标签中混入了空白文件导致模型读到一行空的标签数据。解决在转换脚本里加坐标校验和空标签过滤逐文件打印异常文件名。训前再用一行命令扫描所有标签文件确认每行有4个坐标值且均在0到1之间。这个扫描动作只需要运行一次能避免浪费十几个小时的“白训练”。5.2 轨道AP极高、障碍物AP惨淡现象验证集输出的两类AP差距超过20个点整体mAP被轨道类抬到90以上但现场障碍物一个不漏检轨道却狂报。原因两类样本量悬殊。3900张图中轨道几乎每张都有障碍物可能只出现在几百张里模型自然偏向出现频率高的类别。解决先统计两类框数量。若障碍物框数明显少使用离线增强扩充障碍物图片或按障碍物类别做重复采样。增强时保留原始便签保证同一图片只做轻度平移、翻转和亮度变化强几何变换会把小障碍物扭曲得难以识别。重训后对比两类AP差值目标是把差值收敛到10个点以内。5.3 轨道分段框之间重叠严重NMS把有效框一并压掉现象单张图里轨道框被切成四五段推理时只剩两三个框漏检段数不少用更低iou阈值时框又重叠得无法看。原因标注时各段框之间留了过多重叠区域NMS把重叠度高的框当成重复检测合并掉了。解决返回标注阶段约定段与段之间的重叠像素上限一般不超过10像素即可。如果数据已标注完不方便返工给轨道类单独设iou0.3做NMS同时保持障碍物类iou0.5可以在ultralytics的后处理回调中按类别分支处理。5.4 验证集mAP很高换一条新线路现场漏检率高现象在自家验证集上mAP0.5跑出0.91部署到另一条铁路线后轨道基本能检出但障碍物漏检严重。原因验证集和训练集来自同一段线路的相似机位背景纹理、光照条件都被模型记住新线路的背景分布变化让泛化能力露馅。解决数据划分时不照搬现成的ImageSets强行按“线路来源”切分验证集。如果数据集只给了一张划分文件至少要在验证集里人工挑出与训练集机位不同的图片重新划分后训练再报指标。这个做法比任何数据增强都诚实它能反映真正上线时的表现。5.5 Windows下训练报错“No labels found”路径看着没问题现象dataset.yaml里path和train都写对了训练时仍提示找不到标签命令行检查文件也存在。原因路径中带有中文或空格ultralytics在多级路径拼接时对中文字符支持不稳另一类常见情况是labels目录和images目录的父级名称不一致比如图片在images/train、标签在labels/training。解决统一把数据集放到纯英文、无空格的绝对路径下例如D:/work/track_yolo。目录命名保持images和labels且两者子目录名完全一致。修改后删除运行目录下自动生成的labels.cache缓存文件避免旧缓存干扰重新扫描。6. 进阶滑窗推理与评估集复查把纸面指标变成能上线的检出率如果最终部署场景是1920×1080或更高分辨率的固定监控画面训练时用的imgsz960意味着整帧图会被等比压缩远端障碍物在压缩后只有几个像素模型再强也难找回信息。一个有效的做法是滑窗推理把原图切成多个960×960或640×640的patch每个patch独立预测再把结果合并回原图坐标。这样做相当于让模型在接近训练分辨率的分辨率下看完整幅图轨道细长目标和小障碍物都能保住。代价是推理耗时增加一次全图检测的耗时大约翻2到4倍但对固定机位每秒一帧的巡检场景来说仍然可接受。from ultralytics import YOLO import cv2 model YOLO(best.pt) def slide_detect(img_path, patch960, overlap0.3, conf0.35, iou0.5): img cv2.imread(img_path) h, w img.shape[:2] dets [] step int(patch * (1 - overlap)) for y in range(0, h - patch 1, step): for x in range(0, w - patch 1, step): crop img[y:y patch, x:x patch] results model.predict(crop, confconf, iouiou, verboseFalse)[0] for box in results.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() dets.append([ x x1, y y1, x x2, y y2, float(box.conf[0]), int(box.cls[0]) ]) return dets这段代码的要点在overlap0.3它让相邻patch之间有30%的重叠区域避免目标恰好横跨两个patch边界而被切开漏检。合并后的框没有做全局NMS实际使用时要对重叠框再按类别做一次合并不然跨patch的目标会重复框选。还有一个容易忽略的问题画面最右侧和最下侧不足patch大小的边缘带不会被切到。处理办法是先把原图做零填充pad到patch的整数倍推理完再裁掉填充区域。跑完整个验证集的滑窗推理后再看一次最差的500张图。把这500张里模型误报的框标出来统计它们集中在什么位置是路肩碎石被当成障碍物还是轨道反光被误认为轨道断裂。这个复查动作是模型上线前最重要的防线它能暴露出PR曲线看不到的系统偏差。我自己跑轨道检测这类细长目标数据集时习惯保留一个固定习惯每次换数据集或调标注后先随机打开10张训练图和10张验证图肉眼确认标签框与目标的贴合度再跑训练。这个习惯帮我少跑了不知多少次没有意义的白训练尤其是在多人协作标注的项目里抽查比任何脚本都管用。希望这些来自一线的流程和踩坑记录能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑