编辑器做好了,一个人能顺利画出框。交给 20 个人,会冒出两个完全不同的失败: 大家互相打架,和大家各标各的。这一页讲这两层。
不是效率。甲和乙同时打开同一帧,各画了一半,谁的结果算数?
用过 Google Docs 的人很自然会想:能不能也做成多人同时编辑、实时同步?
Google Docs 模型:一张文档,N 个人同时改 → 靠 OT / CRDT 合并冲突
标注平台模型: N 个互不相交的工单,各改各的 → 冲突从设计上不存在
它把同步问题绕过去了,而不是解决了。这一点比任何同步算法都重要。
关键在于:文档可以切碎,一帧标注不可以。
| 文档 | 一帧标注 | |
|---|---|---|
| 最小单元 | 段落、字符,可分割 | 帧,标注对象之间强耦合 |
| 完整性 | 局部属性,写到哪算哪 | 帧级属性,"标全了没有"无法局部判定 |
| 两人同时改 | 有意义,各写一段 | 没有意义,而且有害 |
第二行是核心。设想甲标了左边 3 个物体、乙标了右边 4 个:
那么【没有任何人】能判断这一帧是不是标全了。
因为"漏没漏"是全帧性质:
不能被两个人各自局部保证
也不能靠"我负责的部分没问题"来推导
所以一帧必须独占。这不是性能取舍,是真值可用性的前提。
拆开看,需要同步的只有四类,没有一类是协同编辑。
| 同步对象 | 频率 | 真正的技术需求 |
|---|---|---|
| 工单状态机 | 分钟到小时级 | 原子性,不是实时性 |
| 本体与类别 | 极低,管理员改 | 版本化,改了历史数据要不要迁移 |
| 进度与看板 | 秒级,晚点无所谓 | 无,派生数据 |
| 审核快照 | 提交时一次 | 不可变,提交即冻结 |
唯一需要原子性的地方,是领取任务的那一刻:
UPDATE task SET owner = :me, lease_until = now() + interval '2 hour'
WHERE id = :fid AND owner IS NULL;
-- 影响行数 = 0 → 领取失败,刷新看板
领取必须带期限。现实中一定会有人领了任务然后人跑了、电脑关了、离职了, 任务必须能自动回收。这是运营问题,不是技术炫技。
把这条约束提炼一下就是 single writer。它的收益非常具体:每一帧有一条线性的版本链。
frame #1234
v1 甲 标注 sourceId = -1
v2 审核员 打回
v3 甲 修正
v4 审核员 通过 ← 当前有效
线性,所以"当前真值是哪个"这个问题永远有唯一答案。
反过来,如果允许并发写,版本历史就变成一张有向无环图,"哪个是真相" 从一个查询变成了一个合并或仲裁问题。你得引入 OT,还得先定义冲突时谁赢。
前面那张对比表里,标注平台的一行是持久化、可追溯。
可追溯这件事,本质上就是 single writer 的直接产物。
有一种情况确实会让人并行处理同一帧:为了质检,平台把同一帧分给 2 到 3 个人各标一遍, 然后比对差异。这叫冗余标注。
基线快照 v1(只读)
├── 甲 独立标 → a1
├── 乙 独立标 → a2
└── 丙 独立标 → a3
↓ 比对、仲裁
v2(融合结论)
它看起来像并发编辑,其实完全不是。三个人读的是同一份只读基线, 写的是三个互不相干的副本。编辑阶段完全没有冲突。
冗余标注的价值建立在两人互不知情之上。差异越大,越说明这里有问题。
而实时协同会让两人互相看见对方的框,不再是独立标注,差异就测不出来, 正好落进后面要讲的锚定偏差陷阱。
对标注来说,实时协同不是少做了一个功能,而是做了一个有害的功能。 独立性比协同性值钱。
| 场景 | 单元可分割吗 | 需要实时协同吗 |
|---|---|---|
| Google Docs | ❌ 一篇文档 | ✅ 需要 OT / CRDT |
| Figma | ❌ 一块画布 | ✅ 需要,几何合并更难 |
| 代码 | 🟡 文件可分割 | 🟡 用 Git,异步合并,不是实时 |
| 标注平台 | ✅ 帧、层、类别都可分 | ❌ 用不上,工单分片即可 |
OT 和 CRDT 解决的是"不可分割的共享可变状态"。而标注数据天然可分割。 可分割,就不需要合并算法。
独占粒度可以调。细到层或类别级别,甲的 2D 框和乙的 3D 框能同时改, 并行度更高。但代价是"标全了没有"这个帧级属性被切碎了,得另找人为它负责。
不在同步,在切分与负载均衡。
| 难点 | 做法 |
|---|---|
| 负载不均,有人分到全是难帧 | 拉模式动态领取,而不是推模式 |
| 专挑简单的做 | 计件加难度权重,或者固定批次下发 |
| 领了不做,做一半跑了 | 租约到期自动回收 |
| 有帧压根没人标 | 状态机加完整性校验,提交时要求必填项齐 |
工作流解决了打架。接下来的失败不是没人标,而是大家标得不一样。
这一层被称作标注质量的头号杀手。这个说法值得展开,因为不展开会觉得夸张。
把三类错误放一起对比就看明白了:
| 错误类型 | 看得见吗 | 后果 |
|---|---|---|
| 框画歪了 | ✅ 看得见 | 审核能抓 |
| 漏标一个物体 | 🟡 难,但可对比 | 有机制,冗余标注或抽样 |
| 卡车的定义不一致 | ❌ 完全看不见 | 模型学到两种定义的混合,在边界上必然出错 |
第三行是关键。两个人对"什么算卡车"的理解不同,各自都觉得自己标得完全正确。 没有报错,没有告警,画面上看起来都正常。分歧是静默的。
这不是抽象的担忧,它天天在发生:
"卡车"包不包括皮卡?
带挂车的牵引车,算一个还是算两个?
停在路边卸货的货车,是卡车还是静止物?
被树挡住一半的卡车,遮挡多少才标?
"行人"包不包括骑车的人、坐轮椅的人、交警?
远处只有 2 px 高的人,标不标的阈值定在哪?
每一行都是真实存在的争议。20 个标注员乘上每行一种理解,就是 20 种定义混进同一批数据。
更麻烦的是它不可事后修复:
标完 5 万帧 → 才发现"卡车"的定义有问题
→ 这 5 万帧全部可疑
→ 而且挑不出哪些有问题(错误是定义级的,不是画错的)
→ 只能重标
先澄清一个容易困惑的点:Ontology 既是通用概念,也是一个模块名。
Ontology(本体) 通用概念,来自哲学和知识表示,比任何平台都早几十年
Ontology Center 某个平台给"实现这个概念的那个模块"起的名字
通用定义是(Gruber, 1993):对某个领域里概念及其相互关系的显式、形式化描述。
概念 + 层级 + 关系 + 约束
一个平铺的类别列表不是 ontology,那只是个标签集合。差别就在后面三项。
以 Xtreme1 为例,配置入口在数据集下面的 Ontology,内容分两大类。
一个 Class = 一个类别 + 绑定的标注工具
(2D 框、多边形、折线、3D 框……)
这是它比类别表强的地方:类别和工具绑死。
车 → 3D 框工具
车道线 → 折线工具
可行驶区域 → 多边形工具
选错工具的可能性从设计上就是零
官方举的例子是"城市、住宅区、园区"这样的三级层级。它的语义和 Class 完全不同:
| 回答的问题 | 作用范围 | |
|---|---|---|
| Class | 图里有哪些东西 | 框、多边形、折线,区域级 |
| Classification | 这段数据是什么场景 | 整张图、整帧点云,数据级 |
Classification 其实就是数据治理的技术化。后面讲的长尾挖掘、按场景切分、配比策略, 在这里就是靠它落地的。
没有 Ontology:规范写在 PDF 里 → 靠人记住 → 靠培训 → 靠审核事后抓
有 Ontology: 规范写进数据库 → 系统不给选 → 从源头消除
传统做法是写一份标注规范、培训标注员、靠审核员抓。 Ontology 把它变成了系统强制的约束:类别只能从下拉选,属性只能是枚举值, 必填项不填就提交不了。
这是把"规范"这个概念工程化了,从文档约定升级到系统强制。
事后检查:结果不规范 → 检测出来 → 打回重标 已经错了
事前约束:输入时就没有"不规范"这个选项 错不了
单元测试、审核员 ≈ 运行时检查 跑起来才发现
Ontology ≈ 类型系统 编译期就拦掉
类型系统不"检测"类型错误,它让类型错误根本编译不过。Ontology 对标注是同一件事。 所以"保持规范"这个说法可以换成"不允许偏离"。
| 管得住,语法层 | 管不住,语义层 |
|---|---|
| 类别只能从列表里选 | 这一帧里到底有几个卡车 |
| 属性只能是枚举值 | 被挡住一半的卡车该不该标 |
| 类别和工具绑死 | 这个框画得准不准 |
| 必填项不填提交不了 | 标出来的东西是不是真值 |
右边这一列只能靠别的手段解决:规范文档、培训、冗余标注、审核抽样。 所以 Ontology 消除了一整类静默错误,但没消除全部。
两层的失败不一样,解法也不一样。
并行的问题是"打架",解法是分片:让人人独占一帧,冲突从设计上就不存在。 它买到的是线性的版本链,也就是可追溯。
一致性的问题是"各标各的",解法是把规范写进系统:让不规范的输入根本表达不出来。 它买到的是从源头消除一整类静默错误,代价是它只管得住形式。