跳到主要内容

数据标注

YOLO (You Only Look Once) 是一种流行的实时目标检测算法,其训练需要大量带有标注框的图片数据。数据标注是为图像中的目标物体添加边界框(bounding box)并分类的过程,是构建高质量YOLO模型的基础。其核心特点是将目标检测作为单一的回归问题处理,通过单个神经网络直接从完整图像预测边界框和类别概率。与传统的两阶段检测器不同,YOLO实现了端到端的检测,具有速度快、效率高的优势。Yolo算法依赖于高质量的数据集和精确的数据标注。

  • YOLO模型的性能直接取决于训练数据的质量

  • 高质量数据集需要精确的数据标注作为基础

YOLO标注格式特点

坐标系统定义

  • 图像左上角为原点(0,0)
  • 向右为x轴正方向
  • 向下为y轴正方向

各参数计算方式

参数计算公式示例
x_center(框左x + 框右x)/2 / 图像宽度(100+180)/2/640=0.21875
y_center(框上y + 框下y)/2 / 图像高度(50+150)/2/480=0.20833
width(框右x - 框左x) / 图像宽度(180-100)/640=0.125
height(框下y - 框上y) / 图像高度(150-50)/480=0.20833
  • 每个图像对应一个.txt文本文件
  • 每行表示一个目标:<class_id> <x_center> <y_center> <width> <height>
  • 坐标值是相对于图像宽度/高度归一化的(0-1范围)

常用格式比较

特性YOLO格式Pascal VOCCOCO
文件形式每图一个.txt每图一个.xml单个.json
坐标表示归一化值绝对像素值绝对像素值
存储效率
可读性需解析易读需解析
扩展性
适用场景YOLO训练通用大型数据集

常用标注工具

CVAT

CVAT 即计算机视觉标注工具,是一款功能强大、高效的开源数据标注平台,由英特尔公司开发,适用于机器学习项目,支持YOLO格式的直接导出。在自动驾驶、医疗影像、零售业、安防监控、农业等多个领域都有广泛应用,如标注自动驾驶中的车辆、行人、交通标志,协助医生标注医疗影像中的异常等。

特性

  • 多样化标注类型:支持矩形框标注、多边形分割、语义分割、3D 点云标注等,能满足从简单图像分类到复杂实例分割等各种计算机视觉任务的需求。

  • 自动标注功能:集成了 Segment Anything、Faster RCNN、Mask RCNN 等深度学习模型,支持自动标注,可将标注速度提升至 10 倍,且算法能在 CPU 或 GPU 上运行,为用户提供灵活选择。

  • 协作功能:支持多人协作标注,可给不同用户分配不同角色和权限,让大规模数据标注项目的管理更简单高效。

  • 丰富的数据格式支持:支持 TFRecord 等多种常见数据格式,能无缝集成到各种机器学习工作流程中。

提示

​ 优点:专业性强,功能全面,支持大规模标注项目,良好的性能表现,活跃的开源社区支持

​ 缺点:部署相对复杂,学习曲线较陡峭

安装

安装过程网上自行搜索,建议使用docker部署

使用

安装完成后,访问 http://localhost:8080 登录(无账户先进行注册)

创建项目

选择右侧+号,选择Create a new project创建一个新的项目,

创建项目时,需创建标签,可通过Labels下的Constructor页创建标签,设置完标签名称、类型、颜色等后点击Continue保存,标签名称不要使用中文

创建Task

进入项目后,选择下方的+按钮Create a new task菜单创建一个任务,任务类型分为的Train、Test、Validation分别对应训练集、测试集和验证集

创建Task时需要导入图像,保存后自动生成一个Job

开始标注

点击Task右侧的Open按钮进行Task,Task中Jobs中会默认有一个Job

点击Job的名称(一个超链接,一般为蓝色字体)进入图像标注页面,选择矩形方式(用By 4 Points),在图像上进行标注,在右侧可选择标签名称

