NEU-DET数据集详解:从标签格式转换到YOLOv8训练实战 简介工业视觉在钢铁表面缺陷检测中扮演关键角色而高质量标注数据是算法落地的前提。目标检测任务中VOC与YOLO是两种常见的标签格式其坐标表示与文件组织方式直接影响模型训练效果。东北大学发布的NEU-DET数据集包含六类典型钢板缺陷常被用于YOLO系列算法的快速验证。本文以该数据集为案例深入解析xml与txt标签的结构差异演示坐标归一化换算及批量转换脚本并给出从数据目录划分、data.yaml配置到YOLOv8训练的完整流程。针对图像尺寸小、缺陷形态多变等特点还讨论了输入分辨率、锚框调整、数据增强与类别不均衡等调优策略帮助读者在工程实践中快速构建可靠的表面缺陷检测模型。 如果你在钢铁行业搞过一阵子机器视觉质检对东北大学的NEU-DET数据集应该不会陌生。这套钢板表面缺陷数据网上流传的版本经常标注“适用yolo全系列算法”包里同时给了yolo格式的txt标签和voc格式的xml标签。很多朋友下载完第一反应是这俩格式到底有什么区别我到底该用哪套标签去训练还有人不小心把txt和xml混着用训练出来mAP低得离谱半天找不出原因。这篇文章我就从解压数据包到完整跑通一轮YOLOv8训练把数据结构、标签字段、坐标换算和调优细节一次说清给准备拿NEU-DET做实验或者练手的朋友一个能直接照做的参考。1. 建立项目前我为什么先盯上NEU-DET这组数据1.1 钢厂质检的真实痛点缺陷种类杂、样本难收集钢材表面缺陷检测一直是工业视觉里特别典型的落地场景。热轧带钢在产线上跑的时候速度快、温度高人工肉眼全检根本不现实所以必须靠工业相机拍照再交给目标检测算法去定位和分类缺陷。但真正做过项目的人都知道算法模型能不能落地一大半取决于你有没有好的训练数据。钢厂实际生产中的缺陷类型非常多同一种缺陷在不同光照、不同表面纹理、不同轧制条件下长得完全不一样。今天看到的裂纹可能是细长一条明天可能是一团发暗的网状纹后天又可能变成几毫米的小短线。如果训练数据里没有覆盖这些形态模型上了产线就是各种漏检质检员反而更累。所以工业场景里最稀缺的往往不是算法而是带标注的缺陷样本。NEU-DET恰好把这层难题压缩成了一个能直接下载的离线包。它用6类典型缺陷、每类300张图片模拟了钢铁表面检测中比较常见的困难拿来验证yolo系列算法非常顺手。1.2 NEU-DET数据集的构成与六类缺陷NEU-DET由东北大学发布全称是东北大学表面缺陷数据库。它包含热轧带钢表面6种典型缺陷每类300张总共1800张灰度图原始分辨率大约是200x200像素。六类缺陷的中英文对照和常见缩写我整理成了一张表英文名称中文含义常见缩写Crazing裂纹CrInclusion夹杂InPatches麻点PaPitted Surface凹坑PSRolled-in Scale氧化铁皮压入RSScratches划伤Sc这些缺陷的特征差异很明显。Crazing通常是细长、不规则的裂纹Inclusion是混入金属基体的夹杂物Patches是大片暗色麻点Pitted Surface是凹凸不平的凹坑Rolled-in Scale是氧化铁皮被压入表面Scratches则是方向性很强的划痕。这个覆盖面对于缺陷检测算法来说已经足够丰富但你也不能把它想得太简单。我实际用下来这组数据有几个“个性”必须接受。第一所有图像都是灰度图没有彩色信息第二图像尺寸很小只有200x200放到yolo默认的640输入尺寸下相当于放大了3倍多第三不同类别的目标尺度差异很大Crazing这种裂纹往往细长且面积小而Patches可能覆盖大片区域。这些特点恰好倒逼你去做数据增强和参数调优而不是拿默认配置一跑就完事。1.3 为什么说它适合yolo全系列算法快速验证NEU-DET在yolo社区里被当成“benchmark玩具”不是没道理的。从YOLOv3、YOLOv5到YOLOv8、YOLO11各版本虽然网络结构改了很多次但对标签数据的要求一直很统一每张图片对应一个txt文件文件里每行写一个归一化边界框。NEU-DET的txt标签天然兼容这一套你不用写完YOLOv5的代码之后又去改YOLOv8的格式。另外1800张图的数据规模实在很小哪怕没有高端显卡用一张普通游戏卡训练一百个epoch也就是几分钟到十几分钟的事。这正好适合做两件事一是入门练习把数据准备、训练、评估、推理的全流程跑通二是算法改进实验快速对比是否加入注意力模块、是否改成anchor-free、是否加小目标检测头之类的改动有效果。我见过不少论文里的消融实验就是用NEU-DET做的因为它够小、够快、结果也够稳定。2. 解压数据包两种标签格式的目录结构和字段细节2.1 压缩包内的标准目录布局不同来源的NEU-DET压缩包目录结构不完全一样但常见版本解压后大概是这样的NEU-DET/ ├── IMAGES/ # 钢板表面缺陷图像 │ ├── NEU-Cr-1.jpg │ ├── NEU-In-1.jpg │ └── ... ├── ANNOTATIONS/ # VOC格式xml标签 │ ├── NEU-Cr-1.xml │ └── ... ├── labels/ # YOLO格式txt标签 │ ├── NEU-Cr-1.txt │ └── ... └── classes.txt # 类别清单有些版本会把txt和xml放在同一个目录有些版本会额外提供ImageSets/Main里的train.txt和val.txt划分文件。遇到这种情况我的建议是先别急着复制粘贴路径而是打开压缩包看一遍目录再用脚本统计图片数量和标签数量是否一一对应。我自己拿到一个新的检测数据集最先跑的一个检查脚本就是把图片列表和标签列表做差集。比如图片叫NEU-Cr-1.jpg标签就应该叫NEU-Cr-1.txt。如果发现某些图片没有对应标签要么删掉要么单独处理否则yolo训练时会默认把它当背景。钢板缺陷场景里漏标一个缺陷意味着把缺陷当正常模型会被带歪这一点要特别小心。2.2 VOC的xml标签到底写了什么VOC格式的xml其实是用XML记录每个目标的类别和边界框绝对坐标。打开NEU-Cr-1.xml内容大概是这个样子annotation folderNEU-DET/folder filenameNEU-Cr-1.jpg/filename size width200/width height200/height depth1/depth /size object namecrazing/name bndbox xmin51/xmin ymin89/ymin xmax113/xmax ymax141/ymax /bndbox /object /annotation这段xml里最关键的部分是size和bndbox。size里的width和height是原始图像尺寸单位是像素depth是通道数对NEU-DET来说通常是1。每个object节点对应一个目标的类别名称和边界框坐标。bndbox里的xmin、ymin、xmax、ymax都是绝对像素坐标左上角为原点向右为x正方向向下为y正方向。如果一个图里有多个缺陷xml里就会有多个object节点。用python的xml.etree.ElementTree就能很方便地解析。很多框架比如mmdetection原生支持VOC格式所以xml标签用来做框架迁移和跨库比较时很方便。2.3 YOLO的txt标签的归一化坐标和类别IDYOLO的txt标签和xml的表示方式完全不同。每行一个目标格式是class_id x_center y_center width height注意后面四个数全部是归一化到[0,1]的浮点数不是像素绝对值。坐标换算公式很简单x_center ((xmin xmax) / 2) / image_widthy_center ((ymin ymax) / 2) / image_heightwidth (xmax - xmin) / image_widthheight (ymax - ymin) / image_height类别ID是从0开始编号的整数。NEU-DET常见的类别顺序是0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled-in_scale 5: scratches这个顺序并不是绝对固定的不同发布者可能按字母序排也可能按数据集文档里的顺序排。所以训练前一定要检查classes.txt和xml里的name字段是不是对应否则类别顺序错位了模型训练出来会非常莫名其妙。这里分享一个我自己验证标签正确性的小习惯随机打开一个txt拿里面的归一化坐标反推回像素坐标。比如某一行是0 0.5 0.5 0.1 0.2而图片宽度和高度都是200那真实边界框就是xmin (0.5 - 0.1/2) * 200 90ymin (0.5 - 0.2/2) * 200 80xmax (0.5 0.1/2) * 200 110ymax (0.5 0.2/2) * 200 120如果你随手抽几张把反推出来的坐标画到图片上看基本就能判断这份标签是靠谱的还是错乱的。这个习惯花不了多少时间但能避免你在错误数据上浪费一整天。3. 标签格式互转xml转txt的完整Python脚本和坐标系换算3.1 为什么需要转换各框架要求的输入格式不同标题里这个数据包已经很贴心地同时给了txt和xml但实际工作里你一定会遇到“只有一种格式”的情况。比如bdd100k数据集给的是json标签旧版NEU-DET可能只有xml另一些工业项目可能只有coco的json标注。yolo系列要求的就是txt所以xml转txt是必备技能。哪怕是拿到已经带双格式的压缩包我也建议你亲自跑一遍转换脚本。因为你会有很大概率拿到其他数据集到时候还得转换。而且只有自己在代码里处理过一遍坐标归一化的推导才能理解为什么yolo标签里同一张图的框可以不用管图像尺寸直接用归一化坐标输入模型。3.2 脚本实现解析xml、归一化、输出txt下面这个脚本是我在实际项目里一直用的版本按需改造即可。它的功能是把一个目录下所有VOC格式xml转成yolo格式txt。import os import xml.etree.ElementTree as ET # 类别列表顺序必须与训练时的data.yaml完全一致 CLASSES [ crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches ] def xml_to_yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() image_width int(root.find(size/width).text) image_height int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f跳过未知类别: {name} in {xml_path}) continue class_id CLASSES.index(name) 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) # 归一化换算 x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_height # 防止浮点数越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_width min(max(box_width, 0.0), 1.0) box_height min(max(box_height, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) def batch_convert(xml_dir, out_dir): if not os.path.exists(out_dir): os.makedirs(out_dir) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) txt_name xml_name.replace(.xml, .txt) out_txt_path os.path.join(out_dir, txt_name) xml_to_yolo(xml_path, out_txt_path) print(f已转换: {xml_name} - {txt_name}) if __name__ __main__: batch_convert(ANNOTATIONS, labels)这个脚本有几个细节值得注意。第一我用root.find(size/width)的方式直接读取size下的width字段避免多写两层中间变量。第二未知类别的目标会被跳过而不是直接报错这样在面对部分标签类别名不规范的数据集时不会全盘崩溃。第三输出坐标保留6位小数对于200x200的图来说精度已经完全足够。3.3 转换中容易踩的坑类别顺序、图像尺寸、多目标排序转换脚本看起来简单但实际用起来有几个坑特别容易踩。第一个坑是类别顺序不一致。xml里写的是类别字符串txt里写的是类别数字。如果你在转换脚本中的CLASSES顺序和后来训练时的data.yaml里的names顺序不一致yolo训练时不会报任何错但是模型学到的类别映射完全是乱的表现就是某类的mAP极低而且你很难从loss曲线里看出来。最稳妥的做法是训练前打印几行txt里的class_id和图片上的类别对应一下确认0到底代表crazing还是patches。第二个坑是尺寸字段用错。xml里size的宽高必须用原始图像的尺寸不能用resize之后的尺寸。因为yolo格式的归一化坐标是相对于原图的如果你把resize后的尺寸填进去归一化结果就会错误。而且有些xml里可能根本没有size节点只有object那就要从图片本身读取宽高。第三个坑是多目标时不要随便排序。txt里每行一个目标顺序不影响训练。但如果你要用可视化工具画框或者和ground truth做匹配顺序可能会影响某些解析逻辑。我的习惯是转换时保留xml里目标的原始顺序不去做重排。如果xml里出现xmin等于0且ymin等于0且xmax等于0且ymax等于0这种空框要主动过滤掉否则会造成标签噪声。有些朋友还会问txt转xml是不是也有脚本逻辑其实完全相反把归一化坐标乘回图像尺寸就行。但在yolo为主的工作流里最常见的方向还是xml转txt所以我这边不展开反向脚本了。4. 从零跑通YOLOv5/YOLOv8训练NEU-DET的实操步骤4.1 数据集目录重建与train/val划分yolo系列训练时官方推荐的数据目录结构是图片和标签分离并且train和val分开放。对NEU-DET来说我们需要把它整理成下面这个样子NEU-DET/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── neu_det.yaml从1800张图中按8:2划分训练集和验证集建议做分层采样保证每个类别在train和val里都有分布。因为NEU-DET本身每类各300张所以随机划分基本也能保证类别均衡但固定随机种子还是很有必要的不然每次实验的划分都不同没法公平对比模型改进效果。下面的脚本是我常用的划分逻辑import os import random import shutil random.seed(42) image_dir IMAGES label_dir labels train_image_dir images/train val_image_dir images/val train_label_dir labels/train val_label_dir labels/val for d in [train_image_dir, val_image_dir, train_label_dir, val_label_dir]: os.makedirs(d, exist_okTrue) image_files [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(image_files) split int(len(image_files) * 0.8) train_files image_files[:split] val_files image_files[split:] def copy_file(img_name, img_dst_dir, label_dst_dir): base os.path.splitext(img_name)[0] img_src os.path.join(image_dir, img_name) label_src os.path.join(label_dir, base .txt) if not os.path.exists(label_src): print(f警告: {img_name} 缺少 {base}.txt跳过) return shutil.copy(img_src, img_dst_dir) shutil.copy(label_src, label_dst_dir) for f in train_files: copy_file(f, train_image_dir, train_label_dir) for f in val_files: copy_file(f, val_image_dir, val_label_dir) print(f训练集: {len(train_files)} 张, 验证集: {len(val_files)} 张)注意脚本里有个关键点如果某张图片缺少txt它会被跳过而不是硬复制过去。你可能会想这不就是少了一两张训练样本吗但在缺陷检测里一个真正的缺陷样本如果没标签会被默认当成背景对模型学习是负反馈所以跳过比留着更安全。另外要确认图片后缀。NEU-DET不同版本的图片可能是.jpg、.png或.bmp后缀判断逻辑要跟着改。如果你是从一个压缩包里拿到的数据先看文件后缀再写脚本。4.2 配置data.yaml并选择预训练权重数据目录整理好之后在NEU-DET目录下新建一个neu_det.yaml内容如下train: NEU-DET/images/train val: NEU-DET/images/val nc: 6 names: [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches]这里的train和val路径建议写成绝对路径或者相对你执行训练命令的目录的相对路径避免yolo解析不到目录。nc是类别数NEU-DET是6。names的列表顺序一定要和txt标签里的class_id一致否则类别就乱了。预训练权重的选择上YOLOv5可以用yolov5s.ptYOLOv8可以用yolov8n.pt或yolov8s.pt。工业场景里我一般先选nano或small版本跑通流程之后再去试大模型。NEU-DET是灰度图和预训练模型的RGB分布不完全一致但预训练权重依然比随机初始化有效得多至少能少训练很多epoch。如果你已经在别的地方用过YOLOv8会发现它还支持直接从yaml文件创建新模型那通常用于自定义网络结构的场景。对NEU-DET这种标准数据集直接用官方预训练权重做迁移学习就够了。4.3 训练命令、关键超参数和评估指标YOLOv5的训练命令是这样python train.py --data neu_det.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --project NEU_DET_yolov5YOLOv8的命令更简洁yolo detect train dataneu_det.yaml modelyolov8n.pt imgsz640 batch16 epochs100 projectNEU_DET_yolov8这里我想多说一句输入尺寸。很多教程喜欢直接抄640但NEU-DET原图只有200x200放大到640之后缺陷边缘会变模糊小裂纹反而不容易学。我实测过在300x300附近能得到不错的速度与精度平衡。但如果你要发表对比实验或者想和公开结果公平比较那就统一用640这是多数论文采用的设定。建议至少跑一组640再根据你的实际需求决定是否降低输入尺寸。训练结束后重点看这几个指标mAP0.5IoU阈值0.5下的平均精度NEU-DET上用yolov8n跑出来一般在0.7到0.8以上。mAP0.5:0.95更严格的评价指标对定位精度更敏感。Recall缺陷检测场景里漏检率往往比误检率更让人头疼所以Recall不能太低。把结果表格保存下来每类单独看AP。如果发现Crazing这类小目标AP明显低于其他类那就需要针对性做调优下面一章会展开讲。4.4 不同YOLO版本之间的通用套路不管YOLOv5、YOLOv7、YOLOv8还是YOLO11训练流程基本是同一套准备images和labels两个目录、写data.yaml、选预训练权重、运行训练脚本。你只需要换用相应的官方仓库或pip包命令格式稍作调优即可。因为yolo的txt标签格式从v3开始就没怎么变过NEU-DET里的txt标签到哪个版本都能用。如果你切换到mmdetection或detectron2这类框架它们往往要求coco格式或自定义格式那就得再转换一次。这时候压缩包里带的voc xml标签就派上用场了因为mmdetection原生支持VOC格式解析起来比coco json更直观。所以标题里强调“双标签”的价值就在这里兼容面广换框架时不用从头找标签。5. 训练时避不开的调优细节与小目标缺陷处理建议5.1 小图放大后模糊输入分辨率与锚框的取舍NEU-DET的200x200图像分辨率和真实工业相机拍到的钢板表面图像相比其实是偏小的。很多钢板表面缺陷的宽高只有几个像素到几十个像素放到200x200的图里就已经很勉强再放大到640后边界模糊的问题会被放大。所以训练时首先要弄清楚你最终部署的输入尺寸是多少。如果部署端相机采集的原图就是小图那训练和推理最好使用同一个输入尺寸不要训练用640、推理用256那样会出现明显的精度损失。锚框方面如果你用YOLOv5可以开启--autoanchor它会根据你的训练数据自动重新聚类锚框。YOLOv8默认会自动计算但你可以通过日志里输出的锚框信息确认它有没有更新。对NEU-DET这种小图默认锚框往往偏向中等尺寸重新聚类后会更贴合裂纹、划伤这类细长目标。如果你有精力做算法改进小目标检测头是一个热门方向。在YOLOv8的head部分增加一个面向小目标的P2检测层虽然会增加计算量但对小裂纹的召回率通常有帮助。NEU-DET正好适合这种实验因为它图像小P2层带来的额外计算不会大到让人跑不动。5.2 灰度图与三通道输入的兼容处理NEU-DET原始图是单通道灰度图但yolo系列大多要求三通道输入。我自己最常用的处理方式是在读取图片时直接用cv2.imread默认模式它会把灰度图读成三通道BGR三个通道的值完全相同。这时候图片形状是(height, width, 3)直接喂给yolo训练没有任何问题。你不需要做额外的通道复制但如果是为了保存成显式三通道图片可以这样写import cv2 img_gray cv2.imread(NEU-Cr-1.jpg, cv2.IMREAD_GRAYSCALE) img_3c cv2.merge([img_gray, img_gray, img_gray]) cv2.imwrite(NEU-Cr-1_3c.jpg, img_3c)需要注意的一点是千万不要在读取的时候用IMREAD_GRAYSCALE强制变成单通道再扔给yolo。很多模型的输入层写死了三通道单通道图片会报错或者导致通道数不匹配。灰度图对预训练模型的影响也要有预期。预训练权重是在ImageNet这种多彩图像上训练的灰度图等于丢失了色彩通道上的多样性但钢铁表面的缺陷本身就和颜色关系不大纹理和亮度反差才是关键。实际测试下来用预训练权重微调依然比随机初始化效果好很多。如果你想做得更精细可以尝试把灰度图转成伪彩色图比如用颜色映射表增强纹理对比不过这样会引入额外的颜色假设效果并不一定稳定。5.3 数据增强策略和类别不平衡的应对NEU-DET每类300张类别数量是平衡的所以不会出现某类样本极少导致模型完全学不到的情况。但各类缺陷的框数量不平衡因为有的图里有多个缺陷有的只有一两个而且缺陷尺度差异很大。这就导致模型在小尺度目标上的表现往往不如大目标。我在训练NEU-DET时会明确开启mosaic增强。mosaic会把四张图拼成一张让模型在每张训练图里同时看到更多目标也间接增加了小目标的数量。YOLOv5和YOLOv8都默认开了mosaic但我一般会在训练后期关掉它因为mosaic生成的图片和真实产线图像风格有差异如果一直用到最后模型在真实图像上的泛化可能会受影响。如果发现某个类别的AP特别低可以采用两种方法。第一种是类别重加权在loss里给低AP类别更高的权重第二种是Copy-Paste增强把该类别的目标区域裁剪下来随机粘贴到其他图片上人为扩充样本。每种方法都要控制增强强度NEU-DET总共就1800张增强太猛容易过拟合。训练轮数也要谨慎。NEU-DET数据量小1800张图训练100个epoch就可能接近过拟合。我一般用100到200个epoch配合早停观测验证集mAP是否还在上升。如果你在做模型改进对比实验最好固定随机种子、固定batch size和优化器参数每组配置至少跑三次取平均避免因为随机性得出错误的结论。最后再分享一个我自己养成的检查习惯。训练前我会写一个脚本在原始图上把txt标签里的框画出来随机挑几十张肉眼过一遍。这个操作看起来简单却能发现很多隐藏问题比如某张图的框画到图像外了、类别对不上、或者某张图压根没有txt但背景里明显有缺陷。跑这一遍最多花十分钟但能避免你在错误标注上白训一个晚上。NEU-DET这类工业表面缺陷数据尤其需要这种检查因为生产线上留下的真实样本永远比你想象的更复杂。本文还有配套的精品资源点击获取