news 2026/10/18 6:02:07

基于YOLOv5的钢轨缺陷检测:VOC转YOLO与训练避坑全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv5的钢轨缺陷检测:VOC转YOLO与训练避坑全流程

简介:基于YOLOv5的钢轨缺陷检测项目,面向铁路设施维护人员及计算机视觉学习者,目标是通过深度学习实现对钢轨裂纹、磨损、剥离等表面缺陷的自动定位与分类,提升巡检效率与安全性。该资源压缩包共2000个文件,其中1994个txt标注文件构成数据集核心,配合Python脚本用于数据集划分与格式转换,另有yaml配置文件描述模型结构,整体仅28.77MB,轻量易用。目前已有168人学习下载。内容涵盖YOLOv5/YOLOv7两种模型的相关代码与标注数据,支持直接训练、验证及推理,可帮助读者完整复现钢轨缺陷检测流程,也可作为改进算法、引入注意力机制或多尺度检测的实验基础。对于希望快速上手目标检测在工业质检中应用的开发者,是一份具备实践价值的入门材料。

1. 基于YOLOv5钢轨缺陷检测:这份资源包先解决什么

拿到这份基于YOLOv5钢轨缺陷检测的压缩包,第一眼印象是“碎”:三个Python脚本、一串按类别命名的txt、外加一个备份好的yolov7-master目录。真正按流程跑一遍才发现,它处理的恰好是钢轨表面缺陷检测里最耗时的一环——把形态接近、占画面不到1%的缺陷从背景里分出来,再整理成YOLO能直接吃的标注格式。它适合两类人:一是手里囤了一批VOC标注的钢轨图片、想快速落进YOLO训练流程的开发者;二是要在YOLOv5和YOLOv7之间做精度对照的算法岗。下文从标签转换开始,把每一步参数和常见翻车点逐行拆开。

2. 把VOC标注转成YOLO训练集:split脚本与标签管线

2.1 压缩包里的脚本和txt到底在管什么

解压后第一件事不是找权重,而是先读README和这几个脚本的分工。split.py和split_train_val.py名字相近,但职责不同:前者偏通用划分,后者更侧重按给定比例把样本切成训练集和验证集。voc_labelhrsc.py是核心转换脚本,负责把VOC格式的XML标注转成YOLO训练的txt标签,hrsc这个后缀暗示它可能脱胎于遥感旋转框项目,但用在钢轨缺陷这种水平框场景完全没问题。

那一串txt文件才是数据集的“家底”。从命名习惯看,inclusion对应钢轨内部的夹杂/内裂类缺陷,patches对应表层剥离/剥落类缺陷;每个txt里保存的应该是一批图片路径或标签文件的清单。这类缺陷在巡检影像里非常容易和锈迹、水渍混在一起,人工标注的尺度差异也大,所以后文所有脚本都要围绕着“别让标签在转换中失真”来写。

还有一个容易被忽略的点:压缩包备份了一整份yolov7-master目录,说明原作者在YOLOv5之外还做了YOLOv7的对照尝试。这意味着数据集、标注脚本和训练参数在两套模型上是通用的,差别主要在模型结构、训练命令和导出环节。这一点会在第5章详细展开。

2.2 VOC XML转YOLO txt:像素坐标与归一化坐标的换算

LabelImg生成的XML里,边界框是xmin/ymin/xmax/ymax的像素值;YOLO训练时读取的是归一化到[0,1]的中心点坐标和宽高。换算逻辑本身不复杂,但容易在类别顺序和浮点精度上出错。下面是与压缩包中脚本逻辑等价的标准化版本,我习惯把它单独存成voc_label.py:

