@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 +493 -250
- package/README_EN.md +498 -250
- package/docs/assets/index-BcqmlFff.js +1 -0
- package/docs/assets/{index-Dqqq7qvI.css → index-dq8tjOSG.css} +1 -1
- package/docs/index.html +2 -2
- package/package.json +2 -1
- package/project-docs/ARCHITECTURE.md +345 -354
- package/project-docs/REQUIREMENTS.md +232 -500
- package/skills/project-context.md +402 -0
- package/src/App.vue +502 -17
- package/src/components/PointAnnotation.vue +639 -219
- package/src/elements/PointAnnotationElement.ts +115 -17
- package/src/types/index.ts +22 -5
- package/src/utils/CanvasBrush.ts +20 -0
- package/docs/assets/index-CPn8AE3g.js +0 -1
package/README.md
CHANGED
|
@@ -2,19 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README_EN.md) | 中文
|
|
4
4
|
|
|
5
|
-
基于
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- 🎨
|
|
12
|
-
-
|
|
13
|
-
- 📤
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
68
|
-
url: 'https://example.com/image.jpg'
|
|
100
|
+
url: 'https://example.com/sample.jpg'
|
|
69
101
|
}))
|
|
70
102
|
|
|
71
|
-
|
|
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: '#
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
.
|
|
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
|
-
|
|
131
|
+
## Props 配置
|
|
134
132
|
|
|
135
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
135
|
+
| 字段 | 类型 | 说明 |
|
|
136
|
+
|------|------|------|
|
|
137
|
+
| `url` | `string` | 图片地址(远程 URL 或 dataURL) |
|
|
138
|
+
| `id` | `string` | 可选,业务标识 |
|
|
151
139
|
|
|
152
|
-
|
|
140
|
+
> 不传 `imageSource` 时,组件显示大面积上传区域,支持**点击选择文件**和**拖拽文件**两种本地加载方式。
|
|
153
141
|
|
|
154
|
-
|
|
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
|
-
|
|
168
|
-
console.log('标注点变化:', points)
|
|
169
|
-
}
|
|
144
|
+
完整的 `OptionsSource`:
|
|
170
145
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
146
|
+
```ts
|
|
147
|
+
interface OptionsSource {
|
|
148
|
+
// ============ 功能开关 ============
|
|
149
|
+
enableBrush?: boolean // 是否启用笔刷(默认 true);
|
|
150
|
+
// false 时笔刷按钮/面板不渲染,
|
|
151
|
+
// brushTool/eraserTool/mask 导出等方法失效
|
|
175
152
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
</style>
|
|
182
|
-
```
|
|
153
|
+
// ============ UI 开关 ============
|
|
154
|
+
showToolbar?: boolean // 是否显示内置工具栏(默认 true)
|
|
155
|
+
showZoomController?: boolean // 是否显示内置缩放控制器(默认 true)
|
|
156
|
+
canvasBackground?: string // 画布背景色(默认 '#f6f6f6')
|
|
183
157
|
|
|
184
|
-
|
|
158
|
+
// ============ 缩放 ============
|
|
159
|
+
zoomMin?: number // 最小缩放比例(默认 0.2)
|
|
160
|
+
zoomMax?: number // 最大缩放比例(默认 4)
|
|
185
161
|
|
|
186
|
-
|
|
162
|
+
// ============ 点标注 ============
|
|
163
|
+
pointStyle?: Partial<PointStyle> // 点标注样式(覆盖默认)
|
|
164
|
+
maxPoints?: number // 最大点数(可选)
|
|
187
165
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
// ============ 笔刷 ============
|
|
167
|
+
brushStyle?: Partial<BrushStyle> // 笔刷样式(覆盖默认)
|
|
168
|
+
brushLayers?: BrushLayerConfig[] // 笔刷图层配置(不传则单图层 "default")
|
|
169
|
+
maxBrushLayers?: number // 最大图层数(可选)
|
|
192
170
|
|
|
193
|
-
|
|
171
|
+
// ============ 历史 ============
|
|
172
|
+
maxUndoSteps?: number // 最大撤销步数(默认 100)
|
|
194
173
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
223
|
-
|
|
207
|
+
labelPadding: number | number[]
|
|
208
|
+
|
|
209
|
+
// 固定大小
|
|
210
|
+
fixedSizeOnZoom?: boolean // 开启则点不随画布缩放变大
|
|
211
|
+
fixedSizeScale?: number // 固定大小系数
|
|
224
212
|
}
|
|
225
213
|
```
|
|
226
214
|
|
|
227
|
-
|
|
215
|
+
默认值参考 [`src/types/index.ts`](src/types/index.ts)。
|
|
228
216
|
|
|
229
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
|
261
|
-
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
|
286
|
-
|
|
287
|
-
|
|
|
288
|
-
|
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
470
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
471
|
+
const activeLayer = ref('foreground')
|
|
472
|
+
</script>
|
|
473
|
+
```
|
|
326
474
|
|
|
327
|
-
### Mask
|
|
475
|
+
### 后端上传 Mask(Blob/File)
|
|
328
476
|
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
554
|
+
# 本地开发(App.vue 为演示入口)
|
|
555
|
+
pnpm dev
|
|
355
556
|
|
|
356
|
-
|
|
557
|
+
# 构建库产物(dist/)
|
|
558
|
+
pnpm build
|
|
357
559
|
|
|
358
|
-
|
|
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
|
-
|
|
363
|
-
- [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS 撤销/重做插件
|
|
606
|
+
MIT © zzalai
|