信息

​ 按N键快速标注下一个目标

​ 按F键进入下一个图像

导出数据集

当所有任务完成之后,进入Projects页,点击项目下方的三个点的按钮,选择Export dataset导出数据集,导出时会导出所有任务

数据格式选择Ultralytics YOLO Detection 1.0,选择Save Images,当导出完成后会提示,点击提示或在Requests页选择下载到本地

使用限制
关键限制点具体说明
云服务配额官方云(app.cvat.ai)限 10 个任务、500MB 数据;本地部署无此限制
任务拆分仅在上传时可拆分任务 / 作业,标注后不支持直接拆分数据集为 train/val/test
验证集(QA)无强制数量,建议 5–15%;小样本(如 30 帧)可到 30%,以保证评估可靠性

Label Studio

Label Studio 是一款由 Human Signal(原 Heartex)推出的免费开源数据标注工具。适合开发人员微调大型语言模型、准备训练数据或验证 AI 模型。

特性

  • 多数据类型支持:支持图像、文本、音频、视频、时间序列等多种数据类型的标注,适用于自然语言处理、图像识别、语音识别等多种应用场景。
  • 可定制标注配置:用户可通过配置文件自定义标注界面,以适应不同标注任务需求,可创建边界框、多边形、分类、关键点、文本标签等。
  • 机器学习集成:允许用户集成机器学习模型,在标注过程中使用模型进行预测,帮助标注者加速标注工作。
  • 协作和项目管理:支持多用户协作标注,用户可分配任务,审核和管理标注过程,适合团队协作。
  • 数据导入和导出:支持从本地文件、云存储等多种来源导入数据,标注完成的数据可导出为多种格式,以便用于深度学习模型的训练。
  • 丰富的社区和插件:具有活跃的开源社区,提供许多插件和集成,用户可根据需求添加功能。
提示

​ 优点:界面友好,易于上手,支持多种数据类型,灵活的定制选项,部署简单

​ 缺点:对纯CV任务功能不如CVAT专业,大规模图像标注性能稍逊

安装

安装过程网上自行搜索,建议使用docker部署

使用

安装完成后,访问 http://localhost:8080 登录(无账户先进行注册)

创建项目

Label Studio中没有task(CVAT中要中以创建多个task),如果需要创建训练集、验证集和测试集,在Label Studio中就需要创建三个项目,点击Create Project按钮创建一个新的项目

导入数据
设置标签

Settings下的Labeling Interface页下可以添加标签,如果未设置标签在打开标注页时也会自动打开该页面(Before you can annotate the data, set up labeling configuration)。这里标签就是对应的类及类索引,标签不要使用中文,第一次如果不知道如何添加标签,可以通过Browser Templates按钮选择一个模板(因为Label Studio可以标注多种类型和风格的数据,这里选择Object Detection with Bounding Boxes带边框的目标检测),如图

删除(或修改)模板自带的标签,可以通过Add新增标签,为了标注时区分,建议使用不同颜色区分不同的类

提示

​ 可以使用Code方式快速添加或修改标签

开始标注

通过图像右侧的快捷操作可以实现图像移动,缩放等,在图像的下方显示了设置的标签的,点击某个标签就可以在图像上进行标注了(可通过标签上的数字快速 选择标签),标注完成后通过按钮Submit(或Update)提交(快捷键Ctrl + 回车)

导出数据集

当所有图像都标注完成之后,可以导出数据集,数据格式选择 Yolo width Images

除了CVAT和Label Studio标注工具,常用的还有LableMe、AnyLabeling(X-AnyLabeling)等, 有些标注工具还可以借助模型自动标注如CVAT,并且可以标其它类型的样本,如声音、视频、文本等。

