ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

钢板表面缺陷检测:4282张VOC数据集转YOLO格式实战指南

钢板表面缺陷检测:4282张VOC数据集转YOLO格式实战指南 简介面向钢板表面缺陷检测与工业视觉目标识别场景数据集由铝型材数据集和德国DAGM数据集混合制作而成精选划伤、孔洞、焊缝三类典型缺陷并引入高斯噪声扩充样本既保留真实工业表面纹理又增强模型对噪声干扰的鲁棒性适合用于训练YOLO、SSD、Faster R-CNN等目标检测模型也可作为缺陷分类与分割研究的起步数据。压缩包共2000个文件主体为jpg图像与对应xml标签xml采用VOC格式标注缺陷位置另附1个txt说明文件整体约755MB解压后目录结构清晰便于直接接入常见深度学习框架。部分标签经手工标注修正质量较稳定能够帮助使用者快速完成数据集划分与模型迭代。目前已有1023人学习下载适合刚接触工业缺陷检测的初学者以及需要扩充样本的进阶开发者使用。1. 钢板表面缺陷数据集4282 张图能撑起什么样的缺陷检测任务想做钢板表面缺陷检测第一道坎往往不是模型而是数据。工业现场的缺陷图不能随便外传公开的钢板数据集又少得可怜很多项目就卡在没数据可练这一步。这份钢板表面缺陷数据集把划伤、孔洞、焊缝三类最常见的缺陷整理成 4282 张 jpg 图加 4282 个 xml 标签Pascal VOC 格式解压就能用。它的来源比较特殊由铝型材数据集和德国 DAGM 数据集各取对应三个类别混合制作再叠了一部分手工标注最后用高斯噪声做了数据扩充。DAGM 是机器视觉圈熟知的纹理缺陷基准集铝型材数据集提供金属表面的真实瑕疵形态两套图混在一起后背景纹理更杂模型不容易对某一种光照过拟合。文件一共 1.67G分两次上传这次下载的是第一部分。适合正在跑缺陷检测、目标检测的从业者拿现成数据先验证流程、比对模型能省掉到处找数据的功夫。2. 数据集构成与 XML 标签DAGM 和铝型材的混合场景怎么组织拿到数据集第一件事不是直接丢进训练脚本而是搞清楚这批数据是怎么拼出来的、标签里存的到底是什么。这一章把构成逻辑和 XML 结构拆开讲后面转格式、训练时就不至于对着报错信息发呆。2.1 三类缺陷样本从哪来铝型材与 DAGM 各取其三制作方说明写得很清楚铝型材数据集有 10 个类别德国 DAGM 数据集也有 10 个类别两边都挑了划伤、孔洞、焊缝这三类混合成这一份钢板缺陷集。这个选材逻辑是合理的。钢板和铝型材同属金属表面划痕形态、凹坑边界、焊缝纹理在视觉上高度相似迁移过来比从布匹、纸张这类非金属缺陷集借图靠谱得多。DAGM 是德国一个工业纹理缺陷基准集它的特点是背景纹理相对干净、缺陷区域边界规整检测头很容易收敛。但真实钢板表面有氧化皮、油渍、光照不均单靠 DAGM 训练出来的模型换个现场大概率掉点。铝型材那部分图恰好补充了金属表面的真实干扰。两类来源按类别混在一起相当于人为拉大了背景分布这对泛化是有帮助的。另外注意摘要里那句「部分标签文件手工标注」。这句话翻译一下就是这批数据不全是官方精标有一部分框是后来人补的框的贴合度参差。手工标注的框常见问题是整体偏松或者把缺陷周边的锈斑、水渍也圈进去了。这些框训练时不算致命但验证集里如果混太多松框mAP 会被拉低。我拿到手会先随机画几十张图看一遍心里有个底具体画框脚本放到第 6 章。类别分布方面摘要没给每个类具体的张数解压后用脚本统计一下是最快的。一般这种混合数据集最容易出现某个类样本特别少的情况如果划伤有 2000 张、焊缝只有 300 张训练前就要考虑要不要做类别的过采样或者针对性增强。2.2 XML 标签结构每个字段对应训练脚本里的哪个参数这批 xml 是标准 Pascal VOC 结构训练脚本里最常见的几个字段如下字段路径含义训练脚本里怎么用它folder / filename图片来源目录与文件名拼接图片路径时用 filenamesize/width原图宽度像素转 YOLO 归一化时的分母size/height原图高度像素转 YOLO 归一化时的分母size/depth通道数通常为 3预处理时确认是彩色还是灰度object/name类别名映射成 class idobject/truncated目标是否被图像边界截断可选择性过滤object/difficult是否属于难例建议过滤掉否则拖低训练稳定性object/bndboxxmin、ymin、xmax、ymax计算中心点坐标与宽高bndbox 里存的是绝对像素坐标不是归一化值。比如 xmin120y min80指的是目标左上角在原图第 120 列、第 80 行。这个细节在转 YOLO 格式时非常重要很多人漏了除以宽高直接把绝对坐标写进 txt训练出来的框就全偏到一边。手工标注的部分object 的数量不一定每个文件都一样有的图可能同时有两个缺陷就会有两个 object 块。训练脚本通常按图片遍历所有 object这个逻辑没问题但要注意同一张图如果有两个同类别目标转 YOLO 时会产生两行相同 class id 的标注这是正常的别当成重复数据删掉。2.3 文件命名规律1519.jpg 这种编号能看出什么数据集的图片名是纯数字类似 1519.jpg、1323.jpg、1301.jpg对应同名 xml。从命名跳跃能看出这是重新编号过的不是沿用的原数据集文件名。制作者大概率做过一轮筛选把模糊、漏标的图剔掉了剩下的才编成连续批次。这对使用者是好事至少说明这批数据过了人眼。按摘要给的总量和体积估算1.67G 分两次上传4282 张图平均单张三百到四百 KB分辨率通常在 512 到 1024 这个区间具体尺寸以每张 xml 里 size 字段为准。解压后 jpg 和 xml 要么平铺在同一目录要么分 annotations 和 images 两个目录以实际包内结构为准不管哪种同名对应关系是确定的。还有一点值得确认图片到底是彩色还是灰度。DAGM 官方数据里有部分是灰度纹理图铝型材那边多是彩色图混合后如果训练脚本统一按三通道读图灰度图会被复制成三通道这是兼容做法不影响框坐标。但如果脚本按单通道读彩色图就会出错所以训练前随机打印几张图的 shape 是值得做的动作。3. 把 VOC XML 转成 YOLO 格式三个转换步骤与归一化细节这一章是动手的核心。数据集给的是 xml可大多数人训练用的 YOLO 系列框架默认吃 txt 标注。不把格式统一后面全是报错。3.1 为什么大多数训练流程会先转成 YOLO txtUltralytics 的 YOLOv5、YOLOv8 默认的数据组织方式是每张图片对应一个同名 txttxt 里一行一个目标格式是class_id x_center y_center width height五个值全部归一化到 0~1 之间。VOC 的 xml 是绝对像素坐标而且一个文件里塞了多个 object 块框架没法直接读。不转格式硬训也行写个自定义 Dataset 类在读取时解析 xml但这属于给自己找活。更稳的做法是写个一次性的转换脚本把 xml 全部转成 YOLO txt后面换框架、换模型都不用再碰原始标签。转换的本质就是两件事类别名到数字 id 的映射以及绝对坐标到归一化坐标的换算。3.2 归一化坐标计算核心三行代码的逻辑YOLO 的归一化中心点公式很简单中心点 x 等于 (xmin xmax) / 2 再除以图片宽度中心点 y 同理目标宽等于 (xmax - xmin) 除以宽度高同理。除以的是原图宽高不是 416 也不是 640这是最容易错的地方。如果训练时做了 letterbox 缩放框架自己会同步变换标签不需要你提前缩。import xml.etree.ElementTree as ET def parse_voc_bbox(xml_path, class_map, skip_difficultTrue): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) if w 0 or h 0: print(f[skip] 尺寸为0: {xml_path}) return [] lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class_map: print(f[warn] 未知类别 {name}: {xml_path}) continue if skip_difficult: difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) if x2 x1 or y2 y1: print(f[warn] 非法框 {name}: {x1},{y1},{x2},{y2}) continue # 核心换算绝对像素 - 归一化中心点 归一化宽高 x_center (x1 x2) / 2.0 / w y_center (y1 y2) / 2.0 / h box_w (x2 - x1) / w box_h (y2 - y1) / h lines.append(f{class_map[name]} {x_center:.6f} f{y_center:.6f} {box_w:.6f} {box_h:.6f}) return lines这段代码里有三个值得注意的点。一是skip_difficult开关默认滤掉 difficult1 的框这类框通常是严重遮挡或模糊的难例混进训练集容易让 loss 震荡二是x2 x1的校验手工标注偶尔会画出右小于左的框不拦截的话转换出来的 width 是负数训练直接报错三是格式化输出保留六位小数足够表达亚像素精度多了也没意义。3.3 完整转换脚本与 train/val 划分单张转换逻辑有了接下来把 4282 个 xml 批量处理顺手按比例划分训练集和验证集。我一般习惯按 9:1 划分固定随机种子保证每次复现结果一致。from pathlib import Path import random class_map {scratch: 0, hole: 1, weld: 2} ann_dir Path(annotations) out_root Path(yolo_data) random.seed(42) xmls sorted(ann_dir.glob(*.xml)) random.shuffle(xmls) val_n int(len(xmls) * 0.1) val_set, train_set set(xmls[:val_n]), set(xmls[val_n:]) for split, samples in [(train, train_set), (val, val_set)]: out_dir out_root / split out_dir.mkdir(parentsTrue, exist_okTrue) image_list [] for xml_path in samples: lines parse_voc_bbox(xml_path, class_map) if not lines: continue txt_path out_dir / (xml_path.stem .txt) txt_path.write_text(\n.join(lines) \n) image_list.append(xml_path.stem .jpg) (out_root / f{split}.txt).write_text(\n.join(image_list) \n) print(f{split}: {len(image_list)} 张图)class_map 里三个键的命名以你解压后 xml 里实际的 object name 为准。如果手工标注用的是中文「划伤、孔洞、焊缝」就改成中文字符串映射别硬套英文否则会触发上面代码里的未知类别告警那些框会被静默跳过。划分完之后Ultralytics 的典型目录结构是images/train、images/val放图labels/train、labels/val放对应 txt把脚本输出的 yolo_data 里两个子目录分别挪进去即可。train.txt 和 val.txt 这种清单文件只在部分框架里用Ultralytics 其实用不到留着不碍事。提示转换完别急着删 xml。后续做数据清洗、重新划分、换成别的框架时xml 才是原始资产txt 随时可以再生成。4. 高斯噪声扩充噪声参数怎么设才不伤缺陷特征数据集制作说明里提到用了高斯噪声扩充这一章说清楚噪声在这个场景里到底帮了什么忙以及自己动手扩的时候参数怎么给。4.1 高斯噪声在缺陷检测里的作用边界工业相机在低照度、高增益条件下传感器读出噪声和暗电流噪声近似服从高斯分布。模型如果只见过干净纹理图到了现场遇到有噪点的图缺陷特征容易被当成背景跳过去。在训练集里混入带高斯噪声的样本等于提前让模型适应传感器的脾气。但噪声是把双刃剑。划伤是细长的低对比度结构孔洞是边缘锐利的小目标焊缝是区域型纹理三者对噪声的容忍度完全不同。噪声强度太大会把划伤边缘糊掉模型学到的不是缺陷特征而是噪声纹理。所以高斯噪声在缺陷检测里只能做辅助扩充不能喧宾夺主。不同 sigma 取值的效果差异很明显下面这张表是实践里的大概分档sigma 取值视觉效果对训练的影响0原图无3~5轻微颗粒感肉眼几乎不可见能提升一点鲁棒性风险极低8~12可见噪点类似低照度工业相机画面推荐的扩充区间能保住大多数缺陷边缘20 以上明显雪花感划伤、细裂纹容易被吞掉不建议sigma 指的是高斯分布的标准差也就是噪声的振幅。均值一般取 0这样加噪不会整体改变图像亮度否则模型会学到亮度偏移这种假特征。4.2 判断哪些图加过噪用方差说话拿到这份数据集你可能想知道哪些图是被高斯噪声扩出来的。肉眼看小图不一定分得清一个客观办法是取背景平坦区域算标准差。干净纹理图的平坦区域标准差通常很小加了噪声的区域会明显偏高。import cv2 img cv2.imread(1519.jpg, cv2.IMREAD_GRAYSCALE) # 选一块看起来没有缺陷的平坦背景区域 patch img[200:260, 200:260] print(f背景区域标准差: {patch.std():.2f})这个数值只是一个相对判断依据。DAGM 的干净纹理背景平坦处标准差常常在 3 以下加过噪声的图普遍到 8 以上铝型材真实表面因为本身有金属拉丝纹理区域标准差到 10~15 也正常。所以单个数值不能直接定性要把同批图横向对比明显偏高那一批基本就是加噪图。做这个检查的意义在于如果训练集里加噪图比例非常高验证集又是原始干净图会人为拉大训练和验证的分布差距评测结果会虚低。4.3 自己再扩充一份代码与参数建议如果训练过程中发现某个类别样本量明显不足可以自己再补一轮噪声扩充。常见做法是直接对原图加高斯噪声标签完全不用动因为加噪不改变目标的位置和大小。import cv2 import numpy as np def add_gaussian_noise(img_path, sigma10, seedNone): img cv2.imread(str(img_path)) if img is None: return None if seed is not None: np.random.seed(seed) # 固定种子保证可复现 noise np.random.normal(loc0.0, scalesigma, sizeimg.shape) noisy img.astype(np.float32) noise noisy np.clip(noisy, 0, 255).astype(np.uint8) # 越界像素截断 return noisy参数上loc0 保证亮度不漂移scale 就是上面表格里的 sigma建议从 8 到 12 之间取别一上来就试 25clip 到 0~255 是必须的否则 float 转 uint8 时溢出会出现奇怪的条纹伪影。扩的时候每个原始图生成一到两版带噪图就够了同一个原始图扩太多份模型会把噪声模式背下来跟过拟合是一回事。我一般不建议把扩充图直接写盘尤其数据集本身已经 1.67G再存一份加噪版很占空间。更常见的做法是在训练脚本的数据增强管线里挂一个噪声层每个 epoch 现加现用相当于每次训练看到的噪声版本都不一样鲁棒性反而更好。这份数据集自带的加噪图是固定的你拿来做基准测试没问题想进一步提点的话管线内实时加噪是更好的路子。5. 踩坑排查加载、转换、训练前最容易翻车的五个点这批数据整理得算干净但「整理过、直接能用」这句话不能全信。下面五个问题是我处理同类混合数据集时真实踩过的坑按现象到原因到解决写出来供你排查时对照。5.1 现象转换出来的 txt 类别 id 全是 0训练 loss 不收敛原因class_map 里用的类别名和 xml 里实际的 object name 对不上。比如 xml 里存的是中文「划伤」映射表里写的是 scratch代码查不到就跳过或者某些转换脚本的兜底逻辑把未知类别全写成 0 类导致所有目标都被归成划伤。解决转换前先跑一段脚本把全部 xml 的 name 字段去重打印出来用真实存在的字符串建映射表。不要凭摘要里的描述猜类别名解压后以实际文件为准。5.2 现象训练时报标签文件为空或找不到标签原因图片文件存在但对应的 txt 没生成。常见是那张图的 xml 里 bndbox 坐标非法被过滤了或者该 xml 解析时图片尺寸字段读出来是 0转换函数直接返回空列表。Ultralytics 遇到空标签不会报致命错但会跳过这张图导致图数对不上。解决转换脚本里把每次过滤的原因都打出来转换完统计原始 xml 数和生成的 txt 数数字不一致就回去看日志。我在第 3 章的代码里已经埋了告警输出跑一遍就知道哪些图被丢了。5.3 现象训练时框全偏目标在左上角框却跑到右下角原因转换时归一化的分母用错了。一种是把 center 坐标写成了 (xmax - xmin) / 2 这种宽度一半的值当中心点另一种是用 640 或 416 这种固定值当分母而不是用 xml 里 size 字段的真实宽高。手工标注的图片尺寸不统一时这种错误特别隐蔽因为部分图碰巧尺寸接近 640看起来只偏了一点点。解决严格按x_center (xmin xmax) / 2 / width来算分母必须来自同一张 xml 的 size 字段。转换完抽三张图把框画在图上肉眼确认一下就露馅了。5.4 现象1.67G 数据一次性加载内存直接爆掉原因习惯用 PIL 或 cv2 把所有图片一次性读进 list 再转 numpy4282 张图全量加载内存一两个 G 根本扛不住尤其是训练脚本里还同时存了原始图和增强图。解决改用数据加载器按 batch 流式读取Ultralytics 默认就是这么做的不需要额外配置。自己写训练循环的话用 PyTorch 的 DataLoadernum_workers 给到 4 或 8别在循环外面提前 load 全部图片。内存实在紧张就把图片路径存成 txt按行读。5.5 现象验证集 mAP 虚高换到现场数据掉点严重原因训练集里高斯噪声扩充图占比过高验证集里也混了同类噪声图模型等于在噪声风格上过拟合学到的不是缺陷本质而是噪声模式。真实现场的噪声分布和合成噪声并不完全一致换场景就现原形。解决验证集尽量用原始干净图别放加噪图训练时把 sigma 设成区间随机而不是固定值比如 5 到 15 里均匀采样让模型见过多种噪声强度而不是盯死一种。这个做法能明显缓解迁移掉点。6. 训练前最后一道自查可视化脚本替你核对标签对齐转换完格式、配好 yaml先别急着开训。花十分钟跑一遍下面这套自查能挡掉大部分低级错误。训练到一半发现标签有问题再返工成本高得多。6.1 随机抽图把框画出来看import cv2 import xml.etree.ElementTree as ET from pathlib import Path import random def draw_boxes(img_path, xml_path): img cv2.imread(str(img_path)) if img is None: print(f图片缺失: {img_path}) return tree ET.parse(xml_path) for obj in tree.getroot().iter(object): name obj.find(name).text box obj.find(bndbox) x1 int(float(box.find(xmin).text)) y1 int(float(box.find(ymin).text)) x2 int(float(box.find(xmax).text)) y2 int(float(box.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, name, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(check, img) cv2.waitKey(0) xmls list(Path(annotations).glob(*.xml)) random.seed(0) for xml_path in random.sample(xmls, 30): draw_boxes(xml_path.with_suffix(.jpg), xml_path) cv2.destroyAllWindows()这段脚本直接读取原始 xml 画框不经过转换后的 txt目的是验证原始标签本身的质量。重点看三点框是否包住完整缺陷、有没有把无关背景圈进去、类别名和图形是否匹配。手工标注的框偏松是常态但偏得离谱就要考虑清洗。6.2 统计类别分布确认没有断层from collections import Counter import xml.etree.ElementTree as ET from pathlib import Path counter Counter() empty 0 for xml_path in Path(annotations).glob(*.xml): tree ET.parse(xml_path) objs list(tree.getroot().iter(object)) if not objs: empty 1 for obj in objs: counter[obj.find(name).text] 1 print(f类别分布: {dict(counter)}) print(f无目标标签数: {empty})类别分布打印出来之后对照任务需求判断要不要做处理。如果焊缝只有一两百个框而划伤有两千个训练时少数类很容易被淹没。常见处理是复制少数类样本做几轮增强或者给损失函数加类别权重。另外无目标标签的数量也值得留意空 xml 通常是制作者标记了「无缺陷」这类图可以留作负样本但要在数据加载时明确区分别让它们混进有标签的目录里干扰训练。6.3 固定动作每次拿到外部数据都走一遍最后这步是自查的收尾也是我给自己定的规矩。第一遍按 6.1 的脚本把框画在图上人工过一遍第二遍按 6.2 的脚本看类别分布和空标签数量第三遍才是转格式开训。三步加起来不超过十五分钟但能拦住绝大多数换数据翻车。整套脚本不依赖框架xml 和 jpg 的路径改一下就能跑。后来我把这三步从手动执行改成了一段串行脚本argparse 传数据根目录进去一键输出抽样画框图和分布报告团队里的同事拿新数据也先跑这个已经成了固定习惯。也因为吃过亏从那以后我每次拿别人整理的数据集不管对方怎么保证「整理过、直接能用」都会强制走一遍这套流程。数据这东西训练前多花十分钟比训练中花三天排查要划算得多。希望帮到你训练顺利。本文还有配套的精品资源点击获取
返回列表