import xml.etree.ElementTree as ET import glob import os # 类别顺序必须与后续 rail_defect.yaml 里的 names 完全一致 CLASS_NAMES = ["inclusion", "patches"] OUT_LABEL_DIR = "labels" os.makedirs(OUT_LABEL_DIR, exist_ok=True) def convert_one(xml_path): root = ET.parse(xml_path).getroot() # 读取原图宽高,用来做归一化 img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name").strip() if name not in CLASS_NAMES: continue cls_id = CLASS_NAMES.index(name) box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) # 像素坐标 -> 中心点 + 宽高,再除以图宽高做归一化 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if not lines: return False out_path = os.path.join( OUT_LABEL_DIR, os.path.splitext(os.path.basename(xml_path))[0] + ".txt", ) with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") return True xml_list = glob.glob("VOC2007/Annotations/*.xml") ok = sum(1 for p in xml_list if convert_one(p)) print(f"converted {ok}/{len(xml_list)} xml files")

逻辑说明:先从XML的size节点拿到原图宽高,这是归一化的基准,不能用某个固定值代替。每个object里的name先查是否在类别表里,不在的直接跳过,避免一个错误标注拖垮整个训练集。坐标换算公式就是中心点减半相加、宽高做差,最后全部除以图宽高。输出行格式必须是类别ID 中心x 中心y 宽 高,顺序不能写反,否则训练时边界框会完全错位。

参数说明:输出保留6位小数,对1280×1280以内的训练分辨率足够。difficult标记的样本默认不特殊处理,如果你的钢轨缺陷图里大量存在模糊难例,建议在转换时直接跳过difficult=1的目标,否则它们会被当作普通正样本参与训练,拉低整体精度。

2.3 划分train/val:别让同一条钢轨的缺陷同时出现在两边

转换完标签后,下一步是把图片列表按比例切成train.txt和val.txt。这里有个容易被忽略的原则:钢轨缺陷图往往是一段钢轨连续拍摄的序列帧,相邻帧高度相似。如果随机划分,同一段钢轨的缺陷会同时出现在训练集和验证集里,验证集的mAP虚高,换到真实巡检视频就崩盘。我一般会先按文件名前缀或拍摄组做分组,再按组划分。

import os import random from collections import Counter IMAGE_DIR = r"D:\rail_defect\images" LABEL_DIR = r"D:\rail_defect\labels" VAL_RATIO = 0.15 random.seed(2024) exts = (".jpg", ".jpeg", ".png") imgs = [f for f in os.listdir(IMAGE_DIR) if f.lower().endswith(exts)] def label_path(img): return os.path.join(LABEL_DIR, os.path.splitext(img)[0] + ".txt") valid = [] class_dist = Counter() for img in imgs: lp = label_path(img) if not os.path.exists(lp) or os.path.getsize(lp) == 0: continue with open(lp, encoding="utf-8") as f: cls_ids = [int(line.split()[0]) for line in f if line.strip()] if cls_ids: valid.append(img) class_dist.update(cls_ids) random.shuffle(valid) n_val = max(1, int(len(valid) * VAL_RATIO)) val_set = set(valid[:n_val]) train_set = valid[n_val:] def write_list(split_name, items): with open(split_name, "w", encoding="utf-8") as f: for img in items: full = os.path.join(IMAGE_DIR, img).replace("\\", "/") f.write(full + "\n") write_list("train.txt", train_set) write_list("val.txt", val_set) print("train:", len(train_set), "val:", len(val_set)) print("class distribution:", dict(class_dist))

逻辑说明:脚本先把有标签且非空的图片筛出来,用Counter统计每个类别的样本数,便于确认两个类都没被丢掉。然后做一次shuffle,按15%的比例切出验证集。注意replace("\\", "/")这一步非常关键——如果数据在Windows上标注,脚本很容易写出反斜杠路径,而训练环境往往是Linux,反斜杠会导致图片数量直接读成0。

参数说明:VAL_RATIO = 0.15在数据量充足时合理,但钢轨缺陷缺陷类样本量可能只有几百张,我会把验证集会手动提到0.2到0.25,让val的类别分布更稳定。random.seed(2024)固定随机种子,保证每次复现划分结果一致。

2.4 路径与目录校验:图片和标签对不上时看哪里

