用于分割任务的多边形标注
在 Potato 中搭建一个多边形分割任务:配置怎么写、标注者绘制时按哪些键、导出坐标是什么格式,以及什么时候画笔蒙版才是更合适的工具。
本教程在 Potato 中搭建一个图像分割任务,标注者用多边形描出目标边界,内容涵盖配置、标注者要按的键,以及导出坐标的形状。
多边形是一个顶点一个顶点地描出边界,这正是语义分割和实例分割需要的东西,也是边界框给不了的。代价是标注者的时间,所以第一个要做的判断是:你的任务到底需不需要边界。
在多边形、边界框和蒙版之间做选择
Potato 里有三种工具能产生区域标注,它们的区别在于能表达什么,而不只是快慢。
| 边界框 | 多边形 | 画笔蒙版 | |
|---|---|---|---|
| 边界精度 | 只能是矩形 | 逐顶点 | 逐像素 |
| 孔洞与不相连的部分 | 不行 | 别扭 | 自然 |
| 细长结构(电线、头发) | 不行 | 差 | 好 |
| 格式支持 | 最广 | 广:COCO、YOLO、CVAT、LabelMe | 较窄:COCO RLE、蒙版 PNG |
| 编辑方式 | 拖角 | 拖顶点 | 重涂一片区域 |
当粗略定位就够用时就用边界框,多数检测训练都属于这一类。当模型要消费边界、而且目标是实心的,多边形才值得多花标注者的时间。一旦目标带有孔洞或细丝,画笔蒙版才是老实的工具,因为硬给自行车轮圈一个多边形,画出来的形状谁也没法核对。Potato 还有一个开放路径的 polyline 工具,用于车道线、血管这类线形特征,它导出时面积为零,而不是凭空编一个内部区域。
任务配置
多边形工具是按 scheme 通过 tools 启用的,labels 就是标注者可以赋给某个形状的类别。item_properties.text_key 指明数据文件里存放图像 URL 的字段,Potato 靠它知道要显示什么:
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_outputkey_value 这些条目的作用比看上去大。一个七类任务,如果标注者每画一个形状都要去点工具栏,速度只有把类别放在数字键上的零头,而这个代价全部落在标注者身上。颜色必须是十六进制,因为蒙版是逐像素画的,而 CSS 颜色名会退化成红色。
数据文件里每个条目给出一个 id 和图像:
{"id": "img_001", "image_url": "https://example.com/street_001.jpg"}
{"id": "img_002", "image_url": "/static/images/street_002.jpg"}用这份配置启动服务:
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 的范围,所以即使之后图像以不同尺寸提供,标注依然正确:
[
{
"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 能把它暴露出来。