本文以通用图像分割任务为例,介绍如何启动 Label Studio、配置 Brush 标注界面、导入图片、运行模型生成 mask,并通过 API 将自动标注结果上传为 Label Studio Predictions。文中的路径、类别名、模型名和项目 ID 都是占位示例,实际使用时请替换成自己的配置。

一、整体流程

自动标注的推荐流程如下:

  1. 启动 Label Studio。
  2. 新建图像标注项目。
  3. 配置 BrushLabels 分割标注界面。
  4. 导入待标注图片。
  5. 使用训练好的模型对图片生成预测 mask。
  6. 将预测 mask 转换为 Label Studio brush result。
  7. 通过 Label Studio API 上传为 Predictions。
  8. 人工在页面中检查、修改并提交正式标注。

推荐把自动结果上传为 Predictions,不要一开始就直接写入 Annotations。这样自动结果只是模型预测,人工可以逐张确认和修改,不会直接覆盖已有人工标注。

二、环境准备

1. 安装 Label Studio

如果还没有安装,可以使用 pip 安装:

pip install label-studio

如果使用 Conda,建议创建或激活自己的环境:

conda create -n labelstudio-env python=3.10
conda activate labelstudio-env
pip install label-studio

2. 安装模型推理依赖

下面是常见图像分割推理脚本需要的依赖:

pip install opencv-python numpy torch requests

如果你的模型不是 PyTorch,请替换为对应框架的依赖。

三、启动 Label Studio

进入你的工作目录:

cd /path/to/your_project

启动 Label Studio:

label-studio

部分版本也可以显式写成:

label-studio start

启动成功后,浏览器访问:

http://localhost:8080

如果要指定端口:

label-studio --port 8081

四、创建项目并配置标注界面

新建项目后,进入 Settings -> Labeling Interface,使用下面的 XML 配置。

注意:

  • Imagename 需要和后续脚本的 --to-name 保持一致。
  • BrushLabelsname 需要和后续脚本的 --from-name 保持一致。
  • Label value 需要和模型输出的类别目录名保持一致。
<View>
  <Image name="image" value="$image" zoom="true" zoomControl="true" rotateControl="true"/>
  <BrushLabels name="tag" toName="image">
    <Label value="label_a" background="#ff00ff"/>
    <Label value="label_b" background="#00dcdc"/>
    <Label value="label_c" background="#ff0000"/>
    <Label value="label_d" background="#5050ff"/>
  </BrushLabels>
</View>

如果你的任务只有两个类别,可以删掉多余的 Label。如果类别更多,继续添加即可。

五、导入图片

建议将待标注图片统一放到一个目录,例如:

data/images/

然后在 Label Studio 项目页面点击 Import,上传该目录中的图片。

导入完成后,进入项目数据页,浏览器地址通常类似:

http://localhost:8080/projects/<project_id>/data

其中 <project_id> 就是后续 API 脚本要用的项目 ID。

六、获取 API Token

进入右上角用户菜单:

Account & Settings -> Access Token

复制 Personal Access Token。

后续示例中统一用:

<your_token>

作为占位符。

新版 Personal Access Token 通常使用:

--token-type pat

旧版固定 Token 可以使用:

--token-type legacy

七、推荐目录结构

建议使用下面的通用目录结构:

your_project/
  data/
    images/
      image_001.jpg
      image_002.jpg
  models/
    model.pt
  scripts/
    predict_multilabel_unet.py
    labelstudio_api_predictions.py
    make_labelstudio_multilabel_results.py
  outputs/
    predictions/

其中:

  • data/images/:待标注图片。
  • models/model.pt:训练好的分割模型。
  • outputs/predictions/:模型预测输出。
  • scripts/labelstudio_api_predictions.py:读取 Label Studio tasks,并上传 Predictions。

八、运行模型生成自动 mask

下面示例假设模型推理脚本会为每张图片输出:

outputs/predictions/
  masks_binary/
    label_a/
      image_001.png
      image_002.png
    label_b/
      image_001.png
      image_002.png
  masks_semantic/
  overlays/
  summary.csv

其中 masks_binary/<label_name>/<image_stem>.png 是每个类别对应的二值 mask。

运行示例:

python scripts/predict_multilabel_unet.py \
  --model models/model.pt \
  --images-dir data/images \
  --out-dir outputs/predictions \
  --thresholds "label_a=0.50,label_b=0.50,label_c=0.50,label_d=0.50" \
  --prob-upsample

Windows PowerShell 写法:

