数据标注与数据集工程

本文系统讲解数据标注与数据集工程,回答 VOC、COCO、YOLO 三种标注格式怎么选、标注一致性如何度量、数据泄漏如何避免、半自动标注怎么落地等实战问题。覆盖主流数据集规模、标注工具、标注类型、一致性计算、数据划分与防泄漏、SAM 预标注与主动学习、合成数据与域差距,并给出 COCO 统计与 VOC 转 COCO 两段可运行代码,附格式对比表、权衡与常见坑。

引言

在 CV 项目里,数据决定了上限,模型只是逼近上限的工具。标注是把原始像素变成监督信号的环节,它的质量直接决定模型能走多远。一个常见现象是:团队花两周换更强的模型提升 1 个点,却因为标注规范不清、类别定义含糊损失 5 个点。

本文按「数据集 → 格式 → 工具 → 类型 → 质量 → 划分 → 半自动 → 合成」的顺序讲清数据集工程,并给出可直接运行的格式转换与统计代码。

目录

  1. 主流数据集
  2. 标注格式
  3. 标注工具
  4. 标注类型
  5. 标注质量保障
  6. 数据划分与防泄漏
  7. 半自动标注与主动学习
  8. 合成数据与域差距
  9. 代码:COCO 统计
  10. 代码:VOC 转 COCO
  11. 数据版本管理与流水线
  12. 标注团队与流程管理
  13. 格式对比表
  14. 权衡取舍
  15. 常见坑清单
  16. 小结

1. 主流数据集

数据集规模类别数任务用途
ImageNet-1K128 万训练图1000分类预训练骨干
COCO33 万图 / 150 万实例80检测分割关键点检测分割基准
Pascal VOC约 1.1 万图20检测分割轻量实验
Open Images900 万图600检测大规模预训练
Objects365200 万图 / 3000 万框365检测大规模检测预训练

公开数据集的价值有两个:一是直接用作预训练,二是作为标注规范的参考模板。使用前务必确认许可协议,商用场景要留意限制。

2. 标注格式

三种主流格式的差异:

  • Pascal VOC:每张图一个 XML 文件,记录绝对像素坐标的 xmin、ymin、xmax、ymax。
  • COCO:所有标注集中在一个 JSON 里,bbox 是左上角加宽高的绝对像素,还支持分割多边形与关键点。
  • YOLO:每张图一个 txt,每行 class cx cy w h,坐标是相对图像宽高的归一化值。

选择建议:训练 YOLO 直接用 txt;需要分割与关键点用 COCO JSON;老项目遗留的 VOC XML 需要转换。

3. 标注工具

工具部署支持任务特点
Label Studio自建服务分类框分割关键点灵活,支持多模态
CVAT自建服务框分割关键点跟踪视频标注强
LabelImg桌面框轻量,导出 VOC/YOLO
RoboflowSaaS全类型托管,含增强与导出
Labelme桌面多边形分割分割标注常用

团队协作优先选自建服务(Label Studio、CVAT),能统一规范、分配任务、统计进度;个人快速标注用桌面工具。

4. 标注类型

  • 分类标签:整图一个或多个标签,成本最低。
  • 边界框(bbox):检测用,成本中等。
  • 多边形 / 掩码:分割用,成本最高,边界越精细越贵。
  • 关键点:姿态用,需要定义点的顺序与可见性。
  • 视频跟踪:给每帧目标分配一致 ID,成本高但支撑跟踪任务。

标注成本的经验值:分类小于检测小于分割,分割的标注时间常常是检测的三到五倍。

5. 标注质量保障

质量保障的核心是「可度量的规范」:

  • 标注规范文档:明确定义每个类别的边界、遮挡、截断、最小可标尺寸,配正反例图。
  • 一致性度量:用 IAA(标注者间一致性)量化分歧,常用 Cohen’s Kappa 或 IoU 匹配率。
  • 双盲标注与仲裁:关键样本由两人独立标注,分歧交由第三人裁决。
  • 抽检审核:按比例抽查,不合格的批次返工。
  • 迭代校准:定期对齐标注员的理解,修正规范文档。
import numpy as np

def cohen_kappa(a, b):
    # a, b 为两位标注者的标签序列
    a, b = np.asarray(a), np.asarray(b)
    po = (a == b).mean()
    classes = np.unique(np.concatenate([a, b]))
    pe = sum((a == c).mean() * (b == c).mean() for c in classes)
    return (po - pe) / (1 - pe + 1e-12)

labels_a = [0, 1, 1, 0, 1, 1, 0]
labels_b = [0, 1, 0, 0, 1, 1, 1]
print("kappa:", round(cohen_kappa(labels_a, labels_b), 4))

