
简介基于YOLOv5的钢材表面缺陷识别项目包面向工业质检人员、目标检测初学者及算法工程师旨在解决钢材表面裂纹、凹坑、划痕等缺陷的自动检测与定位问题。包内含2000个文件以1971张jpg图像、1954个txt标注、1800个xml标签为主同时提供yaml配置、pt权重、Python训练脚本与onnx模型等全套工程文件压缩包约501MB。已有2452人学习下载。数据标注、模型训练、推理部署全流程均有对应文件支撑包含train.py、test.py等核心脚本与清晰目录结构便于读者快速复现实验掌握YOLOv5中CSPNet、SPP、FPN、PANet等关键模块在真实工业场景下的应用与调优方法。数据集涵盖裂纹、凹坑、划痕等多种缺陷附带数据增强与多尺度训练策略说明结合标注文件可快速完成格式转换与训练集划分。无论是课程设计、毕业设计还是工业落地都能直接作为可运行的基线工程。1. 写在前面为什么要做钢材表面缺陷检测先说个背景。钢材生产线上表面缺陷是绕不开的质量痛点——划痕、麻点、夹杂、氧化皮压入这些缺陷如果靠人工肉眼质检不仅累而且漏检率跟工人的疲劳程度直接挂钩。一条热轧产线动辄每秒几米的速度盯久了谁都扛不住。所以用深度学习做自动质检是工业视觉里非常典型的落地场景。我选 YOLOv5 钢材表面缺陷数据集来跑这个项目原因很直接YOLOv5 在工业部署里实在太成熟了推理速度快、生态完善、从训练到导出的链路顺畅而钢材表面缺陷数据集NEU-DET是公开数据集里最常用的一个专门针对热轧带钢表面的六类典型缺陷拿来练手或者做毕设、做实际项目预研都非常合适。这篇文章我会按一个完整项目的顺序来写数据集长什么样、怎么标注和划分、YOLOv5 环境怎么搭、模型怎么训练和调参、最后怎么评估和导出部署。全程用我自己实际跑过的参数和踩过的坑说话适合准备入门目标检测、或者正在做工业质检相关课题的朋友直接抄作业。2. 先摸透数据集NEU-DET 的六个缺陷类别2.1 数据集概览NEU-DET 全称是 Northeastern University Surface Defect Database由东北大学中国发布是热轧带钢表面缺陷检测领域的基准数据集。它包含 1800 张灰度图像每张分辨率是 200×200 像素按缺陷类型分为六类类别名称中文俗称典型特征样本数量Crazing网纹/龟裂表面细密网状裂纹300Inclusion夹杂颗粒状或条状异物嵌入300Patches斑块大面积灰度不均区域300Pitted Surface麻点细小凹坑群300Rolled-in Scale氧化皮压入压入表面的鳞片状氧化物300Scratches划伤连续或断续的线状划痕300每类 300 张类别分布绝对均衡这在工业数据集里相当少见。不过要注意所有图像都是灰度图虽然 YOLOv5 训练时要求输入三通道但直接把单通道灰度图喂进去也能训练代码内部会处理通道复制只是灰度图对模型的特征提取能力要求更高——毕竟没有颜色信息可用全靠纹理、形状、灰度对比度来区分缺陷。2.2 数据格式与标注特点NEU-DET 官方提供的标注格式是 XML类似 PASCAL VOC 风格即每张图对应一个 XML 文件里面用bndbox标记目标的xmin, ymin, xmax, ymax。但 YOLOv5 训练需要的是 YOLO 格式的 txt 标注文件也就是每行class_id x_center y_center width height且坐标全部归一化到 0~1。这里有个比较麻烦的细节NEU-DET 原始标注里有一部分目标的标注框非常小甚至有的目标在 200×200 的图上只占几个像素。如果你直接拿原始标注去训练模型很容易学不好小目标。后面我会讲怎么处理这个问题。还有一个值得注意的地方官方标注不是特别干净。比如某些 Scratches 目标一个连续的划痕可能只框了一部分有些图像中同一片区域同时存在两种缺陷重叠的情况。我的建议是如果做毕设或竞赛可以直接用官方标注如果做实际项目最好花时间人工复核一遍标注把明显漏标、错标的挑出来修正。2.3 数据划分策略与代码实现通常按照 8:1:1 或 7:2:1 划分训练集、验证集、测试集。由于数据量只有 1800 张我建议使用 8:1:1也就是训练集 1440 张、验证集 180 张、测试集 180 张。数据划分时注意两个原则一是按照缺陷类别做分层采样保证每一类在各集合中的比例与整体一致避免某个类全挤到测试集里二是尽量不要让同一块钢板相邻位置的图像同时出现在训练集和测试集否则会高估模型性能。不过 NEU-DET 每张图是独立采集的没有明确的序列信息所以分层采样就够用了。下面是我实际使用的一个 Python 脚本用来把 VOC 格式转为 YOLO txt 格式同时完成数据集划分import os import random import xml.etree.ElementTree as ET import shutil # 类别映射顺序要和模型训练时的类别列表一致 CLASS_MAPPING { crazing: 0, inclusion: 1, patches: 2, pitted_surface: 3, rolled-in_scale: 4, scratches: 5 } src_img_dir NEU-DET/IMAGES src_xml_dir NEU-DET/ANNOTATIONS dst_img_dir dataset/images dst_label_dir dataset/labels os.makedirs(dst_img_dir, exist_okTrue) os.makedirs(dst_label_dir, exist_okTrue) # 收集所有样本并按类别分组 images [] for xml_name in os.listdir(src_xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(src_xml_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() # 读取该图包含所有类别用于分层划分 classes set() for obj in root.findall(object): cls_name obj.find(name).text classes.add(cls_name) img_name xml_name.replace(.xml, .jpg) images.append((img_name, classes)) # 分层划分按类别做8:1:1 random.seed(42) random.shuffle(images) train_imgs, val_imgs, test_imgs [], [], [] # 统计每个类别数量后按比例分配 # 简化起见这里直接用全局随机划分实践中建议类别感知采样 train_imgs images[:1440] val_imgs images[1440:1620] test_imgs images[1620:] def convert_and_copy(img_list, subset_name): with open(fdataset/{subset_name}.txt, w) as f: for img_name, _ in img_list: base_name img_name.replace(.jpg, ) # 复制图像 shutil.copy(os.path.join(src_img_dir, img_name), os.path.join(dst_img_dir, img_name)) # 转换并写入标签 xml_path os.path.join(src_xml_dir, base_name .xml) tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) label_lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in CLASS_MAPPING: continue cls_id CLASS_MAPPING[cls_name] bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) # 转换为YOLO格式并归一化 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 # 过滤掉无效框避免训练时报错 if box_w 0 or box_h 0: continue label_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) # 写入标签文件 label_path os.path.join(dst_label_dir, base_name .txt) with open(label_path, w) as lb_f: lb_f.write(\n.join(label_lines)) # 在txt中记录路径供训练使用 f.write(f{os.path.abspath(os.path.join(dst_img_dir, img_name))}\n) convert_and_copy(train_imgs, train) convert_and_copy(val_imgs, val) convert_and_copy(test_imgs, test)脚本里我故意简化了分层采样逻辑直接用随机打乱后切分对于 NEU-DET 这种类别均衡的数据集问题不大。但如果你的数据类别不均衡务必改成按类别比例采样。3. 环境搭建与数据预处理细节3.1 YOLOv5 环境配置YOLOv5 的依赖不复杂核心是 PyTorch、OpenCV 和几个基础库。环境版本上我踩过一个坑numpy版本太高会导致部分旧版本 YOLOv5 报错建议用 1.23.xopencv-python默认装最新版即可但如果是在树莓派等 ARM 设备上需要额外注意编译问题。创建虚拟环境并安装依赖conda create -n yolov5 python3.9 -y conda activate yolov5 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txtrequirements.txt在 YOLOv5 仓库根目录下直接安装就行。如果你的显卡显存不大比如 6GB 以下建议安装 CPU 版 PyTorch 先在小 batch 下调试代码确认没问题再换 GPU 版跑正式训练。我第一次跑的时候就是没注意显存直接 batch size 设为 32结果爆显存白白浪费时间。另外强烈建议开 Weights Biaseswandb或者至少把训练日志保存下来。YOLOv5 默认会把训练曲线图存在runs/train/exp*/目录下不用额外配置后面分析训练情况会用到。3.2 图像尺寸与数据增强策略NEU-DET 原始图是 200×200这个尺寸喂给网络偏小。YOLOv5 默认输入是 640×640如果你直接把 200×200 的图像 resize 到 640×640缺陷的纹理细节会有一定程度的损失尤其是麻点这类微小缺陷。我的经验是把--img参数设为 320 或 384能保留更多原始细节同时训练和推理速度更快。实测下来NEU-DET 上用 320 训练比 640 的 mAP 更高——因为 640 只是把图放大了并没有增加有效信息反而让本来就小的目标在 feature map 上更难以被正确定位。数据增强方面YOLOv5 自带的增强已经很强了mosaic、random affine、HSV 扰动、flip 等。但需要注意三个陷阱Mosaic 增强会同时把 4 张图拼接对于 200×200 的小图拼接后目标变得非常小不利于小目标学习。建议在训练后期关闭 mosaicYOLOv5 提供了--close_mosaic 10参数表示最后 10 个 epoch 关闭 mosaic 增强。HSV 扰动对灰度图像没有意义因为灰度图只有一个通道颜色扰动不会带来有效增益反而可能破坏对比度。不过 YOLOv5 内部实现里 HSV 扰动会对所有通道操作灰度图被复制成三通道后扰动仍有影响但效果有限实测影响不大。翻转增强对某些缺陷不适合。比如划痕有方向性水平划痕 vs 垂直划痕水平翻转后划痕方向变化这不一定符合实际场景。如果你做实际项目要结合产线情况考虑是否需要关闭某种翻转。我的建议配置是python train.py --img 320 --batch 16 --epochs 100 --data steel.yaml --cfg yolov5s.yaml --weights yolov5s.pt --close_mosaic 10这里的--cfg yolov5s.yaml是模型结构配置yolov5s是 YOLOv5 系列里最小的标准模型参数量约 7.2M在工业部署中性价比最高。如果追求更高精度可以换yolov5m或yolov5l但推理速度会相应下降。3.3 构造训练数据集配置文件YOLOv5 需要一个 YAML 文件来描述数据集的路径和类别信息。我这里创建一个steel.yaml# 训练集和验证集图片路径列表 train: /path/to/dataset/train.txt val: /path/to/dataset/val.txt # 类别数 nc: 6 # 类别名称顺序必须与标签文件的类别id对应 names: [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches]注意train.txt和val.txt里每一行是图片的绝对路径YOLOv5 会自动根据图片路径找到同名的 txt 标签文件。路径写错是新手最常见的报错之一如果你遇到image not found或者label not found的警告首先检查 txt 文件里的路径是不是绝对路径、文件是否存在。4. 训练过程与关键调参4.1 训练策略与超参数YOLOv5 的主要超参数包括学习率lr、权重衰减weight_decay、动量momentum、锚框大小anchors等。对于 NEU-DET 这种数据量较小的任务我的经验是学习率保持默认即可YOLOv5 使用 SGD cosine LR schedule默认lr00.01lrf0.2。如果你的模型训练后出现严重的过拟合训练 loss 持续下降但验证 mAP 不涨可以调低到lr00.005。训练轮数100 epoch 是够用的。NEU-DET 类别区分度不算特别大尤其是 Crazing 和 Patches 容易混淆。我实测在 100 epoch 时 mAP 已经趋于收敛150 epoch 收益有限还有过拟合风险。Batch size取决于显存。6GB 显存用 batch 88GB 用 batch 1612GB 以上可以尝试 batch 32。batch 太小会引入噪声但影响没那么大关键是别爆显存。预训练权重一定不要从头训练。用--weights yolov5s.pt加载在 COCO 上预训练好的权重做迁移学习。虽然 COCO 数据集和钢材缺陷领域差别很大但通用特征提取层边缘、纹理、形状等的权重是有效的。从头训练的话1800 张图远远不够mAP 会低一截。训练过程中还有一个容易被忽略的参数是--cache。这个参数可以把图像缓存到内存中大幅减少数据读取时间。对于小数据集直接用--cache ram即可速度提升非常明显。4.2 训练结果的解读Loss 曲线和指标训练完成后在runs/train/exp*/目录下会生成results.png包含 box_loss、obj_loss、cls_loss、precision、recall、mAP0.5、mAP0.5:0.95 等曲线。我跑完 100 epoch 的典型结果大致如下mAP0.5约 0.72~0.78。mAP0.5:0.95约 0.45~0.52。Precision约 0.8。Recall约 0.68。这个指标单独看不算高但在 NEU-DET 这个数据集上即便是学术界精调过的模型mAP0.5 也就在 0.8 上下。原因是这六类缺陷里Crazing网纹和 Patches斑块视觉特征相近且边界模糊Rolled-in Scale 又容易和 Inclusion 混淆类别间的类内差异和类间相似度都很高。4.3 超参数进化与二次训练如果对默认训练结果不满意YOLOv5 提供了超参数进化Hyperparameter Evolution功能——通过遗传算法在指定搜索空间内寻找更优的超参数组合。命令如下python train.py --data steel.yaml --cfg yolov5s.yaml --weights yolov5s.pt --hyp hyp.scratch-low.yaml --evolve 50这个功能会启动大量子训练任务非常耗时我一般只在数据量大、算力充足的时候才用。对于 NEU-DET 这种小数据集我建议手动调节以下三个超参数效果可能更好anchor 锚框YOLOv5 默认会根据你的数据集自动计算锚框--noautoanchor可以关闭。训练开始时会自动计算训练集中标注框的聚类结果无需额外配置。但如果某个类别目标特别小可以在models/yolov5s.yaml中手写更小的锚框。类损失权重YOLOv5 默认cls0.5如果某些类容易混淆可以调大到 1.0提升分类损失占比。代价是定位精度可能略有下降。正样本匹配阈值这个藏在代码里常规训练不用动。但如果你的小目标误检太多可以去utils/loss.py里调整anchor_t参数降低正样本匹配的 IOU 阈值让更多小目标参与训练。严格来说YOLOv5 的超参数是一个组合整体单独调一个数值不一定有效。我建议新手先跑默认参数拿到基线再每次只调整一个参数观察变化不要同时改多个否则出了问题根本不知道是哪个参数导致的。5. 模型评估与常见问题排查5.1 验证与测试评估训练完成后首先要看验证集上的指标分布是否均衡。我遇到过一种情况整体 mAP 还可以但Scratches这一类的 mAP 特别低而其他类很高。这时大概率是标注问题——训练集里划痕的框质量差或者划痕和背景对比度太低导致模型无法学习到有效特征。额外建议单独跑一下测试集python val.py --data steel.yaml --weights runs/train/exp/weights/best.pt --img 320best.pt是验证集上 mAP 最高的权重last.pt是最后一轮权重。千万别用last.pt做验证因为最后几轮可能已经过拟合验证指标反而下降。如果你想知道模型在哪张图上表现差可以加上--save-conf --save-txt参数把预测结果保存下来人眼翻一遍图片通常能直观发现常见的误检和漏检模式。5.2 推理速度从 GPU 到边缘部署很多人跑完训练就开始部署但实际项目里还要考虑推理速度。YOLOv5 提供了多种导出方式TorchScript、ONNX、TensorRT、OpenVINO、CoreML 等。python export.py --weights runs/train/exp/weights/best.pt --img 320 --include onnxONNX 是跨平台部署的通用格式适合转到树莓派或 Jetson 等边缘设备上跑。实测在树莓派 5 上部署 YOLOv5s 320 输入CPU 推理速度大约 5~8 FPS做实时检测不太够但如果对实时性要求不高比如离线质检、定时巡检完全够用。如果在 GTX 1660 这类老显卡上用 TensorRT FP16 推理YOLOv5s 320 可以达到 200 FPS 以上做产线实时检测没有问题。5.3 我踩过的几个坑这个项目我前后重跑了好几遍总结几个典型的坑给你避雷坑一灰度图训练后出现偏色阴影。因为灰度图复制成三通道时值完全一样理论上不会出现偏色。但如果你的数据集中个别图像是 JPEG 格式压缩伪影可能在边缘处产生异常导致模型学到不正确的特征。解决办法是统一格式转成 PNG并做一次简单的直方图均衡化。坑二小目标漏检严重。麻点类和部分划痕属于小目标在 320×320 的输入下原始 200×200 图缩小后目标只有几十个像素。我后来做了滑窗切图——把一张 200×200 的图像切分成四张 100×100 的图再拼接训练效果反而下降。比较有效的做法是保持 320 输入并增加小目标采样权重。坑三验证集 mAP 和测试集 mAP 相差大。这通常意味着过拟合或数据划分泄露。NEU-DET 的图像虽然理论上每张独立但同一种缺陷的纹理样式高度相似模型很容易“记住”训练集的纹理模式导致验证集表现虚高。解决方法是使用严格的数据划分并在训练时增大 dropout 或 weight decay。坑四混淆矩阵里 Crazing 和 Patches 互相混。这个属于数据集本身的问题两类缺陷在灰度图上确实很接近。我的做法是额外增加了一张图像的对比度和锐化预处理稍微缓解了混淆但没法完全消除。如果应用场景中这两类缺陷的处置策略不同建议单独训一个二分类模型专门区分它们。6. 推理可视化与结果验证训练配置好且模型评估过关后要在实际图像上做可视化推理验证。这一步非常关键很多新手只关注 mAP 数值忽略了对单张图片的预测结果进行人工检查。先跑一个简单的推理python detect.py --weights runs/train/exp/weights/best.pt --source /path/to/test/images --img 320 --conf-thres 0.25 --iou-thres 0.45这里conf-thres置信度阈值建议设定为 0.25iou-thres设定为 0.45。输出在runs/detect/exp*/目录下。人工检查时重点关注是否存在同一个小区域出现多个重叠框如果有说明 NMS 没有处理好可能是模型对相邻目标响应过强。是否存在置信度很高但实际是背景误检的情况如果有考虑是否要后处理加一个分类器过滤。是否存在漏检、尤其是小目标漏检如果有考虑是否要调低置信度阈值。我个人的习惯是在视觉验证后将所有预测结果绘制成混淆矩阵图YOLOv5 的 val.py 默认生成confusion_matrix.png从矩阵里看哪个类的召回率明显偏低再去针对性调策略。另外特别提醒一点detect.py默认输出的置信度阈值是 0.25但在部署时实际使用的阈值应该根据业务需求调整。在工业质检场景漏检的代价远大于误检所以建议把置信度阈值调低到 0.15 左右宁可多一些误检也尽量不漏检。然后再用后处理逻辑把误检过滤掉。调低阈值后误检会不会爆炸实际上 YOLOv5 的置信度输出比较可靠THRESHOLD 从 0.25 降到 0.15误检数量增加并不夸张但召回率提升明显。7. 部署到边缘设备的一些思考从 YOLOv5 训练完成到真正部署到产线或边缘设备中间还有不少事。我在标题相关热词里看到“树莓派 5 上部署自己训练的 YOLOv5 模型”和“基于 STM32 的边缘端 YOLOv5 车辆检测”顺带聊一下我的看法。树莓派 5 跑 YOLOv5s用 ONNX Runtime 或 NCNN 部署输入尺寸降到 256 或 320CPU 推理大约 5~10 FPS。如果真的要做实时视频流检测建议用 TensorRT 或者换用更轻量的模型结构比如 YOLOv5n 或 YOLOv8n。设备端算力紧张时“更高压缩率 低输入分辨率”是常规组合。至于 STM32 这类 MCU 级别部署说实话 YOLOv5 原始模型很难直接跑起来内存和算力都不够。如果非要做只能走量化裁剪到极致的路线或者换用专门为 MCU 设计的检测模型。这已经超出本文范围但方向是这样。NCNN 是我在边缘端用得最多的框架腾讯开源对 ARM 平台优化不错。YOLOv5 官方仓库支持导出 NCNN 格式python export.py --weights best.pt --img 320 --include ncnn实际部署时从 ONNX 转 NCNN 有一步要注意YOLOv5 的导出模型默认带有 NMS 后处理但在 NCNN 里执行 NMS 效率不高通常要导出不带 NMS 的版本在端侧自己写 NMS 逻辑。具体做法是使用--no-nms或--nms参数控制。8. 最后再说点实在的钢材表面缺陷检测这个方向模型只是中间一环真正的难点在数据标注质量和业务逻辑设计。NEU-DET 数据集只有 1800 张图作为学习和小型项目足够了但真要上产线起码要有上万张标注精细的真实产线数据并且要覆盖不同光照、不同轧制速度、不同钢材型号的条件。我在实际使用中发现YOLOv5 默认的权重文件对灰度图特征的提取效率并不算高。如果你把数据集的灰度信息做一次归一化预处理即把图像的均值方差对齐到 ImageNet 统计量上mAP 会有小幅提升。这个技巧在公开讨论里很少有人提但实测有效。这个项目后续还可以这样扩展尝试用 YOLOv8 或 RT-DETR 做对比实验或者把模型输出接入一个 PLC 控制的分拣系统实现缺陷自动标记和剔除。由简到繁先把本文的链路跑通后续再加功能就不会手忙脚乱了。本文还有配套的精品资源点击获取