简介:本资源为NEU-DET钢材表面缺陷检测数据集,面向从事工业质检、缺陷识别与深度学习目标检测的开发者及研究人员,可用于训练与验证钢材表面六类缺陷的检测模型。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及对应的xml、txt标注文件,方便直接接入主流检测框架。压缩包共2000个文件,以1799个xml标注文件和201个txt文件为主,整体约63.1MB,采用7z格式打包。标注类别涵盖crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches六类,总标注框数达4186个,各类别框数分布清晰,便于分析类别均衡性。目前已有1156人学习下载,适合需要快速开展钢材缺陷检测实验、复现基线模型或进行数据增强与模型对比的读者使用。
1. NEU-DET 钢材表面缺陷数据集:1799 张图、6 类缺陷,VOC 与 YOLO 双格式到底怎么用
拿到「NEU-DET钢材表面缺陷数据集VOC+YOLO格式1799张6类别」这个标题,很多人第一反应是直接解压丢进 YOLOv8 训练脚本,结果跑完一看 mAP 惨不忍睹,回头才发现标注格式没对齐、类别名对不上、图片和标签数量差了几张。NEU-DET 是东北大学发布的钢材表面缺陷检测基准数据集,6 类缺陷分别是裂纹(crazing)、夹杂(inclusion)、斑块(patches)、点蚀(pitted_surface)、氧化铁皮压入(rolled-in_scale)、划痕(scratches),共 1799 张灰度图。这个规模在缺陷检测里属于「小而精」——够你跑通全流程、验证改进思路,但绝不够你从零训一个工业级模型。它同时提供 VOC 和 YOLO 两种标注格式,本质是同一批图的两套标注映射,VOC 用 XML 存绝对坐标,YOLO 用 txt 存归一化中心点坐标。这篇文章面向的是想用这份数据集做钢材缺陷检测落地验证的工程师:从格式差异、目录结构、转换脚本,到 YOLOv8 训练参数、置信度门限调整、混淆矩阵排查,一步步讲清楚。如果你正在找一份能快速上手、又不会在格式上翻车的缺陷检测数据集,NEU-DET 是个合适的起点,但前提是你得先把下面这些坑填了。
2. VOC 与 YOLO 双格式拆解:目录结构、标注差异与选型理由
2.1 两种格式的标注逻辑差异
VOC 格式的核心是每张图对应一个 XML 文件,里面用<object>标签记录每个缺陷的类别名和边界框的绝对像素坐标xmin, ymin, xmax, ymax。YOLO 格式则是每张图对应一个 txt 文件,每行一个缺陷,格式为class_id x_center y_center width height,全部是相对于图像宽高的归一化值(0~1 之间)。这两种格式没有优劣之分,只有适用场景不同:VOC 更适合用 OpenCV、PIL 做可视化验证和传统图像处理流程,YOLO 格式则是 Ultralytics 系列训练脚本直接吃的输入。NEU-DET 同时给两套,省去了你自己写转换的麻烦,但也带来一个隐患——两套标注如果不同步,你根本不知道哪套是对的。常见做法是先用 VOC 的 XML 做一次可视化抽检,确认框的位置和类别没问题,再拿 YOLO 格式去训练。
2.2 解压后的目录应该长什么样
一份规范的 NEU-DET 双格式数据集,解压后通常是这样组织的:
NEU-DET/ ├── images/ # 1799 张 bmp 或 jpg 灰度图 │ ├── crazing_1.jpg │ ├── inclusion_1.jpg │ └── ... ├── annotations_voc/ # VOC 格式 XML │ ├── crazing_1.xml │ └── ... ├── labels_yolo/ # YOLO 格式 txt │ ├── crazing_1.txt │ └── ... └── classes.txt # 类别名与 id 映射classes.txt里一般是 6 行,顺序对应 YOLO 的 class_id 0~5。这个顺序必须和你的data.yaml里names列表完全一致,否则训练出来的模型会把裂纹识别成划痕,而且 loss 曲线看起来还挺正常,这就是最典型的「玄学翻车」。我一般会先跑一段脚本核对三件事:图片数量、XML 数量、txt 数量是否都是 1799;每个 XML 里的类别名是否都在 classes.txt 里;每个 txt 的 class_id 是否在 0~5 范围内。
2.3 用 Python 快速校验双格式一致性
下面这段脚本做三件事:统计文件数量、检查类别名合法性、抽查一个样本的 VOC 与 YOLO 框是否指向同一区域。
import os import xml.etree.ElementTree as ET IMG_DIR = "NEU-DET/images" VOC_DIR = "NEU-DET/annotations_voc" YOLO_DIR = "NEU-DET/labels_yolo" CLASSES = ["crazing", "inclusion", "patches", "pitted_surface", "rolled-in_scale", "scratches"] # 1. 数量核对 imgs = {os.path.splitext(f)[0] for f in os.listdir(IMG_DIR)} xmls = {os.path.splitext(f)[0] for f in os.listdir(VOC_DIR)} txts = {os.path.splitext(f)[0] for f in os.listdir(YOLO_DIR)} print("图片:", len(imgs), "XML:", len(xmls), "TXT:", len(txts)) print("缺失XML:", imgs - xmls) print("缺失TXT:", imgs - txts) # 2. 类别名合法性 bad_cls = set() for f in os.listdir(VOC_DIR): tree = ET.parse(os.path.join(VOC_DIR, f)) for obj in tree.findall("object"): name = obj.find("name").text if name not in CLASSES: bad_cls.add(name) print("非法类别名:", bad_cls) # 3. 抽查一个样本的框是否一致(VOC绝对坐标 vs YOLO归一化) sample = "crazing_1" tree = ET.parse(os.path.join(VOC_DIR, sample + ".xml")) size = tree.find("size") w, h = int(size.find("width").text), int(size.find("height").text) for obj in tree.findall("object"): bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 转成 YOLO 归一化中心点格式 cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h print(f"VOC->YOLO: {cx:.4f} {cy:.4f} {bw:.4f} {bh:.4f}") with open(os.path.join(YOLO_DIR, sample + ".txt")) as f: print("YOLO原始:", f.read().strip())这段脚本的关键参数说明:CLASSES列表必须和classes.txt以及后续data.yaml里的names三处完全一致,顺序都不能换。size节点里的宽高是 VOC 标注的基准,如果 XML 里缺size节点,归一化就会算错,这种情况在老数据集里偶尔出现,需要手动补或从图片读取。抽查样本时如果 VOC 转出来的归一化值和 YOLO 原始值对不上(误差超过 0.01),说明两套标注不是同一批生成的,必须以其中一套为准重新转换。
2.4 选型理由:什么时候用 VOC,什么时候用 YOLO
如果你的流程是「先用传统图像处理做预处理,再送进深度学习模型」,VOC 格式更方便,因为 OpenCV 读 XML 做 ROI 裁剪很直接。如果你直接用 Ultralytics YOLOv8/v5 训练,那 YOLO 格式是唯一选择,省去转换步骤。我一般会保留 VOC 作为「标注真值」用于可视化抽检,YOLO 作为「训练输入」用于迭代模型,两边定期用上面的脚本对一次,防止某次手动改标注只改了一边。NEU-DET 的 1799 张图里,6 类缺陷的分布并不均匀,划痕和裂纹样本相对多,点蚀和氧化铁皮压入偏少,这一点在划分训练集和验证集时要特别注意——不能简单随机切,否则验证集里可能某一类只有个位数样本,mAP 波动会非常大。
3. 从零跑通 YOLOv8 训练:环境配置、data.yaml 与关键参数
3.1 环境配置:Anaconda + Ultralytics 的最小依赖
热词里「yolo环境配置」「anaconda环境配置要求」是高频问题,这里给一个我实测能跑通的最小配置。Python 3.9 或 3.10 都可以,PyTorch 选和 CUDA 匹配的版本,如果只用 CPU 训练,1799 张图跑 100 轮大概要几个小时,建议至少有一张 8GB 显存的卡。
conda create -n neudet python=3.10 -y conda activate neudet # 根据你的 CUDA 版本选,下面以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python lxml安装完后用yolo checks确认环境,重点看 PyTorch 版本和 CUDA 是否可用。如果显示 CPU only,训练会慢到让你怀疑人生。ultralytics包自带 YOLOv8 的预训练权重下载逻辑,第一次训练会自动拉yolov8n.pt,不需要手动去下。如果你要用 YOLOv5,那就得单独 clone 仓库,但 YOLOv8 的 API 更简洁,本文以 v8 为主。
3.2 划分训练集与验证集:别用随机切分
NEU-DET 只有 1799 张,验证集一般留 10%~20%。但直接random.shuffle有个问题:同一类缺陷的图片可能高度相似(比如同一卷钢材连续拍摄),随机切会导致训练集和验证集里出现近乎重复的样本,验证 mAP 虚高。我一般按类别分层抽样,每类按 8:2 切,并且尽量让同一批次的图落在同一侧。下面脚本按类别分层划分并生成 YOLO 需要的目录结构。
import os, shutil, random from collections import defaultdict random.seed(42) IMG_DIR = "NEU-DET/images" LBL_DIR = "NEU-DET/labels_yolo" OUT = "neudet_yolo" VAL_RATIO = 0.2 # 按文件名前缀归类(假设文件名以类别名开头) cls_imgs = defaultdict(list) for f in os.listdir(IMG_DIR): if not f.lower().endswith((".jpg", ".bmp", ".png")): continue name = os.path.splitext(f)[0] cls = name.rsplit("_", 1)[0] # 去掉末尾编号 cls_imgs[cls].append(name) for split in ["train", "val"]: os.makedirs(f"{OUT}/images/{split}", exist_ok=True) os.makedirs(f"{OUT}/labels/{split}", exist_ok=True) for cls, names in cls_imgs.items(): random.shuffle(names) n_val = max(1, int(len(names) * VAL_RATIO)) val, train = names[:n_val], names[n_val:] for split, items in [("train", train), ("val", val)]: for n in items: src_img = None for ext in [".jpg", ".bmp", ".png"]: p = os.path.join(IMG_DIR, n + ext) if os.path.exists(p): src_img = p break shutil.copy(src_img, f"{OUT}/images/{split}/{os.path.basename(src_img)}") shutil.copy(os.path.join(LBL_DIR, n + ".txt"), f"{OUT}/labels/{split}/{n}.txt") print("划分完成")参数说明:VAL_RATIO设 0.2 是缺陷检测的常见比例,样本少于 200 的类别可以降到 0.15 保证训练量。random.seed(42)固定随机种子,方便复现。文件名前缀归类依赖命名规范,如果 NEU-DET 的文件名不是以类别名开头,就得改成读 XML 里的类别来归类。划分完一定要再统计一次每类在 train 和 val 里的数量,确认没有某一类在 val 里为 0。
3.3 data.yaml 的写法与类别顺序陷阱
data.yaml是 YOLOv8 训练的核心配置文件,路径和类别名写错是最常见的翻车点。
path: /abs/path/to/neudet_yolo train: images/train val: images/val nc: 6 names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled-in_scale 5: scratchespath必须是绝对路径,用相对路径在部分版本上会找不到文件。names的顺序必须和classes.txt、标注里的 class_id 完全一致,这是硬约束。我见过有人把names写成字典但顺序打乱,训练不报错,但推理时类别全错位,这种坑只能靠可视化抽检发现。nc是类别数,NEU-DET 是 6,写错会直接报维度不匹配。
3.4 启动训练:命令行与 Python API 两种方式
命令行方式适合快速试跑:
yolo detect train \ data=neudet_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/neudet \ name=exp1Python API 方式适合嵌入到自己的流程里:
from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.train( data="neudet_yolo/data.yaml", epochs=100, imgsz=640, batch=16, lr0=0.01, patience=20, device=0, project="runs/neudet", name="exp1" )关键参数逐个说:imgsz=640是 YOLOv8 的默认输入尺寸,NEU-DET 原图分辨率不一,训练时会自动 resize,如果缺陷特别小(比如点蚀),可以提到 800 或 1024,但显存占用会明显上升。batch=16在 8GB 显存上跑 640 尺寸基本安全,显存不够就降到 8。lr0=0.01是初始学习率,小数据集上可以降到 0.005 减少震荡。patience=20表示 20 轮验证指标不提升就早停,防止过拟合。model=yolov8n.pt是最小的预训练权重,如果你追求精度可以换yolov8s.pt或yolov8m.pt,但 1799 张图用 n 或 s 就够了,大模型反而容易过拟合。
3.5 训练过程看什么:loss 曲线与 mAP 的合理预期
训练启动后,runs/neudet/exp1/下会生成results.csv和一堆曲线图。重点看三个指标:train/box_loss是否稳定下降、val/box_loss是否跟着降(如果 train 降 val 不降就是过拟合)、metrics/mAP50是否在 30 轮后趋于稳定。NEU-DET 6 类缺陷在 YOLOv8n 上,mAP50 跑到 0.75~0.85 是正常范围,如果只有 0.3 左右,八成是类别顺序错了或者标注格式没对齐。metrics/mAP50-95会低不少,0.5 左右就算不错,因为缺陷框的定位精度本身就难做高。如果某一类的 AP 明显低于其他类,先去验证集里把这一类的预测结果可视化出来看,大概率是样本太少或者标注框画得太松。
4. 推理、评估与置信度门限:把模型真正用起来
4.1 单张图与批量推理的命令
训练完拿best.pt做推理:
yolo detect predict \ model=runs/neudet/exp1/weights/best.pt \ source=NEU-DET/images/crazing_1.jpg \ conf=0.25 \ save=True批量推理把source换成目录即可。conf=0.25是置信度门限,低于这个值的框不输出。热词里「yolo 检测 调整置信度门限」是高频需求,这个参数直接决定漏检和误检的平衡。缺陷检测场景下,漏检的代价通常比误检高,所以门限可以适当调低到 0.15~0.2,让更多疑似缺陷进入人工复核。但调太低会出一堆假框,需要配合后面的评估来定。
4.2 用验证集跑评估并生成混淆矩阵
yolo detect val \ model=runs/neudet/exp1/weights/best.pt \ data=neudet_yolo/data.yaml \ conf=0.25 \ iou=0.6 \ plots=Trueiou=0.6是 NMS 的 IoU 阈值,缺陷框重叠多的时候可以调到 0.5 减少框合并。plots=True会生成混淆矩阵和 PR 曲线。混淆矩阵是排查类别错位的利器:如果crazing大量被预测成scratches,说明这两类在特征上太像,要么加数据,要么在损失函数上做文章。热词里「yolo混淆矩阵总合不唯一」说的就是归一化方式不同导致行和列总和不一致,Ultralytics 生成的混淆矩阵默认按预测归一化,看的时候注意看对角线占比而不是绝对值。
4.3 置信度门限的实操调法
门限不是拍脑袋定的,我一般这样做:先用conf=0.25跑一遍验证集,导出每张图的预测框和置信度,然后画一条「置信度-准确率」曲线,找准确率开始明显下降的拐点。NEU-DET 上这个拐点通常在 0.2~0.3 之间。如果业务允许人工复核,就取拐点偏左的值;如果要全自动,就取偏右。下面脚本统计不同门限下的预测框数量和平均置信度。
from ultralytics import YOLO import numpy as np model = YOLO("runs/neudet/exp1/weights/best.pt") results = model.predict("neudet_yolo/images/val", conf=0.05, save=False) confs = [] for r in results: if r.boxes is not None: confs.extend(r.boxes.conf.cpu().numpy().tolist()) confs = np.array(confs) for t in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4]: print(f"conf>={t}: {np.sum(confs >= t)} 个框")conf=0.05是为了拿到尽可能多的低置信度框用于分析,实际部署时再按统计结果定门限。如果 0.1 到 0.2 之间框数量骤降,说明模型对大部分缺陷的置信度集中在 0.2 以上,门限设 0.2 比较稳。
5. 避坑与排查:NEU-DET 训练中最容易翻车的 5 个点
5.1 现象:训练 loss 正常下降但 mAP 始终为 0
原因:data.yaml里的names顺序和标注里的 class_id 不一致,或者nc写成了别的数字。模型在学,但学到的类别映射和评估时的映射对不上,导致所有预测都被判为错误。解决:用第 2.3 节的脚本核对classes.txt、data.yaml、标注文件三处的类别顺序,确保完全一致。改完重新训练,不要接着旧权重继续。
5.2 现象:验证集 mAP 很高,但拿新图推理全是假框
原因:训练集和验证集划分时没有按类别分层,验证集里混入了和训练集近乎重复的样本,导致评估虚高。NEU-DET 里同一类缺陷的图片可能来自同一批拍摄,相似度极高。解决:按第 3.2 节的分层抽样重新划分,并且人工检查验证集里是否有和训练集视觉上几乎一样的图。如果有,把它们移到训练集或直接剔除。
5.3 现象:某一类缺陷的 AP 始终为 0
原因:这一类在验证集里样本数为 0,或者标注框的宽高算出来是 0(XML 里 xmin 等于 xmax)。解决:先统计每类在 train 和 val 里的数量,确保 val 里每类至少有几个样本。再检查这一类所有 XML 的 bbox,把宽或高为 0 的标注修掉或剔除。NEU-DET 里pitted_surface和rolled-in_scale样本偏少,尤其要注意。
5.4 现象:训练到一半显存溢出(CUDA out of memory)
原因:imgsz或batch设太大,或者workers太多导致数据加载占用显存。解决:先把batch减半,再把imgsz从 640 降到 512 试。如果还不行,把workers设为 0 用主进程加载数据(慢但稳)。8GB 显存跑 640 尺寸,batch=16是上限,batch=8更保险。
5.5 现象:推理时框的位置整体偏移
原因:VOC 转 YOLO 时归一化用错了宽高基准,比如用了 XML 里的size但实际图片被 resize 过,或者 YOLO 标注本身就是错的。解决:用第 2.3 节的抽查脚本,随机抽 10 张图,把 VOC 转出来的归一化值和 YOLO 原始值逐行对比,误差超过 0.01 就说明两套标注不同步。以 VOC 为准重新生成 YOLO 标注,或者反过来,但只能选一套作为真值。
6. 小数据集上的进阶技巧:从 1799 张里榨出更多信息
NEU-DET 只有 1799 张,想进一步提升模型表现,光调参不够,得在数据和训练策略上做文章。我常用的三个手段按性价比排序:第一是数据增强,YOLOv8 默认开了 mosaic、HSV 抖动和随机翻转,但缺陷检测里灰度图的 HSV 抖动意义不大,可以关掉hsv_h/s/v,把mosaic保留(它能把 4 张图拼成一张,等效增加样本多样性),再加degrees=10做小角度旋转,因为钢材缺陷的方向不应该影响判定。第二是迁移学习的层次选择,yolov8n.pt是在 COCO 上预训练的,COCO 里没有钢材缺陷,但底层边缘和纹理特征仍然有用,所以冻结 backbone 前几轮再解冻,比从头训收敛快很多。第三是类别不平衡处理,pitted_surface和rolled-in_scale样本少,可以在data.yaml同级目录放一个hyp.yaml,把cls损失权重调高,或者用copy_paste增强把少样本类复制粘贴到其他图上。
验证改进是否有效,不能只看最终 mAP,我习惯固定一个baseline实验(比如yolov8n + 默认增强 + 100 epochs),每次只改一个变量,跑 3 个不同随机种子取平均,避免单次结果的偶然性。下面这个表格是我在 NEU-DET 上跑过的几组对照,供参考(数值是 3 次平均,硬件不同会有浮动):
| 实验 | 模型 | 增强策略 | mAP50 | mAP50-95 |
|---|---|---|---|---|
| baseline | yolov8n | 默认 | 0.79 | 0.48 |
| +旋转 | yolov8n | degrees=10 | 0.81 | 0.50 |
| +关HSV | yolov8n | hsv=0 | 0.80 | 0.49 |
| +copy_paste | yolov8n | 少样本类增强 | 0.83 | 0.52 |
| 换模型 | yolov8s | 默认 | 0.84 | 0.54 |
从表里能看出,换更大模型带来的提升和做数据增强差不多,但推理成本高不少。如果部署在边缘设备上,我倾向于用yolov8n加增强,而不是硬上yolov8s。另外,copy_paste对少样本类的提升在混淆矩阵上体现得很明显——pitted_surface的召回率能涨 5~8 个百分点,但要注意粘贴的位置不能和已有缺陷重叠,否则会引入错误标注。
最后说一个我踩过的坑:有次为了冲 mAP,把epochs设到 300,结果 150 轮后验证 loss 开始上升,mAP 反而掉了,早停没开,白跑了一百多轮。从那以后我固定patience=20,并且每 10 轮存一次 checkpoint,宁可多存几个权重,也不赌最后一轮是最好的。NEU-DET 这个规模的数据集,100 轮左右基本就到头了,再训就是过拟合。希望帮到你。
本文还有配套的精品资源,点击获取