Skip to content
教程阅读约需 6 分钟

用于分割任务的多边形标注

在 Potato 中搭建一个多边形分割任务:配置怎么写、标注者绘制时按哪些键、导出坐标是什么格式,以及什么时候画笔蒙版才是更合适的工具。

Potato Team

本教程在 Potato 中搭建一个图像分割任务,标注者用多边形描出目标边界,内容涵盖配置、标注者要按的键,以及导出坐标的形状。

多边形是一个顶点一个顶点地描出边界,这正是语义分割和实例分割需要的东西,也是边界框给不了的。代价是标注者的时间,所以第一个要做的判断是:你的任务到底需不需要边界。

在多边形、边界框和蒙版之间做选择

Potato 里有三种工具能产生区域标注,它们的区别在于能表达什么,而不只是快慢。

边界框多边形画笔蒙版
边界精度只能是矩形逐顶点逐像素
孔洞与不相连的部分不行别扭自然
细长结构(电线、头发)不行差好
格式支持最广广:COCO、YOLO、CVAT、LabelMe较窄:COCO RLE、蒙版 PNG
编辑方式拖角拖顶点重涂一片区域

当粗略定位就够用时就用边界框,多数检测训练都属于这一类。当模型要消费边界、而且目标是实心的,多边形才值得多花标注者的时间。一旦目标带有孔洞或细丝,画笔蒙版才是老实的工具,因为硬给自行车轮圈一个多边形,画出来的形状谁也没法核对。Potato 还有一个开放路径的 polyline 工具,用于车道线、血管这类线形特征,它导出时面积为零,而不是凭空编一个内部区域。

任务配置

多边形工具是按 scheme 通过 tools 启用的,labels 就是标注者可以赋给某个形状的类别。item_properties.text_key 指明数据文件里存放图像 URL 的字段,Potato 靠它知道要显示什么:

yaml
annotation_task_name: "Urban Scene Segmentation"
task_description: "Trace every object in the street scene."
 
data_files:
  - data/images.json
 
item_properties:
  id_key: id
  text_key: image_url
 
annotation_schemes:
  - annotation_type: image_annotation
    name: segments
    description: "Draw a polygon around each object."
    tools: [polygon]
    labels:
      - name: road
        color: "#808080"
        key_value: "1"
      - name: sidewalk
        color: "#F5F5DC"
        key_value: "2"
      - name: building
        color: "#8B4513"
        key_value: "3"
      - name: vegetation
        color: "#228B22"
        key_value: "4"
      - name: sky
        color: "#87CEEB"
        key_value: "5"
      - name: vehicle
        color: "#FF6B6B"
        key_value: "6"
      - name: person
        color: "#4ECDC4"
        key_value: "7"
 
output_annotation_dir: annotation_output

key_value 这些条目的作用比看上去大。一个七类任务,如果标注者每画一个形状都要去点工具栏,速度只有把类别放在数字键上的零头,而这个代价全部落在标注者身上。颜色必须是十六进制,因为蒙版是逐像素画的,而 CSS 颜色名会退化成红色。

数据文件里每个条目给出一个 id 和图像:

json
{"id": "img_001", "image_url": "https://example.com/street_001.jpg"}
{"id": "img_002", "image_url": "/static/images/street_002.jpg"}

用这份配置启动服务:

bash
potato start config.yaml -p 8000

画一个多边形,以及要按的键

画多边形就是沿着目标每个顶点点一下,然后双击闭合。一个多边形至少要三个点才能围出东西,而双击留下的重复顶点,Potato 会丢掉而不是存起来。

按键来自一套键位方案。默认的 v7 沿用 V7 Darwin 和 CVAT 的习惯,所以从这两个工具过来的标注者可以保留肌肉记忆:

按键动作
p选择多边形工具
v选择与移动模式,不激活任何绘制工具
双击闭合当前多边形
Delete 或 Backspace删除选中的标注
1 到 9按 key_value 选择标签
+ 或 = / -放大与缩小
0让图像适配视图
按住 Space 或 Alt 并拖动平移
Ctrl/Cmd+Z、Ctrl/Cmd+Shift+Z撤销与重做
h、Shift+H隐藏当前标签,或只显示该标签

如果你的研究已经在收数据,就在 scheme 上设 keybinding_profile: legacy,因为 v7 重新绑定了 b、f 和 l,研究中途换键位会损失准确率。在平板上,单指绘制,双指平移,双指捏合缩放。

要改一个已经画好的多边形,切到选择模式再拖它的顶点。处理曲线边界的标注者,先铺一条粗略轮廓再细化,比第一遍就想把每个顶点放准效果更好,而且描细碎部分之前就该先放大,而不是描完再放大。

重叠的目标与实例

两个目标重叠时,标注者就画两个相互重叠的多边形。Potato 把每个形状作为独立条目存储,所以一个有四辆车的场景就是四个多边形,无论它们是否互相遮挡,而且实例身份会一直保留到导出结果里。画多边形时,语义任务和实例任务在配置上不需要有任何区别;区别在于你怎么消费输出,因为语义模型要的是同类形状合并,实例模型要的是把它们分开。

对画笔蒙版来说,这个区别是一个设置项而不是消费方的决定,因为 mask_mode: semantic 会把同一类的每一笔合并成一个区域,而 mask_mode: instance 会把目标分开。

输出格式

标注以一个扁平的 JSON 数组返回,所有形状都在同一个列表里。坐标相对图像归一化到 0 到 1 的范围,所以即使之后图像以不同尺寸提供,标注依然正确:

json
[
  {
    "type": "polygon",
    "label": "vehicle",
    "color": "#FF6B6B",
    "coordinates": [
      {"x": 0.02, "y": 0.04}, {"x": 0.20, "y": 0.04},
      {"x": 0.20, "y": 0.21}, {"x": 0.02, "y": 0.21}
    ]
  },
  {
    "type": "bbox",
    "label": "person",
    "color": "#4ECDC4",
    "coordinates": {"x": 0.125, "y": 0.104, "width": 0.3125, "height": 0.625}
  }
]

每种形状类型对自己几何结构的命名各不相同。多边形的 coordinates 是一串点,边界框是一个带宽高的对象,画笔蒙版则带一个行优先游程编码的 rle 和一个 [height, width] 形式的 size。要在 Python 里转换这些形状,请用 potato.export.cv_utils 里的 normalize_annotation_object() 和 to_client_object(),不要直接读字段,这样存储方式变了你的代码还能用。

多边形边界上的一致性

标注者之间的平均 IoU 是多数工具会报的数字,而它回答的是错的问题。如果一个语料里每张图都是一个居中的大目标,那么无论谁来画,分数都在 0.95 左右,包括一个根本没看图的人,因为 IoU 里没有任何机遇校正。

Potato 在 /admin/iaa 为几何 schema 报告经过机遇校正的指标,并把问题拆成三部分:detection_alpha 看标注者是否找到了同样的目标,classification_alpha 看他们对每个目标是什么是否一致,sigma 和 ks 看他们是否画在了同一个位置。这样拆开是有诊断价值的。检测高而定位低,说明绘制规范太松;反过来则说明目标的定义不清楚。测量边界框上的一致性讲了适用于所有形状类型的方法。

另一件值得看的事情是顶点数。一个曲线目标只用四个顶点的多边形其实是伪装的边界框,绘制遥测里的 vertices_median 能把它暴露出来。

下一步