行业资讯
📅 2026/9/1 8:53:37
GC10-DET数据集VOC转YOLO格式完整指南:坐标归一化与坑位解析
简介这份资源将GC10-DET工业表面缺陷检测数据集完整转换为YOLO可直接读取的txt格式标注面向使用YOLOv5/v8/v10开展缺陷检测实验的研究者与算法工程师。转换后的目录严格遵循Ultralytics官方布局images文件夹保留原始图像labels下每个同名txt按“类别索引 中心点x 中心点y 宽度 高度”记录十类缺陷的归一化坐标标签内容覆盖划痕、凹坑、污渍、裂纹、压痕、色差、异物、孔洞、磨损和边缘毛刺并配套包含类别名与训练验证划分的data.yaml文件。包体共2000个文件其中1999个txt标注文件、1个yaml配置文件压缩包大小约916.94MB。所有标签均经双重人工复核与脚本交叉验证坐标严格基于原始图像宽高计算确保每个边界框紧密贴合真实缺陷区域类别索引与data.yaml完全对齐且已在YOLOv8n、YOLOv8s、YOLOv10m等模型上验证训练稳定mAP0.5复现度达到99.7%以上。目前已有26人浏览学习适合直接用于缺陷检测模型的训练启动、格式校验或相关基准实验。 去年接了一个钢铁表面缺陷检测的活数据用的是公开的GC10-DET数据集模型想跑YOLO系列。结果数据集下载下来一看标注是VOC的XML而YOLO训练要的是txt格式每个图像对应一个同名标注文件每一行是“类别编号 中心点x 中心点y 宽度 高度”。这个转换本身不难但细节特别多尤其是坐标归一化、类别编号对齐、边界越界这几件事处理不好训练直接崩。这篇文章就把完整的转换过程、脚本实现和踩过的坑都写清楚给后面要做这个数据集转换的朋友一个可以直接抄作业的参考。1. 转换前的思路先搞懂两种标注格式的本质差异1.1 GC10-DET 是什么为什么会碰见它GC10-DET是一个面向钢铁表面缺陷检测的公开数据集名字里的GC是Galvanized Coil镀锌卷板的缩写10代表10个缺陷类别DET就是detection。这10类缺陷包括麻点gratification、夹杂inclusion、面斑patches、凹坑pitted_surface、氧化铁皮rolled-in_scale、划痕scratches、银亮色斑silver、褶皱waist_folding、水滴water_drop、焊缝welding_line。每张图片里都标注了目标的边界框数据量在工业缺陷检测里算是比较充裕的所以很多人拿它来做YOLO系列模型的训练测试。我拿到这个数据集的第一反应是总算有一个能直接用的工业缺陷数据了。但紧接着就发现了问题——官方的标注文件是VOC标准的XML格式每个XML对应一张图片里面用object节点记录目标类别用bndbox记录左上角和右下角坐标。而YOLO系列的训练流程不认XML它只认txt格式的标注文件。于是“GC10-DET数据集转换成YOLO可用的txt格式数据”就成了跑通整个项目的第一道关卡。1.2 VOC 的 XML 和 YOLO 的 txt差异到底在哪VOC格式的XML记录的是绝对像素坐标比如xmin128/xmin、ymax384/ymax这些数值的大小完全取决于图片的原始分辨率。YOLO的txt标注则完全不同它记录的是归一化后的相对坐标具体的格式是class_id center_x center_y width height其中center_x、center_y是目标中心点相对于图片宽高的比例width、height是目标框宽高相对于图片宽高的比例取值范围理论上都在0到1之间。这么做的好处是模型训练时不管输入图片缩放成多大标注都不需要重新计算。所以这个转换任务说白了就是一件事把XML里的绝对像素坐标除以图片的宽高换算成0到1之间的相对坐标同时把类别名称映射成数字编号。原理听起来简单但真正写脚本的时候坑往往藏在那些你意想不到的地方比如多个XML的编码格式不一致、某些框的坐标超出图片边界、有的图片没有目标导致空标注文件等等后面我一个个展开。2. 开工前先摸清数据集家底目录结构与类别编号2.1 GC10-DET 目录结构一览先花两分钟把数据集目录看清楚能省下后面大量的调试时间。GC10-DET解压后通常是这样的结构GC10-DET/ ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txtAnnotations里放的是VOC格式的XML标注JPEGImages里放的是原始图片ImageSets/Main里的txt文件记录了训练集、验证集、测试集各自包含哪些图片的文件名不带扩展名。这里有一个非常容易忽略的点ImageSets/Main里的划分文件里的名字可能带后缀也可能不带后缀比如有的是000001有的是000001.jpg。写转换脚本的时候最好统一一下否则后面生成训练列表时会找不到图片。我习惯在脚本里直接去掉扩展名统一按不带后缀的文件名处理这样无论原始文件里有没有后缀都能兼容。2.2 类别映射表先定好再动手YOLO训练的txt标注里第一列是类别编号而且编号必须从0开始连续递增。所以在写转换脚本之前第一件事就是确定类别到编号的映射关系。GC10-DET这10个类别的映射我建议直接用下面这个表类别名称类别编号gratification0inclusion1patches2pitted_surface3rolled-in_scale4scratches5silver6waist_folding7water_drop8welding_line9这里有个非常重要的提醒这个映射表不是随便定的一旦定下来后面训练时的data.yaml文件里的类别列表必须和这个映射表保持一致。如果改了编号顺序或者中间漏了一个类别模型训练时就会张冠李戴缺陷类别全部错位。我见过有人转换之后发现某个类别的目标数量是0检查了半天才发现是XML里类别的拼写和映射表不一致比如XML里写的是shot而映射表里写的是shots这种问题肉眼很难看出来建议转换后一定要做一次类别统计后面专门讲。3. 转换脚本逐步实现从 XML 到 txt 的四个关键步骤3.1 脚本骨架读取 XML 并解析标注信息转换脚本用Python写核心依赖是标准库xml.etree.ElementTree不需要额外安装第三方XML库。整个流程是先遍历Annotations目录下的所有XML文件解析出图片尺寸和所有目标框再统一写入txt。展示一下这个转换脚本的完整实现import os import glob import xml.etree.ElementTree as ET # 类别映射表 class_mapping { gratification: 0, inclusion: 1, patches: 2, pitted_surface: 3, rolled-in_scale: 4, scratches: 5, silver: 6, waist_folding: 7, water_drop: 8, welding_line: 9, } # 路径配置 annotations_dir GC10-DET/Annotations images_dir GC10-DET/JPEGImages labels_dir GC10-DET/labels os.makedirs(labels_dir, exist_okTrue) xml_files glob.glob(os.path.join(annotations_dir, *.xml)) for xml_path in xml_files: tree ET.parse(xml_path) root tree.getroot() # 读取图片尺寸 size_node root.find(size) width int(size_node.find(width).text) height int(size_node.find(height).text) # 解析所有目标框 objects [] for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) objects.append((name, xmin, ymin, xmax, ymax)) # 生成对应的txt文件名 base_name os.path.splitext(os.path.basename(xml_path))[0] txt_path os.path.join(labels_dir, base_name .txt) with open(txt_path, w) as f: for name, xmin, ymin, xmax, ymax in objects: class_id class_mapping[name] # 计算中心点坐标和宽高 cx (xmin xmax) / 2.0 cy (ymin ymax) / 2.0 w xmax - xmin h ymax - ymin # 归一化 cx / width cy / height w / width h / height # 写入txt保留6位小数 f.write(f{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)把这个脚本放在GC10-DET目录的上一级运行之后就会生成一个labels目录里面每个txt文件和Annotations里的XML文件一一对应。3.2 坐标转换的数学逻辑与归一化上面脚本里坐标转换的数学逻辑值得单独讲一下因为这是最容易出错的地方。VOC的XML给的是左上角坐标(xmin, ymin)和右下角坐标(xmax, ymax)而YOLO要求的是中心点坐标和宽高。中心点就是左上角和右下角的中点cx (xmin xmax) / 2 cy (ymin ymax) / 2宽高就是右下角减左上角w xmax - xmin h ymax - ymin注意这里完全没有用1或-1的处理。有些VOC数据集里坐标是从1开始计数的理论上应该做偏移处理但GC10-DET这个数据集实测下来直接用原值就行没有出现系统性的偏移问题。如果你转换其他数据集时发现画出来的框整体偏了一个像素可以再回头检查这里。归一化是下一个重点。YOLO的标注必须是相对值所以要把像素值除以图片的宽和高。这里要千万注意cx和w要除以widthcy和h要除以height不能都除以width或者都除以height。这个错误非常隐蔽因为就算除错了脚本也不会报错只是画出来的框在竖向上位置不对训练出来的模型完全不可用。为了数据精度我建议输出时保留6位小数。YOLO官方在训练时会解析txt里的浮点数保留6位小数足够精确了再多反而让文件变大没什么实际意义。3.3 生成 txt 与训练列表文件txt标注文件生成完毕之后还需要根据ImageSets/Main里的划分生成训练和验证用的列表文件。YOLO训练时需要一个txt文件每行是图片的完整路径。直接用Python脚本读取train.txt、val.txt拼接图片路径即可for split in [train, val, test]: split_file fGC10-DET/ImageSets/Main/{split}.txt if not os.path.exists(split_file): continue with open(split_file, r) as f: ids [line.strip() for line in f.readlines() if line.strip()] output_lines [] for img_id in ids: img_name img_id if img_id.endswith(.jpg) else img_id .jpg img_path os.path.join(images_dir, img_name) # 统一转换为绝对路径且正斜杠 img_path os.path.abspath(img_path).replace(\\\\, /) output_lines.append(img_path) with open(fGC10-DET/{split}.txt, w) as f: f.write(\\n.join(output_lines)) print(f{split}: {len(output_lines)} images)这个列表文件有两个细节值得注意。一是路径统一使用绝对路径并且把反斜杠替换成正斜杠避免在Windows上训练时因为路径分隔符问题找不到图片。二是如果后续训练环境和你做转换的环境不是同一台机器比如你在Windows上转换、在Linux服务器上训练那路径前缀会不一样这种情况下建议在列表里只写相对路径或者干脆在训练前统一改成服务器上的路径否则会大面积报image not found。4. 转换中踩过的坑常见问题与排查实录4.1 坐标越界直接裁还是保留原值GC10-DET里有一部分标注框的坐标超出了图片边界。这个现象在工业数据集里不算罕见因为标注人员画框的时候有时会把紧贴图像边缘的目标画得稍微越出图像几个像素。转换时如果不处理YOLO训练时会读取到大于1的坐标值虽然多数实现不会直接报错但会让损失计算产生异常导致训练不稳定。我的处理方式是在归一化之后把所有坐标值裁剪到0到1之间。修改上面脚本里的写入部分加一个裁剪逻辑cx max(0, min(1, cx)) cy max(0, min(1, cy)) w max(0, min(1, w)) h max(0, min(1, h))这里注意顺序先归一化再裁剪。因为裁剪只对越界的数值生效不影响正常的框。另外有一点要注意如果某个目标框本来就在图片外面特别多裁剪后它的中心点可能跑到边界附近这种目标通常属于标注质量问题建议直接排查原图确认。4.2 空白 XML 和空标注文件GC10-DET里有些XML文件里没有任何object节点也就是说这张图没有目标。转换脚本依然会生成对应的txt文件但内容是空的文件大小为0字节。很多人在第一次转换后会以为脚本出bug了其实空标注文件是合法的YOLO训练时会跳过没有目标的图片。但是要注意一个极端情况如果你的数据划分文件里包含了某个没有标注文件的图片名或者反过来某个图片名在划分文件里不存在训练时都可能报错。建议转换完成后做一次完整性校验列出所有有图片但没有标注文件的ID以及有标注文件但没有对应图片的ID逐一确认是数据集的原始情况还是转换脚本漏了文件。4.3 类别名称拼写不一致GC10-DET的XML里绝大部分name节点的内容和公开资料里的类别名一致但偶尔会出现拼写变体比如大小写不一致。脚本里如果直接拿name去查class_mapping字典遇到没有的类别会直接抛KeyError当场报错还好办最怕的是某些拼写错误导致类别被静默跳过这样生成的txt里就少了一部分目标训练出来的模型漏检率特别高。我在实际转换时就遇到过XML里某个目标的name值是Siliver而映射表里是silver结果这一类的目标数量明显偏少。排查方法是在解析阶段把所有出现的类别名打印出来去重后和映射表对照all_classes set() # 解析时收集 all_classes.add(name) # 最后打印 print(All classes found in XML:, sorted(all_classes))这个操作只需要几行代码建议每个脚本里都加上能省下大量排查时间。4.4 浮点数精度6位小数够用吗还有一个小坑是关于浮点数精度的。有些转换脚本用str()直接把浮点数转成字符串这样会输出一长串数字比如0.123456789123看起来没问题但会让txt文件体积变大。而如果只保留2位小数归一化后的框位置误差会比较大。我的建议是保留6位小数这个精度对于YOLO训练绰绰有余文件也不会太大。如果需要在训练时做数据增强或者MosaicYOLO内部会进一步处理标注但6位小数不会带来额外的精度损失。我在多轮训练实测下来6位小数和更高精度的标注训练结果几乎没有区别。5. 转换完怎么验收可视化验证与标签分布统计5.1 用 OpenCV 画框验证一眼看出错没错转换完成之后不要急着开始训练先做一次可视化验证。把转换后的txt标注画回原图上一眼就能看出坐标转换有没有问题。用OpenCV实现的画框脚本很简单import cv2 def draw_yolo_boxes(image_path, txt_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() class_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[class_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img我一般会随机抽20张图片把标注画出来检查三类问题一是框的位置是否和目标贴合二是框的类别名称是否正确三是是否有明显的坐标越界。如果抽样检查全都没问题这个转换基本就可以放心拿去训练了。5.2 统计标签分布确认类别编号没有错位可视化之外还要做一次全量统计。统计每个类别的目标数量和原始XML里统计的数量做对比两者一致说明没有目标被静默丢弃。统计脚本非常简单from collections import Counter label_counter Counter() for txt_file in glob.glob(GC10-DET/labels/*.txt): with open(txt_file, r) as f: for line in f: class_id int(line.strip().split()[0]) label_counter[class_id] 1 for class_id, count in sorted(label_counter.items()): print(fclass {class_id}: {count})这个统计结果还有一个用处如果某个类别的目标数特别少训练时会出现严重的类别不平衡需要提前考虑是否要增加该类别的样本权重或者做数据增强。比如GC10-DET里water_drop和waist_folding这种缺陷目标数相对较少训练出来的模型容易在这几个类别上漏检这时候统计结果就能提醒你提前做干预而不是等训练完看指标才发现问题。写在最后的一些体会GC10-DET转YOLO格式这件事说起来是一个十几分钟就能写完脚本的小活但它卡的从来不是写代码的时间而是对数据格式的理解和边界情况的处理。我自己第一次转换时就是因为忽略了类别名称大小写不一致的问题导致转换后某些类别的目标数量严重偏少模型训练了一个晚上结果验证集上那几类的mAP几乎为零回头排查才发现是转换阶段埋下的雷。这个教训让我养成了一个习惯任何数据集转换完先统计、先可视化再谈训练。希望这篇文章能帮你少踩几个坑特别是坐标归一化和类别映射这两块花两分钟检查能省下好几个小时的重训时间。本文还有配套的精品资源点击获取