train.txt和val.txt生成后,训练前先跑一遍静态检查。常见情况是“labels目录放错位置”或“图片存在但标签为空”。YOLO的标签查找规则是:如果训练列表里写着/data/images/xxx.jpg,它会自动去/data/labels/xxx.txt找标签,不会去别的目录碰运气。所以images和labels必须同级。

import os for split_name in ("train.txt", "val.txt"): if not os.path.exists(split_name): continue with open(split_name, encoding="utf-8") as f: lines = [l.strip() for l in f if l.strip()] missing_img = [l for l in lines if not os.path.exists(l)] missing_label = [] empty_label = [] for l in lines: base, _ = os.path.splitext(l) lbl = base.replace("/images/", "/labels/", 1) + ".txt" if not os.path.exists(lbl): missing_label.append(l) elif os.path.getsize(lbl) == 0: empty_label.append(l) print(split_name, f"total={len(lines)}") print(" missing_img:", len(missing_img)) print(" missing_label:", len(missing_label)) print(" empty_label:", len(empty_label))

逻辑说明:脚本对每个图片路径做三件事——图片是否存在、对应labels目录下的txt是否存在、txt是否为空。replace("/images/", "/labels/", 1)是把图片路径中第一处/images/替换为/labels/,前提是数据集目录结构严格按照images/和labels/布局。如果你的目录名不是这两个,直接把替换字段改成实际目录名。

参数说明:空标签文件会让YOLO在该图上正常计算loss但没有正样本,表现就是训练loss不降、mAP卡在低位。这一步排查脚本应放在每次新增图片之后,而不是训练失败之后。

3. 训练与调参全链路:把钢轨缺陷mAP跑到能用的临界点

3.1 模型选型:s、m、l怎么匹配缺陷尺度

YOLOv5的s、m、l三档模型参数量和推理速度差异明显:s约7.3M参数,m约21.2M,l约46.5M。钢轨缺陷检测这个场景有两个特点:类别只有两类、缺陷目标非常小。类别少意味着模型不需要很大的容量去区分几十类,小目标问题则更依赖输入分辨率和anchor设置,而不是模型宽度。

所以我通常建议先用YOLOv5s起步,把img从默认640提到1280,观察mAP变化。如果1280输入下s模型的mAP能到0.7以上,就没必要上m或l;只有在验证集稳定收敛后仍然欠拟合、且训练数据量超过两千张,才考虑用m。贸然上l模型在几百张样本的条件下很容易过拟合,表现为验证loss先降后升、P和R剧烈震荡。

3.2 数据配置:rail_defect.yaml里的nc、names和路径

训练数据配置是另一个高频翻车点。下面这份yaml对应前面第二章的转换脚本,类别顺序不能改:

# rail_defect.yaml path: /data/rail_defect # 数据集根目录,改成你自己的绝对路径 train: train.txt # 训练图片列表,注意这里是文件而不是目录 val: val.txt nc: 2 names: 0: inclusion 1: patches

逻辑说明:path字段决定了后续相对路径的解析基准。如果train.txt里写的是绝对路径,path字段就是摆设;如果写的是images/train/xxx.jpg这种相对路径,那path必须指向数据集根目录。两种写法都能跑,但不要混用,混用是训练时dataset为空最常见的原因。

参数说明:names的顺序直接对应转换脚本里的CLASS_NAMES顺序。如果两个地方顺序不一致,模型学到的类别和推理时的标注文字会对不上,最典型的表现是打印出来“inclusion的框显示成patches”,而实际框的位置是对的。

3.3 训练命令与超参数说明

配置完毕后的训练命令,我一般这么起:

python train.py \ --data rail_defect.yaml \ --img 1280 \ --batch 16 \ --epochs 150 \ --weights weights/yolov5s.pt \ --cache ram \ --device 0

