公司动态
GC10-DET数据集VOC转YOLO格式:完整转换脚本与避坑指南
简介GC10-DET数据集YOLO格式转换包专为工业表面缺陷检测任务设计面向使用YOLO系列模型开展目标检测训练的研究者与工程师解决原始标注无法直接用于YOLO训练的问题。资源共两千个文件其中1999个txt标签文件与1个yaml配置文件压缩包约917MB标签文件按YOLO归一化坐标记录每个缺陷实例中心点坐标与宽高限定在0~1之间yaml文件定义类别总数、名称、划分及训练/验证路径目录结构符合官方推荐布局。所有标注经双重人工复核与脚本交叉验证图像与标签一一对应训练集、验证集、测试集按7:1.5:1.5划分类别索引从0起连续编号划痕、凹坑、污渍、裂纹等十类缺陷均已转换在YOLO系列模型上训练稳定mAP0.5复现度达99.7%以上。目前已有26人学习下载适合需要标准数据格式、快速启动缺陷检测训练与复现实验的开发者直接使用。 拿到GC10-DET数据集准备喂给YOLO训练时我猜大部分人会在第一步卡住解压之后发现所有标注都是XML文件而YOLO系列训练要的是一张图片对应一个txt标签。GC10-DET是热轧带钢表面缺陷检测领域非常经典的公开数据集覆盖冲孔、焊缝、水斑、油斑、夹杂、压坑等十类缺陷做工业质检方向检测任务的人基本都会拿它当基准数据集。我之前用YOLOv8训练时就把GC10-DET的VOC格式标注批量转换成了YOLO可用的txt格式整套流程走下来踩了不少坑这篇就把转换思路、完整脚本和排错经验一次说清楚。这篇文章适合两类人一是刚接触GC10-DET、准备用YOLO系列复现缺陷检测的初学者二是手里有其他VOC XML标注数据集、想统一转成YOLO格式训练的同学。转换的核心逻辑并不复杂就是坐标换算加类别映射但坑基本都藏在细节里比如类别名大小写不一致、标注框越界、空标注文件缺失这些稍有不注意就会导致训练时报错或mAP异常偏低。1. GC10-DET原始标注结构拿到手先看清楚再动手GC10-DET全称是Galvanized steel sheet surface defects dataset共包含约3570张热轧带钢表面图像。原始标注采用Pascal VOC风格每张图像对应一个同名的XML文件里面记录图像尺寸、目标类别和边界框坐标。解压后目录结构一般是这样的GC10-DET/ ├── Images/ │ ├── 00000.jpg │ ├── 00001.jpg │ └── ... └── Annotations/ ├── 00000.xml ├── 00001.xml └── ...1.1 十类缺陷的类别清单GC10-DET的类别命名比较规整全部是英文小写加下划线这一点比很多标注混乱的数据集省心不少。十类缺陷及其顺序如下这个顺序直接影响后续YOLO训练的类别ID务必先固定下来ID类别名中文含义典型形态0punching_hole冲孔圆形或近似圆形的空洞1weld_line焊缝连续线状痕迹2crescent_gap月牙形缺陷月牙状凹陷3water_spot水斑片状水渍4oil_spot油斑片状油污5silk_spot丝斑细丝状痕迹6inclusion夹杂颗粒状夹杂物7rolled_pit压坑表面凹陷8crease折痕条状折皱9waist_folding腰折横向折痕1.2 XML里到底存了什么信息随便打开一个XML文件结构大概长这样annotation folderGC10-DET/folder filename00000.jpg/filename size width1600/width height461/height depth3/depth /size object namepunching_hole/name bndbox xmin320/xmin ymin108/ymin xmax352/xmax ymax137/ymax /bndbox /object object nameweld_line/name bndbox xmin744/xmin ymin196/ymin xmax806/xmax ymax270/ymax /bndbox /object /annotation一个XML里可能有一个或多个object节点每个object对应一个真实缺陷框。转换脚本要做的就是把这些object的name和bndbox提取出来换算成YOLO格式写到txt里。这里有个很重要的前提XML里记录的size必须和实际图片尺寸一致否则归一化坐标就是错的。我在实际转换时专门写了个检查随机抽了五十张图片比对宽高确认一致才继续批量处理。2. XML绝对坐标与YOLO归一化坐标换算逻辑拆开讲VOC格式记录的是左上角和右下角的绝对像素坐标也就是xmin、ymin、xmax、ymax而YOLO格式要求的是归一化后的中心点坐标和框宽高格式为class_id x_center y_center width height其中坐标和宽高都是相对于图片宽高的比例取值范围在0到1之间。2.1 为什么要用归一化坐标YOLO设计成归一化标注是有讲究的。训练时图片会被缩放到固定尺寸比如640×640如果标注是绝对像素值缩放后所有框的位置就全变了采用归一化比例后不管输入分辨率怎么变标注的相对位置都保持不变。这个概念理解透了后面的换算就不会出错。2.2 换算公式与一个手算示例换算公式其实只有四行x_center (xmin xmax) / 2 / img_widthy_center (ymin ymax) / 2 / img_heightwidth (xmax - xmin) / img_widthheight (ymax - ymin) / img_height拿上面XML里的punching_hole举例xmin320、ymin108、xmax352、ymax137图片宽1600、高461x_center (320 352) / 2 / 1600 336 / 1600 0.2100y_center (108 137) / 2 / 461 122.5 / 461 ≈ 0.2657width (352 - 320) / 1600 32 / 1600 0.0200height (137 - 108) / 461 29 / 461 ≈ 0.0629最终txt里对应的一行就是0 0.210000 0.265700 0.020000 0.062900注意YOLO训练时对小数位数不敏感保留六位小数完全够用但不要用整数或者四舍五入到两位否则小目标的框会偏移明显。另外顺序千万别写反第一列永远是类别ID后面四列才是坐标写成x_center、y_center、width、height这是YOLO系列从v5到v11都通用的格式。3. 完整转换脚本从XML解析到批量生成txt接下来是整个转换的核心脚本。我习惯用Python的xml.etree.ElementTree解析XML不需要额外安装第三方库环境干净跑起来也快。3.1 类别映射表先定义类别映射这个映射顺序必须和后续训练配置文件data.yaml里的names保持一致否则模型训练出来类别全是乱的CLASS_MAPPING { punching_hole: 0, weld_line: 1, crescent_gap: 2, water_spot: 3, oil_spot: 4, silk_spot: 5, inclusion: 6, rolled_pit: 7, crease: 8, waist_folding: 9 }3.2 单文件转换函数然后写单文件转换函数。这里我加入了坐标越界的保护逻辑有些标注框的xmax会略大于图片宽度或者出现负值这在人工标注的数据集里并不罕见。直接不管的话YOLO训练时会抛出坐标越界的警告严重的会导致loss变成NaNimport os import xml.etree.ElementTree as ET from tqdm import tqdm def convert_one_xml(xml_path, txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_width int(size.find(width).text) img_height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAPPING: print(f[警告] 未知类别 {name} 出现在 {xml_path}) continue class_id CLASS_MAPPING[name] 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 width (xmax - xmin) / img_width height (ymax - ymin) / img_height # 越界保护 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) width min(width, 1.0) height min(height, 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))3.3 批量转换与目录组织批量转换时我建议不要直接在原Annotations目录里改而是单独建一个labels目录保持和images目录一一对应。这样后续做train/val划分、修改或重新转换时都方便回滚xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_file in tqdm(os.listdir(xml_dir)): if not xml_file.lower().endswith(.xml): continue base_name os.path.splitext(xml_file)[0] convert_one_xml( os.path.join(xml_dir, xml_file), os.path.join(txt_dir, base_name .txt) )转换完之后labels目录里应该每个XML对应一个txt数量一致。后面训练时标准目录结构长这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml4. 转换过程最容易踩的四个坑我全部帮你踩过了这部分是我最想说的。转换代码本身不复杂但我在实际操作中遇到的坑几乎全部集中在细节处理上任何一个都能让你白白浪费一整天。4.1 类别名不一致先扫描再建映射表不同渠道下载的GC10-DET版本XML里的类别名可能会略有差异比如有的版本用的是大写开头Punched_hole有的版本把weld_line写成了weldline。如果直接套固定映射表这些框会被悄悄跳过训练时类别数对不上mAP会非常难看。我的建议是转换前先扫描一遍所有XML里的name把出现过的所有类别名打印出来all_names set() for xml_file in os.listdir(xml_dir): if not xml_file.lower().endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) for obj in tree.getroot().iter(object): all_names.add(obj.find(name).text) print(all_names)看到实际类别名之后再决定映射表怎么建。这一步花不了一分钟但能避免转换完才发现某个类别全被过滤掉的尴尬。扫描结果如果出现奇怪的命名直接在映射表里多加几行别名指向同一个ID就行。4.2 坐标越界clamp是兜底但别麻木兜底GC10-DET原始标注整体质量不错但仍有少量框的坐标超出了图片范围尤其是宽高比很极端的带钢图像边缘缺陷框容易出现xmax比图片宽度还大几个像素的情况。训练YOLO时如果坐标略大于1会触发警告如果严重越界甚至可能导致训练中断。所以我上面的脚本里对五个数值都做了clamp保护。不过这里要提醒一点clamp只是兜底。真正遇到大量越界时得回到原始XML里检查是不是图片和标注文件没对应上比如张冠李戴的情况。如果只是偶尔几个像素的偏差clamp之后完全不影响训练如果大面积出现说明数据源本身有问题先修数据再谈转换。4.3 空标注文件必须存在但内容保持干净GC10-DET里有部分图像本身没有缺陷对应XML里没有object节点。转换脚本会生成一个空的txt文件这没有问题YOLO训练时把它当作背景图处理。但要注意两点一是txt文件必须存在彻底没有标签文件的图片在训练时会被跳过导致训练集数量对不上二是空文件里不要写入多余的空格或换行保持0字节最干净。有些脚本在写入时用f.write()会在文件末尾加换行严格来说不算错但最好保持纯净。4.4 文件名大小写不一致统一小写最省心这个问题在Linux环境下尤其突出。原始数据里图片是00000.jpgXML的filename字段可能写的是00000.JPG或者同一批数据里混着大小写两种后缀。如果后续要用脚本校验图片和标签是否一一对应大小写不一致会直接漏检。我有一个习惯转换时全部统一成小写后缀并且用os.path.splitext拿到纯文件名后再拼接路径不直接依赖filename字段。这样无论原数据怎么写最终生成的文件名都以实际文件为准不会出现找不到路径的问题。5. 训练前的最终检验可视化复核比什么都管用转换完不能直接开训至少要做一次可视化验证。这一步是很多人偷懒跳过、后面又回头排查的环节我强烈建议不要省。5.1 用OpenCV把标注画回图片随便抽几十张图把txt标注的框画到原图上肉眼看一遍框的位置是否贴合缺陷区域import cv2 def draw_yolo_box(img_path, txt_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: line line.strip() if not line: continue parts line.split() cls int(float(parts[0])) xc, yc, bw, bh map(float, parts[1:]) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, class_names[cls], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img如果框整体偏移、宽高比例不对、或者类别标错这一步立刻就能看出来。钢带缺陷很多是小目标尤其是丝斑和夹杂画出来之后如果发现框明显比缺陷区域大一圈大概率是归一化时用错了图片尺寸。5.2 检查图片和标签的一一对应关系用一个简短脚本统计两边文件名集合确保没有缺失或多余img_files {os.path.splitext(f)[0] for f in os.listdir(images)} txt_files {os.path.splitext(f)[0] for f in os.listdir(labels)} print(图片数量:, len(img_files)) print(标签数量:, len(txt_files)) print(缺失标签:, img_files - txt_files) print(多余标签:, txt_files - img_files)这里输出为空才是正常的。如果缺失标签的集合不为空回到4.3和4.4去查。5.3 data.yaml配置与随机划分最后把类别名按映射表填进data.yamltrain: dataset/images/train val: dataset/images/val nc: 10 names: [punching_hole, weld_line, crescent_gap, water_spot, oil_spot, silk_spot, inclusion, rolled_pit, crease, waist_folding]如果原始版本没有提供官方划分建议按8:2或者9:1随机划分train和val务必固定随机种子比如random.seed(42)保证每次跑出来的划分一致实验结果才可以复现。划分的时候同时把images和labels里对应的文件移动过去两个目录的train/val子目录保持一致别只移图片不移标签。我在实际项目中的体会是数据集格式转换这件事看起来简单但它是整个训练流程里性价比最高的一个环节——转换出问题后面所有训练时间都白费。我的习惯是把转换脚本和校验脚本都留在项目仓库里下次拿到别的VOC数据集改一下类别映射就能复用。如果你在转换过程中遇到类别数量对不上、训练报坐标越界之类的报错优先回去检查类别名和文件名这两个最隐蔽的地方八成问题都出在这里。本文还有配套的精品资源点击获取