ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

带钢表面缺陷YOLO训练实战:VOC转YOLO格式与数据集处理全流程

2026/9/28 6:40:14 拓冰建站 浏览量
带钢表面缺陷YOLO训练实战:VOC转YOLO格式与数据集处理全流程 简介面向YOLO目标检测及带钢表面缺陷识别任务这份已标注数据集可直接用于模型训练与验证。内容覆盖常见带钢表面缺陷类型图像与XML标注文件一一对应标注框质量较高适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计也可作为目标检测算法入门与调参的练习数据。压缩包共2000个文件以JPG/BMP图像、XML标注文件及配套数据库文件为主整体约78.62MB解压后目录清晰便于直接导入YOLO系列框架也可作为缺陷检测算法对比的基准数据。已有1028人学习下载对于缺少数据采集与标注条件的初学者这份资源可显著降低重复劳动帮助快速掌握数据组织、模型训练和缺陷检测效果评估的完整流程同时为复现论文或开展对比实验提供可靠基础。1. 带钢表面缺陷数据集到手1800张已标注图像怎么喂给YOLO最省事做工业视觉的人应该都经历过这个场景拿到一批带钢表面缺陷的图像说是“已标注可以直接使用”解压一看全是xml文件心里先松一口气再一看训练脚本里要求的是txt格式又得折腾一遍。带钢表面缺陷检测是目标检测在制造业里最典型的落地场景之一——热轧产线上每秒几十米的速度跑过去靠人眼盯屏幕早晚漏检。这个1800张图像加xml标注的数据包走的就是VOC格式的标注路线能不能直接训练YOLO答案是能但中间要补几个关键步骤格式转换、类别核对、训练集划分。这篇文章就把从解压到训练再到部署验证的完整路径讲清楚适合正在做工业质检项目、毕设选型或者想用真实数据跑通yolo训练pipeline的读者。2. 拆开数据包看家底xml标注里藏着哪些带钢缺陷信息和坑2.1 带钢表面缺陷为什么是目标检测的“硬骨头”场景带钢表面缺陷和通用目标检测里的猫狗车不一样它有几个特别别扭的特点。第一是尺度差异极大氧化铁皮压入可能是一大片几十万像素的区域麻点却可能只有十几个像素同一张图里大目标和小目标混在一起对多尺度特征融合的要求很高。第二是类间相似度高划伤和辊印在灰度图上看都是细长条结疤和麻点都表现为局部纹理异常人眼都要仔细分辨模型很容易把类别搞混。第三是表面反光和光照不均同一类缺陷在不同卷次、不同产线拍出来灰度分布完全不同。所以拿到这个数据集之后第一件事不是马上写训练脚本而是先搞清楚包里的缺陷类型有几类、每类多少样本、标注框的质量怎么样。这些信息全都在xml里但没人帮你整理得自己动手统计。我见过不止一个项目拿到数据集直接开训训到mAP怎么都上不去回头一查才发现某个类只有几十个样本另一个类占了快一半模型全去拟合大头了。2.2 xml标注文件的结构和关键字段VOC格式的xml每个文件对应一张图像文件名通常和图像名一致。一个典型的标注文件里会有folder、filename、source、size、segmented这几个全局字段然后每个目标是一个object节点里面包含name类别名、pose、truncated、difficult、bndboxxmin、ymin、xmax、ymax四个绝对像素坐标。对于带钢表面的场景pose和truncated基本没什么用difficult字段倒是值得留意——它标注的是一些“很难辨认”的目标转格式的时候如果无脑全转容易把低质量样本带进训练集。size节点里记录的是图像的真实宽高这是转换时最关键的信息。坑也往往出在这有的标注工具会把size写成标注时窗口的尺寸或者xml是从不同批次数据合并来的size和实际图像对不上。转换前用脚本把xml里的filename和size批量打印出来再和实际的图像文件做一次交叉核对这一步能省下后面调bug的大量时间。写一个Python脚本把数据包的家底摸清楚。脚本放在数据集根目录下运行逻辑是遍历所有xml文件、解析object和bndbox再按类别累积数量并顺便统计每个标注框的像素面积分布这样能一眼看出小目标占比import xml.etree.ElementTree as ET from pathlib import Path import collections xml_dir Path(./annotations) # 标注文件所在目录 class_counter collections.Counter() area_list [] missing_files [] for xml_path in sorted(xml_dir.glob(*.xml)): tree ET.parse(xml_path) root tree.getroot() # 核对标注里引用的图像文件是否存在 img_name root.findtext(filename) img_file Path(./images) / img_name if not img_file.exists(): missing_files.append(img_name) continue size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) for obj in root.iter(object): cls_name obj.findtext(name) class_counter[cls_name] 1 box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 计算框面积后面用来判断小目标占比 area (xmax - xmin) * (ymax - ymin) area_list.append((cls_name, area, img_w, img_h)) print(类别分布:, class_counter) print(缺失图像文件数:, len(missing_files), missing_files[:5])这段代码的核心是先把“标注里的文件名”和“磁盘上的图像文件”对齐再去遍历object。类别统计用Counter面积直接按像素算不归一化——因为后面训练时YOLO自己会做归一化这里保留原始像素值方便你判断小目标数量。运行结果里如果missing_files不为空说明数据包本身就不完整后面训练时会出现“读取不到图像”的报错如果某个类别的面积列表里大量出现几十像素的小框那么训练时要特别留意小目标的表现。2.3 数据集的边界1800张图像够不够用1800张图对目标检测来说是一个偏小的规模但不算完全没法用。公开的NEU-DET带钢表面缺陷数据集也就是六类共1800张左右很多论文就是用这个量级做的实验。问题的关键不在于总数而在于两个比例各类别之间的样本均衡度以及单张图像的目标密度。如果统计出来某类只有几十个框那么训练时模型会偏向样本多的类别mAP的类别平均会被拉低。补数据的方式一般是用增强而不是硬凑Mosaic、随机缩放、HSV扰动都能把有效样本量放大几倍。单张图像的目标密度也很重要带钢表面缺陷常常一张图里密集出现几十个框这在yolov8里训练时不会报错但会显著影响NMS的收敛表现。先做完这一步统计心里有数了再继续往下走。3. 把VOC格式xml转成YOLO格式转换脚本与目录规划3.1 为什么要转txt标注和xml标注的差别在哪YOLO系列训练时读取的标注不是xml而是和每张图像同名的txt文件txt里每一行代表一个目标格式是class_id x_center y_center width height其中x_center、y_center、width、height全部是相对于图像宽高的归一化值取值范围在0到1之间。VOC的xml存的是绝对像素坐标的左上角和右下角训练时YOLO不会直接认这种格式所以转换这一步绕不开。另一个差异在类别信息的表达方式。xml里用字符串记录类别名比如“oxide”或“scratch”而YOLO的txt里只有整数索引这个索引必须和训练配置里的names列表一一对应。如果xml里的类别名和你的类别索引表对不上轻则类别全乱重则训练直接报错。转换脚本里要先把类别名到一个固定整数索引的映射建立好而且这个映射一旦生成就不要再改否则前面训出来的模型和后面部署时的类别顺序对不上推理结果没法用。这里顺带说一下网上流传的很多现成转换脚本只做了坐标变换没有检查difficult字段和越界框用在这种工业缺陷数据上很容易埋雷。我的做法是自己写一个稍完整的脚本把坐标越界、零面积框、difficult样本的处理全部包进去。3.2 VOC转YOLO的Python脚本坐标归一化与边界过滤转换脚本的输入是xml文件夹和图像文件夹输出是YOLO训练所需的labels目录。核心步骤是解析每个xml的size得到真实宽高遍历object得到每个目标的类别和bndbox绝对坐标做归一化计算再过滤掉坐标非法或面积为零的标注最后写入同名txt文件。归一化的公式不复杂中心点x等于(xminxmax)除以2再除以图像宽度宽等于(xmax-xmin)除以图像宽度y方向同理。import xml.etree.ElementTree as ET from pathlib import Path # 自定义类别映射顺序将用于训练配置中的names列表 CLASS_MAPPING { oxide: 0, # 氧化铁皮压入 scratch: 1, # 划伤 patch: 2, # 结疤 pitted: 3, # 麻点 crack: 4, # 裂纹 roll: 5, # 辊印 } xml_dir Path(./annotations) image_dir Path(./images) label_dir Path(./labels) label_dir.mkdir(exist_okTrue) skip_stat {difficult: 0, zero_area: 0, out_of_bounds: 0, unknown_cls: 0} for xml_path in sorted(xml_dir.glob(*.xml)): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) img_name root.findtext(filename) label_name Path(img_name).stem .txt lines [] for obj in root.iter(object): # 跳过difficult为1的样本避免低质量标注进入训练 if obj.findtext(difficult) 1: skip_stat[difficult] 1 continue cls_name obj.findtext(name) if cls_name not in CLASS_MAPPING: skip_stat[unknown_cls] 1 continue cls_id CLASS_MAPPING[cls_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)) # 过滤零面积框防止训练时loss崩溃 if xmax xmin or ymax ymin: skip_stat[zero_area] 1 continue # 过滤越界框常见于标注时手抖拖出了图像边界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: skip_stat[out_of_bounds] 1 continue # 统一转换为YOLO格式的归一化中心点坐标和宽高 center_x ((xmin xmax) / 2.0) / img_w center_y ((ymin ymax) / 2.0) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}) if lines: # 只有包含有效目标的图像才生成标签文件 with open(label_dir / label_name, w, encodingutf-8) as f: f.write(\n.join(lines) \n) print(跳过统计:, skip_stat) print(有效标签文件数:, len(list(label_dir.glob(*.txt))))转换脚本的运行结果会告诉你这批数据的真实质量。skip_stat里的difficult数量如果很大说明原始标注里本来就有一批难例你可以选择保留它们在后期做增量训练zero_area和out_of_bounds数量不为零说明标注工具或标注员存在手误这部分框直接丢弃是对的硬保留只会让模型学坏。坐标统一用六位小数输出精度足够文件体积也小。注意标签文件和图像文件同名不同后缀YOLO训练时会自动做这个匹配。3.3 训练集和验证集的划分按图划分别按目标划分划分数据是另一个极其容易翻车的环节。很多人图省事直接把所有txt文件按行随机拆分结果是同一张图像的不同目标框被拆到了训练集和验证集里验证时模型相当于开卷考试mAP虚高得离谱换到真实现场马上原形毕露。正确的做法是按图像文件名划分整张图的所有目标要么全在训练集要么全在验证集。常见的比例是8比2也可以用官方一点的k折验证来估算数据集的整体难度。对1800张图的规模8比2或7比3都行。划分时最好设置一个固定随机种子方便复现结果否则每次跑出来的划分不一样你很难判断模型指标的涨跌到底是算法改进了还是数据划分变了。下面这段脚本按图像粒度做划分打成YOLO目录格式#!/bin/bash # 按图像划分训练集和验证集8:2固定随机种子保证可复现 mkdir -p data/images/train data/images/val data/labels/train data/labels/val find images -name *.jpg -o -name *.png -o -name *.bmp | sort all_images.txt # 按种子随机打乱后取前80%做训练 shuf --random-source(seq 2024) all_images.txt shuffled_images.txt total$(wc -l shuffled_images.txt) train_count$((total * 8 / 10)) head -n $train_count shuffled_images.txt train_images.txt tail -n $((train_count 1)) shuffled_images.txt val_images.txt # 图像和标签一一对应地复制到目标目录 while read -r img_path; do base$(basename $img_path) stem${base%.*} cp $img_path data/images/train/ cp labels/$stem.txt data/labels/train/ done train_images.txt while read -r img_path; do base$(basename $img_path) stem${base%.*} cp $img_path data/images/val/ cp labels/$stem.txt data/labels/val/ done val_images.txtshuffle那一步用--random-source固定随机种子而不是直接调shuf -n是因为这里既要取前80%又要在同一个打乱序列下保持训练和验证不重叠。验证集里的每个标签文件和图像已经通过文件名绑定后面的data.yaml里只需要指定两个目录数据准备到这里就闭环了。有图像但没有对应txt文件的样本说明转换脚本过滤后为空这些图要么被跳过要么单独处理别让它们混进训练目录导致读取报错。到这一步你的数据集已经从VOC格式完整变成了YOLO原生格式可以开始配训练了。4. 用yolov8在本地跑通训练data.yaml配置与关键参数4.1 data.yaml和模型选型1800张图适合哪个规模数据准备好之后下一步是写data.yaml。这是YOLO训练时第一个读取的配置文件作用是指定数据集路径、训练集和验证集目录、类别数量和类别名称列表。一个容易犯的错是把path写成绝对路径换台机器就失效正确做法是path用相对位置或统一约定一个固定的数据根目录。模型选型上1800张图像的数据量撑不起yolov8x这种大模型硬训的结果基本是过拟合。我一般用yolov8n或yolov8s起步先用小模型把数据流程跑通看mAP基线再根据结果决定要不要换更大的模型。yolov8n参数量小训练快适合第一次跑通yolov8s精度更高但也没大到不可控缺陷检测场景通常两个都试一轮。yolo系列对比里还有个细节是不同模型的预训练权重不同yolov8n.pt和yolov8s.pt首次训练时如果网速不稳一直卡在下载阶段可以手动把预训练权重下载好放到执行目录下训练脚本会优先使用本地文件。# data.yaml # path是数据集的根目录train和val是相对路径 path: ./data train: images/train val: images/val names: 0: oxide # 氧化铁皮压入 1: scratch # 划伤 2: patch # 结疤 3: pitted # 麻点 4: crack # 裂纹 5: roll # 辊印names里的索引必须和第3章转换脚本里的CLASS_MAPPING完全一致顺序错一个训练的模型就废了。类别名用英文小写不要用中文避免个别版本在读取标签时出编码问题。如果你在统计阶段发现实际类别数和这里对不上先回头修转换脚本的映射再回来改这个文件。YOLO会检查txt标签里的class_id是否超出names长度超出就直接报错这个报错信息还算友好按提示修就好。4.2 训练命令与关键参数imgsz、batch、epochs怎么定训练命令在yolov8里统一走yolo命令detect子命令负责训练。最小可用命令是yolo detect train datadata.yaml modelyolov8n.pt epochs150 imgsz640 batch16但只跑这个默认配置不够好我习惯把几个关键参数显式写出来一来方便调参二来给队友复现时不用到处猜。yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ device0 \ patience20 \ close_mosaic10 \ lr00.01 \ hsv_h0.015 hsv_s0.7 hsv_v0.4 \ fliplr0.5 \ scale0.5这个命令本质上是把yolov8训练时默认的超参数覆盖了一遍。下面对应说明几个参数的作用和调整思路。imgsz640是输入图像的缩放尺寸。带钢缺陷里小目标多的话可以试1280但显存和训练时间几乎翻倍建议先用640跑基线再决定。batch16是每次迭代的样本数显存够就加大到32梯度更稳定8G以下显存建议保持16或更小。epochs150是总轮数配合patience20做早停连续20轮没提升就自动停不用傻等。close_mosaic10是最后10轮关闭Mosaic增强。这个参数很关键因为Mosaic在最后阶段会让模型的BN统计量波动导致验证集表现不稳关了以后模型能稳定收敛。lr00.01是初始学习率yolov8默认就是这个值一般不用动。如果训练log里loss从一开始就NaN把它降到0.001再试。scale0.5是随机缩放范围带钢表面缺陷的尺度跨度大适度放缩能让模型更适应不同尺寸的缺陷。数值不要超过0.9否则小目标直接被缩放没了。4.3 训练日志里到底看什么loss曲线和指标含义训练过程中终端会实时打印每个epoch的box_loss、cls_loss、dfl_loss以及P、R、mAP50、mAP50-95这几个指标。对第一次跑这个数据集的场合重点不是追求数值多高而是看loss是不是在稳定下降。box_loss衡量预测框和真实框的位置偏差cls_loss衡量类别分类的对错dfl_loss是yolov8特有的分布焦点损失处理边界框的不确定性。三条loss如果都是稳中有降训练就是健康的。P是精确率R是召回率mAP50表示IoU阈值0.5下的平均精度mAP50-95则是在0.5到0.95范围内多个阈值下的平均后者更严格也更接近真实部署时的表现。缺陷检测场景里漏检比误检更危险——漏掉的缺陷可能直接流到下一道工序造成退货所以在P和R之间我更看重R。如果你发现mAP50不错但是mAP50-95偏低说明模型对框的定位精度不够缺陷的边界没有咬住这时候优先调高训练分辨率和调整scale增强范围。训练结束后会在runs/detect/train目录下生成weights文件夹里面best.pt是验证集上表现最好的权重last.pt是最后一轮权重。平时调参用best.pt重新训练时从last.pt接着跑可以省时间但换数据集之后不要沿用别人的last.pt预训练和继续训练是两回事。5. 常见问题排查坐标画框飘了、loss变NaN、过拟合的5条踩坑记录5.1 xml转出来的txt标签画回原图发现框全飘了现象转换脚本运行正常txt文件也生成了但把归一化坐标乘回图像尺寸画框时框的位置明显偏移有的框跑到目标旁边有的直接横跨两个目标。原因最常见的原因是xml的size字段和实际图像尺寸不一致。工业数据经常被压缩过图像分辨率被改写过但xml里的size没跟着更新。转换脚本如果用了xml里的错误宽高做归一化转出来的坐标自然是偏的。另一个可能原因是bndbox坐标的顺序不对有的工具生成的xmin和xmax是反的或者ymin和ymax反了。解决先跑一次回验用下面这段代码把txt标签映射回图像上画框人工看几张图像素级确认转换正确。import cv2 import numpy as np img cv2.imread(./data/images/train/001.jpg) img_h, img_w img.shape[:2] with open(./data/labels/train/001.txt, r) as f: lines f.read().strip().splitlines() for line in lines: cls_id, cx, cy, bw, bh map(float, line.split()) # 把归一化坐标乘回像素尺寸 x1 int((cx - bw / 2) * img_w) y1 int((cy - bh / 2) * img_h) x2 int((cx bw / 2) * img_w) y2 int((cy bh / 2) * img_h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(./check.jpg, img)如果画出来的框都偏一个固定方向基本就是size字段错了如果框的大小对但位置不对问题的方向同理。解决方法是重新获取每张图像的真实宽高用Opencv读一遍原图shape覆盖xml里的size再做一次转换。这个回验步骤我每次都做省下的调bug时间远超执行脚本消耗的几分钟。5.2 训练到一半loss直接变NaN现象训练前几十个epoch一切正常某个epoch突然所有loss和指标全部变成nan终端打印出一堆错误训练直接中断。原因常见的原因有三个。一是学习率过大导致梯度爆炸尤其在yolov8的某些版本里lr0设成0.01对部分数据集就是偏高二是标注数据里存在某些异常框比如某个bndbox坐标是极大值或者归一化后中心点超出0到1范围模型在计算损失时梯度异常三是Mosaic增强拼图时如果图中包含空标签或极端尺寸的目标也可能触发数值不稳定。解决第一步先降低学习率重训lr0从0.01降到0.001或0.005这个调整能把大部分NaN问题消掉。第二步是回到转换脚本在过滤逻辑里把归一化坐标超出0到1的框全部清除再检查是否有txt文件行为空数据。第三步是把close_mosaic提前几轮比如从第10轮改成第5轮让Mosaic在高频迭代阶段的影响更小。按这个顺序排查基本能定位到根因。5.3 训练集太小1800张图让模型学“背答案”了现象训练时mAP一路飙升到0.95以上验证集上也看似完美但换到产线实际采集的图像上漏检一堆误检也不少。典型的过拟合特征是对训练集的强记忆。原因1800张图本身就是偏小的缺陷类别之间相似度高模型很容易把训练图里的背景纹理和光照当成了判别特征。数据增强如果开得不够模型更没有机会接触多样化样本。另一个隐患是训练集和验证集划分不当如果验证集里混入了和训练集同场景的图像模型的表现是虚高的。解决把增强手段拉满Mosaic、Mixup、HSV扰动全部打开scale和translate适当增大。yolov8里可以直接在训练命令里加mixup0.2 mosaic1.0。同时把模型规模降一到两档yolov8n换yolov8s不是万能药小数据量下大模型就是副作用更大。最后别忘了看类别分布如果有一类样本只有几十张用类别加权或者对这类做额外增广来平衡。5.4 mAP不低但现场漏检的还是多现象训练日志里mAP50有0.8以上看起来不错但部署到现场用同一个模型去检测真实采集的图像漏检率明显偏高特别是小缺陷根本框不出来。原因PA50和真实场景之间的差距通常来自两个地方。一是数据分布差距训练集里的图像多为较均匀的照明条件现场产线的打光角度和相机位姿有差别二是小目标问题带钢表面缺陷中尺寸占比很小的小目标在resize到640后可能不足10个像素模型没有足够的特征响应。解决现场采集一些真实样本挑出漏检的图回灌到训练集里做增量训练这是最有效的方向。另一个思路是把输入分辨率从640提升到1280再训一版小目标的特征会显著改善代价是显存占用翻倍。推理时把conf阈值调低一点比如从默认0.25降到0.1用NMS去过滤重叠框召回率通常会上来一截。5.5 混淆矩阵总和为什么不为1现象训练结束后查看混淆矩阵把每一行或者每一列加起来发现总和不是1有的行加起来只有0.8几看起来像模型分类能力有问题。原因混淆矩阵的行是按“真实类别”归一化的每行总和最多为1列是按“预测类别”归一化的每列总和也各自有独立的分母。当数据集中存在难例被模型预测到背景类别时该行总和自然小于1背景类的行和列格式和其他类别不同不要放在一起算总和。这不是bug而是混淆矩阵的信息呈现方式。解决读矩阵时按行看召回能力——某行对角线数值低说明真实类别A的目标常被预测成其他类按列看误检来源——某列非对角线有值说明哪些类别容易错认成该列。比如oxide这一列被scratch填了一格说明两类在特征空间中距离太近优先考虑合并类别或为这两个类增加区分性标注。这里也提醒一句yolo混淆矩阵总合不唯一不是模型坏了先看矩阵数值本身再加具体判断。6. 验证模型能不能上线混淆矩阵、阈值调节与导出部署训练结束生成best.pt之后不要急着标榜项目完成还要做三件事看混淆矩阵确认类别混淆情况调推理阈值导出部署格式。第一件事直接用yolov8自带的验证命令可以看到验证集上的混淆矩阵曲线图重点看对角线数值和最容易混的类别。如果某两个类的混淆点集中在一个区域要么补充这两类的边界样本要么考虑在业务层面把这两类合并成一个“其他缺陷”很多工业质检项目并不需要细到每个缺陷类型都分对只需要把“有缺陷”和“无缺陷”分开。第二件事是针对推理阈值的调节。yolo训练输出的conf默认0.25但在缺陷检测场景里这个值经常偏高。用验证集跑一遍预测把每个阈值的P和R打印出来选P和R交点的阈值通常都在0.1到0.2之间。这个阈值对应的就是产线上真正能用的“捡出率”——漏一个缺陷到客户端可能产生质量索赔多标几个可疑区域最多是让后道工序多看一眼在这种代价结构下阈值宁低勿高。第三件事是把训练好的模型导出成部署格式这一步也踩过不少坑。用yolov8的export命令能直接导出成ONNX格式工控机上用onnxruntime做推理比在目标检测框架里直接跑顺手得多。命令如下yolo detect export modelruns/detect/train/weights/best.pt formatonnx dynamicFalse imgsz640dynamicFalse的意思是固定输入尺寸640部署时输入图像会先resize到640再进模型输出的是归一化坐标后处理要自己做NMS。这里最容易出的问题是预处理不一致——训练时的归一化方式和你推理代码里的归一化方式必须相同一个用0到1一个用0到255检测结果会明显变差。还有一个部署细节ONNX输出的坐标是相对于640输入图的归一化坐标要映射回原图必须乘回原图的宽高不要直接拿640去乘否则小目标会偏。做带钢表面缺陷检测这段时间我最大的教训就是一个目标检测项目的成败在标注数据和数据预处理阶段就注定了七八成模型选型反而只是按部就班的工作。每次拿到新的带钢数据包我都先把统计脚本、转换脚本、回验脚本这一套流水线跑完确认无误再开训练这个习惯省掉的返工时间比训练本身多得多。希望这篇文章里的转换脚本、参数表和排查经验能帮你少走几段弯路也希望帮到你把手里的这个数据集用起来跑出真正能在产线上扛住现场环境的模型。本文还有配套的精品资源点击获取