Label Studio + label-studio-ml 自动标注技术指南
Label Studio + label-studio-ml 自动标注技术指南
本文档详细阐述 Label Studio 安装配置、检测模板创建、label-studio-ml 自动标注后端搭建的完整流程。
示例使用动物目标检测类别(猫、狗、鸟),可替换为任意自定义类别。
一、环境准备
1.1 创建 Conda 环境
conda create -n labelstudio python=3.10 -y
conda activate labelstudio
1.2 安装依赖
# Label Studio 标注平台
pip install label-studio -i https://pypi.tuna.tsinghua.edu.cn/simple
# ML Backend 框架
pip install label-studio-ml -i https://pypi.tuna.tsinghua.edu.cn/simple
# YOLOv8(用于目标检测模型)
pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple
# Windows 环境下的 WSGI 服务器(替代 Linux 专用的 gunicorn)
pip install waitress -i https://pypi.tuna.tsinghua.edu.cn/simple
1.3 依赖清单(requirements.txt)
label-studio>=1.8.0
label-studio-ml>=1.0.0
ultralytics>=8.0.0
waitress>=3.0.0
numpy
Pillow
requests
二、启动 Label Studio
2.1 基本启动
conda activate labelstudio
label-studio start -p 8080
浏览器访问 http://localhost:8080,首次访问需注册管理员账号。
2.2 自定义数据存储目录(推荐)
默认数据存储在 C 盘用户目录下,建议迁移到 D 盘:
# Windows
label-studio start --data-dir "D:\label-studio-data" -p 8080
# 或创建快捷启动脚本 start_label_studio.bat
@echo off
echo Starting Label Studio...
label-studio start --data-dir "D:\label-studio-data" -p 8080
pause
2.3 启用本地文件访问
在数据目录下创建 .env 文件:
LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=D:/label-studio-data/media
三、创建检测项目与模板
3.1 新建项目
- 登录 Label Studio → 点击 Create Project
- 填写项目名称(如 "动物目标检测")
- 在 Labeling Setup 中选择 Custom Template
3.2 检测模板(XML)
将以下模板粘贴到 Label Studio 的模板编辑器中:
<View>
<Image name="image" value="$image" zoom="true" crossOrigin="anonymous"/>
<Header value="目标检测标注 - 框选目标并选择类别"/>
<RectangleLabels name="bbox_labels" toName="image" strokeWidth="3">
<Label value="猫" background="#FF4444" shortcut="1"/>
<Label value="狗" background="#44AAFF" shortcut="2"/>
<Label value="鸟" background="#44FF44" shortcut="3"/>
<Label value="鱼" background="#FFAA00" shortcut="4"/>
<Label value="马" background="#AA44FF" shortcut="5"/>
</RectangleLabels>
</View>
模板要素说明:
| 元素 | 作用 |
|---|---|
<Image> |
声明图像字段,zoom 允许缩放 |
<RectangleLabels> |
矩形框标注容器,toName 绑定到图像 |
<Label> |
单个类别,value 为显示名称,shortcut 为键盘快捷键 |
3.3 导入图片
- 点击 Import → 选择本地图片文件(支持 JPG/PNG)
- 或使用 CSV/JSON 批量导入:
[
{"image": "https://example.com/cat.jpg"},
{"image": "https://example.com/dog.jpg"}
]
3.4 标注操作
- 点击图片进入标注界面
- 在图像上拖拽绘制矩形框
- 弹出类别选择器,选择对应类别(或按快捷键 1-5)
- 点击 Submit 提交标注
- 使用 → 键切换到下一张图片
四、配置 ML Backend 自动标注
4.1 项目结构
ml_backend/
├── server.py # ML Backend 核心代码
├── run_server.py # 启动脚本
├── model_weights/ # 模型权重目录
│ └── yolo_animals.pt # YOLOv8 模型
└── server.log # 运行日志
4.2 server.py - ML Backend 核心代码
"""
Label Studio ML Backend - 目标检测自动标注服务
支持 YOLOv8 检测模型
"""
import os
import io
import logging
from pathlib import Path
import numpy as np
from PIL import Image
from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.utils import get_image_size
logger = logging.getLogger(__name__)
MODEL_DIR = os.getenv("MODEL_DIR", os.path.join(os.path.dirname(__file__), "model_weights"))
# 自动标注配置
AUTO_ANNOTATION_CONFIG = {
"detection": {
"enabled": True, # 设为 False 使用 mock 预测
"model_path": os.path.join(MODEL_DIR, "yolo_animals.pt"),
"confidence_threshold": 0.35,
"iou_threshold": 0.5,
},
}
class AnimalDetectionModel(LabelStudioMLBase):
"""目标检测自动标注 (YOLOv8)"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.model_dir = MODEL_DIR
self.model = None
self.config = AUTO_ANNOTATION_CONFIG["detection"]
logger.info(f"[Detection] config: {self.config}")
self._load_model()
def _load_model(self):
if not self.config["enabled"]:
logger.info("[Detection] YOLOv8 未启用,使用 mock 预测")
return
logger.info("[Detection] 启用YOLOv8进行预标注")
model_path = self.config["model_path"]
if os.path.exists(model_path):
try:
from ultralytics import YOLO
self.model = YOLO(model_path)
logger.info(f"[Detection] YOLO model loaded from {model_path}")
except Exception as e:
logger.warning(f"[Detection] Failed to load YOLO model: {e}")
else:
logger.warning(f"[Detection] Model not found: {model_path}, using mock predictions")
def predict(self, tasks, context=None, **kwargs):
logger.info(f"[Detection][predict] 收到 {len(tasks)} 个任务")
predictions = []
for task in tasks:
result = self._predict_single(task)
predictions.append({
"result": result,
"model_version": "1.0.0",
})
return predictions
def _predict_single(self, task):
image_url = task["data"]["image"]
# 获取图像尺寸
img_width, img_height = 640, 640
if image_url.startswith("/data/"):
local_path = self._resolve_labelstudio_path(image_url)
if local_path:
try:
with Image.open(local_path) as img:
img_width, img_height = img.size
except Exception:
pass
else:
try:
img_width, img_height = get_image_size(image_url)
except Exception:
pass
if self.model is not None:
return self._predict_with_model(image_url, img_width, img_height)
else:
return self._mock_prediction(img_width, img_height)
def _predict_with_model(self, image_url, img_width, img_height):
img = self._read_image(image_url)
results = self.model(
img,
conf=self.config["confidence_threshold"],
iou=self.config["iou_threshold"],
)
annotations = []
for r in results:
boxes = r.boxes
if boxes is not None:
for box in boxes:
x1, y1, x2, y2 = box.xyxy[0].tolist()
conf = float(box.conf[0])
cls = int(box.cls[0])
label = self._class_to_label(cls)
annotations.append({
"from_name": "bbox_labels",
"to_name": "image",
"type": "rectanglelabels",
"value": {
"x": x1 / img_width * 100,
"y": y1 / img_height * 100,
"width": (x2 - x1) / img_width * 100,
"height": (y2 - y1) / img_height * 100,
"rectanglelabels": [label],
},
"score": conf,
})
return annotations
def _read_image(self, image_url):
if image_url.startswith("/data/"):
local_path = self._resolve_labelstudio_path(image_url)
if local_path:
return Image.open(local_path)
raise FileNotFoundError(f"无法解析 Label Studio 路径: {image_url}")
import requests
response = requests.get(image_url)
return Image.open(io.BytesIO(response.content))
def _resolve_labelstudio_path(self, url_path):
possible_dirs = [
"D:/label-studio-data/media",
os.path.expanduser("~/.label-studio/media"),
os.path.join(os.environ.get("LOCALAPPDATA", ""), "label-studio", "label-studio", "media"),
]
relative = url_path.replace("/data/", "", 1)
filename = os.path.basename(url_path)
for ls_data_dir in possible_dirs:
if not ls_data_dir or not os.path.exists(ls_data_dir):
continue
local_path = os.path.join(ls_data_dir, relative)
if os.path.exists(local_path):
return local_path
for root, dirs, files in os.walk(ls_data_dir):
if filename in files:
return os.path.join(root, filename)
return None
def _mock_prediction(self, img_width, img_height):
"""模型未加载时返回模拟预测结果"""
cx, cy = img_width * 0.5, img_height * 0.5
bw, bh = img_width * 0.3, img_height * 0.3
return [{
"from_name": "bbox_labels",
"to_name": "image",
"type": "rectanglelabels",
"value": {
"x": (cx - bw / 2) / img_width * 100,
"y": (cy - bh / 2) / img_height * 100,
"width": bw / img_width * 100,
"height": bh / img_height * 100,
"rectanglelabels": ["猫"],
},
"score": 0.75,
}]
def _class_to_label(self, cls_id):
# 与训练时的 classes 顺序一致
labels = ["猫", "狗", "鸟", "鱼", "马"]
return labels[cls_id] if cls_id < len(labels) else f"class_{cls_id}"
def fit(self, event, data, **kwargs):
pass
4.3 run_server.py - 启动脚本
"""
ML Backend 启动脚本(跨平台)
- Windows: 使用 waitress
- Linux: 使用 gunicorn
"""
import sys
import os
import socket
import subprocess
import logging
sys.path.insert(0, os.path.dirname(__file__))
def kill_port(port):
"""检测端口是否被占用,如果是则杀掉占用进程"""
if sys.platform == "win32":
result = subprocess.run(["netstat", "-ano"], capture_output=True, text=True)
for line in result.stdout.splitlines():
if f":{port}" in line and "LISTENING" in line:
pid = line.strip().split()[-1]
print(f"端口 {port} 被进程 {pid} 占用,正在杀掉...")
subprocess.run(["taskkill", "/PID", pid, "/F"], capture_output=True)
else:
result = subprocess.run(["lsof", "-ti", f":{port}"], capture_output=True, text=True)
for pid in result.stdout.strip().split("\n"):
if pid:
print(f"端口 {port} 被进程 {pid} 占用,正在杀掉...")
subprocess.run(["kill", "-9", pid], capture_output=True)
LOG_FILE = os.path.join(os.path.dirname(__file__), "server.log")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(LOG_FILE, encoding="utf-8"),
],
)
from server import AnimalDetectionModel
def run_waitress(host="0.0.0.0", port=9090):
from waitress import serve
from label_studio_ml.api import init_app
model = AnimalDetectionModel
app = init_app(model_class=model)
print(f"Starting ML backend on http://{host}:{port} (waitress)")
serve(app, host=host, port=port)
def run_gunicorn(host="0.0.0.0", port=9090):
import subprocess
print(f"Starting ML backend on http://{host}:{port} (gunicorn)")
subprocess.run([
sys.executable, "-m", "gunicorn",
"--bind", f"{host}:{port}",
"_wsgi:model",
])
if __name__ == "__main__":
host = "0.0.0.0"
port = 9090
kill_port(port)
if sys.platform == "win32":
run_waitress(host, port)
else:
run_gunicorn(host, port)
4.4 启动 ML Backend
conda activate labelstudio
cd ml_backend
python run_server.py
启动成功输出:
Starting ML backend on http://0.0.0.0:9090 (waitress)
2026-09-15 10:00:00 [INFO] Serving on http://0.0.0.0:9090
五、在 Label Studio 中连接 ML Backend
5.1 配置 ML Backend 地址
- 进入项目 → 点击右上角 Settings
- 选择 Machine Learning 选项卡
- 点击 Add Model
- 填写:
- Name:
YOLOv8 动物检测 - URL:
http://localhost:9090
- Name:
- 勾选 Use for predictions
- 点击 Validate and Save
5.2 触发自动标注
- 返回标注界面,点击任意一张图片
- 等待 1-2 秒,模型会自动生成预测框
- 预测框以半透明虚线显示,与手动标注区分
- 检查预测结果:
- 正确 → 直接接受(或微调位置后提交)
- 错误 → 删除错误框,手动重新标注
- 点击 Submit 提交最终标注
5.3 自动标注工作流
上传图片
│
▼
ML Backend 接收预测请求
│
├── 模型已加载 → YOLOv8 推理 → 返回检测框
│
└── 模型未加载 → 返回 mock 预测框(用于流程测试)
│
▼
标注员审查/修正预测结果
│
▼
提交标注 → 数据可用于下一轮模型训练
六、YOLOv8 模型训练
6.1 从 Label Studio 导出数据
- 进入项目 → 点击 Export
- 选择 YOLO with Images
- 下载并解压,目录结构:
result/
├── images/ # 图像文件
├── labels/ # .txt 标注文件(class cx cy w h)
├── classes.txt # 类别列表
└── notes.json
classes.txt 内容示例:
猫
狗
鸟
鱼
马
labels 文件格式(每行一个检测框):
0 0.5 0.4 0.3 0.2 # class_id center_x center_y width height(归一化0-1)
1 0.2 0.6 0.15 0.25
6.2 创建 dataset.yaml
# data/annotations/detection/dataset.yaml
path: D:/project/data/annotations/detection
train: images
val: images
nc: 5
# 类别名称(顺序必须与 classes.txt 一致)
names:
0: 猫
1: 狗
2: 鸟
3: 鱼
4: 马
注意:
names的顺序必须与 Label Studio 导出的classes.txt完全一致,否则标签会错乱。
6.3 训练脚本
# train_yolo.py
from ultralytics import YOLO
import shutil
from pathlib import Path
DATA_YAML = "D:/project/data/annotations/detection/dataset.yaml"
PRETRAINED = "yolov8n.pt"
BEST_WEIGHT = "D:/project/ml_backend/model_weights/yolo_animals.pt"
def main():
print("开始训练 YOLOv8...")
model = YOLO(PRETRAINED)
results = model.train(
data=DATA_YAML,
epochs=50,
imgsz=640,
batch=8,
lr0=0.01,
device="cpu",
project="D:/project/runs/detect",
name="animals_train",
exist_ok=True,
)
best_pt = Path("D:/project/runs/detect/animals_train/weights/best.pt")
if best_pt.exists():
dest = Path(BEST_WEIGHT)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(best_pt, dest)
print(f"最佳权重已复制到: {dest}")
print("训练完成!")
if __name__ == "__main__":
main()
6.4 执行训练
conda activate labelstudio
python train_yolo.py
6.5 切换为训练后模型
训练完成后,模型权重自动复制到 ml_backend/model_weights/yolo_animals.pt。
在 server.py 中更新类别映射(如果类别有变化):
def _class_to_label(self, cls_id):
labels = ["猫", "狗", "鸟", "鱼", "马"] # 与 classes.txt 一致
return labels[cls_id] if cls_id < len(labels) else f"class_{cls_id}"
重启 ML Backend,新模型即生效。
七、完整工作流总结
┌─────────────────────────────────────────────────────┐
│ 完整工作流 │
├─────────────────────────────────────────────────────┤
│ │
│ 第一轮:手动标注 │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 上传图片 │───▶│ 手动标注 │───▶│ 导出 YOLO │ │
│ └──────────┘ └───────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 训练模型 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ 后续轮次:半自动标注 │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 上传图片 │───▶│ 模型预测 │───▶│ 人工修正 │ │
│ └──────────┘ └───────────┘ └──────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌───────────┐ ┌──────────┐ │
│ │ ML Backend │ │ 导出数据 │ │
│ └───────────┘ └──────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌───────────┐ ┌──────────┐ │
│ │ 模型权重 │◀──────│ 迭代训练 │ │
│ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────┘
八、常见问题
Q1: ML Backend 启动报 ModuleNotFoundError
# 确保在正确的 conda 环境中
conda activate labelstudio
pip install label-studio-ml waitress
Q2: Windows 上 gunicorn 报错 No module named 'fcntl'
gunicorn 仅支持 Linux。Windows 下使用 waitress(已在 run_server.py 中自动切换)。
Q3: Label Studio 无法加载本地图片
- 检查
.env文件中LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT路径是否正确 - 确保使用
--data-dir启动时指向正确目录 - ML Backend 中
_resolve_labelstudio_path的possible_dirs需包含实际存储路径
Q4: 预测结果类别显示为 class_0 而非具体名称
_class_to_label 的映射顺序必须与训练时 classes.txt 的顺序一致。从 Label Studio 导出的 classes.txt 查看正确顺序。
Q5: 端口 9090 被占用
run_server.py 已内置端口检测和清理功能,会自动杀掉占用进程后重启。
Q6: 模型预测框位置偏移
- YOLO 输出坐标是像素值,需转换为 Label Studio 要求的百分比坐标
- 转换公式:
x_pct = x_pixel / img_width * 100 - 确保
img_width和img_height是原始图像尺寸,不是缩放后的
九、关键配置速查
| 配置项 | 文件 | 说明 |
|---|---|---|
enabled |
server.py | 控制是否使用模型预测(False 时用 mock) |
confidence_threshold |
server.py | 置信度阈值,低于此值的预测会被过滤 |
iou_threshold |
server.py | NMS 的 IoU 阈值,用于去除重叠框 |
_class_to_label |
server.py | 模型 class ID → 显示标签的映射 |
dataset.yaml |
data/ | YOLO 训练数据集配置 |
classes.txt |
导出目录 | Label Studio 自动生成的类别列表 |
--data-dir |
启动命令 | Label Studio 数据存储目录 |

浙公网安备 33010602011771号