尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GC10-DET数据集标注转换:VOC XML转YOLO txt格式完整指南

GC10-DET数据集标注转换:VOC XML转YOLO txt格式完整指南 简介GC10-DET工业表面缺陷检测数据集已转换为YOLO可用的txt标注格式面向目标检测开发者与工业视觉质检方向学习者省去自行解析XML/JSON标注、坐标归一化处理的繁琐步骤。资源共2000个文件其中1999个txt标签文件与每张原图同名按YOLO标准记录类别索引及归一化中心点、宽高坐标另有1个data.yaml文件用于声明训练集/验证集路径、10个类别名称与索引对应关系压缩包整体约916.94MB。目前已有26人学习下载。所有标签均基于原始图像宽高精确换算经双重人工复核与自动化脚本交叉验证边界框紧密贴合缺陷区域类别索引从0开始连续编号覆盖划痕、凹坑、污渍、裂纹等十类常见工业缺陷目录结构严格遵循Ultralytics YOLO规范划分比例固定为7:1.5:1.5可直接接入YOLOv5/v8/v10训练脚本适合快速复现GC10-DET缺陷检测实验、对比不同模型性能。 拿到GC10-DET数据集准备喂给YOLO训练时最先卡住我的不是模型结构而是标注格式。GC10-DET官方给的是Pascal VOC风格的XML标注而YOLO系列要的是txt格式的归一化坐标这个坎不迈过去后面全是白搭。这篇就把我实际转换的过程完整摆出来包括格式差异、坐标换算、能直接跑的Python脚本以及我踩过的几个经典坑给同样在处理这个数据集的你做个参考。1. 先把数据集的底细摸清楚VOC与YOLO的格式差异1.1 GC10-DET到底是什么数据集GC10-DET是一个工业场景下的金属表面缺陷检测数据集图片来自实际冷轧带钢产线采集环境里光照、角度都挺复杂不是那种实验室里的“干净”数据。它一共包含10类缺陷冲孔punching_hole、焊接线welding_line、月牙弯crescent_gap、水斑water_spot、油斑oil_spot、丝纹silk_spot、夹杂物inclusion、轧制氧化皮rolled_in_scale、折痕crease、疤痕waist_folding。图像总量在2300张左右分辨率普遍偏高很多在2000×1000上下缺陷框的尺寸变化极大有的细长到离谱有的小到只有几十个像素。原始压缩包解压后通常就是两个文件夹Annotations和Images一一对应。Annotations里是XML文件典型的VOC标注结构大概长这样annotation folderImages/folder filename201607190006.jpg/filename size width2048/width height1000/height depth3/depth /size object namepunching_hole/name bndbox xmin1536/xmin ymin422/ymin xmax1587/xmax ymax472/ymax /bndbox /object /annotationXML里记录了两类信息图片尺寸width、height以及每个目标框的左上角坐标xmin、ymin和右下角坐标xmax、ymax。坐标单位是像素直接对应原图上的实际位置。1.2 YOLO要的txt格式是什么样YOLO训练时需要的标注不是XML而是每个图片同名的一个txt文件。比如201607190006.jpg对应201607190006.txt。txt里每行代表一个目标框格式是class_id x_center y_center width height注意这5个值全是数字第一个是类别ID从0开始编号后四个是归一化后的坐标。什么意思呢就是所有值都被缩放到0到1之间跟图片实际分辨率无关。拿上面那个XML举例如果punching_hole映射成类别ID 0图片宽2048、高1000那么txt里的一行就是0 0.762695 0.447000 0.024902 0.050000这行的含义是目标属于类别0框中心点在图片横向76.27%、纵向44.7%的位置框宽度占整张图宽度的2.49%高度占整张图高度的5%。YOLO训练时读的就是这种标准化后的相对位置。1.3 坐标换算从(xmin,ymin,xmax,ymax)到(x_center,y_center,w,h)转换的核心就4个公式推导非常简单但很多人会栽在这里x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height建议对照原始XML手算一遍再写代码。很多转换脚本出错不是公式记错而是除错了尺寸。有一次一个朋友转换后训练一直不收敛排查半天发现他把所有的坐标都除以了图片的短边导致宽图上的标注横向坐标全部越界。这个公式里每个值除的都是对应轴上的尺寸x方向除以宽y方向除以高千万不能搞混。为什么要归一化因为YOLO内部用网格做预测目标框的偏移量、宽高缩放都是在相对坐标上计算的。归一化后不管原始图是1920×1080还是640×480标注都统一到同一尺度模型不用关心输入图片的分辨率差异。2. 转换方案的选型别再用labelimg逐个另存为2.1 手工转换为什么不现实网上很多教程会告诉你“用labelimg打开然后换格式另存为”。这个方法在只有几十张图的时候没问题但面对两千多张图、每个图可能还有多个框的情况手工操作会让你怀疑人生。我试过打开一张图-确认XML已加载-点另存为YOLO格式-再打开下一张平均一张图要15到20秒两千张就是十个小时起步而且期间人必须一直盯着屏幕极易疲劳出错。更要命的是labelimg的另存为依赖它自己维护的类别配置文件如果XML里的类名和配置文件里的不一致它会悄悄跳过或者乱标最后你得到的类别编号可能跟预期完全对不上。这种错误非常隐蔽训练时loss照样降但预测出来的框全是错的。2.2 脚本批量转换的整体思路所以我的原则是一切能脚本化的操作就别手工做。转换逻辑其实很固定本质就是“读XML-提取框坐标-归一化-写txt”完全可以写一个Python脚本一次性搞定。核心思路分四步先扫描所有XML动态收集类别名称并建立类别到ID的映射然后对每个XML读取size和object信息按上面的公式换算坐标并写入同名txt接着按比例划分训练集和验证集最后生成YOLO训练必需的data.yaml文件。这里面有个关键的工程决定类别映射不写死而是先扫一遍全量XML把所有出现过的类名收集起来再统一排序、编号。这样做的原因很简单不同渠道下载的GC10-DET数据集XML里的类名可能略有差异比如有的版本用下划线连接有的版本用短横线有的甚至直接写中文注释。写死一个列表遇到命名不一致就会崩动态收集则能自适应。3. 完整转换脚本与实操记录3.1 第一步目录准备和类别映射先把原始数据集整理成这种结构GC10-DET/ ├── Annotations/ │ ├── 201607190006.xml │ └── ... └── Images/ ├── 201607190006.jpg └── ...转换后的目标目录我习惯放在另一个文件夹里避免污染原始数据。YOLO系列ultralytics版期望的数据集结构一般是GC10-YOLO/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml这个结构里images和labels是平级目录里面的train/val子目录名字要完全一致因为YOLO会根据图片路径自动去labels目录下找同名txt。如果这里路径对不上训练时会报“Label not found”这是新手最常踩的坑。3.2 第二步XML解析与txt生成直接贴我的转换脚本核心函数其实很短import os import random import xml.etree.ElementTree as ET from pathlib import Path import shutil # 换成你自己的绝对路径 ANNOTATIONS_DIR Path(/data/GC10-DET/Annotations) IMAGES_DIR Path(/data/GC10-DET/Images) OUTPUT_DIR Path(/data/GC10-YOLO) random.seed(42) # 固定随机种子保证每次划分结果一致 # 1. 先扫描所有XML动态收集类别 def collect_classes(annotations_dir: Path): classes set() for xml_file in annotations_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() for obj in root.iter(object): name obj.find(name).text.strip() classes.add(name) return sorted(classes) # 排序保证顺序稳定 classes collect_classes(ANNOTATIONS_DIR) class_map {name: idx for idx, name in enumerate(classes)} print(类别映射) for name, idx in class_map.items(): print(f {idx}: {name}) # 2. 单个XML转YOLO txt def convert_xml_to_yolo(xml_path: Path, class_map: dict): tree ET.parse(xml_path) root tree.getroot() # 优先读XML里的size节点速度最快 img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) yolo_lines [] for obj in root.iter(object): name obj.find(name).text.strip() class_id class_map[name] 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) x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height yolo_lines.append( f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} ) return yolo_lines # 3. 批量转换 for xml_file in ANNOTATIONS_DIR.glob(*.xml): image_name xml_file.stem image_path IMAGES_DIR / f{image_name}.jpg if not image_path.exists(): print(f警告{xml_file.name} 对应的图片不存在跳过) continue lines convert_xml_to_yolo(xml_file, class_map) # 为了后面划分方便先统一写到临时目录 tmp_label_dir OUTPUT_DIR / tmp_labels tmp_label_dir.mkdir(parentsTrue, exist_okTrue) with open(tmp_label_dir / f{image_name}.txt, w) as f: f.write(\n.join(lines))这个脚本读XML用的是ElementTree标准库不需要额外装依赖。注意img_width和img_height一定要从XML的size节点读不要自己去cv2读图。原因有二一是批量处理时IO更快二是如果图片被压缩过读取出来的尺寸可能跟标注时不一致导致坐标偏差。3.3 第三步划分训练集和验证集并生成data.yaml转换完txt之后把图片和标签一起划分。注意不是把txt全部丢进一个目录就完了而是要train和val分开。划分比例我习惯用8比2GC10-DET总量两千多张训练集一千八百多张对工业缺陷检测来说基本够用。如果后续想上更重的数据增强可以适当调高验证集比例但不要低于15%。# 4. 划分训练集和验证集 all_images list(IMAGES_DIR.glob(*.jpg)) random.shuffle(all_images) val_count int(len(all_images) * 0.2) train_images all_images[val_count:] val_images all_images[:val_count] def build_dataset(images, split_name): img_out OUTPUT_DIR / images / split_name label_out OUTPUT_DIR / labels / split_name img_out.mkdir(parentsTrue, exist_okTrue) label_out.mkdir(parentsTrue, exist_okTrue) for img_path in images: label_path OUTPUT_DIR / tmp_labels / f{img_path.stem}.txt if not label_path.exists(): continue shutil.copy(img_path, img_out / img_path.name) shutil.copy(label_path, label_out / label_path.name) build_dataset(train_images, train) build_dataset(val_images, val) # 5. 生成data.yaml yaml_content ftrain: images/train val: images/val nc: {len(classes)} names: {classes} with open(OUTPUT_DIR / data.yaml, w) as f: f.write(yaml_content) print(f完成共 {len(classes)} 个类别) print(f训练集 {len(train_images)} 张验证集 {len(val_images)} 张)data.yaml里的train和val路径我用的是相对路径也就是“在数据集根目录下启动训练时的写法”。如果你习惯在别的目录下执行训练命令这里就要改成绝对路径否则会报数据集找不到。3.4 第四步转换后立即可视化验证转换完别急着开训练先花两分钟检查结果。我会写一个简单的可视化脚本随机抽几张图把txt里的框画回去肉眼看有没有偏移import cv2 import random from pathlib import Path OUTPUT_DIR Path(/data/GC10-YOLO) def draw_yolo_boxes(image_path, label_path, class_namesNone): img cv2.imread(str(image_path)) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, x_center, y_center, bw, bh map(float, parts) x1 int((x_center - bw / 2) * w) y1 int((y_center - bh / 2) * h) x2 int((x_center bw / 2) * w) y2 int((y_center bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) if class_names: cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img # 随机抽5张验证 val_images list((OUTPUT_DIR / images / val).glob(*.jpg)) sample random.sample(val_images, min(5, len(val_images))) for img_path in sample: label_path OUTPUT_DIR / labels / val / f{img_path.stem}.txt if label_path.exists(): viz draw_yolo_boxes(img_path, label_path, classes) cv2.imshow(img_path.name, viz) cv2.waitKey(0) cv2.destroyAllWindows()这一步非常值得做。我第一次转完直接用yolov8训练跑了50个epoch发现mAP只有0.2回头用可视化脚本一看很多框整体往右下偏移了半个身位原因是XML里的坐标是像素坐标但原始图被外部工具改过尺寸。可视化能让你在投入训练前就发现这类问题省下的时间远比脚本运行时间长。4. 常见问题排查与避坑清单4.1 我踩过的5个典型坑现象大概率原因解决办法训练时报“Label not found”labels目录结构不对或图片和txt不同名确认txt与jpg同名且labels/train对应images/trainloss一直在4到6之间不下降类别ID错乱模型把不同类当成同一类学检查类别映射日志确认class_map输出符合预期预测框严重偏移但看起来又像目标XML里size和实际图片尺寸不一致用PIL或cv2读图实际尺寸替换XML里的量txt文件里出现大于1的数字坐标没归一化或除错了方向宽高互换回顾公式x系列除以宽y系列除以高某张图没有对应txt转换脚本跳过了该图或原XML本身有问题单独检查XML格式确认文件路径无特殊字符还有一个非常隐蔽的坑GC10-DET部分公开版本里图片是灰度图而YOLO默认按三通道读取。虽然OpenCV读灰度图也能读进来但通道数不一致会导致后续数据加载报错或者训练时shape mismatch。稳妥的做法是在预处理阶段统一转成RGB三通道或者直接将Images全部用cv2.cvtColor转一次再存成jpg。这个操作很简单但能避免训练到一半莫名崩掉。4.2 训练前必须做的三项检查转换完数据后我强烈建议跑一遍这三项检查每一项只需要一两分钟但能过滤掉80%的后期问题。第一项检查标注文件是否有空文件和异常值。遍历所有txt统计行数、检查坐标范围如果发现某个值是负数或大于1立刻定位是哪张图哪一行回看原始XML。第二项检查图片数量和标注数量是否一一对应。用脚本对比images/train下的jpg数量和labels/train下的txt数量两边必须完全一致。多出来的、缺失的都要查清楚原因。第三项也是最重要的一项用一个小模型快速跑验证。直接拿yolov8n在转换好的数据集上训练10个epoch观察loss是否正常下降、验证集mAP是否在合理范围。这个步骤能快速暴露格式问题比直接上大模型跑半天才发现问题要划算得多。我有一次转换后跑yolov8n第1个epoch就报错错误信息提示某个坐标值非法一查发现是某个XML里的bbox宽度为0被脚本原样写进txt了加个过滤条件就好了。最后再分享一点个人经验转换数据集这件事本身不复杂但它是整个训练流程的地基地基歪了后面全白搭。我的习惯是转换脚本永远保留一份参数抽成配置文件这样以后拿到其他VOC格式的数据集比如自己标注的工业缺陷数据也能直接复用。说实话GC10-DET转完之后后续的模型选择、超参调整、导出部署都需要建立在这个数据基础上所以花半小时把转换这步做扎实非常值得。另外转完格式之后如果有余力可以顺手统计一下每个类别的目标框数量分布。GC10-DET的类别样本量本身就不均衡有的类只有一两百个框有的类框特别多。提前掌握这个分布后续做类别权重或采样策略调整时会很有帮助。这是我这次转换过程中得到的额外收获也分享给你。本文还有配套的精品资源点击获取
返回列表