使用限制
关键限制点具体说明
导入规模单批导入最多 250,000 个任务或 50MB 文件;大规模建议用云存储而非 UI 上传Label Studio
拆分能力社区版无内置自动拆分 UI;需用脚本、实验性功能(EXPERIMENTAL_FEATURES=1)或导出后划分
性能超大批量导出可能因反向代理超时(如 90 秒)失败,需改用存储同步或分批导出Label Studio
API导入

Label Studio 支持通过 API 接口对项目或任务进行访问与操作,可将图片素材、已完成的标注信息等数据,通过该接口上传至 Label Studio 平台。该接口支持两种主流访问方式:HTTP 请求(推荐 Token 认证)和 Label Studio 官方 SDK,具体使用规范可参考 Label Studio 官方文档。以下将以 HTTP 请求(采用 Token 认证方式)为例,详细介绍其具体使用方法。

创建API_KEY

  1. 进入Lable Studio的首页打开主菜单找到Organization菜单并进入,点击上方的API Tokens Settings按钮,打开Legacy Tokens选项并保存。
  1. 创建Access Token,点击用户头像在下拉菜单中选择Account & Settings,进入后选择菜单Legacy Token,这里的Access Token就是API_KEY(Copy按钮复制不能用,通过右键Copy)

导入图片

项目ID是一个整型数(不要填成项目名称),在Label Studio中进入项目后可以在地址栏中看到项目ID

import os
import sys
import requests
from pathlib import Path

# Label Studio地址
LABEL_STUDIO_URL = "http://localhost:8080"
# API Key
API_KEY = "0f127358a84ccfafbe0d51ba29d52c6a8f8f9412"
# 项目ID
PROJECT_ID = 14
# 默认图片文件夹路径
DEFAULT_IMAGE_DIR = "./images"
# 支持的图片格式
IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp")

def upload_to_label_studio(image_path):
"""
将图片上传到Label Studio
:param image_path: 图片文件路径
"""
# 上传图片文件
files = {"file": open(image_path, "rb")}
headers = {"Authorization": f"Token {API_KEY}"}

# 上传文件到Label Studio
upload_url = f"{LABEL_STUDIO_URL}/api/projects/{PROJECT_ID}/import"
response = requests.post(upload_url, headers=headers, files=files)

if response.status_code != 201:
# print(f"❌ 上传失败 {image_path}: {response.status_code} - {response.text}")
return False

# print(f"✅ 上传成功 {image_path}")
return True

def get_image_files(directory):
"""
递归获取目录及其子目录中的所有图片文件
:param directory: 根目录路径
:return: 图片文件路径列表
"""
image_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if file.lower().endswith(IMAGE_EXTENSIONS):
image_files.append(os.path.join(root, file))
return image_files

if __name__ == "__main__":
# 获取命令行参数指定的路径,或使用默认路径
if len(sys.argv) > 1:
image_dir = sys.argv[1]
else:
image_dir = DEFAULT_IMAGE_DIR

# 检查路径是否存在
if not os.path.exists(image_dir):
print(f"❌ 路径不存在: {image_dir}")
print(f"用法: python {sys.argv[0]} [图片文件夹路径]")
sys.exit(1)

# 递归获取所有图片文件
image_files = get_image_files(image_dir)

# print(f"📁 路径: {image_dir}")
print(f"📁 找到 {len(image_files)} 个图片文件")

# 遍历上传所有图片
total = len(image_files)
for index, img_path in enumerate(image_files, start=1):
try:
# 上传到Label Studio
if upload_to_label_studio(img_path):
print(f"✅ [{index}/{total}] 上传成功: {img_path}")
else:
print(f"❌ [{index}/{total}] 上传失败: {img_path}")

except Exception as e:
print(f"❌ [{index}/{total}] 上传失败: {img_path} - {str(e)}")

导入标注

导入已标注的数据集,一般包括目录images和labels。

import os
import requests
from pathlib import Path

