☰
NEU-DET钢材表面缺陷数据集详解:从标注格式到YOLO训练实战
2026/10/20 23:56:18 网站建设 项目流程

简介:钢材表面缺陷数据集NEU-DET是一份面向机器学习与计算机视觉研究者的工业质检数据集,聚焦真实轧钢场景下的表面缺陷识别,适用于目标检测、图像分类及深度学习教学实验。资源共包含2000个文件,以1800张jpg图像及对应xml标注为主,另配套1800个txt格式的YOLO标签,以及转换脚本xml2yolo.py和yaml配置文件,便于直接接入常见检测框架;压缩包整体约26.68MB,结构紧凑,适合快速下载实验。目前已有4952人浏览学习。借助这份数据,研究者可直接开展CNN等模型的训练、验证与测试,省去数据采集和标注的时间;配套脚本和配置文件也能帮助将XML标注一键转为YOLO格式,降低环境搭建门槛。无论是入门计算机视觉,还是研究钢铁制造中的缺陷检测算法,这套资源都能提供完整的实测数据支撑与便捷的上手路径。

1. 钢材表面缺陷数据集NEU-DET:1800张真实热轧带钢图,缺陷检测入门的可靠基准

做工业质检的开发者,最头疼的往往不是模型选型,而是找不到带干净标注的缺陷数据。钢材表面缺陷数据集NEU-DET是被引用最多的工业视觉公开数据集之一,1800张200×200的热轧带钢表面图像,涵盖龟裂、夹杂、斑块、麻点、氧化铁皮压入、划痕六类典型缺陷,每类300张,且都带有目标框级别的标注。它主要解决两类问题:一是给缺陷检测模型提供可直接训练的监督数据,二是作为算法对比的统一benchmark,让你在不同网络结构、不同增强策略之间有一个标准的度量尺。适合刚切入工业质检的算法工程师、做检测方向课程设计的学生,以及需要快速验证数据增强思路的研究者。这份资源不是单纯的图片包,还包括整理好的标注说明文档、格式转换脚本和训练配置模板,能帮你把从解压到跑通训练的时间压缩到半天以内。

2. 六类缺陷与目录结构:训练前必须搞清楚的标注对应关系

2.1 六类缺陷的视觉特征与易混点

NEU-DET的成功率,很大程度上取决于你对六类缺陷形态差异的熟悉程度。很多新人直接扔进训练脚本,结果发现模型分数上不去,回头查才发现是没理解缺陷本身的长相。

类别每类数量典型形态视觉特征
crazing300龟裂网状细密裂纹,像干燥泥地,对比度低
inclusion300夹杂颗粒状或块状异物,边界不规则
patches300斑块片状变色区域,边缘模糊,灰度不均
pitted_surface300麻点密集小凹坑,点状分布,面积小
rolled-in_scale300氧化铁皮压入大块暗色压入物,常有层状纹理
scratches300划痕线条状,方向各异,长宽比大

这里最容易翻车的组合是 rolled-in_scale 和 inclusion,两者都表现为“表面上多了一块东西”,仅靠单张图常让人工标注都犹豫。处理方式我放在第4章的避坑清单里。另外注意,图像原始分辨率就是200×200,缺陷目标往往只占画面的一小块,尤其是 pitted_surface 和 scratches,这对模型输入分辨率的设计有直接影响,后面会专门讲。

2.2 目录组织:Images 与 Annotations 的命名对应

解压后先别急着看图片,第一步是梳理目录结构。常见发行版通常包含两个主目录:

NEU-DET/ ├── Images/ │ ├── crazing_1.jpg │ ├── crazing_2.jpg │ ├── ... │ └── scratches_300.jpg ├── Annotations/ │ ├── crazing_1.xml │ ├── crazing_2.xml │ ├── ... │ └── scratches_300.xml ├── label_map.txt └── README.md

命名规则很直观:类别名加序号。比如 crazing_1.jpg 对应的标注文件就是同目录下的 crazing_1.xml,序号一一对应。label_map.txt 通常记录类别 ID 顺序,这个文件是后面的雷区——不同转换版本对类别排序不一样,有的按字母序,有的按官方原始顺序,如果训练脚本的类别列表与标注文件里的 class_id 对不上,整个模型就等于在乱学。