Kappa 大于 0.8 通常表示高度一致,低于 0.6 说明规范需要重新对齐。

6. 数据划分与防泄漏

划分看似简单,实则最容易出错:

  • 训练 / 验证 / 测试按约 8:1:1 或 7:1.5:1.5,测试集只在最后用一次。
  • 同一来源不跨集:同一段视频的相邻帧、同一病人的切片、同一场景的连拍,必须整组划分。
  • 时间切分:涉及时间演进的任务(如销量、路况),用过去训练、未来验证。
  • 类别均衡:每类在三个集里都要有足够样本,稀有类可适当加权。
  • 划分固定:划分结果存盘复用,避免每次重划分导致指标不可比。

7. 半自动标注与主动学习

降低标注成本的三条路径:

  • 模型预标注:用 SAM 或已有检测器生成候选,人工只做修正,效率可提升数倍。
  • 主动学习:挑模型最不确定的样本优先标注,用最少标注换最大提升。
  • 弱监督:用图像级标签训练检测(如 CAM 类方法),省去框级标注。
import numpy as np

def select_uncertain(confidences, k=10):
    # 置信度越接近 0.5 越不确定,优先标注
    uncertainty = 1 - np.abs(np.asarray(confidences) - 0.5) * 2
    order = np.argsort(-uncertainty)
    return order[:k].tolist()

conf = [0.9, 0.51, 0.99, 0.48, 0.75, 0.5]
print("to label:", select_uncertain(conf, k=3))

主动学习的关键是「采样策略」与「批量大小」:一次标注太多会浪费,太少则迭代慢。经验上每轮选几百张,训练后重新评估不确定性。

8. 合成数据与域差距

合成数据用图形引擎或扩散模型生成带标注的图,能低成本造出罕见场景(如事故、缺陷)。难点是域差距(domain gap):合成图与真实图在纹理、光照、噪声上分布不同,直接训练会掉点。缩小差距的手段有域随机化、风格迁移与少量真实数据微调。

合成数据的定位是「补充稀有场景」,不能完全替代真实数据。

9. 代码:COCO 统计

拿到一个 COCO 数据集,先统计类别分布与框尺寸,判断是否长尾。

import json
from collections import Counter
import numpy as np
from pathlib import Path

def analyze_coco(coco_json):
    data = json.loads(Path(coco_json).read_text())
    cats = {c["id"]: c["name"] for c in data["categories"]}
    counts = Counter(a["category_id"] for a in data["annotations"])
    areas = np.array([a["area"] for a in data["annotations"]])

    print("images:", len(data["images"]), "annotations:", len(data["annotations"]))
    for cid, n in counts.most_common():
        print(f"{cats[cid]:<20s} {n}")
    print("area p50/p90/max:",
          round(float(np.percentile(areas, 50)), 1),
          round(float(np.percentile(areas, 90)), 1),
          round(float(areas.max()), 1))
    small = (areas < 32 ** 2).mean()
    print("small object ratio:", round(float(small), 4))

analyze_coco("annotations/instances_train2017.json")

小目标占比高时,要提前规划输入分辨率与增强策略,否则训练完才发现小目标几乎检测不到。

10. 代码:VOC 转 COCO

老项目常需要把 VOC 转成 COCO 以复用现代训练框架。

import json
import xml.etree.ElementTree as ET
from pathlib import Path

def voc_to_coco(xml_dir, out_json, class_names):
    images, annotations, categories = [], [], []
    cat_id = {name: i + 1 for i, name in enumerate(class_names)}
    for i, name in enumerate(class_names):
        categories.append({"id": i + 1, "name": name})

    ann_id = 1
    for img_id, xml_path in enumerate(sorted(Path(xml_dir).glob("*.xml")), start=1):
        root = ET.parse(xml_path).getroot()
        w = int(root.find("size/width").text)
        h = int(root.find("size/height").text)
        images.append({"id": img_id, "file_name": root.find("filename").text,
                       "width": w, "height": h})
        for obj in root.findall("object"):
            name = obj.find("name").text
            box = obj.find("bndbox")
            x1 = float(box.find("xmin").text)
            y1 = float(box.find("ymin").text)
            x2 = float(box.find("xmax").text)
            y2 = float(box.find("ymax").text)
            annotations.append({
                "id": ann_id, "image_id": img_id, "category_id": cat_id[name],
                "bbox": [x1, y1, x2 - x1, y2 - y1],
                "area": (x2 - x1) * (y2 - y1), "iscrowd": 0,
            })
            ann_id += 1

    coco = {"images": images, "annotations": annotations, "categories": categories}
    Path(out_json).write_text(json.dumps(coco))
    print("images:", len(images), "annotations:", len(annotations))

