🎨 数据标注 · 像素级精修

分割标注 · 勾勒每个像素

在图像上为每个目标绘制精确的轮廓,做到"像素级"的形状标注。是医学影像、自动驾驶可行驶区域、卫星遥感等高精度场景的核心——比检测框精确 10 倍,但标注成本也高得多。

① 什么是分割标注?

✏️

定义

沿目标轮廓描点形成多边形,或直接对每个像素涂色,精确标注目标形状的边界

🏥

应用场景

医学影像(肿瘤/器官)、自动驾驶(车道/路沿)、卫星遥感(建筑/农田)、AR 背景抠图等

⚡

难度

★★★★★ 最高级,每个目标 10-50 个点,耗时约为检测框的 5-10 倍

💡 分割标注 vs 检测标注的核心区别:检测只画一个粗略的框,分割要描出目标的真实轮廓。一个框 → 一组轮廓点,精度提升 10 倍但工作量也翻数倍。

② 三种分割类型

1

语义分割 Semantic Segmentation

按类别给每个像素涂色——同类目标不区分个体。例如所有"道路"都是蓝色,所有"建筑"都是红色,不论有几条路几栋楼。

sky building road tree
✅ 适合
背景理解(天空/道路/建筑);不关心个体数量;每个像素只需一个类别;模型简单(FCN/U-Net/DeepLab)
⚠️ 局限
同类目标不分个体(3 个人都是"person");遮挡/重叠无法分开实例;无法计数"有几个"
2

实例分割 Instance Segmentation

不仅要分出类别,还要区分同类目标的不同个体——每个人、每辆车都有自己独立的 Mask。

person #1 person #2 person #3
实例分割 vs 语义分割的关键:
语义分割:3 个人 → 整片"person"色块(不分个体)
实例分割:3 个人 → 3 个独立 Mask,可单独计数和追踪
代表模型:Mask R-CNN、SOLOv2、YOLACT、SAM
3

全景分割 Panoptic Segmentation

语义 + 实例的合体——图像中每个像素都有标签:可数目标(人/车)分实例,不可数目标(天空/道路)分语义,一张图全标注。

sky(语义) road(语义) person#1 person#2 car#1
全景分割 = 语义 + 实例:
可数目标(stuff):人/车/狗 → 分实例(每个独立 Mask)
不可数目标(things):天空/道路/草地 → 分语义(整片同色)
代表模型:Panoptic FPN、MaskFormer、Mask2Former
评估指标:PQ(Panoptic Quality,结合分割+识别质量)

③ 分割标注工作全流程

1

定义类别 & 标注精度

确定要分割哪些类别、每个类别的轮廓精度,产出 seg_guide.md。

# 医学影像分割标注规范
tumor: 肿瘤实体 → 精确到 2px 内,含坏死区,不含水肿
organ: 器官轮廓 → 含包膜,不含周围血管
vessel: 血管 → 描内沿,分支处取主干
规则: 遮挡>50% 不标;<10px 跳过;多边形 5-50 点
2

准备图像 & 标注工具

分割标注对工具要求高——必须支持多边形描点、笔刷涂色、SAM 辅助

📤 数据准备
图像格式与标注分辨率
高分辨率 / DICOM/医学 / 遥感影像
🛠️ 标注工具 (分割标注专用)
🖥️ 桌面
Labelme · ITK-SNAP
🌐 Web 服务
CVAT · Label Studio
🤖 AI 辅助
SAM/SAM2 · Roboflow
3

AI 辅助标注(SAM 革命)

分割标注最大的效率提升来自 SAM——从描 50 个点变成点 1-3 下

纯手动描点:最慢,精度高 SAM 点击标注:点 1-3 下出轮廓 SAM 框选批量:框内全出
4

质量控制(QC)

1 Mask IoU 检查 2 边界 F-score 3 Mask 质量排查
Mask IoU 一致性解读:
≥ 0.85 轮廓高度一致 → 高质量
0.70 ~ 0.85 基本一致 → 可接受,需复核边缘
< 0.70 轮廓差异大 → 重新审标注规范
5

导出 & 训练对接

分割标注的数据量远大于检测——每个目标是一组轮廓点或一个 Mask 图像

🔍 数据检查
Mask 面积/连通性/类别平衡
💾 格式转换
→ 多边形/RLE → 统一解码 → 模型训练格式