我一般拿到资源第一件事就是写一个校验脚本,抽 5 张图对比图片名和 XML 里的 filename 字段是否一致,同时打印 label_map.txt 内容。这个动作能避免后面至少一小时的排查时间。

2.3 标注格式:VOC XML 与归一化坐标的转换脚本

NEU-DET 的标注核心是矩形框,官方原始版本常见的是 MATLAB 格式的 .mat 文件,而社区大量流传的是转换后的 VOC XML 或 YOLO TXT 格式。这套资源里给出的是 VOC XML 为主、附带了 YOLO TXT 生成脚本的版本。下面是一张 crazing_1.xml 中的关键结构:

<annotation> <folder>NEU-DET</folder> <filename>crazing_1.jpg</filename> <size> <width>200</width> <height>200</height> <depth>3</depth> </size> <object> <name>crazing</name> <bndbox> <xmin>42</xmin> <ymin>55</ymin> <xmax>158</xmax> <ymax>172</ymax> </bndbox> </object> </annotation>

注意这个框的单位是像素,xmin/ymin 是左上角,xmax/ymax 是右下角,不是中心点加宽高。很多人在手写转换时容易在这一步把坐标搞错。如果你打算拿这套数据喂给 YOLO 系列,需要把像素坐标转成归一化的中心点格式。下面是转换脚本的核心函数:

import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_width=200, img_height=200, class_names=None): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text if class_names is None or name in class_names: class_id = class_names.index(name) if class_names else 0 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) # 像素坐标转归一化中心点坐标 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height # 默认原图是200x200,如果新版本尺寸变化,这里要同步改 lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines

逻辑说明:先解析 XML 根节点,遍历所有 object 元素;从 bndbox 里取出四个像素坐标值,再统一除以图像宽高做归一化。这里的关键点是必须明确 XML 中记录的是绝对像素还是已经归一化的值——NEU-DET 常见发行版中是绝对像素,因此转换时要除以 200。参数说明:img_width 和 img_height 取原图尺寸,不是模型输入尺寸;class_names 的排序必须与后续训练配置里的 names 完全一致,否则类别错位。转换后最好手工验证一下归一化坐标都在 0 到 1 之间,任何超出这个范围的值都说明原始标注或换算逻辑有问题。

3. 划分数据集并跑通训练:从分层抽样到模型配置

3.1 分层划分:确保六个类别在每个子集中均匀存在

很多教程直接 random.shuffle 然后按比例切,这对 NEU-DET 来说不算致命,但不够严谨。六个类别每类 300 张,如果随机切分,个别类别的验证集可能只分到十几张,AP 波动会非常大。正确做法是按类别做分层抽样,保证每类在训练集、验证集、测试集中的比例一致。

import os import random from collections import defaultdict random.seed(42) image_dir = "Images" train_ratio, val_ratio = 0.7, 0.2 # 测试集占比为 0.1 # 按类别分组 images = os.listdir(image_dir) grouped = defaultdict(list) for img in images: cls = img.split("_")[0] # 假设文件名是 crazing_1.jpg 这种格式 grouped[cls].append(img) train_files, val_files, test_files = [], [], [] for cls, imgs in grouped.items(): random.shuffle(imgs) n_train = int(len(imgs) * train_ratio) n_val = int(len(imgs) * val_ratio) train_files.extend(imgs[:n_train]) val_files.extend(imgs[n_train:n_train + n_val]) test_files.extend(imgs[n_train + n_val:]) # 校验每个子集的类别分布 for name, files in [("train", train_files), ("val", val_files), ("test", test_files)]: dist = defaultdict(int) for f in files: dist[f.split("_")[0]] += 1 print(name, dict(dist))

逻辑说明:先把 1800 张图按类别前缀分组,然后对每一类单独做随机洗牌和切片,最后合并三个子集。这样每个子集内部六类数量基本按 7:2:1 分布。参数说明:这里用类别前缀做分组,要求文件名必须规范,如果遇到文件名不是标准格式的情况,建议先检查图片名再跑脚本。print 部分用来快速校验,训练集每类约 210 张,验证集每类约 60 张,测试集每类约 30 张,如果某个子集里缺了一个类别,说明分组逻辑有 bug,直接停下来修。

3.2 训练配置:以 YOLO 系为例的 data.yaml 写法

NEU-DET 图像分辨率低,目标尺寸偏小,训练配置和常规 COCO 数据有明显区别。以 YOLO 系模型为例,data.yaml 需要这样定义:

