
产线上一块钢板过去表面有没有麻点、划伤、氧化皮以前靠老师傅拿眼睛盯。人眼盯久了会疲劳而且同一个缺陷换个时段、换个检验员来判结果可能就不一样。我最近用YOLOv8做了一套钢材表面缺陷检测方案从数据集处理、模型训练一路推到部署推理完整跑通了一遍。这篇文章把整个过程、关键参数和踩过的坑都整理出来适合刚接触目标检测、想拿工业缺陷场景练手的开发者也适合已经在做检测、想对比一下方案选址的朋友。我当时接到这个需求时第一反应不是急着训练模型而是先把数据从头到尾捋了一遍。原因很简单钢材表面缺陷检测任务里模型反而是最不难的部分真正难的是把数据洗干净、把评价指标定对、把部署环境里的各种意外处理好。下面我就按我实际执行的顺序来写。1. 项目初衷与技术选型1.1 钢材表面缺陷检测难在哪先说说这个任务本身。钢材表面缺陷检测本质上是目标检测里的“小目标外观差异大类别不平衡”三重难题叠在一起。常见缺陷包括裂纹、夹杂、麻点、氧化皮、轧制氧化铁皮、划伤这六类形态完全不同。裂纹是细长线状稍不留意就被当成背景纹理麻点是密集小坑单个尺寸可能只有几十个像素氧化皮是成片的不规则暗色区域边缘模糊跟正常的锈蚀还有阴影非常像。这种任务跟我平时做行人检测、车辆检测完全不是一个感受。行人和车辆在图像里占的比例大、外观稳定即使有遮挡也不至于找不着。钢材缺陷不一样很多缺陷放在整幅图里可能只有整张图面积的千分之一甚至更少就像在一张A4纸上找一根头发丝还要说清楚它是裂纹还是划伤。再加上钢厂现场光照暗、粉尘多、钢板表面反光同一个缺陷在不同工况下拍出来可能完全不像同一个东西。还有一个现实问题缺陷样本本身是稀缺的。负样本到处都是正样本要等缺陷出现才能采到。很多公开数据集的图像是在实验室条件拍的背景干净、光照均匀拿这套权重直接上产线大概率会被真实场景教做人。所以数据处理这一环必须花大力气。1.2 对比一圈为什么YOLOv8最合适动手之前我其实比较过Faster R-CNN、YOLOv5和YOLOv8。Faster R-CNN精度确实高但推理速度在普通工控机上跑起来不理想产线对一帧的处理时间往往要求在几十毫秒级别Faster R-CNN很容易卡在边界。YOLOv5也很成熟社区资料多但到了后续导出部署、内置增强这些方面还是YOLOv8更省事。我最终选YOLOv8的核心原因有这么几个。第一YOLOv8是anchor-free设计少掉了调anchor这一大堆事情对于钢材缺陷这种目标尺寸分布特别不规律的数据集省心很多。第二YOLOv8的C2f结构在保持推理速度的同时特征提取更好对小目标的敏感度比旧版有提升。第三官方仓库直接集成了训练、验证、导出、推理一条链不需要自己手写太多胶水代码。第四也是很重要的一点YOLOv8内置的数据增强和训练策略比较完善比如mosaic增强、自动早停这些能少踩不少坑。当然不是说YOLOv8就万能。如果缺陷实在太小YOLOv8照样会漏检这时候需要结合滑窗或者提高输入分辨率来补救。但作为第一版方案YOLOv8是稳妥的起点。1.3 整体方案怎么规划我给自己定了一条完整的执行路径每一步都有明确的输入和输出收集数据评估数据质量和类别分布。数据清洗修掉错误标注和重复图。把标注格式统一转换成YOLO的txt格式按序列切分训练集、验证集和测试集。编写数据集配置文件设置增强参数。选择预训练权重配置超参数开始训练。用best.pt做验证输出per-class指标和混淆矩阵。针对漏检误检案例迭代补数据、改增强、调分辨率。导出ONNX放到推理环境里做最终测试。这套流程看起来很常规但每一步里都有说不完的细节。下面我按这个顺序拆开细讲。2. 数据集拿到了先别急着训练2.1 数据从哪来质量和规模怎么看我手里的钢材缺陷数据主要来自两个来源一个公开的钢材表面缺陷数据集另一个是某厂现场抽拍的钢板表面图像。公开数据集的好处是标注规范能用来快速验证流程坏处是图像背景偏干净、缺陷形态相对典型跟产线实际数据有gap。现场数据更真实但标注成本高格式也乱。拿到数据第一步不是数张数而是做抽样检查。我习惯把每类图片随机抽20%出来用标注工具打开快速翻一遍重点看三件事标注框是否紧贴缺陷边缘有没有把两个相邻缺陷框在一个框里有没有明显漏标。这一步花不了太多时间但能避免后面整个训练过程被错误标注带着走。在规模上我的经验是每个类至少要有300到500张有效图。少于此数量模型很容易过拟合泛化到新工况基本没有保障。当然如果某些类天然稀少可以先通过数据增强硬凑但这只是缓兵之计最终要回到补充真实样本上。2.2 先把六类缺陷的样子和分布摸清楚我把这版数据里的六类缺陷整理成了一张特征表方便后面对照分析。缺陷类别常见成因形态特征裂纹轧制应力、材质不均细长线状方向比较规律有时带分叉夹杂非金属夹杂物暴露斑点或小块状颜色和基体差别明显麻点表面腐蚀或氧化密集小坑面积小、边缘不规则氧化皮高温氧化不规则片状颜色发暗边缘模糊轧制氧化铁皮轧制过程中压入鳞片状颜色偏黑蓝有一定厚度感划伤机械摩擦直线亮痕宽度小、长度长读这批数据时带着一个任务去看哪两类最容易混。我观察下来氧化皮和麻点经常混因为都表现为暗色区域划伤和裂纹也容易混因为都是长条形。搞清楚这个后面再做类别权重和增强策略时就有依据。另一个容易被忽视的工作是统计分析缺陷框的尺寸分布。写个小脚本统计所有标注框的宽高像素值看看中位数是多少、最小的是多少。如果大量缺陷框在40像素以下那输入尺寸就要认真考虑640可能不够。2.3 清洗数据比增强数据更重要我见过不少朋友一上来就把增强拉满各种翻转、旋转、加噪声结果训练出来mAP挺高一测真实场景就垮。原因很简单原始数据本身是脏的增强只会把脏信息放大。我清洗数据时主要有几个操作剔除重复图像用文件的MD5值去重就能干剔除标错类别的图片比如把麻点标成氧化皮剔除严重过曝或严重欠曝的样本这类图即使模型训出来在真实场景里也是负收益对标注框做合法性检查确保xmin小于xmax、ymin小于ymax且都落在图像范围内。清洗完以后我宁愿数据集从1200张变成900张也不愿用那300张脏数据。后来实际训练结果证明这个决策是对的洗过的900张训练效果明显好于没洗的1200张。3. 数据集处理的硬核细节3.1 从XML标注转到YOLO txt一条脚本搞定很多标注工具导出的是XML或JSON格式记录的是目标的左上角和右下角像素坐标。YOLO训练需要的txt不是这种格式它要求一行一个目标类别id、中心点x坐标、中心点y坐标、宽、高并且全部归一化到0到1之间。我写了一个转换脚本把常见的XML标注转成YOLO格式import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() size root.find(size) W float(size.find(width).text) H float(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) assert xmax xmin and ymax ymin, finvalid box in {xml_path} cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H bw (xmax - xmin) / W bh (ymax - ymin) / H lines.append(f{class_names.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) img_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, img_name), w) as f: f.write(\n.join(lines)) # 示例用法 class_names [crazing, inclusion, patches, pitted_surface, rolled_in_scale, scratches] os.makedirs(labels/train, exist_okTrue) for xml_file in os.listdir(xmls): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(xmls, xml_file), labels/train, class_names)这里有两个非常容易踩的坑。第一个是类别id必须从0开始不能从1开始否则训练时类别数对不上。第二个是归一化必须除以图像的原始宽高而不是除以某次resize之后的宽高。我当时就是忘了除以原图宽高只算了相对坐标差值结果训练loss能降但输出的框位置完全不对。3.2 训练集、验证集、测试集怎么切分切分看似简单实际上很讲究。最忌讳的做法是直接把所有文件路径丢进一个随机函数然后按比例切三份。这样很容易让同一现场拍摄的连续帧同时出现在训练集和验证集里模型相当于提前见识过验证集内容最后指标虚高一到新场景就现原形。我通常按序列分组切分。比如现场图是一卷钢一个文件夹或者一批图有一个共同的前缀那就要保证同一来源的图只出现在一个集合里。先对样本做分组再按组随机分配到训练集、验证集和测试集比例控制在85%、10%、5%左右。测试集一旦划定在整个迭代过程中尽量不要动否则你就是在对着测试集调参指标会失真。代码上可以用一个简单的分配逻辑按文件名前缀分组再对组列表打乱然后把组依次填入三个集合。虽然多写几行代码但换来的是可靠指标非常值得。3.3 数据增强哪些对缺陷检测真有用YOLOv8内置了一系列增强手段mosaic、平移、缩放、翻转、HSV色域变换都有。默认配置已经比较合理但针对钢材表面这种特殊数据我做了两处关键调整。第一是HSV增强强度要收一点。钢板颜色本身是识别氧化皮、麻点的重要线索如果把色相、饱和度调得太猛氧化皮的颜色就可能变成类似塑料反光的颜色反而加大误检。我用的是hsv_h0.01、hsv_s0.5、hsv_v0.3效果比默认值更贴合当前数据。第二是翻转方向要谨慎。如果产线相机固定钢板从左往右走那么左右翻转生成的图像在现实中根本不会出现这时候把fliplr设成0或很小值更合理。mosaic增强对多数目标检测任务都有提升但在小缺陷很多的数据集上要控制力度。mosaic会把多张小图拼成一张大图缺陷能被进一步缩小原来就小的麻点可能直接缩到无法辨认。我建议类别分布比较均衡时用默认mosaic1.0如果小目标占主导尝试降到0.5以下。cutout这种挖空增强我也试过在钢材数据上不但没用还会把真实缺陷挖掉不建议使用。3.4 dataset.yaml和目录结构YOLOv8训练需要一份数据集配置文件指定图像和标签的路径以及类别名。我用的是这样的目录结构steel/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/对应的dataset.yaml如下path: ./steel train: images/train val: images/val test: images/test names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled_in_scale 5: scratches这个文件有两点必须注意。一个是names的顺序必须和txt标注里的数字id严格对应一旦开始训练就不能随便改改完整个模型输出对不上。另一个是path建议用绝对路径或者相对当前命令行的路径别用会在不同机器上变化的路径不然换一台机器训练时容易排查半天。4. 训练流程与参数调优实战4.1 环境准备与基础训练我的环境是Python 3.9、PyTorch 2.1、CUDA 12.1安装YOLOv8直接用pip就行conda create -n steel python3.9 conda activate steel pip install ultralytics torch torchvision opencv-python基础训练命令看起来很简单yolo detect train datasteel.yaml modelyolov8s.pt epochs300 imgsz640 batch16 device0但这行命令背后有几个参数要理解清楚。modelyolov8s.pt是预训练权重用它在自己的数据上微调收敛比从零训练快得多。imgsz640是训练时输入网络的尺寸如果缺陷框的中位数尺寸在40像素以下建议直接上960。batch16对8到12GB显存的显卡比较合适想算自己显存够不够可以大概估一下16张640x640x3的输入图像本身占内存才约80MB但梯度、中间特征、优化器状态会把这个数放大许多倍实测下来16的批量需要8GB以上显存。我建议第一次迭代先用yolov8s不要一上来就上l或者x。模型越大对数据质量的要求越高在数据还没洗干净的阶段大模型只会更快地记住噪声。4.2 超参数选择思路训练参数里我重点调整的是epochs、patience和lr0。YOLOv8默认epochs是100但对工业缺陷这种需要精雕细琢的任务100轮往往不够我直接设成300。patience设为50意思是验证集指标连续50轮不提升就自动停止既省时间又防过拟合。学习率lr0默认是0.01如果训练日志显示loss一开始就剧烈震荡就降到0.001重新启动。动量momentum保持默认即可不需要动。还有一个值得关注的参数是close_mosaicYOLOv8会在训练最后10轮自动关闭mosaic增强让模型在更接近真实分布的数据上做最后精修这个机制对小目标缺陷很友好默认打开就好。4.3 训练日志怎么看训练过程会生成results.png和weights目录里面放着best.pt和last.pt。很多人只看最后一个结果这是不对的。best.pt是验证集mAP最高的权重last.pt是最后一轮的结果两者差距有时候很大。我习惯先看train/loss和val/loss两条曲线如果train/loss一直降但val/loss涨就是过拟合信号可以把epochs降下来或者增大增强。如果val/loss一直在震荡不收敛优先检查数据切分和标注。验证集指标里mAP50反应的是粗略定位质量mAP50-95反应的是框和真实框的重合度要求。钢材缺陷这种业务我实际更看重mAP50和每类recall因为现场漏检比框不准更致命。4.4 显存不足和训练太慢训练中经常会遇到显存爆掉的问题解决方案优先级我按这个顺序来先把batch从16降到8这样一般能救回来如果还不够把imgsz从640降到512再不行换yolov8n。注意YOLOv8默认开了amp混合精度这个能省不少显存别莫名其妙关掉。训练太慢的话最大的瓶颈往往是CPU在做数据增强尤其是mosaic。可以把workers从默认的8升到16如果CPU核心多的话会有明显改善。硬盘IO也有影响强烈建议把数据集放到NVMe固态硬盘上而不是机械盘。5. 评估指标与错误分析5.1 别只报一个mAP第一轮训练跑完之后我拿到一份看起来不错的mAP数据但真正有用的信息藏在per-class指标里。当时我的验证集整体mAP50到了0.78看起来能接受但拆开看后发现裂纹这一类的AP50只有0.42recall更是低到0.55大量裂纹被漏成背景。这种情况下整体mAP被其他容易分类的缺陷抬高了直接上线肯定会出事。所以我强烈建议每次评估都输出per-class表格和混淆矩阵。YOLOv8验证时直接会生成这些结果在runs/detect/val目录下可以找到confusion_matrix.png和results.csv。我习惯把验证结果按类别整理成一张表比如这个格式类别AP50Recall裂纹0.620.58夹杂0.950.92麻点0.880.85氧化皮0.840.80轧制氧化铁皮0.780.74划伤0.820.78这是示例数据但这类表才是真正能指导迭代的东西。AP低的那一类就是要优先补数据、修标注、调参数的方向。5.2 漏检率比mAP更值得关注我把漏检样本单独抽出来建了一个文件夹每张图按失败原因分类。看下来发现漏检主要集中在这几种情况缺陷尺寸太小经过640缩放后辨识度急剧下降缺陷对比度低和周围钢板表面的灰度差太小缺陷边缘模糊标注框本身就不确定。针对小目标漏检我先试过把imgsz从640提到960效果有提升但显存压力变大。后来又改成滑窗推理把大图切成若干小图分别送进模型推理再把结果合并回原坐标。这一招在缺陷尺寸极小的情况下非常有效代价是推理时间增加数倍。折中方案是只对怀疑区域做滑窗或者用更大模型结合更高分辨率。如果业务允许优先采集更多近景图像从数据源头解决问题。5.3 第二次迭代怎么改第一次迭代后不要急着乱调参数。我给自己定了一个规则每次迭代只改一个变量。先补正好清洗过的数据不动增强再单独调输入尺寸再试增强系数最后才是换模型体积。实际做了两轮之后我发现提升最大的一步不是从s换到l而是把标注中的错误框修好同时把输入尺寸从640提到960。这样做还有一个好处可以回滚。如果某个改动让指标下降立刻知道是哪个变量导致的不用把整个训练配置推倒重来。6. 推理部署与工程化落地6.1 导出ONNX并脱离训练环境推理训练结束后部署端往往不装PyTorch直接用ONNX模型会更轻量。导出命令如下yolo export modelbest.pt formatonnx imgsz640 dynamicTrue opset12dynamicTrue表示输入尺寸可以是动态的方便在推理时传入任意分辨率。但要注意如果后面要转TensorRT动态尺寸会增加转换复杂度固定尺寸会更稳。我在这版项目里先用ONNX Runtime跑固定成640x640逻辑更简单。ONNX推理的关键是预处理必须和训练时完全一致。我用的核心流程是读图、letterbox缩放、RGB通道转换、归一化、转CHW送入模型、解码输出、画框。其中最容易出错的是letterbox。直接resize会把图片横向拉伸检测框坐标跟着偏所以在后处理时要把letterbox的缩放比例和填充偏移记下来还原到原图坐标。6.2 部署环境最容易出问题的三个点部署中最常遇到的问题我捋了三个。第一是通道顺序。训练时用的是RGBOpenCV默认读进来是BGR如果忘了转检测效果会明显下降而且不会报错。第二是NMS参数。推理时置信度阈值如果按默认0.25来很多小缺陷会被过滤掉我在钢材缺陷场景里通常把置信度阈值调到0.1到0.15但这样误检会增加得用NMS的IoU阈值来控制重叠框。第三是输入归一化。YOLOv8官方推理时会对图像除以255如果在自己的推理脚本里忘了这一步模型输出基本等于废的。我给自己写了个推理小脚本用OpenCV和ONNX Runtime跑单张图整体流程大约几十行代码。实测下来CPU推理一张640x640的图大概在几百毫秒GPU上可以到几十毫秒基本能满足产线慢速分检的需求。6.3 与产线场景对接的注意事项部署后还要面对真实环境的变量。相机一旦装好焦距和安装距离就不要随便动否则目标尺度变化会直接影响模型表现。光照是最核心的干扰因素钢厂现场照明变化剧烈模型的误检率会跟着变。我的做法是在不同时段、不同灯光下各采集一批负样本专门测试模型有没有把阴影和反光当成缺陷。还有输出坐标的处理。模型输出的是归一化坐标必须乘回原图宽高才能给下游设备用。如果下游机械臂要根据缺陷坐标做动作那坐标精度就非常关键此时用mAP50-95这类指标去衡量会更贴合业务。7. 踩坑实录与常见问题速查7.1 我踩过的坑按严重程度排第一个大坑是标注格式转换时把xmax和xmin的顺序搞反了。训练过程完全不报错loss也能下降但模型输出的框全部偏到图像边缘。排查了很久才发现是转换脚本里的赋值出错。所以转换完成后一定要做可视化检查随机抽几十张图把标注框画出来人工看一眼。第二个坑是类别id和names顺序对不上。我一开始把names的第一个类当成1号实际上YOLO从0号开始结果训练日志里分类loss一直不收敛。后来我把txt文件里的id和data.yaml里的顺序重新对照了一遍才解决。第三个坑是数据增强过猛。我为了追求“泛化能力”把hsv_v调到了0.8结果钢板表面颜色被改得面目全非模型学到的是错误的颜色特征检测率直线下降。后来我才意识到工业缺陷检测里很多线索恰恰来自颜色和纹理增强不是越多越好。第四个坑是评估集切分没有按序列隔开。最开始直接随机切分训练集和验证集里出现了同一卷钢的连续帧验证mAP虚高到0.9但换到新样本直接掉到0.65。这让我彻底记住了分组切分比随机切分靠谱得多。7.2 常见问题速查表问题现象可能原因解决办法train/loss降但val/loss不降过拟合、标注噪声大清洗数据、增强、增大训练数据、提前早停检测框整体偏移letterbox后处理错误或坐标未还原核对缩放比和填充偏移按原图尺寸还原坐标小缺陷大量漏检输入分辨率不够、置信度阈值过高提高imgsz、滑窗推理、降低置信度阈值两类缺陷互相误检类别特征重叠、样本不足补样本、减少颠覆性增强、做一个专业分类器模型在真实环境误检多训练数据背景单一、新场景光照不同补边界样本、加入真实负样本、降低增强里的颜色变换导出ONNX后输出异常NMS已内嵌、输入预处理不一致重新核对归一化和通道顺序必要时在外部做NMS训练速度极慢数据增强CPU瓶颈、硬盘IO慢调高workers、数据放固态盘、适当减小imgsz7.3 写在最后的一点体会跑完整个流程之后我最深的感受是工业缺陷检测项目里模型能力的天花板其实很低数据质量和工作方法才是决定成败的部分。每一次指标不达标先不要怀疑模型选型先回去看数据有没有脏、切分有没有问题、指标有没有看对。把数据吃透再谈模型调优。另外就是每次迭代只改一个变量把中间模型全部保存下来。我现在回头看真正让效果变好的都是那些看起来最笨的步骤人工核对标注、分析漏检图、按序列切分数据。这些步骤不能省也省不得。最后再分享一个小经验如果条件允许训练前先用YOLOv8自带的预训练模型跑一遍公开的钢材表面小数据集看看整体流程通不通再切到自己的数据。先把流水线跑通再追求精度整个过程会顺很多。