python scripts\predict_multilabel_unet.py `
  --model models\model.pt `
  --images-dir data\images `
  --out-dir outputs\predictions `
  --thresholds "label_a=0.50,label_b=0.50,label_c=0.50,label_d=0.50" `
  --prob-upsample

参数说明:

参数 说明
--model 模型权重路径
--images-dir 待预测图片目录
--out-dir 预测输出目录
--thresholds 每个类别的二值化阈值
--prob-upsample 先放大概率图再二值化,边界通常更平滑

九、上传为 Label Studio Predictions

1. 先 dry-run

第一次建议不要直接上传,先生成本地 JSON 文件检查结果。

python scripts/labelstudio_api_predictions.py \
  --base-url http://localhost:8080 \
  --project-id <project_id> \
  --token "<your_token>" \
  --token-type pat \
  --pred-dir outputs/predictions \
  --tasks-out outputs/labelstudio_tasks.json \
  --predictions-out outputs/labelstudio_predictions.json \
  --from-name tag \
  --to-name image \
  --model-version auto-segmentation-v1 \
  --score 0.80 \
  --min-pixels 20

Windows PowerShell 写法:

python scripts\labelstudio_api_predictions.py `
  --base-url http://localhost:8080 `
  --project-id <project_id> `
  --token "<your_token>" `
  --token-type pat `
  --pred-dir outputs\predictions `
  --tasks-out outputs\labelstudio_tasks.json `
  --predictions-out outputs\labelstudio_predictions.json `
  --from-name tag `
  --to-name image `
  --model-version auto-segmentation-v1 `
  --score 0.80 `
  --min-pixels 20

如果输出中能看到类似信息,说明本地转换成功:

generated predictions=<prediction_count>
dry-run: not uploading predictions

2. 正式上传

确认 JSON 没问题后,加上 --upload

python scripts/labelstudio_api_predictions.py \
  --base-url http://localhost:8080 \
  --project-id <project_id> \
  --token "<your_token>" \
  --token-type pat \
  --pred-dir outputs/predictions \
  --tasks-out outputs/labelstudio_tasks.json \
  --predictions-out outputs/labelstudio_predictions.json \
  --from-name tag \
  --to-name image \
  --model-version auto-segmentation-v1 \
  --score 0.80 \
  --min-pixels 20 \
  --upload

上传完成后,刷新 Label Studio 项目页面,打开任务,应该能看到模型预测的 brush mask。

十、核心 Python 代码示例

下面给出一个精简版上传脚本,演示完整思路:拉取项目 tasks、根据图片文件名匹配 mask、构造 Prediction payload、调用 API 上传。

实际生产中建议增加日志、异常重试、分页处理和结果校验。

import base64
import json
from pathlib import Path

import cv2
import numpy as np
import requests


BASE_URL = "http://localhost:8080"
PROJECT_ID = "<project_id>"
TOKEN = "<your_token>"
PRED_DIR = Path("outputs/predictions")
FROM_NAME = "tag"
TO_NAME = "image"
MODEL_VERSION = "auto-segmentation-v1"

LABELS = ["label_a", "label_b", "label_c", "label_d"]


def request_json(method, url, token, **kwargs):
    headers = kwargs.pop("headers", {})
    headers["Authorization"] = f"Token {token}"
    response = requests.request(method, url, headers=headers, **kwargs)
    response.raise_for_status()
    if response.text:
        return response.json()
    return None


def fetch_tasks():
    url = f"{BASE_URL}/api/projects/{PROJECT_ID}/tasks/"
    return request_json("GET", url, TOKEN)


def image_stem_from_task(task):
    image_url = task["data"]["image"]
    filename = image_url.split("/")[-1]
    return Path(filename).stem


def mask_to_brush_result(mask_path, label_name, original_width, original_height):
    mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE)
    if mask is None:
        return None

    mask = (mask > 0).astype(np.uint8) * 255
    if int(mask.sum()) == 0:
        return None

    success, encoded = cv2.imencode(".png", mask)
    if not success:
        return None

    png_base64 = base64.b64encode(encoded.tobytes()).decode("ascii")

    return {
        "from_name": FROM_NAME,
        "to_name": TO_NAME,
        "type": "brushlabels",
        "origin": "prediction",
        "value": {
            "format": "brushlabels",
            "brushlabels": [label_name],
            "rle": png_base64,
            "width": original_width,
            "height": original_height,
        },
    }


def build_prediction_for_task(task):
    stem = image_stem_from_task(task)
    results = []

    # 如果能从 task 里拿到真实宽高,建议使用真实宽高。
    # 这里为了示例,使用任务数据中的 width/height 字段;没有时需要自行读取原图尺寸。
    width = task["data"].get("width")
    height = task["data"].get("height")
    if not width or not height:
        raise ValueError("task data must contain width and height, or read them from local images")

    for label_name in LABELS:
        mask_path = PRED_DIR / "masks_binary" / label_name / f"{stem}.png"
        if not mask_path.exists():
            continue
        result = mask_to_brush_result(mask_path, label_name, width, height)
        if result:
            results.append(result)

    if not results:
        return None

    return {
        "task": task["id"],
        "model_version": MODEL_VERSION,
        "score": 0.8,
        "result": results,
    }