# Label Studio地址
LABEL_STUDIO_URL = "http://localhost:8080"
# API Key
API_KEY = "0f127358a84ccfafbe0d51ba29d52c6a8f8f9412"
# 项目ID
PROJECT_ID = 14
# 图片文件夹路径
IMAGE_DIR = "./images"
# YOLO标注文件夹路径
LABEL_DIR = "./labels"
# YOLO类别ID→Label Studio标签名
CLASS_MAPPING = {0: "car", 1: "door_close", 2: "door_open", 3: "forklift", 4: "label"}
# 支持的图片格式
IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp")


def parse_yolo_label(label_path, img_width, img_height):
"""
解析YOLO格式标注,转换为Label Studio的标注格式
:param label_path: YOLO标注文件路径
:param img_width: 图片宽度
:param img_height: 图片高度
:return: Label Studio格式的标注数据
"""
annotations = []
if not os.path.exists(label_path):
return annotations # 无标注则返回空

with open(label_path, "r", encoding="utf-8") as f:
lines = f.readlines()

for line in lines:
line = line.strip()
if not line:
continue

# YOLO格式:class_id x_center y_center width height(归一化值0-1)
parts = line.split()
class_id = int(parts[0])
x_center = float(parts[1])
y_center = float(parts[2])
w = float(parts[3])
h = float(parts[4])

# 转换为Label Studio的百分比坐标(x, y, width, height范围0-100)
# YOLO: x_center, y_center是中心点;Label Studio: x, y是左上角
x = (x_center - w/2) * 100
y = (y_center - h/2) * 100
width = w * 100
height = h * 100

# 构建Label Studio标注格式(适用于Object Detection项目)
annotations.append({
"original_width": img_width,
"original_height": img_height,
"image_rotation": 0,
"value": {
"x": x,
"y": y,
"width": width,
"height": height,
"rotation": 0,
"rectanglelabels": [CLASS_MAPPING.get(class_id, f"class_{class_id}")]
},
"id": f"anno_{len(annotations)}",
"from_name": "label", # 需匹配Label Studio项目中的标注控件名称
"to_name": "image", # 需匹配Label Studio项目中的图片控件名称
"type": "rectanglelabels"
})

return annotations

def upload_to_label_studio(image_path, annotations):
"""
将图片和标注上传到Label Studio
:param image_path: 图片文件路径
:param annotations: Label Studio格式的标注数据
"""
# 上传图片文件
files = {"file": open(image_path, "rb")}
headers = {"Authorization": f"Token {API_KEY}"}

# 上传文件到Label Studio
upload_url = f"{LABEL_STUDIO_URL}/api/projects/{PROJECT_ID}/import"
response = requests.post(upload_url, headers=headers, files=files)

if response.status_code != 201:
return False, f"❌ 上传图片失败 {image_path}: {response.status_code} - {response.text}"

# 获取上传后的任务ID
try:
resp_data = response.json()
# print(f"DEBUG: 上传响应: {resp_data}")

# 方式1: 直接返回任务列表
if isinstance(resp_data, list) and len(resp_data) > 0:
task_id = resp_data[0]["id"]
# 方式2: 返回单个任务
elif isinstance(resp_data, dict) and "id" in resp_data:
task_id = resp_data["id"]
# 方式3: 返回包含tasks字段
elif isinstance(resp_data, dict) and "tasks" in resp_data:
task_id = resp_data["tasks"][0]["id"]
# 方式4: 返回导入统计信息,需要通过file_upload_ids查询任务
elif isinstance(resp_data, dict) and "file_upload_ids" in resp_data:
file_upload_id = resp_data["file_upload_ids"][0]
# 查询项目任务列表找到对应的任务
tasks_url = f"{LABEL_STUDIO_URL}/api/projects/{PROJECT_ID}/tasks"
tasks_resp = requests.get(tasks_url, headers=headers)
if tasks_resp.status_code == 200:
tasks = tasks_resp.json()
# 查找匹配的任务
for task in tasks:
if task.get("file_upload") == file_upload_id:
task_id = task["id"]
break
else:
return False, f"❌ 未找到对应的任务,file_upload_id: {file_upload_id}"
else:
return False, f"❌ 获取任务列表失败: {tasks_resp.status_code}"
else:
return False, f"❌ 无法解析任务ID,响应数据: {resp_data}"
except Exception as e:
return False, f"❌ 解析响应失败: {str(e)}, 响应内容: {response.text}"

