cs-label-tool 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +824 -0
- package/dist/components/annotator-element.d.ts +14 -0
- package/dist/components/annotator-element.d.ts.map +1 -0
- package/dist/components/define.d.ts +6 -0
- package/dist/components/define.d.ts.map +1 -0
- package/dist/components/styles.d.ts +2 -0
- package/dist/components/styles.d.ts.map +1 -0
- package/dist/core/annotator.d.ts +8 -0
- package/dist/core/annotator.d.ts.map +1 -0
- package/dist/core/commands.d.ts +34 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/events.d.ts +25 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/immutability.d.ts +4 -0
- package/dist/core/immutability.d.ts.map +1 -0
- package/dist/core/state.d.ts +42 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/types.d.ts +64 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/cs-label-tool.d.ts +247 -0
- package/dist/cs-label-tool.d.ts.map +1 -0
- package/dist/geometry/matrix.d.ts +5 -0
- package/dist/geometry/matrix.d.ts.map +1 -0
- package/dist/geometry/polygon.d.ts +10 -0
- package/dist/geometry/polygon.d.ts.map +1 -0
- package/dist/geometry/rect.d.ts +4 -0
- package/dist/geometry/rect.d.ts.map +1 -0
- package/dist/geometry/types.d.ts +19 -0
- package/dist/geometry/types.d.ts.map +1 -0
- package/dist/image/image-commands.d.ts +14 -0
- package/dist/image/image-commands.d.ts.map +1 -0
- package/dist/image/standard-source.d.ts +3 -0
- package/dist/image/standard-source.d.ts.map +1 -0
- package/dist/image/types.d.ts +12 -0
- package/dist/image/types.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2299 -0
- package/dist/index.js.map +1 -0
- package/dist/labels/labels.d.ts +6 -0
- package/dist/labels/labels.d.ts.map +1 -0
- package/dist/mask/rle.d.ts +13 -0
- package/dist/mask/rle.d.ts.map +1 -0
- package/dist/render/canvas-layers.d.ts +14 -0
- package/dist/render/canvas-layers.d.ts.map +1 -0
- package/dist/render/canvas-renderer.d.ts +10 -0
- package/dist/render/canvas-renderer.d.ts.map +1 -0
- package/dist/render/scheduler.d.ts +12 -0
- package/dist/render/scheduler.d.ts.map +1 -0
- package/dist/spatial/grid-index.d.ts +15 -0
- package/dist/spatial/grid-index.d.ts.map +1 -0
- package/dist/tools/api.d.ts +32 -0
- package/dist/tools/api.d.ts.map +1 -0
- package/dist/tools/brush-tool.d.ts +23 -0
- package/dist/tools/brush-tool.d.ts.map +1 -0
- package/dist/tools/controller.d.ts +9 -0
- package/dist/tools/controller.d.ts.map +1 -0
- package/dist/tools/eraser-tool.d.ts +8 -0
- package/dist/tools/eraser-tool.d.ts.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/polygon-tool.d.ts +33 -0
- package/dist/tools/polygon-tool.d.ts.map +1 -0
- package/dist/tools/rect-tool.d.ts +32 -0
- package/dist/tools/rect-tool.d.ts.map +1 -0
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/select-tool.d.ts +15 -0
- package/dist/tools/select-tool.d.ts.map +1 -0
- package/dist/tools/types.d.ts +80 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/viewport/viewport.d.ts +20 -0
- package/dist/viewport/viewport.d.ts.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
# cs-label-tool
|
|
2
|
+
|
|
3
|
+
一个用 Canvas 做图片标注的 TypeScript 库。没有运行时依赖,可以直接调用函数,也可以使用已经绑定好的 `editor` 实例。
|
|
4
|
+
|
|
5
|
+
当前支持:
|
|
6
|
+
|
|
7
|
+
- 图片 URL、`Blob`、`ImageBitmap` 加载。
|
|
8
|
+
- 选择、矩形、多边形、涂抹和橡皮擦工具。
|
|
9
|
+
- 矩形移动与八方向缩放、多边形顶点编辑。
|
|
10
|
+
- Mask 点击选择、拖拽、删除、改标签和近距离合并。
|
|
11
|
+
- 涂抹区域重叠合并;橡皮擦实时透明预览和连通域分割。
|
|
12
|
+
- 缩放、平移、适屏、坐标转换和 DPR 高分屏渲染。
|
|
13
|
+
- 标签管理、区域查询、撤销重做、事件订阅和自定义工具。
|
|
14
|
+
|
|
15
|
+
## 环境与安装
|
|
16
|
+
|
|
17
|
+
- Node.js 22.12+
|
|
18
|
+
- Chrome、Edge、Firefox 最近两个大版本
|
|
19
|
+
- Safari 17+
|
|
20
|
+
- 仅提供 ESM
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install cs-label-tool
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
容器必须具有可计算的宽高:
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div id="annotator" style="width: 100%; height: 600px"></div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
推荐使用绑定实例。实例方法不需要重复传递 `annotator`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { mount, createStandardImageSource } from 'cs-label-tool'
|
|
38
|
+
|
|
39
|
+
const editor = mount('#annotator', { historyLimit: 100 })
|
|
40
|
+
|
|
41
|
+
editor.addLabel({ id: 'person', name: '人物', color: '#ff4d4f' })
|
|
42
|
+
editor.addLabel({ id: 'vehicle', name: '车辆', color: '#1677ff' })
|
|
43
|
+
editor.setActiveLabel('person')
|
|
44
|
+
|
|
45
|
+
await editor.setImage(createStandardImageSource('/images/example.webp'))
|
|
46
|
+
editor.fitToScreen()
|
|
47
|
+
editor.tools.rect()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
完整页面示例:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<div id="toolbar">
|
|
54
|
+
<button id="select">选择</button>
|
|
55
|
+
<button id="rect">矩形</button>
|
|
56
|
+
<button id="polygon">多边形</button>
|
|
57
|
+
<button id="brush">涂抹</button>
|
|
58
|
+
<button id="eraser">橡皮擦</button>
|
|
59
|
+
<button id="delete">删除选中</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div id="annotator" style="width: 100%; height: 600px"></div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import {
|
|
66
|
+
mount,
|
|
67
|
+
createStandardImageSource,
|
|
68
|
+
type AnnotationSnapshot,
|
|
69
|
+
} from 'cs-label-tool'
|
|
70
|
+
|
|
71
|
+
const editor = mount('#annotator', { historyLimit: 100 })
|
|
72
|
+
|
|
73
|
+
editor.addLabel({ id: 'person', name: '人物', color: '#ff4d4f' })
|
|
74
|
+
editor.addLabel({ id: 'vehicle', name: '车辆', color: '#1677ff' })
|
|
75
|
+
editor.setActiveLabel('person')
|
|
76
|
+
|
|
77
|
+
await editor.setImage(createStandardImageSource('/a.webp'))
|
|
78
|
+
editor.fitToScreen()
|
|
79
|
+
|
|
80
|
+
document.querySelector('#select')?.addEventListener('click', () => {
|
|
81
|
+
editor.tools.select()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
document.querySelector('#rect')?.addEventListener('click', () => {
|
|
85
|
+
editor.tools.rect({ minimumSize: 3 })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
document.querySelector('#polygon')?.addEventListener('click', () => {
|
|
89
|
+
editor.tools.polygon()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
document.querySelector('#brush')?.addEventListener('click', () => {
|
|
93
|
+
editor.tools.brush({ size: 24 })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
document.querySelector('#eraser')?.addEventListener('click', () => {
|
|
97
|
+
editor.tools.eraser({ size: 18 })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
document.querySelector('#delete')?.addEventListener('click', () => {
|
|
101
|
+
editor.tools.deleteSelection()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const unsubscribe = editor.subscribe('change', () => {
|
|
105
|
+
const snapshot: AnnotationSnapshot = editor.snapshot()
|
|
106
|
+
console.log(snapshot.annotations)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
window.addEventListener('beforeunload', () => {
|
|
110
|
+
unsubscribe()
|
|
111
|
+
editor.destroy()
|
|
112
|
+
}, { once: true })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 统一工具 API
|
|
116
|
+
|
|
117
|
+
### 实例调用
|
|
118
|
+
|
|
119
|
+
`editor.tools` 封装了所有内置交互工具和常用选择操作:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
editor.tools.select()
|
|
123
|
+
editor.tools.rect()
|
|
124
|
+
editor.tools.polygon()
|
|
125
|
+
editor.tools.brush({ size: 24 })
|
|
126
|
+
editor.tools.eraser({ size: 18 })
|
|
127
|
+
|
|
128
|
+
const activeToolId = editor.tools.activeId()
|
|
129
|
+
const tools = editor.tools.list()
|
|
130
|
+
const brush = editor.tools.get('brush')
|
|
131
|
+
|
|
132
|
+
editor.tools.cancel()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 函数式调用
|
|
136
|
+
|
|
137
|
+
已有原始 `Annotator` 时,可以创建一次绑定工具对象:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import {
|
|
141
|
+
createAnnotator,
|
|
142
|
+
createToolApi,
|
|
143
|
+
createStandardImageSource,
|
|
144
|
+
setImageSource,
|
|
145
|
+
addLabel,
|
|
146
|
+
setActiveLabel,
|
|
147
|
+
} from 'cs-label-tool'
|
|
148
|
+
|
|
149
|
+
const annotator = createAnnotator({
|
|
150
|
+
container: document.querySelector('#annotator')!,
|
|
151
|
+
historyLimit: 100,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
addLabel(annotator, { id: 'person', name: '人物', color: '#ff4d4f' })
|
|
155
|
+
setActiveLabel(annotator, 'person')
|
|
156
|
+
await setImageSource(annotator, createStandardImageSource('/a.webp'))
|
|
157
|
+
|
|
158
|
+
const tools = createToolApi(annotator)
|
|
159
|
+
tools.brush({ size: 20 })
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
也可以直接调用原有函数:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import {
|
|
166
|
+
useSelect,
|
|
167
|
+
useRect,
|
|
168
|
+
usePolygon,
|
|
169
|
+
useBrush,
|
|
170
|
+
useEraser,
|
|
171
|
+
} from 'cs-label-tool'
|
|
172
|
+
|
|
173
|
+
useSelect(annotator)
|
|
174
|
+
useRect(annotator, { labelId: 'person', minimumSize: 3 })
|
|
175
|
+
usePolygon(annotator, { labelId: 'person' })
|
|
176
|
+
useBrush(annotator, { labelId: 'person', size: 24, color: '#ff4d4f' })
|
|
177
|
+
useEraser(annotator, { size: 18 })
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 工具参数
|
|
181
|
+
|
|
182
|
+
| 工具 | 方法 | 参数 |
|
|
183
|
+
| --- | --- | --- |
|
|
184
|
+
| 选择 | `select()` | 无 |
|
|
185
|
+
| 矩形 | `rect(options)` | `labelId?`, `minimumSize?` |
|
|
186
|
+
| 多边形 | `polygon(options)` | `labelId?` |
|
|
187
|
+
| 涂抹 | `brush(options)` | `labelId?`, `size?`, `color?`(预览颜色) |
|
|
188
|
+
| 橡皮擦 | `eraser(options)` | `size?` |
|
|
189
|
+
|
|
190
|
+
未传 `labelId` 时,矩形、多边形和涂抹使用当前激活标签。调用绘制工具前必须加载图片并激活一个标签。
|
|
191
|
+
|
|
192
|
+
### 工具行为
|
|
193
|
+
|
|
194
|
+
- 矩形:按下拖动,抬起完成;选择后可移动和拖动边框缩放。
|
|
195
|
+
- 多边形:逐点点击,按 `Enter` 或双击完成,`Backspace` 删除最后一个点。
|
|
196
|
+
- 涂抹:按下拖动,抬起完成;同标签且重叠的 mask 自动合并。
|
|
197
|
+
- 橡皮擦:按下拖动时实时透明擦除,抬起提交;只影响 mask。
|
|
198
|
+
- Mask 分割:橡皮擦切断区域后,每个连通块成为独立标注。
|
|
199
|
+
- Mask 合并:选择后拖动,同标签区域进入约 8 屏幕像素范围时自动合并。
|
|
200
|
+
- 视图导航:按住空格、中键或 `Alt + 左键` 拖动画布;滚轮以鼠标位置为锚点缩放。
|
|
201
|
+
|
|
202
|
+
缩放时图片层、标注层和交互层共享同一 viewport 变换。矩形、多边形、标签和控制点会按当前 DPR 重新绘制;Mask 放大时关闭插值,保持像素边界清晰。原始位图本身仍受源图片分辨率限制,不能像真正的 SVG 一样无限放大。
|
|
203
|
+
|
|
204
|
+
## 每个工具怎么用
|
|
205
|
+
|
|
206
|
+
下面的例子都接着“快速开始”里的 `editor` 使用。先加载图片、添加标签,再启用工具。
|
|
207
|
+
|
|
208
|
+
### 1. 选择工具
|
|
209
|
+
|
|
210
|
+
选择工具不创建标注。它用来点击、拖动、缩放、改标签和删除已有标注。
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// 启用选择工具,之后直接在画布上点击标注
|
|
214
|
+
editor.tools.select()
|
|
215
|
+
|
|
216
|
+
// 已经知道标注 ID 时,也可以直接选中
|
|
217
|
+
editor.tools.selectAnnotation(annotationId)
|
|
218
|
+
|
|
219
|
+
// 查看当前选中了哪些标注
|
|
220
|
+
const selectedIds = editor.tools.selection()
|
|
221
|
+
|
|
222
|
+
// 修改选中标注的标签,返回修改数量
|
|
223
|
+
const changedCount = editor.tools.setSelectionLabel('vehicle')
|
|
224
|
+
|
|
225
|
+
// 删除选中标注,返回删除数量
|
|
226
|
+
const deletedCount = editor.tools.deleteSelection()
|
|
227
|
+
|
|
228
|
+
// 只取消选择,不删除数据
|
|
229
|
+
editor.tools.clearSelection()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
页面上的常见删除按钮可以这样写:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
document.querySelector('#delete')?.addEventListener('click', () => {
|
|
236
|
+
const count = editor.tools.deleteSelection()
|
|
237
|
+
console.log(`删除了 ${count} 个标注`)
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
函数式写法:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import {
|
|
245
|
+
useSelect,
|
|
246
|
+
selectAnnotation,
|
|
247
|
+
getSelection,
|
|
248
|
+
clearSelection,
|
|
249
|
+
updateSelectedAnnotationsLabel,
|
|
250
|
+
deleteSelectedAnnotations,
|
|
251
|
+
} from 'cs-label-tool'
|
|
252
|
+
|
|
253
|
+
useSelect(annotator)
|
|
254
|
+
selectAnnotation(annotator, annotationId)
|
|
255
|
+
updateSelectedAnnotationsLabel(annotator, 'vehicle')
|
|
256
|
+
deleteSelectedAnnotations(annotator)
|
|
257
|
+
clearSelection(annotator)
|
|
258
|
+
console.log(getSelection(annotator))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 2. 矩形工具
|
|
262
|
+
|
|
263
|
+
启用工具后,在图片上按住鼠标拖动,松开鼠标就会生成矩形。
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
editor.tools.rect({
|
|
267
|
+
labelId: 'person', // 不传就用当前激活标签
|
|
268
|
+
minimumSize: 3, // 小于 3 个原图像素的矩形不保存
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
不用鼠标,也可以直接用代码添加:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const rectId = editor.addRect({
|
|
276
|
+
labelId: 'person',
|
|
277
|
+
x: 100,
|
|
278
|
+
y: 80,
|
|
279
|
+
width: 300,
|
|
280
|
+
height: 500,
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
编辑矩形:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// 先选中。用户也可以切到选择工具后直接点击矩形
|
|
288
|
+
editor.tools.selectAnnotation(rectId)
|
|
289
|
+
editor.tools.select()
|
|
290
|
+
|
|
291
|
+
// 直接修改坐标和大小
|
|
292
|
+
editor.updateAnnotation(rectId, {
|
|
293
|
+
type: 'rect',
|
|
294
|
+
x: 120,
|
|
295
|
+
y: 90,
|
|
296
|
+
width: 320,
|
|
297
|
+
height: 480,
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// 删除
|
|
301
|
+
editor.removeAnnotation(rectId)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
选择工具下,拖动矩形内部可以移动,拖动边框或控制点可以缩放。
|
|
305
|
+
|
|
306
|
+
### 3. 多边形工具
|
|
307
|
+
|
|
308
|
+
启用后逐点点击。按 `Enter` 或双击结束,按 `Backspace` 删除刚加的点,按 `Escape` 取消这次绘制。
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
editor.tools.polygon({ labelId: 'person' })
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
直接用代码添加时,点使用 `{ x, y }`:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const polygonId = editor.addPolygon({
|
|
318
|
+
labelId: 'person',
|
|
319
|
+
points: [
|
|
320
|
+
{ x: 20, y: 20 },
|
|
321
|
+
{ x: 120, y: 20 },
|
|
322
|
+
{ x: 80, y: 140 },
|
|
323
|
+
],
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
移动顶点、删除顶点和保存修改:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { movePolygonVertex, removePolygonVertex } from 'cs-label-tool'
|
|
331
|
+
|
|
332
|
+
const polygon = editor.snapshot().annotations.find(item => item.id === polygonId)
|
|
333
|
+
|
|
334
|
+
if (polygon?.geometry.type === 'polygon') {
|
|
335
|
+
const moved = movePolygonVertex(
|
|
336
|
+
polygon.geometry,
|
|
337
|
+
0,
|
|
338
|
+
{ x: 30, y: 35 },
|
|
339
|
+
)
|
|
340
|
+
editor.updateAnnotation(polygonId, moved)
|
|
341
|
+
|
|
342
|
+
// 多边形至少保留 3 个有效顶点。不能删除时返回 null
|
|
343
|
+
const withoutSecondPoint = removePolygonVertex(moved, 1)
|
|
344
|
+
if (withoutSecondPoint !== null) {
|
|
345
|
+
editor.updateAnnotation(polygonId, withoutSecondPoint)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
editor.removeAnnotation(polygonId)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
快照里的 `PolygonGeometry.points` 是 `[x, y]` 元组;`addPolygon()` 的输入是 `{ x, y }`。这两个格式不要混用。
|
|
353
|
+
|
|
354
|
+
### 4. 涂抹工具
|
|
355
|
+
|
|
356
|
+
按住鼠标开始涂,松开鼠标保存。同标签的涂抹区域发生重叠时会自动合并。
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
editor.tools.brush({
|
|
360
|
+
labelId: 'person',
|
|
361
|
+
size: 24, // 原图坐标中的画笔直径
|
|
362
|
+
color: '#ff4d4f', // 拖动时的预览颜色
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
涂抹保存后是 `mask` 标注,最终显示颜色来自标签颜色。可以不经过鼠标,直接把一组笔迹点转成 mask:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createBrushMaskGeometry } from 'cs-label-tool'
|
|
370
|
+
|
|
371
|
+
const geometry = createBrushMaskGeometry({
|
|
372
|
+
imageWidth: 1920,
|
|
373
|
+
imageHeight: 1080,
|
|
374
|
+
brushSize: 24,
|
|
375
|
+
points: [
|
|
376
|
+
{ x: 300, y: 220 },
|
|
377
|
+
{ x: 340, y: 250 },
|
|
378
|
+
{ x: 390, y: 270 },
|
|
379
|
+
],
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const maskId = editor.addMask({
|
|
383
|
+
labelId: 'person',
|
|
384
|
+
width: geometry.width,
|
|
385
|
+
height: geometry.height,
|
|
386
|
+
rle: geometry.rle,
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
移动、改标签和删除 mask:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import {
|
|
394
|
+
decodeBinaryMaskRle,
|
|
395
|
+
encodeBinaryMaskRle,
|
|
396
|
+
translateBinaryMask,
|
|
397
|
+
} from 'cs-label-tool'
|
|
398
|
+
|
|
399
|
+
const mask = editor.snapshot().annotations.find(item => item.id === maskId)
|
|
400
|
+
|
|
401
|
+
if (mask?.geometry.type === 'mask') {
|
|
402
|
+
const pixels = decodeBinaryMaskRle(
|
|
403
|
+
mask.geometry.rle,
|
|
404
|
+
mask.geometry.width,
|
|
405
|
+
mask.geometry.height,
|
|
406
|
+
)
|
|
407
|
+
const movedPixels = translateBinaryMask(
|
|
408
|
+
pixels,
|
|
409
|
+
mask.geometry.width,
|
|
410
|
+
mask.geometry.height,
|
|
411
|
+
20,
|
|
412
|
+
10,
|
|
413
|
+
)
|
|
414
|
+
editor.updateAnnotation(maskId, {
|
|
415
|
+
...mask.geometry,
|
|
416
|
+
rle: encodeBinaryMaskRle(movedPixels),
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
editor.updateAnnotationLabel(maskId, 'vehicle')
|
|
421
|
+
editor.removeAnnotation(maskId)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
在选择工具下也可以直接拖动 mask。拖到同标签 mask 附近时,两块会合并成一个标注。
|
|
425
|
+
|
|
426
|
+
### 5. 橡皮擦工具
|
|
427
|
+
|
|
428
|
+
橡皮擦不会创建新标注,它只擦已有的 mask。矩形和多边形不会被擦掉。
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
editor.tools.eraser({ size: 18 })
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
按住鼠标时画面会实时显示擦除结果,松开鼠标后写入数据。如果一块 mask 被擦断,剩下的每个连通区域都会变成独立标注。
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
// 擦完以后找到所有 mask
|
|
438
|
+
const masks = editor.snapshot().annotations.filter(
|
|
439
|
+
item => item.geometry.type === 'mask',
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
// 每块都有自己的 ID,可以单独选择、改标签和删除
|
|
443
|
+
const firstMask = masks[0]
|
|
444
|
+
if (firstMask !== undefined) {
|
|
445
|
+
editor.tools.selectAnnotation(firstMask.id)
|
|
446
|
+
editor.tools.setSelectionLabel('vehicle')
|
|
447
|
+
editor.tools.deleteSelection()
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
如果服务端直接返回二进制 mask,可以用这些函数自行处理:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
import {
|
|
455
|
+
encodeBinaryMaskRle,
|
|
456
|
+
decodeBinaryMaskRle,
|
|
457
|
+
getBinaryMaskBounds,
|
|
458
|
+
splitBinaryMaskComponents,
|
|
459
|
+
binaryMasksWithinDistance,
|
|
460
|
+
} from 'cs-label-tool'
|
|
461
|
+
|
|
462
|
+
const pixels = decodeBinaryMaskRle(rle, width, height)
|
|
463
|
+
const bounds = getBinaryMaskBounds(pixels, width, height)
|
|
464
|
+
const blocks = splitBinaryMaskComponents(pixels, width, height)
|
|
465
|
+
const isNear = binaryMasksWithinDistance(
|
|
466
|
+
blocks[0] ?? new Uint8Array(width * height),
|
|
467
|
+
blocks[1] ?? new Uint8Array(width * height),
|
|
468
|
+
width,
|
|
469
|
+
height,
|
|
470
|
+
8,
|
|
471
|
+
)
|
|
472
|
+
const firstBlockRle = blocks[0] === undefined
|
|
473
|
+
? null
|
|
474
|
+
: encodeBinaryMaskRle(blocks[0])
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 6. 取消当前绘制
|
|
478
|
+
|
|
479
|
+
用户可以按 `Escape`。代码里可以这样取消:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
editor.tools.cancel()
|
|
483
|
+
// 旧写法仍然可用
|
|
484
|
+
editor.cancelGesture()
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## 标注查询和历史记录
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
const annotations = editor.queryAnnotations({
|
|
491
|
+
x: 0,
|
|
492
|
+
y: 0,
|
|
493
|
+
width: 500,
|
|
494
|
+
height: 500,
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
if (editor.canUndo()) editor.undo()
|
|
498
|
+
if (editor.canRedo()) editor.redo()
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
函数式版本为 `addRect`、`addPolygon`、`addMask`、`updateAnnotation`、`updateAnnotationLabel`、`removeAnnotation`、`queryAnnotations`、`undo`、`redo`、`canUndo` 和 `canRedo`。这些函数的第一个参数都是 `annotator`。
|
|
502
|
+
|
|
503
|
+
## 标签管理
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
editor.addLabel({ id: 'person', name: '人物', color: '#ff4d4f' })
|
|
507
|
+
editor.setActiveLabel('person')
|
|
508
|
+
|
|
509
|
+
const activeLabelId = editor.getActiveLabel()
|
|
510
|
+
|
|
511
|
+
editor.updateLabel('person', {
|
|
512
|
+
name: '行人',
|
|
513
|
+
color: '#ff7875',
|
|
514
|
+
})
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
函数式版本:`addLabel`、`setActiveLabel`、`getActiveLabel`、`updateLabel`。
|
|
518
|
+
|
|
519
|
+
## 图片与视图
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
import { createStandardImageSource } from 'cs-label-tool'
|
|
523
|
+
|
|
524
|
+
await editor.setImage(createStandardImageSource('/images/example.webp'))
|
|
525
|
+
|
|
526
|
+
editor.fitToScreen()
|
|
527
|
+
editor.zoomTo(2)
|
|
528
|
+
editor.zoomTo(2, { x: 100, y: 100 })
|
|
529
|
+
editor.zoomBy(1.25)
|
|
530
|
+
editor.panBy({ x: 100, y: 50 })
|
|
531
|
+
|
|
532
|
+
console.log(editor.getZoom())
|
|
533
|
+
console.log(editor.hasImage())
|
|
534
|
+
|
|
535
|
+
// 容器尺寸发生变化后调用
|
|
536
|
+
editor.resizeViewport()
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
函数式 API 还提供坐标转换:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { imageToClient, clientToImage } from 'cs-label-tool'
|
|
543
|
+
|
|
544
|
+
const clientPoint = imageToClient(annotator, { x: 100, y: 100 })
|
|
545
|
+
const imagePoint = clientToImage(annotator, clientPoint)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## 事件与快照
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
const unsubscribe = editor.subscribe('change', event => {
|
|
552
|
+
switch (event.kind) {
|
|
553
|
+
case 'annotation:add':
|
|
554
|
+
case 'annotation:update':
|
|
555
|
+
case 'annotation:remove':
|
|
556
|
+
console.log(editor.snapshot().annotations)
|
|
557
|
+
break
|
|
558
|
+
case 'selection:update':
|
|
559
|
+
console.log(editor.tools.selection())
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
const snapshot = editor.snapshot()
|
|
565
|
+
const json = JSON.stringify(snapshot)
|
|
566
|
+
|
|
567
|
+
unsubscribe()
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
快照结构:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
interface AnnotationSnapshot {
|
|
574
|
+
readonly schemaVersion: 1
|
|
575
|
+
readonly revision: number
|
|
576
|
+
readonly annotations: readonly Annotation[]
|
|
577
|
+
readonly labels: readonly LabelDefinition[]
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## 自定义工具
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import { addRect, type Tool } from 'cs-label-tool'
|
|
585
|
+
|
|
586
|
+
const pointMarker: Tool = {
|
|
587
|
+
id: 'point-marker',
|
|
588
|
+
name: '点标记',
|
|
589
|
+
description: '创建一个 8 x 8 的点标记',
|
|
590
|
+
cursor: 'crosshair',
|
|
591
|
+
category: 'drawing',
|
|
592
|
+
shortcuts: [{ key: 'm' }],
|
|
593
|
+
handle(input, context) {
|
|
594
|
+
if (input.type !== 'down') return
|
|
595
|
+
addRect(context.annotator, {
|
|
596
|
+
labelId: 'person',
|
|
597
|
+
x: input.imagePoint.x - 4,
|
|
598
|
+
y: input.imagePoint.y - 4,
|
|
599
|
+
width: 8,
|
|
600
|
+
height: 8,
|
|
601
|
+
})
|
|
602
|
+
},
|
|
603
|
+
cancel() {},
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
editor.registerTool(pointMarker)
|
|
607
|
+
editor.activateToolById('point-marker')
|
|
608
|
+
|
|
609
|
+
console.log(editor.getTool('point-marker'))
|
|
610
|
+
console.log(editor.listTools())
|
|
611
|
+
console.log(editor.listToolsByCategory('drawing'))
|
|
612
|
+
|
|
613
|
+
editor.unregisterTool('point-marker')
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
也可以通过统一工具 API 激活工具对象:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
editor.tools.register(pointMarker)
|
|
620
|
+
editor.tools.activateById('point-marker')
|
|
621
|
+
editor.tools.listByCategory('drawing')
|
|
622
|
+
editor.tools.unregister('point-marker')
|
|
623
|
+
|
|
624
|
+
// 不注册也可以直接激活一个 Tool 对象
|
|
625
|
+
editor.tools.activate(pointMarker)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## 三种入口的区别
|
|
629
|
+
|
|
630
|
+
| 入口 | 返回值 | 适用场景 |
|
|
631
|
+
| --- | --- | --- |
|
|
632
|
+
| `mount('#app')` | `AnnotatorInstance` | 推荐,带 Web Component UI 和绑定方法 |
|
|
633
|
+
| `create({ container })` | `AnnotatorInstance` | 自建 UI,使用绑定方法 |
|
|
634
|
+
| `mountAnnotator/createAnnotator` | 原始 `Annotator` | 函数式 API、框架适配 |
|
|
635
|
+
| 默认导出 `csLabelTool` | 函数集合 | 需要单一命名空间时 |
|
|
636
|
+
|
|
637
|
+
默认导出示例:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import csLabelTool from 'cs-label-tool'
|
|
641
|
+
|
|
642
|
+
const annotator = csLabelTool.mount('#app', { historyLimit: 100 })
|
|
643
|
+
csLabelTool.addLabel(annotator, {
|
|
644
|
+
id: 'person',
|
|
645
|
+
name: '人物',
|
|
646
|
+
color: '#ff4d4f',
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
await csLabelTool.setImage(
|
|
650
|
+
annotator,
|
|
651
|
+
csLabelTool.createImageSource('/a.webp'),
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
const tools = csLabelTool.createToolApi(annotator)
|
|
655
|
+
tools.brush({ size: 24 })
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Web Component
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
import {
|
|
662
|
+
defineAnnotatorElements,
|
|
663
|
+
type CSAnnotatorElement,
|
|
664
|
+
} from 'cs-label-tool'
|
|
665
|
+
|
|
666
|
+
defineAnnotatorElements()
|
|
667
|
+
|
|
668
|
+
const element = document.createElement('cs-annotator') as CSAnnotatorElement
|
|
669
|
+
document.body.append(element)
|
|
670
|
+
const annotator = element.configure({ historyLimit: 100 })
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
卸载自动挂载的组件:
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { unmountAnnotator } from 'cs-label-tool'
|
|
677
|
+
|
|
678
|
+
unmountAnnotator('#app')
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## API 名字清单
|
|
682
|
+
|
|
683
|
+
这里按用途列出包根目录公开导出的 API。实例方法通常省略第一个 `annotator` 参数,功能与函数式版本相同。
|
|
684
|
+
|
|
685
|
+
### 创建和销毁
|
|
686
|
+
|
|
687
|
+
| API | 用途 |
|
|
688
|
+
| --- | --- |
|
|
689
|
+
| `mount` | 创建带默认 Web Component 界面的 `AnnotatorInstance` |
|
|
690
|
+
| `create` | 在指定容器创建 `AnnotatorInstance`,界面由项目自己做 |
|
|
691
|
+
| `mountAnnotator` | 创建带默认界面的原始 `Annotator` |
|
|
692
|
+
| `createAnnotator` | 创建原始 `Annotator` |
|
|
693
|
+
| `destroyAnnotator` | 销毁原始 `Annotator` |
|
|
694
|
+
| `getSnapshot` | 获取只读快照 |
|
|
695
|
+
| `defineAnnotatorElements` | 注册 `<cs-annotator>` |
|
|
696
|
+
| `unmountAnnotator` | 卸载默认组件 |
|
|
697
|
+
|
|
698
|
+
### 标注和标签
|
|
699
|
+
|
|
700
|
+
| API | 用途 |
|
|
701
|
+
| --- | --- |
|
|
702
|
+
| `addRect` / `addPolygon` / `addMask` | 直接添加标注 |
|
|
703
|
+
| `updateAnnotation` | 修改标注几何 |
|
|
704
|
+
| `updateAnnotationLabel` | 修改单条标注的标签 |
|
|
705
|
+
| `removeAnnotation` | 删除单条标注 |
|
|
706
|
+
| `queryAnnotations` | 查询指定图片区域里的标注 |
|
|
707
|
+
| `undo` / `redo` | 撤销和重做 |
|
|
708
|
+
| `canUndo` / `canRedo` | 判断当前能否撤销或重做 |
|
|
709
|
+
| `addLabel` / `updateLabel` | 添加或修改标签 |
|
|
710
|
+
| `setActiveLabel` / `getActiveLabel` | 设置或读取当前绘制标签 |
|
|
711
|
+
|
|
712
|
+
### 内置工具和选择
|
|
713
|
+
|
|
714
|
+
| API | 用途 |
|
|
715
|
+
| --- | --- |
|
|
716
|
+
| `createToolApi` | 创建绑定到某个 annotator 的工具对象 |
|
|
717
|
+
| `useSelect` / `useRect` / `usePolygon` | 启用选择、矩形、多边形工具 |
|
|
718
|
+
| `useBrush` / `useEraser` | 启用涂抹、橡皮擦工具 |
|
|
719
|
+
| `getActiveToolId` | 读取当前工具 ID |
|
|
720
|
+
| `cancelActiveGesture` | 取消当前未完成操作 |
|
|
721
|
+
| `selectAnnotation` / `clearSelection` / `getSelection` | 管理选择状态 |
|
|
722
|
+
| `updateSelectedAnnotationsLabel` | 修改选中标注的标签 |
|
|
723
|
+
| `deleteSelectedAnnotations` | 删除选中标注 |
|
|
724
|
+
|
|
725
|
+
### 工具注册
|
|
726
|
+
|
|
727
|
+
| API | 用途 |
|
|
728
|
+
| --- | --- |
|
|
729
|
+
| `activateTool` / `activateToolById` | 激活工具对象或指定 ID 的工具 |
|
|
730
|
+
| `registerTool` / `unregisterTool` | 注册和移除自定义工具 |
|
|
731
|
+
| `getTool` / `listTools` / `listToolsByCategory` | 查询工具 |
|
|
732
|
+
| `getRegisteredTools` / `getRegisteredToolsByCategory` | 读取底层注册表中的工具 |
|
|
733
|
+
| `createToolRegistry` / `createDefaultToolRegistry` | 创建空注册表或默认注册表 |
|
|
734
|
+
|
|
735
|
+
### 图片和视图
|
|
736
|
+
|
|
737
|
+
| API | 用途 |
|
|
738
|
+
| --- | --- |
|
|
739
|
+
| `createStandardImageSource` | 从 URL、Blob 或 ImageBitmap 创建图片源 |
|
|
740
|
+
| `setImageSource` | 加载图片 |
|
|
741
|
+
| `hasImage` | 判断图片是否加载完成 |
|
|
742
|
+
| `fitToScreen` | 让整张图片进入画布 |
|
|
743
|
+
| `zoomTo` / `zoomBy` / `getZoom` | 设置、调整和读取缩放比例 |
|
|
744
|
+
| `panBy` | 平移画布 |
|
|
745
|
+
| `resizeViewport` | 容器尺寸变化后更新画布 |
|
|
746
|
+
| `imageToClient` / `clientToImage` | 原图坐标与浏览器坐标互转 |
|
|
747
|
+
|
|
748
|
+
### 几何和 Mask
|
|
749
|
+
|
|
750
|
+
| API | 用途 |
|
|
751
|
+
| --- | --- |
|
|
752
|
+
| `normalizeRect` / `pointInRect` | 矩形计算和命中判断 |
|
|
753
|
+
| `pointInPolygon` / `validatePolygon` | 多边形命中判断和有效性检查 |
|
|
754
|
+
| `moveRect` / `resizeRect` | 计算移动或缩放后的矩形 |
|
|
755
|
+
| `movePolygonVertex` / `removePolygonVertex` | 移动或删除多边形顶点 |
|
|
756
|
+
| `createBrushMaskGeometry` | 把笔迹点转成 MaskGeometry |
|
|
757
|
+
| `encodeBinaryMaskRle` / `decodeBinaryMaskRle` | 二进制 mask 与 RLE 互转 |
|
|
758
|
+
| `getBinaryMaskBounds` | 计算 mask 的实际像素边界 |
|
|
759
|
+
| `translateBinaryMask` | 平移 mask 像素 |
|
|
760
|
+
| `splitBinaryMaskComponents` | 把不相连的区域拆成多个 mask |
|
|
761
|
+
| `binaryMasksWithinDistance` | 判断两块 mask 是否足够接近 |
|
|
762
|
+
|
|
763
|
+
### 底层视口、空间索引和工具状态机
|
|
764
|
+
|
|
765
|
+
这些 API 主要给自定义渲染器、框架适配或自定义工具使用。普通业务通常用不到。
|
|
766
|
+
|
|
767
|
+
| API | 用途 |
|
|
768
|
+
| --- | --- |
|
|
769
|
+
| `createViewport` / `fitViewport` | 创建和适配 viewport |
|
|
770
|
+
| `imageToScreen` / `screenToImage` | 原图坐标与 viewport 坐标互转 |
|
|
771
|
+
| `zoomAt` / `panViewport` | 计算缩放或平移后的 viewport |
|
|
772
|
+
| `createGridIndex` | 创建空间索引 |
|
|
773
|
+
| `insertSpatialItem` / `updateSpatialItem` | 添加或更新索引项 |
|
|
774
|
+
| `removeSpatialItem` / `querySpatialBounds` | 删除或查询索引项 |
|
|
775
|
+
| `createSelectTool` / `createRectTool` / `createPolygonTool` | 创建底层交互工具 |
|
|
776
|
+
| `createBrushTool` / `createEraserTool` | 创建底层 mask 工具 |
|
|
777
|
+
| `createRectToolState` / `reduceRectTool` | 单独使用矩形状态机 |
|
|
778
|
+
| `createPolygonToolState` / `reducePolygonTool` | 单独使用多边形状态机 |
|
|
779
|
+
|
|
780
|
+
### 事件
|
|
781
|
+
|
|
782
|
+
| API | 用途 |
|
|
783
|
+
| --- | --- |
|
|
784
|
+
| `subscribe(annotator, 'change', listener)` | 监听标注、标签、图片、选择和历史变化 |
|
|
785
|
+
| `subscribe(annotator, 'error', listener)` | 监听订阅回调抛出的错误 |
|
|
786
|
+
|
|
787
|
+
公共类型包括 `Annotator`、`AnnotatorInstance`、`Annotation`、`RectGeometry`、`PolygonGeometry`、`MaskGeometry`、`LabelDefinition`、`Tool`、`AnnotationToolApi`、`ImageSource`、`ViewportState`、`Point` 和 `Bounds`。
|
|
788
|
+
|
|
789
|
+
## 核心类型
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
interface RectGeometry {
|
|
793
|
+
readonly type: 'rect'
|
|
794
|
+
readonly x: number
|
|
795
|
+
readonly y: number
|
|
796
|
+
readonly width: number
|
|
797
|
+
readonly height: number
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
interface PolygonGeometry {
|
|
801
|
+
readonly type: 'polygon'
|
|
802
|
+
readonly points: readonly (readonly [number, number])[]
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
interface MaskGeometry {
|
|
806
|
+
readonly type: 'mask'
|
|
807
|
+
readonly width: number
|
|
808
|
+
readonly height: number
|
|
809
|
+
readonly rle: readonly number[]
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
type Annotation = RectAnnotation | PolygonAnnotation | MaskAnnotation
|
|
813
|
+
type ToolCategory = 'selection' | 'drawing' | 'navigation' | 'utility'
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
## 开发命令
|
|
817
|
+
|
|
818
|
+
```bash
|
|
819
|
+
npm run dev
|
|
820
|
+
npm run typecheck
|
|
821
|
+
npm test
|
|
822
|
+
npm run test:e2e
|
|
823
|
+
npm run build
|
|
824
|
+
```
|