④ 常用分割标注工具对比

工具 类型 费用 分割标注 亮点
Labelme ▼ 桌面 GUI 免费 ✅ 强 多边形描点,轻量单机
CVAT ▼ Web 服务 免费 ✅ 极强 内置 SAM2,分割标注效率 10x
Label Studio ▼ Web 服务 免费 ✅ 强 笔刷+多边形,SAM backend 可接
ITK-SNAP ▼ 桌面 GUI 免费 ✅ 专用 医学影像 3D 分割首选

⑤ 分割数据格式对比

📋

COCO RLE

压缩编码 Mask
// annotation 对象
{
  "segmentation": {
    "counts": "Vh2T...",
    "size": [480, 640]
  },
  "category_id": 1
}
counts:RLE 压缩字符串
size:图像高宽
→ 实例分割标准格式
🖼️

PNG Mask

每像素是 class_id
# mask.png 内容
[[0, 0, 0, 1, 1, 1],
 [0, 0, 0, 1, 1, 1],
 [2, 2, 0, 1, 1, 1],
 [2, 2, 2, 0, 0, 0]]
# 0=背景 1=道路 2=建筑
像素值:class_id(0,1,2...)
尺寸:与原图一致
格式:单通道 PNG(无 RGB)
→ 语义分割标准格式
⬡

多边形坐标

轮廓点列表
// Labelme JSON 格式
{
  "label": "tumor",
  "shape_type": "polygon",
  "points": [
   [120,80],[180,60],
   [240,90],[260,150]
  ]
}
points:轮廓坐标点 [x,y]
label:类别名
5-50 点:简单~复杂形状
→ 标注工具原生格式
💡 格式转换速查: 多边形 → Mask · Mask → RLE · RLE → Mask

⑥ 实战:分割标注 → 转换 → 训练 U-Net

# === 1. 目录结构(语义分割 PNG Mask 格式) ===
dataset/
├── images/
│   ├── train/    # 0001.jpg, 0002.jpg ...
│   └── val/
├── masks/        # 与 images 同名,但 PNG,像素=class_id
│   ├── train/    # 0001.png, 0002.png ...
│   └── val/
└── config.yaml

# === 2. 多边形 JSON → PNG Mask 转换 ===
import json, glob, os
import cv2, numpy as np

# 类别映射:label_name → class_id
CLASS_MAP = {'road': 1, 'building': 2, 'sky': 3}

for json_path in glob.glob("labels/*.json"):
    data = json.load(open(json_path))
    h, w = data['imageHeight'], data['imageWidth']
    mask = np.zeros((h, w), dtype=np.uint8)  # 0=背景

    for shape in data['shapes']:
        cls_id = CLASS_MAP[shape['label']]
        points = np.array(shape['points'], dtype=np.int32)
        cv2.fillPoly(mask, [points], cls_id)  # 填充多边形

    out_name = os.path.basename(json_path).replace('.json', '.png')
    cv2.imwrite(f"masks/train/{out_name}", mask)

# === 3. 检查 Mask 质量 ===
from collections import Counter
mask = cv2.imread("masks/train/0001.png", cv2.IMREAD_GRAYSCALE)
print("各类像素数:", dict(Counter(mask.flatten())))
# {0: 150000, 1: 80000, 2: 50000, 3: 30000}  ← 背景 150k 最多

# === 4. 训练 U-Net(segmentation_models_pytorch) ===
import torch
from torch.utils.data import DataLoader
import segmentation_models_pytorch as smp

model = smp.Unet(
    encoder_name="resnet34",
    encoder_weights="imagenet",
    in_channels=3,
    classes=4  # 背景+road+building+sky
)

# 损失函数:Dice Loss(分割专用,解决类别不平衡)
loss_fn = smp.losses.DiceLoss(mode='multiclass')
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

# 训练循环...
for epoch in range(100):
    for imgs, masks in train_loader:
        preds = model(imgs)
        loss = loss_fn(preds, masks)
        optimizer.zero_grad(); loss.backward(); optimizer.step()
    print(f"Epoch {epoch}, Loss: {loss:.4f}")
# 训练完 → model 为分割模型,输入图片输出每像素类别
✕
点击任意处关闭 · ESC 关闭