参数说明:--img 1280是钢轨小目标场景里最值得先改的参数,分辨率翻倍意味着缺陷在特征图上的有效像素更多,但显存占用几乎翻四倍。8G显存的卡建议把batch降到8,16G卡可以尝试batch 16。--cache ram是把训练图片一次性加载进内存,几百张图的场景完全放得下,训练时GPU不会因为频繁读盘而饿着;如果图片总量上千,改成--cache disk更均衡。--weights weights/yolov5s.pt是预训练权重路径,如果本地已经有一份下载好的,直接指过去即可。

如果训练中断,可以继续接着跑:

python train.py \ --data rail_defect.yaml \ --img 1280 \ --weights runs/train/exp/weights/last.pt \ --resume

--resume会读取训练日志里保存的epoch、优化器状态和超参数,不需要重新指定epochs。注意断点续跑时不要改img和batch,改了之后优化器状态和lr schedule会错乱。

超参数方面,默认的hyp.scratch-low在小数据集上基本够用。如果发现模型老是漏检小缺陷,常见做法是把cls loss权重调大一点,让模型更倾向把候选框分对类别;如果背景误检多,就把mosaic开启概率降低。anchor方面,YOLOv5训练时默认会启用anchors自适应搜索,但如果缺陷目标尺寸和默认anchor差距太大,建议先用标注好的txt统计一轮bbox宽高分布,把统计结果直接写进模型的yaml文件里,比让模型自己搜索更可控。

3.4 训练日志解读:从P、R、mAP看模型是否够用

训练结束后,结果都在runs/train/exp/目录下。weights/best.pt是验证集上mAP最高的权重,weights/last.pt是最后一个epoch的权重,两者可能是同一个文件,也可能差很多。优先用best.pt做推理,如果last.pt的mAP更高,说明模型还在上升期,可以加大epochs继续训。

我更关注results.csv里的几个曲线字段:train/box_loss、val/box_loss、metrics/precision、metrics/recall、metrics/mAP_0.5和metrics/mAP_0.5:0.95。如果mAP_0.5高但mAP_0.5:0.95很低,说明框的位置大致对了但不够准;如果precision高、recall低,说明模型检出的都是高置信度但漏掉了很多小缺陷,这时推理阶段就该降conf_thres;反过来prec低、recl高,则是误检太多,需要提高置信度阈值或增加负样本。

pr_curve.png这张图值得放大看。曲线与坐标轴围成的面积直接反映模型在不同置信度下的综合表现。钢轨缺陷场景里,漏检的代价远高于误检,所以我对模型的最低要求是recall在0.85以上,mAP_0.5在0.75以上,才能放到现场试运行。

4. 钢轨缺陷检测避坑指南:小目标漏检和标签误差排查

4.1 小缺陷检测不到:loss在降,框就是出不来

现象:训练日志一切正常,验证集mAP也在涨,但推理时只检得到较大的patches缺陷,inclusion基本全部漏检。

原因:inclusion这类缺陷在1280×1280输入下仍只有十几个像素宽,归一化后的框宽高可能只有0.01到0.03。默认anchor的最小尺度覆盖不到这么小的目标,加上NMS对低置信度小目标不友好,直接被过滤掉。

解决:两个手段一起上。一是推理时把conf_thres从默认的0.25降到0.1,让模型把更多候选框吐出来,即使多检出一堆误检也值得,后续用规则过滤或人工复核;二是针对极小目标做滑动窗口切图,把一张1280宽的原图按512×512重叠切块,分别推理再合并结果,对小缺陷的召回提升非常明显。

4.2 训练集mAP很高,现场视频直接失效

现象:离线测试图片上mAP能到0.8,切到巡检车拍摄的视频后,几乎每帧都出现漏检和误检。

原因:离线训练图多半是静止机位或低速拍摄,光照稳定、没有运动模糊;现场视频是高速运动加振动,同一缺陷在相邻两帧里可能变形、拖影,模型没见过这类分布外数据。

解决:训练阶段不要只做常规mosaic增强,还要在数据配置里加入随机亮度、对比度、高斯模糊和轻微运动模糊。水平翻转可以用,但垂直翻转慎用,钢轨的方向性有物理意义,翻转后会引入不存在的样本分布。

