☰
目标检测数据集制作全流程:VOC/COCO/YOLO格式转换与避坑指南
2026/11/2 4:43:32 网站建设 项目流程

做检测模型这行,绕不开的一件事就是数据集。你可能算法调得飞起,网络结构改得天花乱坠,结果一跑训练发现 mAP 死活上不去,回头一查——标注框偏了、类别 ID 对不上、格式转换时坐标被悄悄截断。我见过太多人把大把时间花在调参上,却在数据这一环反复栽跟头。这篇就把检测数据集从零到一的完整链路捋一遍:怎么收集、怎么标注、VOC/COCO/YOLO 三种格式到底差在哪、互转时哪些坑必须提前避开。不管你是刚入门想跑通第一个自定义数据集,还是已经训练过几轮但总在格式转换上翻车,这篇都能直接拿去对照操作。

1. 先搞清楚三种格式到底在描述什么

很多人一上来就找转换脚本,结果转完发现框全错位了。根本原因是没有理解这三种格式对"一个目标"的描述方式完全不同。它们不是简单的文件后缀差异,而是坐标体系、组织结构、字段语义三个层面都不一样。

1.1 VOC:以像素绝对坐标为核心的 XML 体系

VOC 格式源自 PASCAL VOC 挑战赛,它的核心特点是用像素绝对坐标描述边界框。一张图对应一个 XML 文件,结构大致是这样:

<annotation> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>cat</name> <bndbox> <xmin>120</xmin> <ymin>85</ymin> <xmax>410</xmax> <ymax>360</ymax> </bndbox> </object> </annotation>

这里的关键点:xmin/ymin/xmax/ymax是左上角和右下角的绝对像素值,原点在图片左上角,x 向右递增,y 向下递增。size字段记录了图片宽高,这个信息在格式转换时至关重要——因为 YOLO 需要用它来做归一化。

VOC 的目录结构通常是Annotations/(放 XML)、JPEGImages/(放原图)、ImageSets/Main/(放训练验证划分的 txt)。这个结构是历史约定,很多老代码库默认按这个路径读数据,转换时最好保持。

1.2 COCO:JSON 单文件 + 归一化与绝对坐标混用

COCO 格式用一个大的 JSON 文件描述整个数据集,结构比 VOC 复杂得多,但表达力也更强。它天然支持实例分割、关键点、多类别等任务。核心字段:

{ "images": [ {"id": 1, "file_name": "000001.jpg", "width": 640, "height": 480} ], "annotations": [ { "id": 1, "image_id": 1, "category_id": 3, "bbox": [120, 85, 290, 275], "area": 79750, "iscrowd": 0, "segmentation": [[120,85, 410,85, 410,360, 120,360]] } ], "categories": [ {"id": 3, "name": "cat", "supercategory": "animal"} ] }

注意 COCO 的bbox是[x, y, width, height],不是[xmin, ymin, xmax, ymax]。这是转换时最容易搞错的地方之一。x, y是左上角绝对像素坐标,width/height是框的宽高。另外category_id在 COCO 里不要求从 0 开始连续,但很多训练框架要求连续,转换时要注意重映射。

1.3 YOLO:归一化中心点坐标 + 纯文本

YOLO 格式最简洁,每张图对应一个 txt 文件,每行一个目标:

3 0.414062 0.463542 0.453125 0.572917

五个字段分别是:class_id center_x center_y width height。全部是相对于图片宽高的归一化值,范围 0~1。class_id从 0 开始连续编号,这个编号对应一个classes.txt或data.yaml里的类别名列表。

三种格式的核心差异,我整理成一张表,转换时对着看:

维度VOCCOCOYOLO
存储形式每图一个 XML单个 JSON每图一个 txt
框表示xmin,ymin,xmax,ymaxx,y,w,hcx,cy,w,h(归一化)
坐标类型绝对像素绝对像素归一化 0~1
类别表示字符串 name数字 category_id数字 class_id
类别起始无编号不强制从 0必须从 0 连续
分割支持无有 segmentation有(多边形行)

