@zzalai/leafer-point-annotation 1.1.1 → 1.1.3

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 CHANGED
@@ -2,19 +2,52 @@
2
2
 
3
3
  [English](README_EN.md) | 中文
4
4
 
5
- 基于 Vue3 + LeaferJS 的点标注与笔刷涂抹工具,支持导出 COCO/YOLO/JSON 格式,专为 AI 模型训练数据集标注设计。
6
-
7
- ## 功能特点
8
-
9
- - 📍 **点标注** - 在图片上添加可编辑的标注点
10
- - 🖌️ **笔刷涂抹** - 自由涂抹,支持擦除
11
- - 🎨 **自定义样式** - 笔刷颜色、大小、透明度可配置
12
- - 🔄 **撤销/重做** - 完整的历史记录管理
13
- - 📤 **多格式导出** - JSON/COCO/YOLO/二值图
14
- - 🔍 **画布缩放** - 支持缩放、平移、重置
15
- - ⌨️ **热键支持** - V/P/B/E/Ctrl+Z/Ctrl+Y 等
16
- - 📱 **响应式设计** - Vue3 组件化架构
17
- - 🖼️ **本地上传** - 支持本地图片上传和拖拽上传
5
+ > 基于 **Vue 3 + LeaferJS** 的图像点标注与笔刷涂抹工具,支持导出 COCO/YOLO/JSON 格式,专为 AI 模型训练数据集标注设计。
6
+
7
+ - 📍 点标注(可拖拽、可编辑、自动重排、hover/selected 状态)
8
+ - 🖌️ 多图层笔刷涂抹与擦除(颜色、透明度、大小、连续性可调)
9
+ - 🔀 一键根据点标注的轨迹生成笔刷多边形区域
10
+ - 🖼️ 本地图片上传(点击 + 拖拽)或远程图片
11
+ - 🎨 完整的点/笔刷样式自定义
12
+ - ⬅️ 撤销 / 重做(Command 模式)
13
+ - 📤 多格式导出:JSON / COCO / YOLO / Mask (dataURL / Blob / File)
14
+ - 📱 支持自定义工具栏(隐藏内置工具栏,调用 ref API 自行构建)
15
+ - 🔒 `enableBrush: false` 可完全禁用笔刷,仅保留点标注
16
+ - ⌨️ 丰富的键盘快捷键(`v` `p` `b` `e` `Ctrl+Z` `Ctrl+Y` `Delete` 等)
17
+
18
+ ---
19
+
20
+ ## 目录
21
+
22
+ - [安装](#安装)
23
+ - [快速开始](#快速开始)
24
+ - [Props 配置](#props-配置)
25
+ - [imageSource](#imagesource)
26
+ - [options](#options)
27
+ - [currentLayer(v-model:currentLayer)](#currentlayerv-modelcurrentlayer)
28
+ - [Events 事件](#events-事件)
29
+ - [Ref API(父组件调用)](#ref-api父组件调用)
30
+ - [点标注](#点标注)
31
+ - [图片 & 画布](#图片-画布)
32
+ - [工具切换](#工具切换)
33
+ - [删除 & 清空](#删除-清空)
34
+ - [笔刷图层](#笔刷图层)
35
+ - [笔刷样式](#笔刷样式)
36
+ - [点轨迹生成笔刷区域](#点轨迹生成笔刷区域)
37
+ - [缩放](#缩放)
38
+ - [撤销 / 重做](#撤销-重做)
39
+ - [导入 / 导出](#导入-导出)
40
+ - [使用示例](#使用示例)
41
+ - [最小示例](#最小示例)
42
+ - [完整自定义(隐藏内置工具栏)](#完整自定义隐藏内置工具栏)
43
+ - [多图层笔刷](#多图层笔刷)
44
+ - [后端上传 Mask(Blob/File)](#后端上传-mask-blobfile)
45
+ - [只启用点标注(禁用笔刷)](#只启用点标注禁用笔刷)
46
+ - [快捷键](#快捷键)
47
+ - [开发与构建](#开发与构建)
48
+ - [许可证](#许可证)
49
+
50
+ ---
18
51
 
19
52
  ## 安装
20
53
 
@@ -36,328 +69,538 @@ yarn add @zzalai/leafer-point-annotation
36
69
  pnpm add @zzalai/leafer-point-annotation
37
70
  ```
38
71
 
39
- ## 快速开始
72
+ > ⚠️ 注意:`vue@^3.3.0` 为 peer dependency(不会被自动安装,需宿主项目已存在)。
40
73
 
41
- ### 使用远程图片
74
+ ---
75
+
76
+ ## 快速开始
42
77
 
43
78
  ```vue
44
79
  <template>
45
- <div class="demo-container">
46
- <PointAnnotation
47
- ref="annotationRef"
48
- :imageSource="imageSource"
49
- :options="options"
50
- @pointChange="handlePointChange"
51
- @loadSuccess="handleLoadSuccess"
52
- />
53
- <div class="controls">
54
- <button @click="exportJSON">导出 JSON</button>
55
- <button @click="exportMask">导出 Mask</button>
56
- </div>
57
- </div>
80
+ <PointAnnotation
81
+ ref="annotationRef"
82
+ :image-source="imageSource"
83
+ :options="options"
84
+ @point-change="handlePointChange"
85
+ @load-success="handleLoadSuccess"
86
+ />
58
87
  </template>
59
88
 
60
89
  <script setup lang="ts">
61
90
  import { ref, computed } from 'vue'
62
91
  import { PointAnnotation } from '@zzalai/leafer-point-annotation'
92
+
93
+ // 👇 必须手动导入样式
63
94
  import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
64
95
 
65
96
  const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
97
+
98
+ // 方式一:远程图片
66
99
  const imageSource = computed(() => ({
67
- id: 'demo-image',
68
- url: 'https://example.com/image.jpg'
100
+ url: 'https://example.com/sample.jpg'
69
101
  }))
70
102
 
71
- const options = ref({
103
+ // 方式二:不传 imageSource,用户本地上传
104
+ // const imageSource = null
105
+
106
+ const options = {
107
+ enableBrush: true,
72
108
  pointStyle: {
73
109
  circleFill: '#ff4d4f',
74
- circleStroke: '#ffffff',
75
- labelBackgroundColor: '#ffffff'
110
+ circleStroke: '#ffffff'
76
111
  },
77
112
  brushStyle: {
78
- color: '#ff4d4f',
113
+ color: '#1890ff',
79
114
  opacity: 0.55,
80
115
  size: 100
81
- },
82
- maskExportFormat: 'png',
83
- maskExportForeground: 'black'
84
- })
85
-
86
- const handlePointChange = (points) => {
87
- console.log('标注点变化:', points)
88
- }
89
-
90
- const handleLoadSuccess = () => {
91
- console.log('图片加载成功')
92
- }
93
-
94
- const exportJSON = () => {
95
- const json = annotationRef.value?.exportCanvasJSON()
96
- if (json) {
97
- const blob = new Blob([json], { type: 'application/json' })
98
- const url = URL.createObjectURL(blob)
99
- const a = document.createElement('a')
100
- a.href = url
101
- a.download = 'annotation.json'
102
- a.click()
103
116
  }
104
117
  }
105
118
 
106
- const exportMask = async () => {
107
- const mask = await annotationRef.value?.exportMaskImage('png', 'black')
108
- if (mask) {
109
- const a = document.createElement('a')
110
- a.href = mask
111
- a.download = 'mask.png'
112
- a.click()
113
- }
119
+ function handlePointChange(points: any[]) {
120
+ console.log('点标注变化:', points)
114
121
  }
115
- </script>
116
122
 
117
- <style scoped>
118
- .demo-container {
119
- width: 100%;
120
- height: 600px;
123
+ function handleLoadSuccess(info: any) {
124
+ console.log('图片加载成功:', info)
121
125
  }
122
-
123
- .controls {
124
- margin-top: 16px;
125
- display: flex;
126
- gap: 12px;
127
- }
128
- </style>
126
+ </script>
129
127
  ```
130
128
 
131
- ### 使用本地图片上传
129
+ ---
132
130
 
133
- 不提供 `imageSource` prop 时,会显示本地上传界面,支持点击选择或拖拽上传。
131
+ ## Props 配置
134
132
 
135
- ```vue
136
- <template>
137
- <div class="demo-container">
138
- <PointAnnotation
139
- ref="annotationRef"
140
- :options="options"
141
- @pointChange="handlePointChange"
142
- @loadSuccess="handleLoadSuccess"
143
- />
144
- </div>
145
- </template>
133
+ ### imageSource
146
134
 
147
- <script setup lang="ts">
148
- import { ref } from 'vue'
149
- import { PointAnnotation } from '@zzalai/leafer-point-annotation'
150
- import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
135
+ | 字段 | 类型 | 说明 |
136
+ |------|------|------|
137
+ | `url` | `string` | 图片地址(远程 URL 或 dataURL) |
138
+ | `id` | `string` | 可选,业务标识 |
151
139
 
152
- const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
140
+ > 不传 `imageSource` 时,组件显示大面积上传区域,支持**点击选择文件**和**拖拽文件**两种本地加载方式。
153
141
 
154
- const options = ref({
155
- pointStyle: {
156
- circleFill: '#ff4d4f',
157
- circleStroke: '#ffffff',
158
- labelBackgroundColor: '#ffffff'
159
- },
160
- brushStyle: {
161
- color: '#ff4d4f',
162
- opacity: 0.55,
163
- size: 100
164
- }
165
- })
142
+ ### options
166
143
 
167
- const handlePointChange = (points) => {
168
- console.log('标注点变化:', points)
169
- }
144
+ 完整的 `OptionsSource`:
170
145
 
171
- const handleLoadSuccess = () => {
172
- console.log('图片加载成功')
173
- }
174
- </script>
146
+ ```ts
147
+ interface OptionsSource {
148
+ // ============ 功能开关 ============
149
+ enableBrush?: boolean // 是否启用笔刷(默认 true);
150
+ // false 时笔刷按钮/面板不渲染,
151
+ // brushTool/eraserTool/mask 导出等方法失效
175
152
 
176
- <style scoped>
177
- .demo-container {
178
- width: 100%;
179
- height: 600px;
180
- }
181
- </style>
182
- ```
153
+ // ============ UI 开关 ============
154
+ showToolbar?: boolean // 是否显示内置工具栏(默认 true)
155
+ showZoomController?: boolean // 是否显示内置缩放控制器(默认 true)
156
+ canvasBackground?: string // 画布背景色(默认 '#f6f6f6')
183
157
 
184
- ## API 文档
158
+ // ============ 缩放 ============
159
+ zoomMin?: number // 最小缩放比例(默认 0.2)
160
+ zoomMax?: number // 最大缩放比例(默认 4)
185
161
 
186
- ### Props
162
+ // ============ 点标注 ============
163
+ pointStyle?: Partial<PointStyle> // 点标注样式(覆盖默认)
164
+ maxPoints?: number // 最大点数(可选)
187
165
 
188
- | 属性名 | 类型 | 默认值 | 说明 |
189
- |--------|------|--------|------|
190
- | imageSource | `{ id?: string; url: string }` | `null` | 图片源配置(可选,不提供时显示本地上传入口) |
191
- | options | `Object` | `{}` | 配置选项 |
166
+ // ============ 笔刷 ============
167
+ brushStyle?: Partial<BrushStyle> // 笔刷样式(覆盖默认)
168
+ brushLayers?: BrushLayerConfig[] // 笔刷图层配置(不传则单图层 "default")
169
+ maxBrushLayers?: number // 最大图层数(可选)
192
170
 
193
- #### Options 配置
171
+ // ============ 历史 ============
172
+ maxUndoSteps?: number // 最大撤销步数(默认 100)
194
173
 
195
- ```typescript
196
- interface Options {
197
- pointStyle?: Partial<PointStyle>
198
- brushStyle?: Partial<BrushStyle>
199
- maskExportFormat?: 'png' | 'jpg' | 'jpeg'
200
- maskExportForeground?: 'black' | 'white'
201
- maxUndoSteps?: number
174
+ // ============ Mask 导出 ============
175
+ maskExportFormat?: 'png' | 'jpeg' | 'jpg' // Mask 默认导出格式(默认 png)
176
+ maskExportForeground?: 'black' | 'white' // Mask 默认前景色(默认 black)
202
177
  }
203
178
  ```
204
179
 
205
- #### PointStyle 配置
180
+ #### PointStyle(点标注样式)
206
181
 
207
- ```typescript
182
+ ```ts
208
183
  interface PointStyle {
209
184
  circleRadius: number
210
185
  circleFill: string
211
186
  circleStroke: string
212
187
  circleStrokeWidth: number
188
+
189
+ // hover
213
190
  hoverCircleFill: string
214
191
  hoverCircleStroke: string
192
+
193
+ // selected
215
194
  selectedCircleFill: string
216
195
  selectedCircleStroke: string
217
196
  selectedCircleScale: number
197
+
198
+ // 文字
199
+ circleTextFontSize: number
200
+ circleTextFontFamily: string
201
+ circleTextFill: string
202
+
203
+ // label
218
204
  labelBackgroundColor: string
219
205
  labelTextColor: string
220
206
  labelFontSize: number
221
- labelPadding: number[]
222
- fixedSizeOnZoom: boolean
223
- fixedSizeScale: number
207
+ labelPadding: number | number[]
208
+
209
+ // 固定大小
210
+ fixedSizeOnZoom?: boolean // 开启则点不随画布缩放变大
211
+ fixedSizeScale?: number // 固定大小系数
224
212
  }
225
213
  ```
226
214
 
227
- #### BrushStyle 配置
215
+ 默认值参考 [`src/types/index.ts`](src/types/index.ts)。
228
216
 
229
- ```typescript
217
+ #### BrushStyle(笔刷样式)
218
+
219
+ ```ts
230
220
  interface BrushStyle {
231
- color: string
232
- opacity: number
233
- size: number
234
- minSize: number
235
- maxSize: number
236
- continuity: number
221
+ color: string // 笔刷颜色(十六进制)
222
+ opacity: number // 透明度 0~1(通过 Group.opacity 控制)
223
+ size: number // 笔刷大小(像素)
224
+ minSize: number // 滑块最小
225
+ maxSize: number // 滑块最大
226
+ continuity: number // 两点间最大距离阈值(超过则断开)
227
+ }
228
+ ```
229
+
230
+ #### BrushLayerConfig(多图层配置)
231
+
232
+ ```ts
233
+ interface BrushLayerConfig {
234
+ label: string // 图层显示名
235
+ value: string // 图层唯一标识
236
+ color?: string // 该图层默认颜色
237
+ opacity?: number // 该图层默认透明度
238
+ size?: number // 该图层默认笔刷大小
237
239
  }
238
240
  ```
239
241
 
240
- ### Events
241
-
242
- | 事件名 | 参数 | 说明 |
243
- |--------|------|------|
244
- | pointChange | `PointAnnotation[]` | 点标注数据变化时触发 |
245
- | loadStart | - | 图片开始加载时触发 |
246
- | loadSuccess | - | 图片加载成功时触发 |
247
- | loadError | `error` | 图片加载失败时触发 |
248
- | undoStateChange | - | 撤销状态变化时触发 |
249
- | redoStateChange | - | 重做状态变化时触发 |
250
-
251
- ### Methods
252
-
253
- 组件暴露以下方法:
254
-
255
- | 方法名 | 参数 | 返回值 | 说明 |
256
- |--------|------|--------|------|
257
- | getPointAnnotations | - | `PointAnnotation[]` | 获取所有点标注数据 |
258
- | getImageInfo | - | `Object` | 获取图片信息 |
259
- | exportCanvasJSON | - | `string` | 导出完整 JSON 数据 |
260
- | exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | 导出二值图 |
261
- | exportCOCO | - | `string` | 导出 COCO 格式 JSON |
262
- | exportYOLO | - | `{ annotations: string; classNames: string }` | 导出 YOLO 格式 |
263
- | importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | 导入 JSON 数据 |
264
- | loadImage | `url?` | `Promise<void>` | 加载图片 |
265
- | clearBrush | - | `void` | 清除笔刷内容 |
266
- | zoomIn | - | `void` | 放大画布 |
267
- | zoomOut | - | `void` | 缩小画布 |
268
- | resetZoom | - | `void` | 重置缩放 |
269
- | undo | - | `void` | 撤销操作 |
270
- | redo | - | `void` | 重做操作 |
271
- | getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | 获取当前工具 |
272
- | setTool | `tool` | `void` | 设置当前工具 |
273
- | createPointAnnotation | `x`, `y` | `string\|null` | 创建标注点 |
274
- | removePointAnnotation | `id` | `boolean` | 删除指定标注点 |
275
-
276
- ## 热键说明
277
-
278
- | 热键 | 功能 |
242
+ 不传 `brushLayers` 时默认为单图层 `{label:'默认图层', value:'default'}`。
243
+
244
+ ### currentLayer(v-model:currentLayer)
245
+
246
+ 受控图层切换。例如:
247
+
248
+ ```vue
249
+ <PointAnnotation
250
+ :options="{ brushLayers: [
251
+ { label: '前景', value: 'foreground' },
252
+ { label: '遮挡', value: 'occlusion' }
253
+ ]}"
254
+ v-model:current-layer="activeLayer"
255
+ />
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Events 事件
261
+
262
+ | 事件 | 参数 | 触发时机 |
263
+ |------|------|---------|
264
+ | `point-change` | `(points: PointAnnotation[])` | 点新增 / 删除 / 修改 / 重排 |
265
+ | `load-start` | - | 开始加载图片 |
266
+ | `load-success` | `{ url, width, height }` | 图片加载成功 |
267
+ | `load-error` | `{ error }` | 图片加载失败 |
268
+ | `undo-state-change` | `{ canUndo }` | 撤销栈状态变化 |
269
+ | `redo-state-change` | `{ canRedo }` | 重做栈状态变化 |
270
+ | `update:currentLayer` | `layerValue` | 当前笔刷图层变化(配合 v-model) |
271
+ | `layer-change` | `layerValue` | update:currentLayer |
272
+
273
+ ---
274
+
275
+ ## Ref API(父组件调用)
276
+
277
+ 通过 `ref` 访问组件实例后可调用以下方法。
278
+
279
+ ```ts
280
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
281
+
282
+ // 示例:
283
+ annotationRef.value?.pointTool() // 切到点标注工具
284
+ annotationRef.value?.createBrushFromPoints() // 按点轨迹生成笔刷区域
285
+ annotationRef.value?.getMaskBlob() // 导出当前图层 Blob(上传后端)
286
+ ```
287
+
288
+ ### 点标注
289
+
290
+ | 方法 | 说明 |
291
+ |------|------|
292
+ | `getPointAnnotations(): PointAnnotation[]` | 获取当前所有点 |
293
+ | `createPointAnnotation(x: number, y: number, label?: string): boolean` | 程序化新增一个点 |
294
+ | `removePointAnnotation(id: string): boolean` | 程序化删除一个点 |
295
+ | `updatePointAnnotationLabel(id: string, label: string): boolean` | 修改某个点的 label |
296
+
297
+ ### 图片 & 画布
298
+
299
+ | 方法 | 说明 |
300
+ |------|------|
301
+ | `getImageInfo()` | `{ url, width, height }` |
302
+ | `loadImage(url: string)` | 动态加载一张新图片 |
303
+
304
+ ### 工具切换
305
+
306
+ | 方法 | 说明 |
307
+ |------|------|
308
+ | `getCurrentTool(): 'select' \| 'point' \| 'brush' \| 'eraser'` | - |
309
+ | `setTool(tool)` | 切换到指定工具(`enableBrush=false` 时 brush/eraser 被拦截) |
310
+ | `selectTool()` | 选择工具 |
311
+ | `pointTool()` | 点标注工具 |
312
+ | `brushTool(openPanel?: boolean)` | 笔刷工具 |
313
+ | `eraserTool()` | 橡皮擦工具 |
314
+
315
+ ### 删除 & 清空
316
+
317
+ | 方法 | 说明 |
318
+ |------|------|
319
+ | `deleteSelected()` | 删除当前选中的点(带 `confirm`) |
320
+ | `clearAllAnnotationsAndBrush()` | 清空所有点 + 笔刷(带 `confirm`) |
321
+ | `clearBrush()` | 清空当前图层的笔刷 |
322
+ | `clearAllBrushLayers()` | 清空所有图层的笔刷 |
323
+
324
+ ### 笔刷图层
325
+
326
+ | 方法 | 说明 |
327
+ |------|------|
328
+ | `getCurrentLayer(): string` | 当前激活图层 value |
329
+ | `setActiveLayer(value: string): boolean` | 切换到指定图层 |
330
+ | `getAllLayers(): BrushLayerConfig[]` | 所有图层配置 |
331
+
332
+ ### 笔刷样式
333
+
334
+ | 方法 | 说明 |
335
+ |------|------|
336
+ | `getBrushStyle(): BrushStyle` | 返回当前样式的拷贝 |
337
+ | `updateBrushStyle(partial: Partial<BrushStyle>): void` | 动态更新(如颜色/透明度/大小) |
338
+
339
+ ### 点轨迹生成笔刷区域
340
+
341
+ | 方法 | 说明 |
342
+ |------|------|
343
+ | `createBrushFromPoints(): boolean` | 按 `sequenceNumber` 顺序将点的像素坐标连成闭合多边形,使用当前笔刷样式填充;点数量 < 3 时不操作 |
344
+
345
+ ### 缩放
346
+
347
+ | 方法 | 说明 |
348
+ |------|------|
349
+ | `zoomIn()` | 放大 |
350
+ | `zoomOut()` | 缩小 |
351
+ | `resetZoom()` | 重置到 100% |
352
+
353
+ ### 撤销 / 重做
354
+
355
+ | 方法 | 说明 |
279
356
  |------|------|
280
- | V | 选择工具 |
281
- | P | 点标注工具 |
282
- | B | 笔刷工具 |
283
- | E | 擦除工具 |
284
- | Ctrl + Z | 撤销 |
285
- | Ctrl + Y | 重做 |
286
- | Delete | 删除选中/清除所有 |
287
- | Ctrl + + | 放大 |
288
- | Ctrl + - | 缩小 |
289
- | Ctrl + 0 | 重置缩放 |
290
- | Alt | 显示/隐藏热键提示 |
291
-
292
- ## 导出格式
293
-
294
- ### JSON Full
295
-
296
- 包含完整的标注数据和笔刷 mask。
297
-
298
- ```json
299
- {
300
- "version": "1.0",
301
- "imageUrl": "https://example.com/image.jpg",
302
- "imageWidth": 1280,
303
- "imageHeight": 720,
304
- "pointAnnotations": [
305
- {
306
- "id": "point_xxx",
307
- "pixel": { "x": 100, "y": 200 },
308
- "normalized": { "x": 0.078, "y": 0.278 },
309
- "label": "#1",
310
- "createdAt": 1716960000000,
311
- "updatedAt": 1716960000000
312
- }
313
- ],
314
- "brushMask": "data:image/png;base64,...",
315
- "exportTime": 1716960000000
357
+ | `undo()` | 撤销上一步 |
358
+ | `redo()` | 重做上一步 |
359
+
360
+ ### 导入 / 导出
361
+
362
+ | 方法 | 说明 |
363
+ |------|------|
364
+ | `exportCanvasJSON(): string` | 全量导出(点 + 笔刷快照 + 图片信息) |
365
+ | `importCanvasJSON(data: string \| object): boolean` | 从导出的 JSON 恢复 |
366
+ | `exportMaskImage(format?, fg?)`: `Promise<string \| null>` | 当前图层 mask(dataURL) |
367
+ | `exportMaskImageByLayer(layerValue, format?, fg?)`: `Promise<string \| null>` | 指定图层 mask |
368
+ | `exportAllMaskImages(format?, fg?)`: `Promise<Record<string, string>>` | 所有图层 mask |
369
+ | `getMaskBlob(layerValue?, format?, fg?)`: `Promise<Blob \| null>` | 当前/指定图层 Blob(后端上传) |
370
+ | `getMaskFile(layerValue?, filename?, format?, fg?)`: `Promise<File \| null>` | 当前/指定图层 File |
371
+ | `getAllMaskBlobs(format?, fg?)`: `Promise<Record<string, Blob>>` | 所有图层 Blob 集合 |
372
+ | `exportCOCO(): string` | 导出 COCO JSON(点标注 = keypoints) |
373
+ | `exportYOLO(): string` | 导出 YOLO 标注 |
374
+
375
+ > 参数说明:`format` = `'png' \| 'jpeg' \| 'jpg'`;`fg` = `'black' \| 'white'`(mask 前景色)。
376
+ > 注:所有 Mask/Blob/File 相关方法需在浏览器环境调用,且在 `enableBrush=false` 时返回 null/{}。
377
+
378
+ ---
379
+
380
+ ## 使用示例
381
+
382
+ ### 最小示例
383
+
384
+ ```vue
385
+ <template>
386
+ <PointAnnotation
387
+ ref="annotationRef"
388
+ :image-source="{ url: 'https://example.com/image.jpg' }"
389
+ :options="{ enableBrush: false }"
390
+ />
391
+ </template>
392
+
393
+ <script setup lang="ts">
394
+ import { ref } from 'vue'
395
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
396
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
397
+
398
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
399
+ </script>
400
+ ```
401
+
402
+ ### 完整自定义(隐藏内置工具栏)
403
+
404
+ ```vue
405
+ <template>
406
+ <div class="my-toolbar">
407
+ <button @click="() => annotationRef.value?.pointTool()">点标注</button>
408
+ <button @click="() => annotationRef.value?.brushTool()">笔刷</button>
409
+ <button @click="() => annotationRef.value?.eraserTool()">橡皮</button>
410
+ <button @click="() => annotationRef.value?.deleteSelected()">删除</button>
411
+ <button @click="() => annotationRef.value?.clearAllAnnotationsAndBrush()">全部清空</button>
412
+ <button @click="() => annotationRef.value?.undo()">撤销</button>
413
+ <button @click="() => annotationRef.value?.redo()">重做</button>
414
+ <button @click="() => annotationRef.value?.createBrushFromPoints()">点→多边形</button>
415
+ <button @click="uploadMask">上传 Mask</button>
416
+ </div>
417
+
418
+ <PointAnnotation
419
+ ref="annotationRef"
420
+ :image-source="{ url: 'https://example.com/image.jpg' }"
421
+ :options="{ showToolbar: false, showZoomController: false }"
422
+ @point-change="handlePoints"
423
+ />
424
+ </template>
425
+
426
+ <script setup lang="ts">
427
+ import { ref } from 'vue'
428
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
429
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
430
+
431
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
432
+
433
+ function handlePoints(points: any[]) {
434
+ console.log('points:', points)
316
435
  }
436
+
437
+ async function uploadMask() {
438
+ const blob = await annotationRef.value?.getMaskBlob()
439
+ if (!blob) return
440
+ const fd = new FormData()
441
+ fd.append('file', blob, 'mask.png')
442
+ await fetch('/api/upload', { method: 'POST', body: fd })
443
+ }
444
+ </script>
317
445
  ```
318
446
 
319
- ### COCO
447
+ ### 多图层笔刷
320
448
 
321
- 适用于关键点检测任务。
449
+ ```vue
450
+ <template>
451
+ <PointAnnotation
452
+ ref="annotationRef"
453
+ :image-source="{ url: 'https://example.com/image.jpg' }"
454
+ :options="{
455
+ brushLayers: [
456
+ { label: '前景', value: 'foreground', color: '#1890ff', opacity: 0.55 },
457
+ { label: '遮挡', value: 'occlusion', color: '#faad14', opacity: 0.55 },
458
+ { label: '背景', value: 'background', color: '#52c41a', opacity: 0.55 }
459
+ ]
460
+ }"
461
+ v-model:current-layer="activeLayer"
462
+ />
463
+ </template>
322
464
 
323
- ### YOLO
465
+ <script setup lang="ts">
466
+ import { ref } from 'vue'
467
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
468
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
324
469
 
325
- 适用于 YOLO 系列模型训练。
470
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
471
+ const activeLayer = ref('foreground')
472
+ </script>
473
+ ```
326
474
 
327
- ### Mask Image
475
+ ### 后端上传 Mask(Blob/File)
328
476
 
329
- PNG/JPG 格式的二值图,前景为黑色/白色,背景透明/白色。
477
+ ```vue
478
+ <template>
479
+ <PointAnnotation ref="annotationRef" :image-source="{ url: '...' }" />
480
+ <button @click="uploadAllMasks">上传所有图层 Mask</button>
481
+ </template>
330
482
 
331
- ## 项目文档
483
+ <script setup lang="ts">
484
+ import { ref } from 'vue'
485
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
486
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
332
487
 
333
- - [需求文档](./project-docs/REQUIREMENTS.md) - 详细的功能需求说明
334
- - [架构文档](./project-docs/ARCHITECTURE.md) - 系统架构设计
335
- - [实现计划](./project-docs/IMPLEMENTATION_PLAN.md) - 开发任务规划
336
- - [开发指南](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS 开发实战指南
488
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
337
489
 
338
- ## 浏览器支持
490
+ async function uploadAllMasks() {
491
+ const blobs = await annotationRef.value?.getAllMaskBlobs('png', 'black')
492
+ if (!blobs) return
493
+ for (const [layerValue, blob] of Object.entries(blobs)) {
494
+ const fd = new FormData()
495
+ fd.append('file', blob, `${layerValue}.png`)
496
+ await fetch('/api/mask-upload', { method: 'POST', body: fd })
497
+ }
498
+ }
499
+ </script>
500
+ ```
339
501
 
340
- - Chrome 60+
341
- - Firefox 55+
342
- - Safari 12+
343
- - Edge 79+
502
+ ### 只启用点标注(禁用笔刷)
344
503
 
345
- ## 依赖
504
+ ```vue
505
+ <template>
506
+ <PointAnnotation
507
+ ref="annotationRef"
508
+ :image-source="{ url: '...' }"
509
+ :options="{ enableBrush: false }"
510
+ @point-change="handlePoints"
511
+ />
512
+ </template>
346
513
 
347
- - Vue 3.3.0+
348
- - LeaferUI 2.0.8+
349
- - Tinykeys 3.0.0+
350
- - @zzalai/leafer-undo-redo 1.0.3+
514
+ <script setup lang="ts">
515
+ import { ref } from 'vue'
516
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
517
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
351
518
 
352
- ## 许可证
519
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
520
+ function handlePoints(points: any[]) {
521
+ console.log('标注:', points)
522
+ }
523
+ </script>
524
+ ```
525
+
526
+ ---
527
+
528
+ ## 快捷键
529
+
530
+ > 生效条件:**画布获得焦点** 或 **鼠标 hover 在画布上**
531
+
532
+ | 按键 | 功能 | 限制 |
533
+ |------|------|------|
534
+ | `v` | 选择工具 | - |
535
+ | `p` | 点标注工具 | - |
536
+ | `b` | 笔刷工具 | 需 `enableBrush=true` |
537
+ | `e` | 橡皮擦工具 | 需 `enableBrush=true` |
538
+ | `Ctrl + Z` | 撤销 | - |
539
+ | `Ctrl + Y` | 重做 | - |
540
+ | `Delete` | 删除选中的点 | - |
541
+ | `Ctrl + +` | 放大 | - |
542
+ | `Ctrl + -` | 缩小 | - |
543
+ | `Ctrl + 0` | 重置缩放 | - |
544
+ | `Alt` | 显示/隐藏快捷键提示浮层 | - |
545
+
546
+ ---
547
+
548
+ ## 开发与构建
549
+
550
+ ```bash
551
+ # 安装依赖
552
+ pnpm install
353
553
 
354
- MIT License
554
+ # 本地开发(App.vue 为演示入口)
555
+ pnpm dev
355
556
 
356
- ## 贡献
557
+ # 构建库产物(dist/)
558
+ pnpm build
357
559
 
358
- 欢迎提交 Issue 和 Pull Request!
560
+ # 构建演示站点(docs/)
561
+ pnpm docs:build
359
562
 
360
- ## 相关项目
563
+ # 同时构建库 + 演示站
564
+ pnpm build:all
565
+
566
+ # 类型检查
567
+ pnpm tsc --noEmit
568
+ ```
569
+
570
+ ### 项目结构
571
+
572
+ ```
573
+ src/
574
+ ├── components/
575
+ │ ├── PointAnnotation.vue # 核心主组件(所有能力整合)
576
+ │ ├── BrushSizeSlider.vue # 笔刷大小滑块
577
+ │ └── BrushStylePanel.vue # 笔刷样式面板
578
+ ├── elements/
579
+ │ └── PointAnnotationElement.ts # 自定义点元素(Group + Ellipse + Text)
580
+ ├── utils/
581
+ │ ├── CanvasBrush.ts # 笔刷底层(canvas + 绘制快照)
582
+ │ ├── BrushCommands.ts # 笔刷撤销命令
583
+ │ ├── PointCommands.ts # 点撤销命令
584
+ │ ├── BrushStroke.ts # 笔画数据
585
+ │ ├── COCOExporter.ts # COCO 导出
586
+ │ └── YOLOExporter.ts # YOLO 导出
587
+ ├── types/
588
+ │ └── index.ts # 所有对外类型与默认值
589
+ ├── App.vue # dev 演示页
590
+ ├── index.ts # 对外导出
591
+ └── main.ts # dev 入口
592
+ ```
593
+
594
+ ### 发布流程
595
+
596
+ 1. `pnpm install` → `pnpm build:all`
597
+ 2. 确认 `dist/` 与 `docs/` 最新
598
+ 3. 更新 `package.json` 的 `version`
599
+ 4. `npm publish`
600
+ 5. `git push` 到 GitHub(触发 Pages 重新部署)
601
+
602
+ ---
603
+
604
+ ## 许可证
361
605
 
362
- - [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - 多区域 ROI 标注工具
363
- - [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS 撤销/重做插件
606
+ MIT © zzalai