def upload_prediction(prediction):
    url = f"{BASE_URL}/api/predictions/"
    return request_json("POST", url, TOKEN, json=prediction)


def main():
    tasks = fetch_tasks()
    predictions = []

    for task in tasks:
        pred = build_prediction_for_task(task)
        if pred:
            predictions.append(pred)

    Path("outputs/labelstudio_predictions_preview.json").write_text(
        json.dumps(predictions, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

    for pred in predictions:
        upload_prediction(pred)

    print(f"uploaded predictions: {len(predictions)}")


if __name__ == "__main__":
    main()

注意:Label Studio brush result 在不同版本中对 mask 编码格式有差异。正式项目中建议优先复用已经验证过的转换脚本,确保生成的 rlewidthheightfrom_nameto_name 与当前 Label Studio 版本一致。

十一、如果要生成 Annotations

一般不建议第一步就把自动结果写成正式 Annotations。如果确实需要,可以先把预测结果转换成 annotations payload:

python scripts/make_labelstudio_multilabel_results.py \
  --tasks-json outputs/labelstudio_tasks.json \
  --pred-dir outputs/predictions \
  --out outputs/labelstudio_annotations.json \
  --format annotations-api \
  --start-id <start_task_id> \
  --end-id <end_task_id> \
  --from-name tag \
  --to-name image \
  --model-version auto-segmentation-v1 \
  --score 0.80 \
  --min-pixels 20

然后再通过替换脚本进行 dry-run:

python scripts/labelstudio_replace_annotations.py \
  --base-url http://localhost:8080 \
  --project-id <project_id> \
  --token "<your_token>" \
  --token-type pat \
  --annotations-json outputs/labelstudio_annotations.json \
  --start-id <start_task_id> \
  --end-id <end_task_id> \
  --plan-out outputs/replace_plan.json

确认 replace_plan.json 后,再追加 --execute 执行。

十二、常见问题

1. 页面里看不到自动 mask

重点检查:

  • XML 中 BrushLabels name 是否等于脚本的 --from-name
  • XML 中 Image name 是否等于脚本的 --to-name
  • Label value 是否和 masks_binary/<label_name>/ 目录名一致。
  • mask 文件名是否和任务图片名能匹配。
  • mask 是否为空,是否低于 --min-pixels

2. API 返回 401

常见原因:

  • Token 复制错误。
  • Personal Access Token 和 legacy token 类型混用。
  • 当前版本需要先用 Personal Access Token 换取短期 Bearer token。

可以先确认 token 类型:

python scripts/labelstudio_api_predictions.py \
  --base-url http://localhost:8080 \
  --project-id <project_id> \
  --token "<your_token>" \
  --token-type pat \
  --pred-dir outputs/predictions

3. 图片文件名匹配不上

Label Studio 上传本地图片后,URL 中可能带有额外前缀。脚本中建议做文件名归一化,例如:

from pathlib import Path


def normalize_name(image_url: str) -> str:
    filename = image_url.split("/")[-1]
    stem = Path(filename).stem
    # 如果你的系统会给文件名前面加 UUID,可以在这里去掉前缀。
    return stem

4. 已有人工标注怎么办

建议上传 Predictions 时默认跳过已有人工标注的任务。人工标注通常优先级更高,不建议自动结果直接覆盖。

如果确实需要重新上传预测,可以增加类似参数:

--include-existing-predictions

5. 阈值怎么调

阈值太低会产生很多误检,阈值太高会漏掉目标。建议先对少量图片生成 overlays/ 可视化图,人工查看后再批量处理。

示例:

python scripts/predict_multilabel_unet.py \
  --model models/model.pt \
  --images-dir data/images \
  --out-dir outputs/predictions_v2 \
  --thresholds "label_a=0.60,label_b=0.55,label_c=0.70,label_d=0.50" \
  --prob-upsample

十三、发布前检查清单

正式批量上传前,建议确认:

  • Label Studio 项目能正常打开。
  • 项目 ID 正确。
  • Token 有效。
  • 标注 XML 中的 from_nameto_name 与脚本一致。
  • 图片已全部导入 Label Studio。
  • 本地预测目录存在 masks_binary/<label_name>/xxx.png
  • dry-run 生成的 JSON 数量符合预期。
  • 随机抽查几张任务,mask 位置和类别正确。
  • 确认上传为 Predictions,而不是直接覆盖 Annotations。

十四、总结

Label Studio 自动标注的关键不是“把模型跑起来”这么简单,而是要保证三组信息完全一致:

  1. Label Studio 标注配置中的 from_nameto_nameLabel value
  2. 模型输出目录中的类别名和 mask 文件名。
  3. API payload 中的任务 ID、图片尺寸、brush result 格式。

建议实践顺序是:先少量图片验证,再 dry-run 生成 JSON,最后批量上传 Predictions。这样既能利用模型提高效率,又能保留人工审核和修正的空间。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