# 更新标注信息
if annotations:
update_url = f"{LABEL_STUDIO_URL}/api/tasks/{task_id}/annotations"
annotation_data = {
"result": annotations,
"was_cancelled": False,
"ground_truth": True, # 标记为真值标注(可选)
"lead_time": 0
}
update_response = requests.post(update_url, headers=headers, json=annotation_data)
if update_response.status_code != 201:
return False, f"❌ 更新标注失败 {image_path}: {update_response.text}"
else:
return True, f"✅ 成功上传: {image_path} (任务ID: {task_id})"
else:
return True, f"✅ 成功上传(无标注): {image_path} (任务ID: {task_id})"

def get_image_size(image_path):
"""获取图片的宽高"""
import struct
with open(image_path, "rb") as f:
head = f.read(24)
if len(head) != 24:
raise ValueError("图片格式不支持")

if head[:2] == b"\xff\xd8": # JPEG
f.seek(0)
size = 2
ftype = 0
while not 0xc0 <= ftype <= 0xcf:
f.seek(size, 1)
byte = f.read(1)
while ord(byte) == 0xff:
byte = f.read(1)
ftype = ord(byte)
size = struct.unpack(">H", f.read(2))[0] - 2
f.seek(1, 1)
height, width = struct.unpack(">HH", f.read(4))
elif head[:8] == b"\x89PNG\r\n\x1a\n": # PNG
width, height = struct.unpack(">LL", head[16:24])
else:
raise ValueError(f"不支持的图片格式: {image_path}")
return int(width), int(height)


if __name__ == "__main__":
# 获取IMAGE_DIR中的图片文件(不递归子目录)
image_files = [
os.path.join(IMAGE_DIR, f) for f in os.listdir(IMAGE_DIR)
if f.lower().endswith(IMAGE_EXTENSIONS)
]
print(f"📁 找到 {len(image_files)} 个图片文件")

# 遍历上传所有图片
total = len(image_files)
for index, img_path in enumerate(image_files, start=1):
img_file = os.path.basename(img_path)
label_file = os.path.join(LABEL_DIR, Path(img_file).stem + ".txt")

try:
# 获取图片尺寸
img_w, img_h = get_image_size(img_path)

# 解析YOLO标注
ls_annotations = parse_yolo_label(label_file, img_w, img_h)

# 上传到Label Studio
success, message = upload_to_label_studio(img_path, ls_annotations)
print(f"[{index}/{total}] {message}")
except Exception as e:
print(f"❌ [{index}/{total}] 上传失败: {img_path} - {str(e)}")

建议遵循

