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

资讯详情

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

SWDD钢表面缺陷数据集YOLO/VOC/COCO标注格式转换实战指南

SWDD钢表面缺陷数据集YOLO/VOC/COCO标注格式转换实战指南 做工业视觉的朋友应该都经历过这种场景好不容易找到一个合适的数据集兴奋地解压完发现图片质量不错、缺陷类型也很全结果一看标注格式——Pascal VOC的XML。想直接丢进YOLO跑个基线它要的是每张图配一个txt的YOLO格式想换成COCO预训练模型做迁移学习它又得吃一个包含images、annotations、categories三张表的JSON。SWDD这个钢表面缺陷数据集我最早拿到的时候就是这个心态。SWDD和SWDD v2是工业质检场景里比较有代表性的数据集缺陷类型贴近真实产线不少论文都拿它当基准集。这篇文章就把我从下载、整理标注到完成YOLO/VOC/COCO三种格式互相转换的整个流程写清楚代码可以直接拿走去改适合所有刚开始把公共数据集用于目标检测训练的读者。1. SWDD是什么先确认这个数据集值不值得用1.1 钢表面缺陷检测的场景与SWDD的定位钢铁产线上的表面缺陷检测看着是个找瑕疵的活实际做起来比想象中难很多。连续铸轧的带钢速度快表面光照不均匀背景还有氧化皮和反光干扰缺陷种类和形态变化也大——同一种划痕在粗轧和精轧阶段的表现完全不一样。SWDD这类数据集的价值就是把这些产线真实场景里的缺陷样本集中起来让算法工程师不用自己去现场搭采集装置也能有一个相对标准的验证环境。SWDD的缺陷类型主要集中在钢表面比较典型的几类瑕疵上比如划痕、氧化皮残留、麻点、夹杂等具体类别名会因为发布渠道不同有细微差异下载后以自带的labelmap或说明文档为准。这个数据集的定位既不是像ImageNet那样追求海量也不是像某些合成数据集那样过于干净。它的缺陷尺寸偏小、对比度偏低和真实质检场景的分布很贴近拿来验证模型在小目标低对比度场景下的表现比在Pascal VOC上刷精度更有说服力。1.2 SWDD v2相比v1的差异数据量和标注质量都升级了SWDD v2和v1之间没有那种翻天覆地的变化但改进的点都很切中要害。v2在视觉上最明显的提升是缺陷样本数量增加了尤其是那些难样本——边缘模糊的、反光严重的、被氧化皮部分遮挡的缺陷。这类样本在v1里数量偏少训练出来的模型很容易把它们漏检。v2还做了一轮标注复审把不少边界框的标注位置重新校准过。这一点在工业数据集中格外关键因为表面缺陷的边界往往不像猫狗目标那么清晰标注员的判断差异很大复审后的标注一致性明显更好。我实际对比v1和v2的感受是v2训练出来的模型在验证集上mAP提升不算夸张但漏检率明显下降尤其是小缺陷的那一档。如果你是从零开始做这个方向直接下SWDD v2就好v1的价值更多在于做消融实验时对比不同训练数据规模的影响。1.3 下载渠道、目录结构与收到压缩包后的检查清单SWDD的下载渠道比较分散没有唯一的官方入口。比较常见的是GitHub上作者项目仓库的附加材料Kaggle上也有人做过镜像另外OpenDataLab这类公共数据集平台也能搜到。无论从哪下载建议先看一眼License和引用说明。工业数据集的授权通常比学术数据集严格有些版本只允许研究用途商用前务必确认清楚。下载解压之后先别急着写训练脚本。花十分钟做三件检查能省掉后面好多坑用脚本把图片全部读一遍确认没有损坏的JPEG或PNG尤其要检查是否有通道数不统一的灰度图混在里面。数一下图片数量和标注文件数量是否对得上。公共数据集偶尔会有个别样本漏标注数量对不上时后面转格式很容易莫名报错。看一眼标注的坐标是整数还是浮点数类别名有没有带空格或特殊字符。空格和破折号这类符号在写脚本解析时很容易被忽略但确实会导致奇怪的问题。2. VOC、COCO、YOLO三种标注格式的坐标系差异格式转换的报错九成以上不是代码写错而是坐标系没想清楚。所以在接触代码之前我建议先把这三种格式的存储方式和坐标定义理清楚。2.1 VOC XML像素绝对坐标人类最友好Pascal VOC的标注是每张图一个XML文件根节点annotation下面有filename、size这些图片信息每个object节点里存类别名和bndbox。bndbox里是xmin、ymin、xmax、ymax四个值全部是像素绝对坐标左上角为原点向y轴和x轴正方向延伸。VOC格式的好处是直白打开XML就知道框在哪、有多大。缺点是不利于直接训练——大部分检测框架内部用的都是相对坐标或归一化坐标所以VOC通常需要先经过一个转换层。这也是为什么刚下到SWDD时你会发现很多tutorial都会告诉你先转成YOLO格式。2.2 COCO JSON三张表的结构化存储COCO格式不是按图片拆文件而是整个数据集一个JSON。最简结构包含三张表images每张图片的id、file_name、width、height。annotations每个标注框的id、image_id、category_id、bbox、area、iscrowd。categories类别id与类别名的映射。COCO的bbox格式是[x, y, width, height]依然是像素绝对坐标但表示的语义从VOC的左上角和右下角变成了左上角坐标加宽高。这个差别看起来小却是转换时最容易出错的地方——直接把VOC的xmin、ymin、xmax、ymax四个值原样填进COCO的bbox里检测框就完全错位了。还有一个隐藏细节COCO的category_id通常从1开始而不是从0开始。这一点在后面的转换脚本里要特别注意因为YOLO的类别索引是从0开始的。2.3 YOLO txt一行一个框的归一化坐标YOLO最流行的标注形态是每张图片对应一个同名txt文件每一行表示一个目标格式是class_id x_center y_center width height其中坐标全部做了归一化x_center和y_center是目标中心点相对于图片宽度和高度的比例width和height是目标宽高相对于图片宽高的比例取值都落在0到1之间。类别id从0开始计数。这种格式对训练框架最友好因为不需要在加载数据时再做坐标换算。但对人来说不直观想象一个框在图片里的位置还得在心里反算一遍像素坐标。2.4 一张表格看懂三者差异项目VOC XMLCOCO JSONYOLO txt文件组织每图一个XML整个数据集一个JSON每图一个txt坐标形式xmin, ymin, xmax, ymaxx, y, width, heightx_center, y_center, width, height坐标单位像素绝对坐标像素绝对坐标归一化比例0~1类别索引字符串类别名从1开始的整数ID从0开始的整数ID可读性直观结构化但较繁琐训练友好但不直观把这张表完全吃透之后再去看转换代码会顺畅很多。3. 动手转换前的第一步统一中间数据结构3.1 为什么不建议直接写三套转换代码很多人拿到标注后会直接写一个VOC转YOLO的脚本再用另一个VOC转COCO的脚本第三份数据又单独写一套。三个脚本各有各的逻辑改一个坑得找半天。我的做法是先定义一个统一的内存结构把原始标注解析成这个结构再由这个结构派生出YOLO、COCO、VOC任意一种目标格式。这样解析部分只需要写一遍输出部分各写一段独立代码后续如果还想转YOLO之外的格式只需要加一个新的输出函数。那用什么作为统一结构呢我习惯用list of dict每个dict表示一张图片的标注annotation { image_path: images/xxx.jpg, width: 640, height: 480, objects: [ {category: scratch, bbox: [xmin, ymin, xmax, ymax]}, {category: patches, bbox: [xmin, ymin, xmax, ymax]} ] }bbox统一用VOC的[xmin, ymin, xmax, ymax]像素绝对坐标作为中间表示。为什么选这个因为它不丢失信息可以无损推导出中心点格式和宽高格式反过来则不行——如果你在中间层就转成了中心点加宽高再想写回VOC的xmin就需要额外记录图片尺寸麻烦不少。3.2 解析SWDD的原始标注以XML版本为例假设你拿到的SWDD版本是VOC风格目录图片在images文件夹XML在annotations文件夹姿势如下SWDD/ ├── images/ │ ├── 001.jpg │ └── 002.jpg └── annotations/ ├── 001.xml └── 002.xml用一个基于xml.etree.ElementTree的解析函数就能读完所有标注import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) size root.find(size) width int(float(size.findtext(width))) height int(float(size.findtext(height))) objects [] for obj in root.findall(object): name obj.findtext(name).strip() box obj.find(bndbox) # 这里不强制转int因为有些标注工具导出的是float坐标 xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) objects.append({category: name, bbox: [xmin, ymin, xmax, ymax]}) return { image_path: fimages/{filename}, width: width, height: height, objects: objects }如果你的SWDD版本不是XML而是更现代的images labels的txt结构那么解析函数就换成读txt一行一行按空格或者逗号切分即可。核心不变解析出来后仍然统一塞进上面那个中间结构。有个细节值得提醒解析XML时把坐标用float而不是int。很多人图省事直接写int(box.findtext(xmin))但有些标注工具尤其半自动标注工具导出的坐标是浮点直接截断成int会引入1~2像素偏差。对于钢材表面那种小缺陷差两三个像素有时候就是边界上一整个目标的判定差异了。4. 代码实战从中间结构生成YOLO/VOC/COCO4.1 YOLO格式归一化坐标与目录组织生成YOLO格式时最容易写错的一行代码是宽高的换算。先看一个标准实现def voc_bbox_to_yolo(img_w, img_h, bbox): xmin, ymin, xmax, ymax bbox c_x (xmin xmax) / 2.0 / img_w c_y (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h return c_x, c_y, w, h有一些细节处理也是实际跑训练时才会发现的4.1.1 类别ID映射表的建立中间结构里存的是类别字符串而YOLO需要的是从0开始的整数ID。所以你要维护一个固定顺序的类别列表让字符串和ID一一对应。千万别在遍历过程中用category_set.add(name)这种临时收集方式因为set不保证顺序两次运行出来的ID对应关系可能不同训练和推理对不上就是灾难。正确做法是写死或读配置文件CLASS_NAMES [inclusion, patches, pitted_surface, rolled-in_scale, scratches] CLASS_TO_ID {name: idx for idx, name in enumerate(CLASS_NAMES)} # 后续写入txt时统一用 CLASS_TO_ID[obj[category]]SWDD具体有几个类别、名字怎么拼以你下载版本里的说明文件为准。但我强烈建议把这个列表保存成一个class_names.txt或者写进data.yaml以后每次用数据集都从这里读而不是抄在脚本里。4.1.2 YOLO目标检测的标准目录结构生成YOLO格式后推荐按Ultralytics的习惯组织目录swdd_yolo/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml对应生成脚本大致长这样import os def write_yolo_format(annotations, image_dir, label_dir): os.makedirs(image_dir, exist_okTrue) os.makedirs(label_dir, exist_okTrue) for ann in annotations: img_name os.path.basename(ann[image_path]) stem os.path.splitext(img_name)[0] label_path os.path.join(label_dir, stem .txt) with open(label_path, w, encodingutf-8) as f: for obj in ann[objects]: cat_id CLASS_TO_ID[obj[category]] c_x, c_y, w, h voc_bbox_to_yolo( ann[width], ann[height], obj[bbox] ) # 防止归一化后出现越界的小数比如 1.0000001 c_x min(max(c_x, 0.0), 1.0) c_y min(max(c_y, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) f.write(f{cat_id} {c_x:.6f} {c_y:.6f} {w:.6f} {h:.6f}\n)注意那个clip操作。坐标越界是个很隐蔽的问题某个框紧贴图片右边缘时x_center width/2在浮点计算下可能算出1.0000001写进txt后训练框架读出来就是1的越界框轻则警告重则训练损失异常。4.2 COCO JSON类别ID和bbox的细节COCO格式生成要复杂一些因为有两个ID体系image_id和annotation_id。image_id从1开始递增annotation_id也建议从1开始两者独立。import json def write_coco_format(annotations, output_path): images [] annotations_list [] ann_id 1 for img_id, ann in enumerate(annotations, start1): images.append({ id: img_id, file_name: os.path.basename(ann[image_path]), width: ann[width], height: ann[height] }) for obj in ann[objects]: xmin, ymin, xmax, ymax obj[bbox] w xmax - xmin h ymax - ymin annotations_list.append({ id: ann_id, image_id: img_id, category_id: CLASS_TO_ID[obj[category]] 1, # COCO从1开始 bbox: [xmin, ymin, w, h], area: w * h, iscrowd: 0 }) ann_id 1 categories [ {id: idx 1, name: name} for idx, name in enumerate(CLASS_NAMES) ] coco_dict { images: images, annotations: annotations_list, categories: categories } with open(output_path, w, encodingutf-8) as f: json.dump(coco_dict, f, indent2)这段代码有三个关键点第一category_id必须加1。这是COCO和YOLO之间最典型的差异很多人转完之后训练不报错但mAP一直上不去查到最后发现所有框的类别整体错了一位。第二bbox是[x, y, width, height]不是中心点格式。如果你从中间结构转出来的是VOC的xmin就需要用xmax - xmin换算宽度而不是直接把坐标赋值过去。第三没有提供segmentation字段。像MMDetection这类框架在加载COCO格式时如果只有bbox没有segmentation是可以正常训练检测模型的。除非你后续要做实例分割才需要补上polygon级别的掩码标注。4.3 VOC XML从中间结构反向生成有时候也需要往VOC格式反向转换比如你想用某些只支持VOC的标注工具修改SWDD的标注。反向生成就是解析的逆过程def write_voc_xml(ann, output_path): annotation ET.Element(annotation) ET.SubElement(annotation, folder).text SWDD ET.SubElement(annotation, filename).text os.path.basename(ann[image_path]) size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(ann[width]) ET.SubElement(size, height).text str(ann[height]) ET.SubElement(size, depth).text 3 for obj in ann[objects]: object_elem ET.SubElement(annotation, object) ET.SubElement(object_elem, name).text obj[category] bndbox ET.SubElement(object_elem, bndbox) xmin, ymin, xmax, ymax obj[bbox] ET.SubElement(bndbox, xmin).text f{xmin:.2f} ET.SubElement(bndbox, ymin).text f{ymin:.2f} ET.SubElement(bndbox, xmax).text f{xmax:.2f} ET.SubElement(bndbox, ymax).text f{ymax:.2f} tree ET.ElementTree(annotation) tree.write(output_path, encodingutf-8, xml_declarationTrue)注意反向生成时坐标保留的小数位。写成整数虽然看起来干净但遇到细长的裂缝缺陷时四舍五入带来的1像素偏差在放大后可能占整个目标宽度的20%以上。写成浮点数能保住原始标注的信息。4.4 划分train/val并生成data.yaml格式转换完成后别忘了划分数据集。公共数据集常见的问题是缺陷类别分布不均衡随手切train/val可能让某个类在验证集里只有几个样本评估结果波动极大。比较靠谱的做法是分层划分保证每个类别在train和val里的比例基本一致。简单实现可以按类别做groupby再在每类内部随机划分。比如swdd整体样本量不大train:val按8:2或者9:1都是可以的。data.yaml是Ultralytics YOLO训练时的配置文件里面需要指定路径、类别数和类别名path: /path/to/swdd_yolo train: images/train val: images/val nc: 5 names: 0: inclusion 1: patches 2: pitted_surface 3: rolled-in_scale 4: scratches这个文件里的nc写成别的数字一般也不会报错但会导致最后那层检测头和你实际类别数不匹配迁移效果莫名其妙变差。从class_names.txt读出来自动生成是最稳妥的。4.5 整体调用示例把上面几个函数串起来一次调用完成全部转换import glob annotations [] for xml_path in sorted(glob.glob(annotations/*.xml)): annotations.append(parse_voc_xml(xml_path)) # 按8:2划分 random.shuffle(annotations) split_idx int(len(annotations) * 0.8) train_annotations annotations[:split_idx] val_annotations annotations[split_idx:] # 输出YOLO格式 write_yolo_format(train_annotations, swdd_yolo/images/train, swdd_yolo/labels/train) write_yolo_format(val_annotations, swdd_yolo/images/val, swdd_yolo/labels/val) # 输出COCO格式 write_coco_format(train_annotations, swdd_coco/annotations/train.json) write_coco_format(val_annotations, swdd_coco/annotations/val.json)注意random.shuffle之前最好设一下random seed保证每次跑出来的划分一致不然复现实验结果时你会发现两次训练的val set都不一样。5. 转换完后必须做的检查可视化与常见坐标坑格式转换完了千万别直接开训练。先把标注可视化一遍这一步能拦下绝大多数低级错误。5.1 把YOLO txt画回原图用Pillow加载图片把归一化坐标还原成像素坐标然后画框from PIL import Image, ImageDraw def visualize_yolo_txt(image_path, txt_path, class_namesNone): img Image.open(image_path).convert(RGB) w, h img.size draw ImageDraw.Draw(img) with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if not parts: continue cls_id int(parts[0]) c_x, c_y, box_w, box_h map(float, parts[1:]) # 反归一化 x1 (c_x - box_w / 2) * w y1 (c_y - box_h / 2) * h x2 (c_x box_w / 2) * w y2 (c_y box_h / 2) * h label class_names[cls_id] if class_names else str(cls_id) draw.rectangle([x1, y1, x2, y2], outlinered, width2) draw.text((x1, max(y1 - 10, 0)), label, fillred) img.save(check.png)每周挑十来张图看一眼比写一堆单元测试都管用。5.2 常见坐标坑归一化、越界、ID错位我在这里把排错过程中遇过的坑汇总一下方便你对照查现象根本原因框的位置偏到图片右上角把VOC的xmin当成中心点的x坐标直接除以宽度框的大小明显错误占据了几乎整图width和height用了中心点的两倍忘了除以2或把面积当宽高训练时标签反复警告坐标越界归一化后没有clip浮点运算产生1.0000001所有目标类别整体错一位YOLO转COCO时没有自动加1或COCO转YOLO时忘了减1图片数量与txt数量对不上有文件名带特殊字符或解析索引时用startswith误匹配了前缀相同的文件还有一个容易被忽略的检查txt文件名和图片文件名是否完全一致。很多数据集的命名是00001.jpg和1.txt这种不一致看起来没什么写到脚本里匹配不上就是找不到标签。好一点的处理是统一用zfill补零或者干脆统一用os.path.splitext取stem来拼接。5.3 批量校验脚本写一个简单的标签自检除了可视化我还会跑一个批量检查统计所有标签文件里是否出现以下情况有空文件有些框架会警告但最好提前知道类别ID超出了nc-1坐标值小于0或大于1宽度或高度为0这种脚本不难写但关键时刻能救命。我见过不少人用了一个月才发现某个类别的ID在训练配置里被写反了重跑一次实验的时间成本远比写脚本高。6. 用SWDD训练YOLO的实操建议与几个容易忽略的细节6.1 数据增强的取舍别让增强抹掉小缺陷SWDD这类工业质检数据集的样本量通常不算大很多人习惯开满YOLO自带的增强参数。但这里有个矛盾数据增强是为了增加多样性可对于微小缺陷目标来说剧烈增强可能直接改变缺陷的外观特征。一个典型的例子是mosaic增强。mosaic会把四张图拼接成一张小目标在拼接后进一步缩小很可能小于模型下采样后的最小感受野等于这次训练就是在教模型忽略小目标。所以我在SWDD上训练时会把mosaic概率降下来而把翻转、轻微噪声和颜色扰动保留。具体数值因人而异但你可以从mosaic默认的1.0降到0.5试一下对比一下验证集上小尺寸目标的AP变化。6.2 光照不均和过曝光样本简单但有效的处理钢表面图像最大的干扰不是背景复杂而是光照。同一张图片上反光区和阴影区的缺陷对比度可以差出好几倍。如果直接用原始图训练模型很容易学到高对比度区域才是缺陷在低对比度区域漏检。我试过的最有效做法是加一个轻量的对比度归一化预处理对每张图做局部直方图均衡CLAHE同时保留原图作为一个额外的通道或分支。有些部署环境不支持复杂的预处理那就至少保证训练时的预处理和推理时完全一致不要训练用增强图、部署用原图效果会崩得很惨。6.3 关于YOLO第几代了和版本选择很多人问现在YOLO到第几代了比如YOLOv8、YOLOv9、YOLOv10、YOLO11甚至社区里讨论的更新版本。我的态度是对数据集格式转换这件事版本完全不重要。本文里的YOLO格式就是Ultralytics通用的那种txt格式从v5到最新的版本都兼容。训练时选哪个版本取决于你对部署硬件的了解和实验baseline的对照需求而不是最新就是最好。如果你用的是AMD显卡跑YOLO注意Ultralytics对ROCm的支持需要安装对应版本的PyTorch官方文档里有明确说明安装对了之后训练流程和NVIDIA显卡没太大区别。一条实际可落的命令大概是这样的yolo detect train \ dataswdd_yolo/data.yaml \ modelyolo11n.pt \ epochs100 \ imgsz640 \ batch16先跑一个轻量backbone的模型比如nano或者small系列快速验证数据流程没问题再跑大模型调优。一上来就跑x模型如果报错你根本分不清是数据问题还是训练参数问题。6.4 关于类别不均衡的一个小技巧SWDD的几个缺陷类别样本量之间通常有明显差距高频类别和低频类别可能差出一个数量级。最简单的处理是在损失函数上给低频类别加权重YOLO的训练配置里支持cls loss相关参数这个可以调。更实用的技巧是训练完成后单独按类别看AP而不要只看一个mAP。如果某个低频类别AP接近零而可视化检查发现标注没问题那基本就是样本量不够先收集数据比硬调参有效。我在实际使用SWDD时养成的一个习惯是每次转换完数据集都保留一份转换脚本和class_names.txt放进数据集的根目录连同训练config一起归档。这样几周后想复现实验结果或者数据集更新了需要重新转换不用再回忆当时是怎么处理的。做工业项目不同于打比赛可追溯性和可复现性很多时候比那零点几个点的mAP更重要。
返回列表