提示:转换前务必先确认源格式的框表示到底是xyxy还是xywh,这一步错了后面全错,而且往往训练时才发现,白白浪费几小时。

2. 数据收集:别急着爬,先想清楚你要什么

数据集制作的第一步不是打开标注工具,而是想清楚"我要检测什么、在什么场景下检测"。这个想不清楚,后面标注全是返工。

2.1 明确检测目标与场景边界

我见过一个做试卷题目自动切割的项目,一开始把整张试卷所有文字区域都标了,结果模型学得一塌糊涂。后来收敛到只标"题干区域"和"答题区域"两类,效果立刻上来了。所以第一步是定义清晰的类别体系,类别不宜过多,初期建议控制在 5~10 类,每类至少 200~500 个实例起步。

场景边界同样重要。你要检测的是白天还是夜间?室内还是室外?固定摄像头还是移动拍摄?这些决定了你收集数据时要覆盖的分布。比如做电力红外数据集,就要考虑不同设备、不同拍摄角度、不同环境温度下的红外图像,否则模型换个变电站就废了。

2.2 收集渠道与合法性

常见渠道有几类:公开数据集(COCO、Open Images、CrowdHuman、CCPD 等可以直接下载或筛选子集)、自有业务数据、网络公开图片。这里必须强调:使用任何数据都要确认授权和合规性,尤其是涉及人脸、车牌、个人信息的场景,务必遵守相关法律法规和平台规则。公开数据集一般有明确 license,用之前读一遍。

对于自有数据,收集时要注意多样性:不同光照、不同角度、不同遮挡程度、不同背景。我一般会按"场景维度"建文件夹,比如day/、night/、indoor/、outdoor/,方便后续做分层采样和难例分析。

2.3 数据清洗:去重、去模糊、去损坏

收集来的数据不能直接用。我通常跑三步清洗:

  1. 去重:用感知哈希(pHash)或简单的 MD5 找完全重复和近似重复的图。重复图会导致训练集和验证集泄漏,指标虚高。
  2. 去模糊/去损坏:用拉普拉斯方差判断模糊度,阈值一般设在 100 左右(视场景调整),太模糊的直接剔除;用 PIL 尝试打开每张图,打不开的就是损坏文件。
  3. 尺寸过滤:太小的图(比如短边小于 200px)检测价值低,建议剔除或单独处理。
import cv2 import os def check_blur(image_path, threshold=100): img = cv2.imread(image_path) if img is None: return False, 0 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) score = cv2.Laplacian(gray, cv2.CV_64F).var() return score > threshold, score

这段代码我几乎每个项目都会用,跑一遍把模糊图挑出来人工过一眼,比事后模型学不好再回头找原因高效得多。

3. 标注:工具选型与质量控制的实战细节

标注是数据集质量的分水岭。工具选对了效率翻倍,质量控制做好了模型才稳。

3.1 标注工具怎么选

主流工具就那么几个,各有适用场景:

  • LabelImg:老牌工具,直接输出 VOC XML,也支持 YOLO txt。轻量、离线、上手快,适合小规模矩形框标注。缺点是界面老旧,不支持分割和关键点。
  • Labelme:支持多边形分割标注,输出 COCO 或 Labelme 自己的 JSON。做实例分割或需要精细边界时用它。
  • CVAT:功能最全,支持视频标注、多人协作、自动标注。适合团队和较大规模项目,但部署稍重。
  • Roboflow / 在线平台:适合快速迭代,但数据要上传,涉及隐私或敏感数据时慎用。

我的建议:纯矩形框检测、数据量在几千张以内,LabelImg 足够;需要分割或团队协作,上 CVAT;只是快速验证想法,在线工具也行,但注意数据合规。

3.2 标注规范必须先定后标