4.3 Windows下生成的路径在Linux训练时读取0张图片

现象:训练日志里显示train.txt读取到0张图片,或者报错找不到/data/rail_defect\images\xxx.jpg这样的混合路径。

原因:脚本在Windows上用os.path.join生成了反斜杠路径,而Linux训练环境只认正斜杠。问题不出在数据本身,出在文本文件里的路径分隔符。

解决:在写train.txt和val.txt时统一做一次替换,把路径里的反斜杠换成正斜杠:

full = os.path.join(IMAGE_DIR, img).replace("\\", "/")

这行代码放在写入list之前,一行就解决跨平台路径问题。我经历过一次在Windows上划分好数据集、传到服务器后全部读不到的翻车,从此这个replace就成了标准操作。

4.4 inclusion和patches类别混淆:两类缺陷在PR曲线上大面积重叠

现象:PR曲线显示两类缺陷的召回率相差不大,但precision都不高,且验证集里经常把inclusion预测成patches。

原因:钢轨缺陷的人工标注非常依赖经验,同一类缺陷不同人画的框差异很大——有人贴着缺陷边缘画,有人习惯把周边锈迹也包进去。框的大小尺度不一致,会让模型把“框的尺度”当成类别特征之一,而不是学缺陷本身。

解决:写个小脚本统计所有标注框的宽高分布,把极端框可视化检查一遍:

import glob from collections import Counter for txt in glob.glob("labels/*.txt"): with open(txt, encoding="utf-8") as f: for line in f: parts = line.split() if len(parts) != 5: continue cls, w, h = int(parts[0]), float(parts[3]), float(parts[4]) if w > 0.5 or h > 0.5: print(txt, "unexpected large bbox", cls, w, h)

逻辑说明:循环读取每个标签文件,把宽高大于0.5的框挑出来。归一化后宽高大于0.5意味着该框占了图片一半以上,这种框要么是标注错,要么是图片本身裁剪得过窄,都会干扰模型。建议把这类样本单独抽出来人工复核,而不是直接删除——钢轨图片里确实有整条轨面剥落的大面积缺陷,直接删会导致patches类样本量不足。

5. 从YOLOv5到YOLOv7:用对照推理验证模型边界

5.1 为什么压缩包里会有一份yolov7-master

压缩包里备份YOLOv7目录,说明原作者在YOLOv5之外还做了进阶对照。YOLOv7的ELAN结构和重参数化模块让它推理速度更快,但在钢轨缺陷这种只有两个类、小目标占主导的数据集上,精度提升往往没有速度提升明显。我的习惯是:用完全相同的数据集、相同的img和batch分别用两套模型训50个epoch,先看v5的mAP能不能达到现场可用的及格线,再决定要不要上v7。如果v5已经到0.8,v7大概率只是锦上添花,不值得额外承担部署复杂度。

5.2 用detect.py做一轮快速对照推理

拿到best.pt之后,先用真实场景图做一轮推理,别急着部署:

python detect.py \ --weights runs/train/exp/weights/best.pt \ --source test_seq/test_001.jpg \ --img 1280 \ --conf-thres 0.1 \ --iou-thres 0.45 \ --save-txt \ --save-conf \ --project runs/infer

参数说明:--conf-thres 0.1是我在钢轨场景里坚持的低阈值策略,漏检的代价远高于误检,后续可以用缺陷宽度和长宽比规则把明显误检跑掉。--iou-thres 0.45控制NMS的去重力度,目标本身小且密集时,这个值太低会把相邻的缺陷框合并成一个,调高到0.5以上又可能出现一个缺陷多个框的现象。--save-txt和--save-conf配合使用,推理结果txt里每一行是类别ID x1 y1 x2 y2 置信度,方便后续统计召回率。

如果推理结果里小缺陷的框比预想还少,下一步就要反过来检查训练集的标签质量,而不是继续调模型。

