基于NEU-DET数据集与YOLOv5的钢材表面缺陷检测实战:从训练到RK3568边缘部署
简介这份资源面向工业质检方向的算法工程师与深度学习学习者提供基于YOLOv5的钢材表面缺陷检测完整方案可用于裂纹、凹坑、氧化皮、划痕等常见缺陷的识别任务帮助提升产线自动化水平并降低人工质检成本。压缩包共约2000个文件整体501.32MB以jpg图像、txt标注与xml标签为主配套yaml数据配置、py训练脚本及pt权重文件覆盖数据、训练与推理全流程另有少量onnx、sh、md等辅助文件。资源已积累2502人学习下载热度较高。读者可据此复现从数据增强、多尺度训练到实时检测的完整链路理解CSPNet、SPP-Block、FPN、PANet与Mish激活函数等关键设计并借助现成脚本与权重快速验证模型效果适合作为工业缺陷检测项目的入门与调优参考。1. 钢材表面缺陷检测为什么 YOLOv5 配这套数据集是性价比最高的起点产线上刚下线的冷轧板表面一道 0.3 毫米的划痕人眼在强反光下盯半小时就疲劳漏检而下游冲压件一旦开裂整批退货。钢材表面缺陷检测要解决的就是这件事把划痕、夹杂、结疤、麻点这些缺陷从高速运动的钢带表面稳定框出来。公开数据集里东北大学发布的 NEU-DET 是绕不开的一套——6 类缺陷、每类 300 张灰度图、共 1800 张分辨率 200×200标注是 VOC 格式的 XML。它小、干净、类别均衡正好适合拿来跑通 YOLOv5 训练自己的数据集这条链路。这篇笔记讲的就是从拿到这套数据集到训练出可用权重、再到量化部署到 RK3568 或树莓派 4B 的完整路径适合刚接手视觉质检项目、需要快速出基线的工程师也适合想把模型塞进边缘盒子的人。2. 数据集与 YOLOv5 的匹配先看清 1800 张图能撑起什么2.1 NEU-DET 的六类缺陷与标注结构NEU-DET 的六类分别是 crazing龟裂、inclusion夹杂、patches斑块、pitted_surface麻点、rolled-in_scale压入氧化皮、scratches划痕。目录结构常见做法是按类别分文件夹每张图配一个同名 XMLNEU-DET/ IMAGES/ crazing/ # 300 张 .jpg inclusion/ patches/ pitted_surface/ rolled-in_scale/ scratches/ ANNOTATIONS/ crazing/ # 300 个 .xml ...XML 里关键字段是size的宽高和object下的name与bndbox。这里有个血泪经验NEU-DET 的 XML 里name有时带类别前缀有时是纯类别名不同二次分发版本不一致。转换前先抽 5 个文件确认别等训练完发现类别全错。2.2 为什么 1800 张图够用以及它的边界1800 张听起来少但缺陷检测和通用目标检测不同同一类缺陷的纹理模式高度重复模型学的是纹理统计而非语义多样性。YOLOv5s 在 1800 张上从头训配合强增强mAP0.5 做到 0.75 以上是常见结果。但边界要清楚这套数据是灰度、小尺寸、实验室采集直接迁移到真实产线的彩色高分辨率钢带域差异会让指标掉一大截。我的做法是把它当预训练起点再用产线采几百张做微调。2.3 环境与依赖一条能跑通的安装命令# 建议 Python 3.8~3.10CUDA 11.x conda create -n steel python3.9 -y conda activate steel # 拉 YOLOv5 官方仓库版本以你 clone 到的为准 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 验证环境 python -c import torch; print(torch.__version__, torch.cuda.is_available())逻辑说明先隔离环境避免和系统里的 torch 冲突requirements.txt会装 torch、torchvision、opencv、pyyaml 等。参数说明torch.cuda.is_available()返回 True 才说明 GPU 可用返回 False 时训练会退到 CPU1800 张也要跑很久先解决驱动再继续。这一步翻车最多的是 CUDA 版本和 torch 版本对不上报CUDA error: no kernel image is available按官方矩阵重装即可。3. 从 VOC 到 YOLO 格式转换脚本与四个边界坑3.1 转换脚本一次把 XML 全转成 txtYOLOv5 要的是每张图一个.txt每行class x_center y_center w h全部归一化到 0~1。下面脚本按类别文件夹遍历输出到labels/下同名 txtimport os, glob import xml.etree.ElementTree as ET CLASSES [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches] IMG_DIR NEU-DET/IMAGES ANN_DIR NEU-DET/ANNOTATIONS OUT_DIR NEU-DET/labels def convert(xml_path, img_w, img_h, out_path): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text.strip() # 兼容带前缀的类别名取最后一段 cls name.split(_)[-1] if name not in CLASSES else name if cls not in CLASSES: continue bnd obj.find(bndbox) x1 float(bnd.find(xmin).text) y1 float(bnd.find(ymin).text) x2 float(bnd.find(xmax).text) y2 float(bnd.find(ymax).text) # 归一化中心点与宽高 xc (x1 x2) / 2.0 / img_w yc (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{CLASSES.index(cls)} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) for cls in CLASSES: os.makedirs(os.path.join(OUT_DIR, cls), exist_okTrue) for xml in glob.glob(os.path.join(ANN_DIR, cls, *.xml)): stem os.path.splitext(os.path.basename(xml))[0] # 宽高优先从 XML 的 size 读读不到再兜底 200 root ET.parse(xml).getroot() size root.find(size) iw int(size.find(width).text) if size is not None else 200 ih int(size.find(height).text) if size is not None else 200 convert(xml, iw, ih, os.path.join(OUT_DIR, cls, stem .txt)) print(done)逻辑说明遍历每个类别文件夹逐 XML 解析 object把角点坐标转成中心点加宽高的归一化格式。参数说明CLASSES顺序必须和后面 data.yaml 的names完全一致否则类别索引错位iw/ih从 XML 的size读比硬编码 200 稳因为部分二次分发版本尺寸不同。归一化用.6f保留六位避免小目标坐标被截断成 0。3.2 划分训练集与验证集1800 张按 8:1:1 分 train/val/test每类各取 30 张进 val、30 张进 test保证类别均衡python - PY import os, random, shutil random.seed(42) CLASSES [crazing,inclusion,patches,pitted_surface,rolled-in_scale,scratches] for split in [train,val,test]: for c in CLASSES: os.makedirs(fNEU-DET/images/{split}/{c}, exist_okTrue) os.makedirs(fNEU-DET/labels/{split}/{c}, exist_okTrue) for c in CLASSES: imgs sorted(os.listdir(fNEU-DET/IMAGES/{c})) random.shuffle(imgs) val, test imgs[:30], imgs[30:60] train imgs[60:] for split, files in [(train,train),(val,val),(test,test)]: for f in files: stem os.path.splitext(f)[0] shutil.copy(fNEU-DET/IMAGES/{c}/{f}, fNEU-DET/images/{split}/{c}/{f}) shutil.copy(fNEU-DET/labels/{c}/{stem}.txt, fNEU-DET/labels/{split}/{c}/{stem}.txt) print(split done) PY逻辑说明固定seed42保证可复现先 shuffle 再切片避免按文件名顺序切导致某类分布偏。参数说明val/test 各 30 张是经验值1800 张规模下验证集太小会让 mAP 抖动大太大又浪费训练样本30 张是折中。3.3 data.yaml 与四个必查边界path: ./NEU-DET train: images/train val: images/val test: images/test nc: 6 names: [crazing,inclusion,patches,pitted_surface,rolled-in_scale,scratches]四个边界坑一是names顺序和转换脚本CLASSES不一致模型学出来的类别全乱二是 txt 里出现空文件图里没缺陷YOLOv5 会当负样本少量可以大量要检查标注三是坐标越界x2超过图宽会让归一化值大于 1训练时报non-normalized coordinates四是图片和标签不同名YOLOv5 按同名匹配差一个字符就找不到标签表现为 loss 不降。4. 训练自己的数据集超参数怎么设才不白跑4.1 从预训练权重起步的命令python train.py \ --img 640 \ --batch 16 \ --epochs 200 \ --data NEU-DET/data.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name neu_baseline逻辑说明--weights yolov5s.pt加载 COCO 预训练权重小数据集上比从头训收敛快得多--img 640把 200×200 的图放大到 640小缺陷放大后特征更明显。参数说明--batch 16在 8G 显存上稳显存不够降到 8--epochs 200对 1800 张够配合早停--hyp选 low 增强档因为缺陷纹理对颜色抖动敏感强色彩增强反而有害。4.2 超参数里真正影响结果的三个YOLOv5 的 hyp.yaml 参数几十个实测对钢材缺陷影响最大的是这三个参数默认值建议值原因lr00.010.001小数据集用大学习率容易震荡不收敛mosaic1.00.5缺陷是局部纹理四图拼接会破坏上下文scale0.50.2缺陷尺寸相对固定过度缩放让尺度失真lr0从 0.01 降到 0.001 是最立竿见影的一步很多人训 NEU-DET loss 一直抖就是没改这个。mosaic 降到 0.5 保留一定增强但不过度。scale 调小是因为钢材缺陷的物理尺寸在图像里分布集中随机缩放 0.5 倍会让划痕和麻点混淆。4.3 训练过程看什么指标启动后重点盯三行box_loss、obj_loss、mAP0.5。box_loss 前 20 epoch 快速下降是正常之后平缓obj_loss 如果一直不降多半是标签匹配出问题回去查 3.3 的四个边界。mAP0.5 在 100 epoch 后应稳定在 0.7 以上低于 0.6 先别调模型先查数据。验证集 mAP 涨而训练 loss 不降是过拟合前兆加--patience 30早停。4.4 推理验证一张图看框得准不准python detect.py \ --weights runs/train/neu_baseline/weights/best.pt \ --source NEU-DET/images/test/scratches \ --img 640 \ --conf-thres 0.25 \ --save-txt逻辑说明--source指向测试集某类文件夹批量推理--save-txt把预测框存成 txt 方便和真值对比。参数说明--conf-thres 0.25是缺陷检测常用阈值调高漏检多调低误检多按产线容忍度调。跑完看runs/detect/exp/下的可视化图重点看小目标划痕有没有被漏掉。5. 避坑与排查训练不收敛、指标虚高、部署掉点的真实记录5.1 现象loss 从第一个 epoch 就不降原因九成是标签路径或格式问题。YOLOv5 找不到标签时不会报错直接把图当负样本loss 卡在高位。解决跑python utils/checks.py或用--verbose看每个 batch 的标签数正常每张图应有 1~3 个框全是 0 就是路径错了。检查data.yaml的path是相对还是绝对相对路径以 train.py 所在目录为基准。5.2 现象mAP 很高但实际推理全是误检原因验证集和训练集划分时没 shuffle或者同一张图的增强版本同时进了 train 和 val造成数据泄漏。解决确认 3.2 的划分脚本用了固定 seed 且先 shuffle更稳妥的是按图片原始编号分组划分同一块钢板的多个视角图不能跨集。5.3 现象训练正常导出 ONNX 后推理结果对不上原因YOLOv5 训练时做了 letterbox 填充导出后如果推理端没做同样的预处理坐标会整体偏移。解决导出用python export.py --weights best.pt --include onnx --img 640推理端严格复现 letterbox等比缩放后灰边填充到 640×640输出再反算回原图坐标。这一步在部署到 RK3568 时尤其关键NPU 只吃固定输入尺寸。5.4 现象量化到 RK3568 后精度掉十几个点原因INT8 量化对激活值范围敏感钢材缺陷的灰度图对比度低量化后弱纹理特征被抹平。解决量化时提供至少 200 张有代表性的校准图覆盖六类缺陷对首尾层保留 FP16如果还掉点把输入从 640 降到 416减少量化误差累积。树莓派 4B 上用 ONNX Runtime 跑 FP16 通常比硬上 INT8 更稳。5.5 现象小目标划痕总是漏检原因200×200 原图里划痕可能只有几个像素宽放大到 640 后仍偏小YOLOv5 的 P3 特征图 stride 8对极小目标不友好。解决训练时--img 800再放大一档或在数据增强里加copy_paste把小缺陷复制粘贴到其他位置增加正样本推理时把--conf-thres降到 0.15 并配合 NMS 的--iou-thres 0.5。6. 边缘部署的进阶技巧从权重到 RK3568 与树莓派 4B 的最后一公里训练出 best.pt 只是上半场真正落地要过部署这关。RK3568 带 0.8T NPU树莓派 4B 只有 CPU 和 VideoCore两条路差别很大。RK3568 走 RKNN 工具链先把 best.pt 导 ONNX再用rknn-toolkit2转 rknn转换时指定mean_values[[0,0,0]]、std_values[[255,255,255]]和训练时的归一化对齐否则输入分布错位精度直接崩。量化校准集我一般从训练集每类抽 40 张共 240 张覆盖六类纹理。转完在板端用rknn_lite推理单帧 640 输入大概 30~50ms够 20fps 产线节拍。树莓派 4B 没有 NPU硬跑 YOLOv5s 640 输入只有 2~3fps实用做法是换 YOLOv5n 并把输入降到 320用 ONNX Runtime 开intra_op_num_threads4能到 8~10fps。如果还嫌慢把检测和分类拆开先用轻量分割找候选区域再送小分类网络判类别钢材缺陷类别少这条路往往比端到端检测更快。验证部署是否成功别只看能不能出框要做三件事一是拿 20 张训练集图在 PC 和板端各跑一遍比对框坐标偏差超过 2 像素说明预处理没对齐二是拿 20 张没参与训练的产线图测漏检率这才是真实指标三是连续跑 1 小时看内存有没有涨RK3568 上 rknn 推理如果每帧新建 context 会缓慢泄漏复用同一个 context 就好。我自己踩过最深的坑是量化后忘了改预处理PC 上 mAP 0.78板端掉到 0.4查了两天才发现是归一化系数写反。后来养成习惯任何一次部署前先在板端跑一张训练集里的图和 PC 结果逐像素比对对上了再上产线。这套流程从 NEU-DET 到 RK3568 我走了不下十遍慢就是快。希望帮到你。本文还有配套的精品资源点击获取