这是血泪教训。如果几个人同时标,没有统一规范,标出来的框松紧不一、类别理解不同,模型直接学懵。标注前一定要写一份标注规范文档,至少包含:

  • 每个类别的明确定义和正反例图
  • 框的松紧标准(贴边还是留 2px 余量,统一即可)
  • 遮挡、截断目标怎么处理(标可见部分还是补全)
  • 极小目标(小于多少像素)是否标注
  • 类别边界模糊时的判定规则

我一般会先让所有人标 50 张同一批图,然后交叉比对,把分歧点拿出来讨论定规则,再正式开标。这一步花半天,能省后面几天返工。

3.3 标注质量抽检

标完不能直接用。抽检比例建议 10%~20%,重点看:

  • 漏标:有没有目标没框
  • 错标:类别对不对
  • 框质量:松紧是否一致,有没有明显偏移
  • 边界情况:遮挡、截断、密集目标处理是否一致

抽检发现的问题要反馈给标注员并更新规范。如果错误率超过 5%,建议整批返工或重新培训。

注意:标注一致性比标注绝对精度更重要。一个框稍微松一点没关系,但如果一半人松一半人紧,模型就学不到稳定特征。

4. 格式互转:坐标变换的数学与代码实现

终于到核心部分了。三种格式互转,本质是坐标体系的换算。我把每个方向的转换逻辑和代码都写清楚,你直接抄。

4.1 VOC 转 YOLO:归一化是唯一难点

VOC 的xyxy绝对坐标转 YOLO 的cxcywh归一化,公式:

cx = (xmin + xmax) / 2 / W cy = (ymin + ymax) / 2 / H w = (xmax - xmin) / W h = (ymax - ymin) / H

其中 W、H 是图片宽高,从 XML 的size字段读。代码:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, classes, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') W = int(size.find('width').text) H = int(size.find('height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in classes: continue cls_id = classes.index(name) bnd = obj.find('bndbox') xmin = float(bnd.find('xmin').text) ymin = float(bnd.find('ymin').text) xmax = float(bnd.find('xmax').text) ymax = float(bnd.find('ymax').text) cx = (xmin + xmax) / 2.0 / W cy = (ymin + ymax) / 2.0 / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines))

这里有个坑:坐标可能越界。有些标注框会超出图片边界(xmax > W),归一化后 w 可能大于 1。YOLO 训练时一般会裁剪到 [0,1],但最好在转换时就 clamp 一下,避免异常值干扰。

4.2 YOLO 转 VOC:反归一化 + 边界裁剪

反向转换,把归一化值乘回图片宽高:

def yolo_to_voc(txt_path, img_w, img_h, classes, output_dir): lines = open(txt_path).read().strip().split('\n') xml_lines = [] for line in lines: if not line.strip(): continue parts = line.split() cls_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:5]) xmin = int((cx - w/2) * img_w) ymin = int((cy - h/2) * img_h) xmax = int((cx + w/2) * img_w) ymax = int((cy + h/2) * img_h) # 裁剪到图片边界 xmin = max(0, xmin); ymin = max(0, ymin) xmax = min(img_w, xmax); ymax = min(img_h, ymax) xml_lines.append((classes[cls_id], xmin, ymin, xmax, ymax)) # 后续拼 XML 略

关键点:YOLO 转 VOC 必须知道图片宽高,因为 YOLO 文件里不存这个信息。所以转换时要遍历原图读尺寸,或者从已有的元数据里取。很多人转完发现框全错,就是因为用了错误的宽高。

4.3 VOC 转 COCO:注意 bbox 是 xywh

VOC 的xyxy转 COCO 的xywh:

x = xmin y = ymin w = xmax - xmin h = ymax - ymin

COCO 的 JSON 需要同时维护images、annotations、categories三个数组,image_id和annotation_id要唯一。代码框架:

import json coco = {"images": [], "annotations": [], "categories": []} for i, name in enumerate(classes): coco["categories"].append({"id": i, "name": name, "supercategory": "none"}) ann_id = 1 for img_id, xml_path in enumerate(xml_list, start=1): # 解析 XML 拿 W H 和框 coco["images"].append({ "id": img_id, "file_name": os.path.basename(xml_path).replace('.xml', '.jpg'), "width": W, "height": H }) for (name, xmin, ymin, xmax, ymax) in boxes: coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": classes.index(name), "bbox": [xmin, ymin, xmax-xmin, ymax-ymin], "area": (xmax-xmin)*(ymax-ymin), "iscrowd": 0 }) ann_id += 1 json.dump(coco, open('annotations.json', 'w'))

area字段很多框架不强制,但 COCO 评估时会用,建议填上。iscrowd一般填 0,表示非密集群组。

4.4 COCO 转 YOLO:批量处理与类别重映射

COCO 的category_id可能不连续(比如 1, 3, 7),转 YOLO 时必须重映射成 0, 1, 2:

cat_id_to_yolo = {cat['id']: i for i, cat in enumerate(coco['categories'])} img_info = {img['id']: img for img in coco['images']} for ann in coco['annotations']: img = img_info[ann['image_id']] W, H = img['width'], img['height'] x, y, w, h = ann['bbox'] cx = (x + w/2) / W cy = (y + h/2) / H nw = w / W nh = h / H cls_id = cat_id_to_yolo[ann['category_id']] # 写入对应 txt

这一步的坑是图片可能没有对应标注(负样本),转换时要保证每张图都有 txt 文件(哪怕是空的),否则 YOLO 训练时可能报错或漏掉负样本。

4.5 转换后的验证:别跳过这一步

转完一定要验证。我一般做三件事:

  1. 可视化抽查:随机抽 20~50 张,把转换后的框画回原图,肉眼看是否对齐。
  2. 数量核对:统计转换前后目标总数是否一致,类别分布是否一致。
  3. 边界检查:检查是否有坐标越界、宽高为负、类别 ID 超范围的情况。
def visualize_bbox(img_path, boxes, classes, save_path): img = cv2.imread(img_path) H, W = img.shape[:2] for (cls_id, cx, cy, w, h) in boxes: x1 = int((cx - w/2) * W); y1 = int((cy - h/2) * H) x2 = int((cx + w/2) * W); y2 = int((cy + h/2) * H) cv2.rectangle(img, (x1,y1), (x2,y2), (0,255,0), 2) cv2.putText(img, classes[cls_id], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) cv2.imwrite(save_path, img)

可视化这步千万别省。我遇到过转换脚本把xywh当成xyxy处理,数量核对完全正常,但可视化一看框全偏了。数量对不代表坐标对。

5. 那些年我踩过的格式转换坑

这一节单独拎出来,因为坑太集中了,每个都值得单独说。

5.1 坐标系原点与 y 轴方向

绝大多数图像格式原点在左上角,y 向下。但如果你从某些数学工具或旧系统导出的数据,可能是左下角原点、y 向上。转换前确认清楚,否则框会上下翻转。判断方法:可视化一张图,看框是否镜像。

5.2 归一化用了错误的宽高

YOLO 归一化必须用当前图片自己的宽高,不能用数据集统一尺寸。如果图片尺寸不一,用错宽高会导致框比例全乱。我见过有人图省事用 640x640 统一归一化,结果非正方形图全废。

5.3 类别 ID 不连续或从 1 开始

VOC 用字符串类别名,COCO 的 category_id 可能从 1 开始或不连续,YOLO 要求从 0 连续。转换时一定要做重映射,并且保存一份classes.txt记录映射关系。训练时类别对不上,模型输出的类别名就全错。

5.4 浮点精度与坐标截断

VOC 的坐标是整数,YOLO 是浮点。VOC 转 YOLO 时如果先取整再归一化,会损失精度。建议全程用 float 计算,最后写文件时保留 6 位小数。反过来 YOLO 转 VOC 时,int()截断会导致框偏小,建议用round()。