5.3 推理前先做ROI裁剪,导出onnx前先看目标机算力

钢轨巡检视频里,钢轨通常只占画面中间一条区域,直接把整帧送进模型会让背景里的扣件、道砟、枕木变成大量负样本,误检率明显上升。我会在推理前先用固定ROI把轨道区域裁出来,再送进模型。这个裁剪区域一旦确定,就绑定在推理脚本里,不要轻易改。

部署端如果只能跑onnx,导出时固定输入尺寸能省不少麻烦:

python export.py --weights best.pt --img 1280 --simplify --opset 12

opset选12是为了兼容更多推理框架,--simplify会做一轮计算图精简。导出后务必用同一张测试图把pytorch模型和onnx模型的输出对比一遍,类别一致、边界框差异小于2个像素才算通过。

这两年我处理过不少钢轨缺陷数据集,印象最深的一次是某个缺陷类别只有五十来个样本,无论怎么加增强都无法收敛。后面复盘发现是转换脚本把两个相近类别的名字合并错了一个字符,模型把两类当一类学,mAP自然上不去。从那以后我每次换一个缺陷类型,都强制走一遍“VOC转txt、统计bbox宽高、跑30轮小规模训练看PR曲线”的流程,确认基线没问题再上全量数据。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 6:01:31

AI文档预处理:为什么Markdown化是提升大模型准确率的关键一步

1. 为什么“直接丢文件给AI”正在悄悄拖垮你的效率你有没有过这种经历:把一份20页的PDF产品说明书拖进对话框,等了半分钟,AI只回了一句“已收到”,再问具体问题,它却开始胡说八道?或者把Excel表格截图扔过去…

作者头像 李华
网站建设 2026/10/18 5:57:53

AI全链路问卷工具PaperXie:从无效问卷到可靠数据的完整指南

做问卷的人最怕什么?我猜不是发不出去,而是辛辛苦苦改了三遍题目,回收了几百份数据,一交叉分析,全是废话。问性别和满意度没关系,问收入和购买意愿不相关,维度之间互相打架,信度算出…

作者头像 李华
网站建设 2026/10/18 5:57:52

内存比模型还小的机器,怎么把 30B 大模型跑起来?

文章目录 📖 介绍 📖 🏡 演示环境 🏡 📒 模型装不下内存的一轮完整实测 📒 🔍 内存不够,到底卡在哪一步 🧰 安装:一个压缩包解压就能跑 🚀 小模型先跑通,确认链路没问题 📊 模型比可用内存还大时,会怎样 🧠 省下来的内存,代价是什么 ⚠️ 官方自己划…

作者头像 李华
网站建设 2026/10/18 5:56:28

选择排序与快速排序:从分治思想到工程优化的算法对比与实战

排序(选择和快排)大概是很多开发者入门算法时最先接触的一组对比。老实说,光看“排序”两个字会觉得这有什么好写的,无非就是比大小、换位置。但你真去细抠选择和快排的差异、适用场景、退化条件、工程实现细节之后,会…

作者头像 李华
网站建设 2026/10/18 5:55:48

深入浅出Android NNAPI:从统一抽象到端侧推理性能优化

写到现在,这个系列终于走到第16章:NNAPI,也就是Android神经网络API。做端侧AI的开发者对它不会陌生——它在系统层给你一个统一的推理加速入口,帮你把同一个模型分发给不同厂商的GPU、DSP、NPU去执行。手机芯片的指令集、内存模型…

作者头像 李华
网站建设 2026/10/18 5:55:31

递归入门经典:汉诺塔与青蛙跳台阶的拆解与面试表达

“汉诺塔”和“青蛙跳台阶”这两道题,大概是技术面试与笔试里出现频率最高的递归入门题。我当面试官的时候,经常在算法面试中用它们做“沟通能力”的试金石:会背答案的人很多,能讲清楚为什么这样递归、递归调用栈怎么变化、换一个…

作者头像 李华