简介:面向工业质检、智能制造与深度学习入门人群,这份YOLOv7钢材缺陷检测资源提供了可直接使用的训练权重、配套数据集与完整工程脚本。模型已训练完毕,附带PR曲线、loss曲线、训练日志等文件,能帮助读者直观评估检测效果并复现训练过程;数据集经过LabelImg人工标注,包含jpg原图及xml、txt两种标签格式,分别对应VOC与YOLO格式,方便切换训练框架。压缩包共166个文件,除pt权重、yaml配置与Python源码外,还有TensorRT、ONNX Runtime动态批次转换demo,以及多个ipynb对比分析笔记,整体约203.86MB,目录结构清晰,便于按模块学习与二次开发。目前已有1288人学习下载。对需要快速落地钢材表面缺陷识别、从零搭建数据标注流程或深入理解YOLOv7工程化部署的读者而言,这份资源能明显缩短实践周期,省去搜集数据与从零训练的时间。
1. YOLOv7 钢材缺陷检测:权重、双格式数据集、训练曲线一次给齐
做钢材表面缺陷检测的人大多有过这种处境:产线图像拉回来一大堆,能用的公开数据集却要么没标签、要么只给一个权重文件,训练跑起来心里完全没底。这份 YOLOv7 钢材缺陷检测资源属于少见的“全家桶”形态——检测权重已经训练好,缺陷类型分好了类,附带 PR 曲线、loss 曲线等训练产物,图片是 jpg,标注由 labelImg 完成,且同时保留了 xml 和 txt 两种格式。换句话说,模型能直接拿去推理,数据也能拿来重新训练,还能顺手研究 VOC 与 YOLO 标注格式的转换。适合正在做工业质检、想在 YOLOv7 上快速起跑的人,也适合想搞懂标注坐标系转换的学习者。
2. 先看懂这批数据:双格式标注与 labelImg 的产出约定
拿到任何检测资源,第一步永远是扒目录结构,标注格式决定你后面每一步要不要写转换脚本。这份资源的组织方式很典型:图片单独放,xml 标注一套,txt 标注一套。很多人上来直接把 txt 丢给训练脚本,结果类别对不上、坐标飘了,折腾半天才发现是标注理解错了。先花十分钟把数据看明白,比什么都值。
2.1 文件组织:jpg、xml、txt 三个角色的分工
资源里图片格式为 jpg,xml 与 txt 分别保存在两个文件夹中,用途完全不同。
| 内容 | 格式 | 作用 |
|---|---|---|
| 图片 | jpg | 原始样本,训练和推理的输入 |
| xml 标注 | XML(VOC 风格) | labelImg 原生保存产物,记录绝对像素坐标,适合人工校对、二次裁剪 |
| txt 标注 | 纯文本(YOLO 风格) | 归一化坐标,YOLOv7 训练时直接读取 |
xml 是 labelImg 保存的原生格式,字段可读性很强,打开一个看看:
<annotation> <folder>JPEGImages</folder> <filename>steel_001.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>scratches</name> <bndbox> <xmin>182</xmin> <ymin>224</ymin> <xmax>451</xmax> <ymax>387</ymax> </bndbox> </object> </annotation>这里每个<object>对应一个缺陷框,<name>是缺陷类别名,bndbox里的四个值是框的左上角和右下角绝对像素坐标。<size>里的宽高必须和实际 jpg 图片一致,后面做格式转换时全靠它。文件里<filename>写的是steel_001.jpg,实际文件名要能对得上,这是标注数据最常见的翻车点之一。
对应的 txt 标注只有一行:
0 0.4945 0.4773 0.4203 0.2547五个数字依次是:类别 id、归一化中心点 x、归一化中心点 y、归一化宽度、归一化高度。类别 id 从 0 开始,后面四个值都是 0 到 1 之间的小数,用图片真实宽高去除出来的。比如上面 xml 里那个框,x 中心点就是(182 + 451) / 2 / 640 = 0.4945,宽度是(451 - 182) / 640 = 0.4203,跟 txt 完全对得上。
2.2 labelImg 做了什么:标注产物背后的两套坐标系
labelImg 默认保存 xml,它内部画框时记录的是鼠标点击的绝对像素位置,也就是xmin/ymin/xmax/ymax这套坐标系。YOLO 系列训练需要的是归一化坐标,因为不同图片尺寸不一样,归一化之后模型不用关心输入是 640 还是 1280。这份资源两个文件夹分开放,说明作者特意保留了源标注,又生成了训练用的 txt,属于比较讲究的做法。
拿到数据后我建议先做一次可视化检查,别直接开训。把 xml 坐标画回原图,肉眼确认框的位置、类别名、有没有漏标错标,写个小脚本就行:
import cv2 import xml.etree.ElementTree as ET img = cv2.imread("steel_001.jpg") tree = ET.parse("steel_001.xml") for obj in tree.findall("object"): name = obj.find("name").text box = obj.find("bndbox") x1 = int(box.find("xmin").text) y1 = int(box.find("ymin").text) x2 = int(box.find("xmax").text) y2 = int(box.find("ymax").text) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, name, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite("check.jpg", img)这段做的事情很简单:解析 xml 里的每个框,用绿色画出来,再写上类别名,最后输出一张check.jpg。画出来的框如果和缺陷位置贴合,说明标注没问题;如果明显偏移,多半是图片被 resize 过但标注没跟着改,或者 xml 里<size>和实际图片尺寸不一致。这个检查脚本我会建议每个人都留在手里,后面每次拿到新数据都用它过一遍,能省掉大量训练到一半才发现标签错位的时间。
labelImg 操作上没什么玄机,w 键画框、d 键下一张、a 键上一张、Ctrl+S 保存。它支持在 VOC 和 YOLO 两种模式间切换,VOC 模式存 xml,YOLO 模式存 txt。但要注意,同一张图在两种模式下标注内容是等价的,坐标表达方式不同而已,不是标注了两遍。
2.3 类别 id 的约定:数字背后对应哪个缺陷名
txt 里的数字 id 不是缺陷名称,而是类别表里的下标。常见做法是维护一个classes.txt,每行一个类名,顺序就是 id 编号的依据。比如classes.txt第一行是scratches,那么 txt 里的0就代表划痕。这个顺序一旦定下来,训练配置里的names列表必须跟它完全一致。
提示:txt 里数字 id 对应的类别顺序,就是训练时
data/*.yaml里names的排列顺序。这两处不一致,轻则训练 loss 不收敛,重则模型输出的类别全错,框位置倒是准的,但张冠李戴。
判断顺序有没有搞错,最快的办法是写两行代码统计所有 txt 里出现的类别 id:
import glob ids = set() for f in glob.glob("labels/*.txt"): with open(f) as fp: for line in fp: ids.add(line.split()[0]) print(sorted(ids))输出结果应该在 0 到(类别数减一)之间。如果出现空缺或者跳号,说明标注时类别没用全,或者某个类别的样本数少得可怜,这会直接影响后面的训练策略。
3. VOC 转 YOLO:转换脚本与坐标归一化里的四个边界坑
labelImg 明明能直接存 YOLO 格式,为什么这份资源偏偏保留了两个文件夹?答案很简单:xml 是源数据,txt 是产物。xml 里记录了绝对坐标、图片尺寸、类别名这些完整信息,随时能重新生成任何格式;txt 一旦生成,反向找回绝对坐标反而麻烦。所以常规做法是 xml 作为唯一真源,txt 用脚本生成,这份资源的组织方式就是这个思路的体现。
3.1 为什么保留 xml 又生成 txt:源数据心态
把 labelImg 直接切到 YOLO 模式,确实能省掉转换这一步。但代价是丢失了绝对坐标和图片尺寸信息,后面做数据清洗、按类别抽样、裁图增强,全都得回头重新标注。我一般会坚持“xml 是源,txt 是生成物”的策略:xml 文件夹永远不动,txt 随时删掉重新生成。这也是这份资源把两种格式分开存放的合理性所在——xml 给你兜底,txt 给你训练,两者各司其职。
3.2 转换脚本:从 xml 到 txt 的完整实现
转换逻辑不复杂,无非是把xmin/ymin/xmax/ymax换算成归一化的中心点坐标和宽高。直接上脚本:
import os import xml.etree.ElementTree as ET def voc2yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue # 类别不在列表里直接跳过 cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 越界修正,防止归一化后出现负数或大于1 x1 = max(0, min(x1, img_w)) x2 = max(0, min(x2, img_w)) y1 = max(0, min(y1, img_h)) y2 = max(0, min(y2, img_h)) dw = 1.0 / img_w dh = 1.0 / img_h cx = (x1 + x2) / 2.0 cy = (y1 + y2) / 2.0 w = x2 - x1 h = y2 - y1 lines.append(f"{cls_id} {cx*dw:.6f} {cy*dh:.6f} {w*dw:.6f} {h*dh:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines))调用方式:
class_names = ["scratches", "inclusion", "patches"] # 以资源里的类别清单为准 xml_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for f in os.listdir(xml_dir): if f.endswith(".xml"): voc2yolo(os.path.join(xml_dir, f), out_dir, class_names)逻辑说明分三块:第一,从<size>取图片真实宽高,这是归一化的分母,不能用统一的 640 去套,否则不同尺寸图片的标签全错;第二,class_names列表的顺序决定了 txt 里写的数字 id,前面统计出来的 id 集合如果和这个列表对不上,说明类别清单错了;第三,越界修正放在归一化之前,先把框裁剪进图片范围内,再做除法。
3.3 归一化里的四个边界细节
这几个坑我用血泪经验换来的,列出来你能少走弯路。
第一,越界框必须裁剪。有些标注框的xmax会超出图片宽度几个像素,直接归一化会得到大于 1 的数,YOLO 训练时解析标签会告警,甚至把 loss 拉高。上面脚本里的min(x2, img_w)就是干这个的,转换前先钳制再归一化。
第二,空 xml 生成空 txt 是合法的。一张没有缺陷的背景图,xml 里没有<object>,生成的 txt 是空文件。空标签表示这张图只有背景,是正常样本,不要当成错误删掉。脚本里"\n".join(lines)在 lines 为空时写入的就是空字符串,结果正确。
第三,类别 id 必须和训练配置里的names对齐。你在data/steel.yaml里写names: ['scratches', 'inclusion', ...],txt 里的0就对应scratches。很多人从别处复制一份 yaml 过来改,漏了names顺序,结果模型训练完输出类别整体错位。
第四,归一化精度保留六位小数已经足够。图片宽高再大,六位小数的精度也到亚像素级别了。写太长占空间,写太短框的位置会漂移,统一:.6f最稳妥。
4. 训练自己的数据集:数据配置、超参数与曲线判读
数据摸透了,接下来就是训练。这份资源自带的权重可以直接用,但如果想针对自己的产线数据微调,或者想验证这批数据的训练效果,整个流程得走一遍。YOLOv7 原理里和调参关系最密切的,是训练时的辅助头和动态标签分配——辅助头只在训练时计算,推理时去掉,所以同一套配置训练显存明显大于推理显存,这是正常的,别以为是 bug。
4.1 数据配置:构造 dataset.yaml 与类别对齐
YOLOv7 的训练入口是train.py,数据侧只需要一个 yaml 文件。把路径和类别写清楚:
train: ./data/steel/train.txt val: ./data/steel/val.txt nc: 6 # 按数据集实际类别数修改 names: ['scratches', 'inclusion', 'patches', 'pitted_surface', 'rolled-in_scale', 'crazing']train和val可以指向图片目录,也可以指向 txt 文件列表,每行一张图片的绝对路径,YOLOv7 两种写法都支持。nc是类别总数,names的顺序必须和前面说的classes.txt、txt 标签里的数字 id 完全一致,这里错一位,训练结果就全乱了。不确认类别顺序时,用第 2 章那个统计脚本把 id 打出来,和names逐个对应。
4.2 训练命令与超参数:权重控制的关键位置
训练命令长这样:
python train.py \ --data data/steel.yaml \ --weights yolov7.pt \ --batch-size 8 \ --img 640 \ --epochs 100 \ --workers 4 \ --device 0--weights可以填官方 COCO 预训练权重,也可以直接填这份资源里给好的钢材缺陷权重做微调。如果数据量不大,用预训练权重迁移收敛会快很多,这也是这份资源的价值点——你不需要从零开始。--batch-size看显存,8G 显存跑 640 输入建议从 4 起步,显存不够时别硬顶,把--img降到 512 更实在。
YOLOv7 的超参数写在独立的 hyp 文件里,比如hyp.scratch.p5.yaml,几个关键项直接决定训练质量:
| 超参数 | 常见取值 | 作用与调节方向 |
|---|---|---|
| lr0 | 0.01 | 初始学习率,小数据集微调降到 0.001 更稳 |
| weight_decay | 0.0005 | 权重衰减,防止模型过度拟合训练集 |
| mosaic | 1.0 | 马赛克增强,小数据集建议保持开启 |
| label_smoothing | 0.0 | 标签平滑,缺陷类别相似度高时开到 0.1 有改善 |
| warmup_epochs | 3.0 | 预热轮数,训练初期学习率先从小值爬升 |
weight_decay就是常说的权重控制里最直接的一环,它会让大权重受到惩罚,防止过拟合。但工业缺陷数据往往样本量小、背景复杂,weight_decay调太大模型会欠拟合,调太小 loss 下降慢,一般先保持默认,看验证集表现再动。
如果你之前在 YOLOv8 里训练过自己的数据集,会发现 YOLOv7 的数据入口逻辑类似,但超参文件是独立的一套,别把 YOLOv8 的学习率策略直接套过来。YOLOv7 对lr0更敏感,从头训练用 0.01 没问题,微调时我习惯直接降到 0.001,loss 曲线会平稳很多。
4.3 看曲线判断训练是否翻车
训练产物里最关键的两个文件是 loss 曲线和 PR 曲线。loss 曲线看趋势:训练 loss 逐步下降是正常状态,如果验证集 loss 在某个 epoch 后不再下降甚至回升,而训练 loss 还在降,那就是过拟合信号,该提前停了。看 loss 曲线有点像玄学,但有一条铁律:训练 loss 和验证 loss 之间的 gap 持续扩大,必然有问题。
PR 曲线看上限:曲线越靠近右上角,说明模型在召回率和精确率之间平衡得越好。mAP@0.5和mAP@0.5:0.95两个指标差距过大,说明框定位精度不够,缺陷边缘抓得不准,这时候优先检查标注框是不是画得太松,而不是急着换模型。
训练结束后runs/train/下会生成多个文件,best.pt按验证集 mAP 保存,last.pt是最后一轮的权重。两者差异大说明训练末段在震荡,可以适当降低学习率再跑几轮。资源里还带了 TensorBoard 日志文件,本地想看实时曲线就执行:
tensorboard --logdir runs/train浏览器打开显示的曲线和results.png内容一致,但能缩放、能对比多个实验,排查问题方便得多。
5. YOLOv7 部署避坑指南:从权重到 TensorRT/ONNX 的临门一脚
训练完只是第一步,把权重搬到推理环境才是真正的考验。这章集中讲部署时的高频坑,每条都来自实际跑过的项目,照着检查能省一整天。
5.1 导出前的准备:pt 权重里的隐藏信息
.pt文件不只是模型参数,它同时打包了 stride、anchor、类别名等元数据,导出时这些信息会跟着一起进 ONNX。导出命令:
python export.py \ --weights runs/train/steel/weights/best.pt \ --img 640 \ --batch 1 \ --simplify \ --include onnx--simplify会调用 ONNX Simplifier 对计算图做精简,去掉冗余节点,推理速度通常有提升。--img必须和训练时一致,YOLOv7 的检测头输出尺寸依赖输入分辨率,这里写成 640,推理时预处理就必须往 640 上对齐。--batch 1导出的是固定 batch 模型,如果后续要动态 batch,需要用项目里自带的 Dynamic-Batch TensorRT/ONNX Runtime 那套 notebook 脚本,里面已经把动态维度的配置写好了。
5.2 避坑记录:五条实测高频问题
现象一:TensorRT 转 engine 后,输入可以直接跑 batch=4,但输出结果完全错乱。
原因:动态 batch 配置时没有给 optimization profile 设置 min/opt/max 三个档位,TensorRT 不知道该按哪个形状做 kernel 选型。解决:给输入维度显式指定范围,比如min=(1,3,640,640)、opt=(4,3,640,640)、max=(8,3,640,640),重新构建 engine。
现象二:ONNX 推理结果类别是对的,但所有框集体偏移,跑到缺陷区域外面。
原因:导出时固定了 640,但推理端预处理用了不同尺寸的 resize,或者 letterbox 的 padding 颜色和训练时不一致。YOLOv7 的坐标回归对输入尺寸极其敏感,训练和推理的预处理管线必须完全一致。解决:推理端严格复用训练时的 letterbox 逻辑,包括 Padding 填充值。
现象三:FP16 推理时漏检明显增多,尤其小尺寸的麻点类缺陷。
原因:FP16 动态范围比 FP32 小,小目标特征响应弱,半精度量化后这部分响应被压掉了。解决:工业缺陷场景优先用 FP32,追求速度再用 FP16,且必须用验证集数据做校准,不能直接拿默认配置硬上。
现象四:用 txt 标签可视化,画出来的框全飘。
原因:把归一化坐标当成了像素坐标直接用,没有乘以图片宽高。解决:可视化 txt 标签前,先把中心点坐标和宽高乘以图片实际尺寸,换算回绝对像素。
现象五:换一台机器部署,直接报缺少.so文件。
原因:TensorRT 和 ONNX Runtime 的版本跟 CUDA/cuDNN 强绑定,环境版本一换,底层库对不上。解决:尽量用 Docker 固定环境,资源里带了 Dockerfile,直接基于它构建镜像,把 CUDA、TensorRT 版本锁死,换机器不用重新折腾。
5.3 用 ONNX Runtime 快速验证导出结果
导出 ONNX 后别急着上 TensorRT,先用 ONNX Runtime 跑一遍,确认输出和 PyTorch 推理一致。这也是“yolov7 部署”最稳妥的中间验证步骤:
import onnxruntime as ort import cv2 import numpy as np sess = ort.InferenceSession("best.onnx") input_name = sess.get_inputs()[0].name img = cv2.imread("test.jpg") img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR -> RGB, HWC -> CHW img = img[None].astype(np.float32) / 255.0 out = sess.run(None, {input_name: img}) pred = out[0] # 形状为 (1, 25200, 5 + nc) print(pred.shape)25200是 640 输入下三个尺度的候选框总数:(80*80 + 40*40 + 20*20) * 3。最后一维是5 + nc,其中前 4 个是坐标,第 5 个是目标置信度,后面是每个类别的概率。输出形状对得上,说明导出链路没问题。项目里自带的YOLOv7-Dynamic-Batch-ONNXRUNTIME.ipynb就是干这个的,动态 batch 版本需要额外设置输入输出的维度名,照着 notebook 跑一遍即可。
6. 新数据集落地的一步:可视化回显与三指标判读
模型部署完,最后一步是拿训练好的权重对真实样本做可视化回显,确认框位置、类别名、置信度都对,再到现场。我习惯在每批新数据集上强制做一次“单图回显”自检,脚本很短:
import cv2 import torch model = torch.hub.load("yolov7", "custom", "weights/best.pt") img = cv2.imread("test.jpg") results = model(img) results.show()回显时看三个指标:框是否贴住缺陷边缘、类别名是否对应正确、置信度阈值放到 0.25 后有没有漏检。检测结果可以直接和资源配套博客里的实测图对比,确认自己的推理链路没跑偏。如果框和缺陷贴合、类别正确,再谈批量验证和观察 PR 曲线。不要只看一张图的漂亮结果就收工,工业现场的光照、角度、钢材表面状态和数据集里差异可能很大,至少准备十张没参与训练的现场图过一遍。
从那以后,我每次拿到新的标注数据,都会强制先跑一遍可视化检查脚本,确认 xml、txt 和图片三者对得上才开始训练;每次部署新权重,也固定先做单图回显再谈批量。这套习惯帮我挡掉了至少一半的无效调试。希望帮到你。
本文还有配套的精品资源,点击获取