5.5 空标注文件与负样本

有些图没有目标,VOC 里没有 XML,YOLO 里应该是空 txt。转换时要保证空 txt 存在,否则训练时这张图可能被跳过,负样本丢失。负样本对降低误检很重要,别丢。

5.6 图片文件名与标注文件名不一致

VOC 的 XML 里filename字段和实际文件名可能不一致(改过名没同步)。转换时以实际文件名为准,别信 XML 里的filename。这个坑很隐蔽,训练时找不到图才报错。

提示:转换脚本写完,先在小样本(10~20 张)上跑通并可视化验证,确认无误再全量跑。全量跑完再发现问题,返工成本高得多。

6. 一套可复用的数据集制作流水线

把上面的东西串起来,我通常按这个流程走:

  1. 需求定义:确定类别体系、场景边界、目标数量。
  2. 数据收集:多渠道收集,按场景分文件夹。
  3. 数据清洗:去重、去模糊、去损坏、尺寸过滤。
  4. 标注规范:写规范文档,试标校准,正式标注。
  5. 质量抽检:抽检 10%~20%,问题反馈返工。
  6. 格式统一:确定训练框架需要的格式(YOLO 居多),从标注工具原生格式转换。
  7. 转换验证:可视化抽查 + 数量核对 + 边界检查。
  8. 划分数据集:按 8:1:1 或 7:2:1 划分 train/val/test,注意同一场景的图不要跨集泄漏。
  9. 生成配置文件:YOLO 的data.yaml写好路径、类别数、类别名。
  10. 试训练:先跑几轮看 loss 和指标是否正常,再全量训练。

这套流程我用了很多项目,基本能覆盖 90% 的检测数据集制作需求。剩下的 10% 是特殊场景,比如多模态数据集、视频抽帧、3D 标注,那些另说。

6.1 数据集划分的坑

划分不是随机分就完事。如果同一段视频抽的帧、同一场景的连拍图被分到 train 和 val,会导致验证指标虚高。正确做法是按场景/视频/采集批次划分,保证 val 和 test 里的场景在 train 里没出现过,这样评估才真实。

6.2 配置文件模板

YOLO 的data.yaml长这样:

path: /dataset/root train: images/train val: images/val test: images/test nc: 5 names: ['person', 'car', 'dog', 'cat', 'bicycle']

nc是类别数,names顺序必须和 class_id 对应。这个文件写错,训练直接报错或类别全乱。

7. 关于工具与效率的一些个人经验

最后聊几个提效的点。格式转换脚本我建议封装成命令行工具,支持--src-format和--dst-format参数,一次写好反复用。我自己的脚本支持 VOC/COCO/YOLO 六向互转,加个--visualize参数直接出验证图,省得每次单独写验证代码。

标注阶段如果类别多、目标密集,可以考虑用预训练模型做预标注,人工只做修正。比如先用 COCO 预训练的 YOLO 跑一遍,把置信度高的框导成标注文件,人工在 LabelImg 里改。这样能省 50% 以上的标注时间,但要注意预标注的框质量参差,修正时别偷懒。

数据版本管理也很重要。数据集改了一版又一版,没有版本管理,过两周自己都忘了哪版对应哪个实验结果。我一般用dataset_v1、dataset_v2这样命名,每次改动记录 changelog,训练配置里写清楚用的哪版。这个习惯在复现实验时能救命。

还有个小技巧:转换脚本里加日志,记录每张图转换了多少个框、有没有越界裁剪、有没有类别未匹配。跑完看日志,异常一目了然,比事后排查高效得多。

数据集这活儿,说到底是细活。算法可以调,结构可以换,但数据错了,后面全是白费。把收集、标注、转换、验证这几步做扎实,模型效果自然就上来了。我自己的体会是,花在数据上的时间,最终都会以模型指标的提升回报回来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询