为保证模型效果,数据集需遵守以下划分原则与最小样本建议:

  1. 划分比例(按数据量)

    • 小数据(小于1 万张):6:2:2 或 7:2:1;建议验证 / 测试集各至少 100 样本
    • 中数据(1 万到100 万张):8:1:1;验证 / 测试集各至少 1,000 样本
    • 大数据(大于100 万张):98:1:1;验证 / 测试集各至少 1 万样本
    import os
    import shutil
    import random
    from pathlib import Path

    # 源数据集目录
    IMAGE_DIR = "./images"
    LABEL_DIR = "./labels"
    # 输出目录
    OUTPUT_DIR = "./data"
    # 拆分比例 (训练集, 测试集, 验证集)
    SPLIT_RATIO = (0.6, 0.2, 0.1)
    # 随机种子(保证结果可复现)
    RANDOM_SEED = 42
    # 支持的图片格式
    IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp")


    def get_image_files(directory):
    """获取目录中的所有图片文件(不递归)"""
    return [
    f for f in os.listdir(directory)
    if f.lower().endswith(IMAGE_EXTENSIONS)
    ]

    def create_dirs():
    """创建输出目录结构"""
    splits = ["train", "test", "valid"]
    for split in splits:
    os.makedirs(os.path.join(OUTPUT_DIR, split, "images"), exist_ok=True)
    os.makedirs(os.path.join(OUTPUT_DIR, split, "labels"), exist_ok=True)

    def copy_file(src, dst):
    """复制文件,若目标已存在则跳过"""
    if os.path.exists(src):
    shutil.copy2(src, dst)
    return True
    return False

    def split_dataset():
    """执行数据集拆分"""
    # 获取所有图片文件
    image_files = get_image_files(IMAGE_DIR)
    if not image_files:
    print(f"❌ 未找到任何图片文件: {IMAGE_DIR}")
    return

    # 随机打乱
    random.seed(RANDOM_SEED)
    random.shuffle(image_files)

    total = len(image_files)
    train_end = int(total * SPLIT_RATIO[0])
    test_end = train_end + int(total * SPLIT_RATIO[1])

    split_map = {
    "train": image_files[:train_end],
    "test": image_files[train_end:test_end],
    "valid": image_files[test_end:],
    }

    print(f"📊 数据集总量: {total} 张")
    print(f" 训练集: {len(split_map['train'])} 张")
    print(f" 测试集: {len(split_map['test'])} 张")
    print(f" 验证集: {len(split_map['valid'])} 张")
    print()

    # 创建目录结构
    create_dirs()

    # 复制文件
    for split, files in split_map.items():
    img_missing = 0
    lbl_missing = 0
    for img_file in files:
    stem = Path(img_file).stem

    # 复制图片
    src_img = os.path.join(IMAGE_DIR, img_file)
    dst_img = os.path.join(OUTPUT_DIR, split, "images", img_file)
    if not copy_file(src_img, dst_img):
    img_missing += 1

    # 复制标注(.txt)
    label_file = stem + ".txt"
    src_lbl = os.path.join(LABEL_DIR, label_file)
    dst_lbl = os.path.join(OUTPUT_DIR, split, "labels", label_file)
    if not copy_file(src_lbl, dst_lbl):
    lbl_missing += 1

    print(f"✅ {split:5s}: 已复制 {len(files)} 张图片"
    + (f",{img_missing} 张图片缺失" if img_missing else "")
    + (f",{lbl_missing} 个标注缺失" if lbl_missing else ""))

    print()
    print(f"🎉 拆分完成!输出目录: {os.path.abspath(OUTPUT_DIR)}")


    if __name__ == "__main__":
    split_dataset()
  2. 关键原则

    • 互不重叠:避免信息泄露,确保评估真实
    • 分布一致:分层抽样,保证类别 / 特征分布与整体相同
    • 验证集质量:CVAT 建议验证集由专家审核,作为基准真值(Ground Truth)

实操建议

  1. CVAT

    • 本地部署解除云配额限制,适合大规模标注。

    • 标注前按 train/val/test 分任务上传,或标注后导出全量,用脚本(如 Python 随机抽样)拆分。

    • 质量评估:按 5–15% 抽取验证集,做双重标注与对比。

  2. Label Studio

    • 大规模数据优先配置 S3/OSS 等云存储,避免 UI 上传风险Label Studio。

    • 拆分方式:

      • 简单场景:导出全量后用 sklearn.model_selection.train_test_split 按比例划分。
      • 复杂场景:开启实验性功能,用 choices(['train','val','test'], [0.8,0.1,0.1]) 批量打标后导出。
    • 小样本:每类别至少 30 张标注;模型辅助标注需 50–100 条高质量种子集。

最后更新时间: 2026/7/31 13:40:35|访问次数: 0|豫ICP备2025159864号|