voc_to_coco("VOCdevkit/Annotations", "coco.json", ["person", "car", "dog"])

注意 COCO 的 bbox 是左上角加宽高,VOC 是左上角与右下角,转换时要相减。

11. 数据版本管理与流水线

数据集会不断迭代,必须有版本管理,否则「上次那个效果好的版本」找不回来。

  • 版本工具:DVC、Git LFS、或自建对象存储加清单文件,记录每版数据的哈希与变更。
  • 数据卡(dataset card):记录数据来源、采集条件、类别分布、已知偏差与许可,方便交接。
  • 流水线:采集 → 清洗 → 去重 → 标注 → 划分 → 版本固化,每一步可复现。
  • 与实验追踪联动:训练时记录所用的数据集版本,保证实验可复现。
import hashlib
from pathlib import Path

def file_hash(path, chunk=1 << 20):
    h = hashlib.md5()
    with open(path, "rb") as f:
        while chunk_data := f.read(chunk):
            h.update(chunk_data)
    return h.hexdigest()

def find_duplicates(image_dir):
    seen, dups = {}, []
    for p in Path(image_dir).rglob("*"):
        if p.suffix.lower() in {".jpg", ".png", ".jpeg"}:
            h = file_hash(p)
            if h in seen:
                dups.append((str(seen[h]), str(p)))
            else:
                seen[h] = p
    return dups

print("duplicates:", find_duplicates("images/train")[:5])

去重能防止同一张图同时出现在训练与测试集,是数据泄漏最常见的原因之一。内容近似(裁剪、压缩)的重复可用感知哈希进一步检测。

12. 标注团队与流程管理

标注是「人」的工程,流程管理决定了效率与质量。

  • 任务拆分:按类别或场景分派,让标注员专注相似任务,减少切换成本。
  • 规范培训:先用几十张样本做培训与试标,达标后才进入正式标注。
  • 质量指标:跟踪返工率、抽检合格率、每人日产量,用数据管理而非感觉。
  • 抽检比例:新标注员高比例抽检,熟练后降低,关键类别始终高抽检。
  • 成本估算:按框数或按工时计价,提前估算预算与工期。
  • 反馈闭环:把模型在验证集上的错误按类别汇总,反馈给标注团队修正规范。

一个实用做法是「金标样本」:在标注任务里混入已知答案的样本,用来持续监控标注员准确率,无需逐条人工复核。

13. 格式对比表

维度VOCCOCOYOLO
载体每图一个 XML单个 JSON每图一个 txt
坐标绝对像素绝对像素归一化
框表示左上右下左上加宽高中心加宽高
分割无多边形多边形
关键点无有有
训练直接可用需转换需转换是

14. 权衡取舍

  • 标注成本与质量:精细标注提升上限但成本高,可先用粗标验证可行性再细化。
  • 自建工具与 SaaS:自建可控但要维护,SaaS 省事但有数据合规顾虑。
  • 主动学习与全量标注:主动学习省标注但工程复杂,数据量小时收益明显。
  • 合成与真实:合成补稀有场景,但域差距需要额外手段弥补。
  • 格式选择:跟随训练框架,避免不必要的来回转换。

15. 常见坑清单

  • 类别定义含糊:不同标注员对「遮挡算不算」理解不同,标签噪声大。
  • 坐标越界:框超出图像边界未裁剪,训练时报错或静默丢弃。
  • 坐标系混淆:xywh 与 xyxy 混用,框整体错位。
  • 数据泄漏:同视频相邻帧跨集,验证指标虚高。
  • 类别不平衡:长尾类别样本极少,模型几乎学不会。
  • 重复图片:同一图出现在训练与测试,指标失真。
  • 标注顺序不一致:类别 id 从 0 还是 1 开始不统一,训练全乱。
  • 忽略 IAA:不度量一致性,标注质量无法量化。
  • 预标注不校验:直接信任模型预标注,错误被固化进数据集。
  • 测试集反复使用:调参用测试集,最终指标不可信。
  • 无数据卡:接手的人不了解数据来源与偏差,重复踩坑。
  • 忽视近似重复:裁剪或压缩后的同源图未被识别,造成泄漏。

16. 小结

数据集工程的主线是:定义清晰的标注规范 → 选合适的格式与工具 → 用 IAA 度量一致性 → 整组划分防泄漏 → 用预标注与主动学习降本 → 用合成数据补稀有场景。记住三条原则:规范先于标注、划分先于训练、测试集只用一次。数据质量的收益通常大于换模型的收益,把精力优先投在数据上。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机视觉」更多文章

  1. 检测与分割的评估指标
  2. 多模态视觉语言模型
  3. 3D 视觉:点云与深度估计