path: ./NEU-DET train: train.txt val: val.txt nc: 6 names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled-in_scale 5: scratches

注意这里的 names 顺序不是随便写的,它必须与第 2.3 节转换脚本里 class_names 的顺序保持一致。如果 label_map.txt 给的顺序不同,要以标注文件里的 class_id 为准,不要自己想当然重排。训练命令建议写成:

yolo detect train data=data.yaml model=yolov8n.pt epochs=150 imgsz=256 batch=16 device=0

参数说明:imgsz 我推荐 256 而不是 640。原因有两点,一是原始图只有 200×200,放大到 640 不会增加任何真实细节,反而容易让网络去学习插值产生的伪纹理;二是 y 这个尺寸下批量能开更大,训练收敛更稳定。如果想进一步提高小目标召回,可以把 imgsz 设到 384,但 batch 要相应减半。epochs 设 150 是因为数据量不大,加上 COCO 预训练权重,一般 100 轮左右 loss 就平台期了,150 轮足够观察过拟合拐点。device=0 是单卡训练,如果显存不够,把 batch 降到 8。

3.3 数据增强:哪些变换对缺陷检测真正有效

NEU-DET 这类小目标密集场景,增强策略不是越多越好,有些增强反而会破坏缺陷的物理意义。以下是我在这套数据上调过的增强参数:

增强项推荐值理由
mosaic1.0缺陷之间拼接能提升泛化性,但对小目标可能产生截断
fliplr0.5水平翻转不改变缺陷物理意义,安全
fliplr_v0.0垂直翻转对划痕方向性有影响,不建议开
scale0.3轻微缩放模拟距离变化,不要超过 0.5
hsv_h0.01钢材表面颜色变化小,色相扰动太大会失真
translate0.1小幅平移增强鲁棒性

其中最容易踩坑的是 mosaic 和垂直翻转。mosaic 会把四张图拼在一起,如果目标恰好在拼接缝上会被裁掉一半,pitted_surface 这种小目标尤其明显,所以开了 mosaic 后最好再开一个 close_mosaic 参数,让最后 10 个 epoch 切回普通增强。垂直翻转对 scratches 类影响很大,如果业务场景里划痕只会水平分布,翻转后模型会学到错误的方向先验。参数值没有绝对的正确答案,核心逻辑是:缺陷检测任务里的增强要尽量保持缺陷的物理外观一致性。

4. 常见问题与避坑:NEU-DET 训练效果差的排查清单

4.1 坐标换算错误导致训练完全发散

现象:模型 loss 从一开始就反复震荡,mAP 始终在 0 附近,预测框位置完全不对。 原因:最常见的是把 VOC 的 xmin/ymin/xmax/ymax 像素坐标当成了中心点坐标,或者忘记除以图像宽高。另外一个隐蔽场景是 XML 里记录的图像尺寸与真实图片尺寸不一致,比如标注时宽高是 200,但某次预处理把图 resize 成了 640,坐标没有跟着缩放。 解决:我每次拿到数据先跑一遍转换脚本,并加一行断言检查归一化后的值是否都在 0 到 1 之间。如果出现负值或大于 1 的值,逐个回查原始 XML。强烈建议写一个可视化脚本,把 GT 框画在图上人工看一眼,这一步比任何 loss 曲线都直观。

4.2 类别顺序不一致导致全部预测错位

现象:训练正常、loss 正常,但打印预测结果时,预测类别总是和真实类别错一个位置,比如真实是 scratches,模型输出总是 inclusion。 原因:标注文件里的 class_id 是按 label_map.txt 的顺序编码的,但训练脚本的 names 列表用了另一个顺序,比如字母序。两者映射错位,模型学到的是“第 4 类”而不是“scratches”这个语义。 解决:在训练前定义一个唯一的类别映射表,贯穿转换脚本、data.yaml、模型推理三个环节。我的习惯是把映射表单独存成一个 JSON 文件,训练脚本和推理脚本都从同一个文件读取,禁止在多个地方手写类别列表。

4.3 图像读取后出现意外通道数

现象:cv2.imread 出来的图和网上看到的 NEU-DET 示例不一致,shape 变成了 4 通道,模型预处理直接报错。 原因:部分 BMP 版本的 NEU-DET 图像带有 alpha 通道,imread 默认行为可能保留 4 通道。另外 OpenCV 读入的是 BGR 顺序,如果模型是用 RGB 预训练权重初始化的,颜色通道颠倒会造成轻微的性能损失。 解决:统一用 cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) 转成 RGB 三通道,再保存为 jpg,避免后续每个 pipeline 都要处理通道问题。转换后检查一下 shape[2] == 3,不要只看读图不报错就继续跑。

4.4 输入尺寸过小或过大导致小目标漏检

现象:模型在验证集上 mAP 尚可,但 patches 和 pitted_surface 的 recall 极低,肉眼观察检测框偏好大目标。 原因:输入分辨率设成 128 或 640 是两个极端。128 会把麻点这类小缺陷直接抹平,640 则引入了大量插值噪声,而且 batch 减小导致梯度不稳定。 解决:优先从原始尺寸附近出发,imgsz=256 是相对平衡的点。如果小目标还是漏,配合 3.3 节说的 mosaic 和 scale 增强,或者在验证阶段用多尺度测试,把测试分辨率也调到 256 以上。

4.5 易混淆类别的区分度不足

现象:rolled-in_scale 和 inclusion、crazing 和 scratches 两两之间混淆严重,混淆矩阵里这两对都有大量交叉项。 原因:这属于数据本身的标注边界问题,不同标注者对“块状异物”和“层状压入物”的理解不同,导致 ground truth 本身就有噪声。 解决:先看标注,挑出 20 张混淆严重的图,自己重新审视标注是否合理。如果业务允许,可以把这两类合并成一个“表面异物”类别,把六分类降成五分类,任务难度立刻下降一截。如果必须保留六分类,则考虑增加这两类在损失函数里的权重,或采用困难样本挖掘策略。

5. 进阶用法:用混淆矩阵与可视化验证模型是否真的学会了

训练结束只看 mAP 远远不够,mAP 是一个被平均掉的数字,它会掩盖单个类别的失败。我通常的做法是分三步:先看混淆矩阵,再随机抽 20 张验证集图做预测可视化,最后统计每类的召回率。以下是基于验证集预测结果画混淆矩阵的脚本:

from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import numpy as np # y_true 和 y_pred 是验证集上的真实类别ID和预测类别ID cm = confusion_matrix(y_true, y_pred, labels=list(range(6))) plt.figure(figsize=(7, 6)) plt.imshow(cm, cmap="Blues") plt.colorbar() plt.xticks(list(range(6)), class_names, rotation=45) plt.yticks(list(range(6)), class_names) plt.xlabel("Predicted") plt.ylabel("True") plt.show()

逻辑说明:confusion_matrix 的 labels 参数必须显式传入 0 到 5 的完整列表,否则当某些类别在验证集中没有被预测到时,矩阵会缺行缺列。可视化部分把六类的名称对齐到坐标轴,方便直接定位混淆对。参数说明:cmap 用 Blues 只是视觉偏好,真正要读的是非对角线上的热点区域,哪两个类别靠近,模型就可能在哪个边界上挣扎。

可视化预测框的代码更直接一些,加载训练好的权重,对单张图做推理,用阈值过滤低置信度框:

from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("pitted_surface_10.jpg", conf=0.25, imgsz=256) # results[0].boxes 包含 xyxy 坐标、置信度和类别ID for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls_id = int(box.cls[0]) conf = float(box.conf[0]) print(f"{class_names[cls_id]} {conf:.2f} ({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f})")

conf=0.25 是过滤低置信度预测的阈值,imgsz 必须和训练时的输入分辨率保持一致,否则模型会对尺度变化敏感。打印出来的每条记录包含类别名、置信度和像素坐标,可以和前面 XML 里的 GT 框直接对照。如果发现 pitted_surface 的预测框明显比 GT 大或小,说明学习到的目标尺度有偏,回到第 3 章调整输入尺寸和增强参数再训一轮。

这套流程走完后,我习惯把混淆矩阵图片和每类的召回率表格存进验收报告,作为模型是否可发布的依据。从那以后,每次我跑 NEU-DET 或类似的小目标缺陷数据集,都强制先打印类别映射表和一两张预测可视化图,再进训练,这个习惯帮我挡掉了至少三次“loss 漂亮但业务跑不通”的返工。这套资源里包含了标注格式说明文档、坐标转换脚本、分层划分脚本和一份完整的训练配置模板,拿到后按本文顺序执行,可以省去大量整理数据的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询