@zzalai/leafer-point-annotation 1.0.0
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/LICENSE +21 -0
- package/README.md +308 -0
- package/README_EN.md +308 -0
- package/docs/assets/index-DGiYiG5f.css +1 -0
- package/docs/assets/index-L8gL3x2V.js +1 -0
- package/docs/index.html +14 -0
- package/index.html +13 -0
- package/package.json +64 -0
- package/project-docs/ARCHITECTURE.md +401 -0
- package/project-docs/IMPLEMENTATION_PLAN.md +196 -0
- package/project-docs/REQUIREMENTS.md +517 -0
- package/project-docs/TODO.md +167 -0
- package/project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md +835 -0
- package/project-docs/leafer-development-guide/LEAFER_UNDO_REDO_GUIDE.md +329 -0
- package/project-docs/leafer-development-guide/TINYKEYS_GUIDE.md +407 -0
- package/src/App.vue +464 -0
- package/src/components/BrushSizeSlider.vue +190 -0
- package/src/components/BrushStylePanel.vue +295 -0
- package/src/components/PointAnnotation.vue +1663 -0
- package/src/elements/PointAnnotationElement.ts +155 -0
- package/src/index.ts +4 -0
- package/src/main.ts +4 -0
- package/src/types/index.ts +122 -0
- package/src/utils/BrushCommands.ts +47 -0
- package/src/utils/BrushStroke.ts +96 -0
- package/src/utils/COCOExporter.ts +90 -0
- package/src/utils/CanvasBrush.ts +179 -0
- package/src/utils/PointCommands.ts +74 -0
- package/src/utils/YOLOExporter.ts +39 -0
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.json +24 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +42 -0
- package/vite.docs.config.ts +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 otaku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# @zzalai/leafer-point-annotation
|
|
2
|
+
|
|
3
|
+
[English](README_EN.md) | 中文
|
|
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
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
### npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @zzalai/leafer-point-annotation
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### yarn
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
yarn add @zzalai/leafer-point-annotation
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### pnpm
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add @zzalai/leafer-point-annotation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
### 基础使用
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<div class="demo-container">
|
|
45
|
+
<PointAnnotation
|
|
46
|
+
ref="annotationRef"
|
|
47
|
+
:imageSource="imageSource"
|
|
48
|
+
:options="options"
|
|
49
|
+
@pointChange="handlePointChange"
|
|
50
|
+
@loadSuccess="handleLoadSuccess"
|
|
51
|
+
/>
|
|
52
|
+
<div class="controls">
|
|
53
|
+
<button @click="exportJSON">导出 JSON</button>
|
|
54
|
+
<button @click="exportMask">导出 Mask</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { ref, computed } from 'vue'
|
|
61
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
62
|
+
|
|
63
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
64
|
+
const imageSource = computed(() => ({
|
|
65
|
+
id: 'demo-image',
|
|
66
|
+
url: 'https://example.com/image.jpg'
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
const options = ref({
|
|
70
|
+
pointStyle: {
|
|
71
|
+
circleFill: '#ff4d4f',
|
|
72
|
+
circleStroke: '#ffffff',
|
|
73
|
+
labelBackgroundColor: '#ffffff'
|
|
74
|
+
},
|
|
75
|
+
brushStyle: {
|
|
76
|
+
color: '#ff4d4f',
|
|
77
|
+
opacity: 0.55,
|
|
78
|
+
size: 100
|
|
79
|
+
},
|
|
80
|
+
maskExportFormat: 'png',
|
|
81
|
+
maskExportForeground: 'black'
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const handlePointChange = (points) => {
|
|
85
|
+
console.log('标注点变化:', points)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleLoadSuccess = () => {
|
|
89
|
+
console.log('图片加载成功')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const exportJSON = () => {
|
|
93
|
+
const json = annotationRef.value?.exportCanvasJSON()
|
|
94
|
+
if (json) {
|
|
95
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
96
|
+
const url = URL.createObjectURL(blob)
|
|
97
|
+
const a = document.createElement('a')
|
|
98
|
+
a.href = url
|
|
99
|
+
a.download = 'annotation.json'
|
|
100
|
+
a.click()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const exportMask = async () => {
|
|
105
|
+
const mask = await annotationRef.value?.exportMaskImage('png', 'black')
|
|
106
|
+
if (mask) {
|
|
107
|
+
const a = document.createElement('a')
|
|
108
|
+
a.href = mask
|
|
109
|
+
a.download = 'mask.png'
|
|
110
|
+
a.click()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.demo-container {
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 600px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.controls {
|
|
122
|
+
margin-top: 16px;
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 12px;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API 文档
|
|
130
|
+
|
|
131
|
+
### Props
|
|
132
|
+
|
|
133
|
+
| 属性名 | 类型 | 默认值 | 说明 |
|
|
134
|
+
|--------|------|--------|------|
|
|
135
|
+
| imageSource | `{ id?: string; url: string }` | 必填 | 图片源配置 |
|
|
136
|
+
| options | `Object` | `{}` | 配置选项 |
|
|
137
|
+
|
|
138
|
+
#### Options 配置
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface Options {
|
|
142
|
+
pointStyle?: Partial<PointStyle>
|
|
143
|
+
brushStyle?: Partial<BrushStyle>
|
|
144
|
+
maskExportFormat?: 'png' | 'jpg' | 'jpeg'
|
|
145
|
+
maskExportForeground?: 'black' | 'white'
|
|
146
|
+
maxUndoSteps?: number
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### PointStyle 配置
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface PointStyle {
|
|
154
|
+
circleRadius: number
|
|
155
|
+
circleFill: string
|
|
156
|
+
circleStroke: string
|
|
157
|
+
circleStrokeWidth: number
|
|
158
|
+
hoverCircleFill: string
|
|
159
|
+
hoverCircleStroke: string
|
|
160
|
+
selectedCircleFill: string
|
|
161
|
+
selectedCircleStroke: string
|
|
162
|
+
selectedCircleScale: number
|
|
163
|
+
labelBackgroundColor: string
|
|
164
|
+
labelTextColor: string
|
|
165
|
+
labelFontSize: number
|
|
166
|
+
labelPadding: number[]
|
|
167
|
+
fixedSizeOnZoom: boolean
|
|
168
|
+
fixedSizeScale: number
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### BrushStyle 配置
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
interface BrushStyle {
|
|
176
|
+
color: string
|
|
177
|
+
opacity: number
|
|
178
|
+
size: number
|
|
179
|
+
minSize: number
|
|
180
|
+
maxSize: number
|
|
181
|
+
continuity: number
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Events
|
|
186
|
+
|
|
187
|
+
| 事件名 | 参数 | 说明 |
|
|
188
|
+
|--------|------|------|
|
|
189
|
+
| pointChange | `PointAnnotation[]` | 点标注数据变化时触发 |
|
|
190
|
+
| loadStart | - | 图片开始加载时触发 |
|
|
191
|
+
| loadSuccess | - | 图片加载成功时触发 |
|
|
192
|
+
| loadError | `error` | 图片加载失败时触发 |
|
|
193
|
+
| undoStateChange | - | 撤销状态变化时触发 |
|
|
194
|
+
| redoStateChange | - | 重做状态变化时触发 |
|
|
195
|
+
|
|
196
|
+
### Methods
|
|
197
|
+
|
|
198
|
+
组件暴露以下方法:
|
|
199
|
+
|
|
200
|
+
| 方法名 | 参数 | 返回值 | 说明 |
|
|
201
|
+
|--------|------|--------|------|
|
|
202
|
+
| getPointAnnotations | - | `PointAnnotation[]` | 获取所有点标注数据 |
|
|
203
|
+
| getImageInfo | - | `Object` | 获取图片信息 |
|
|
204
|
+
| exportCanvasJSON | - | `string` | 导出完整 JSON 数据 |
|
|
205
|
+
| exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | 导出二值图 |
|
|
206
|
+
| exportCOCO | - | `string` | 导出 COCO 格式 JSON |
|
|
207
|
+
| exportYOLO | - | `{ annotations: string; classNames: string }` | 导出 YOLO 格式 |
|
|
208
|
+
| importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | 导入 JSON 数据 |
|
|
209
|
+
| loadImage | `url?` | `Promise<void>` | 加载图片 |
|
|
210
|
+
| clearBrush | - | `void` | 清除笔刷内容 |
|
|
211
|
+
| zoomIn | - | `void` | 放大画布 |
|
|
212
|
+
| zoomOut | - | `void` | 缩小画布 |
|
|
213
|
+
| resetZoom | - | `void` | 重置缩放 |
|
|
214
|
+
| undo | - | `void` | 撤销操作 |
|
|
215
|
+
| redo | - | `void` | 重做操作 |
|
|
216
|
+
| getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | 获取当前工具 |
|
|
217
|
+
| setTool | `tool` | `void` | 设置当前工具 |
|
|
218
|
+
| createPointAnnotation | `x`, `y` | `string\|null` | 创建标注点 |
|
|
219
|
+
| removePointAnnotation | `id` | `boolean` | 删除指定标注点 |
|
|
220
|
+
|
|
221
|
+
## 热键说明
|
|
222
|
+
|
|
223
|
+
| 热键 | 功能 |
|
|
224
|
+
|------|------|
|
|
225
|
+
| V | 选择工具 |
|
|
226
|
+
| P | 点标注工具 |
|
|
227
|
+
| B | 笔刷工具 |
|
|
228
|
+
| E | 擦除工具 |
|
|
229
|
+
| Ctrl + Z | 撤销 |
|
|
230
|
+
| Ctrl + Y | 重做 |
|
|
231
|
+
| Delete | 删除选中/清除所有 |
|
|
232
|
+
| Ctrl + + | 放大 |
|
|
233
|
+
| Ctrl + - | 缩小 |
|
|
234
|
+
| Ctrl + 0 | 重置缩放 |
|
|
235
|
+
| Alt | 显示/隐藏热键提示 |
|
|
236
|
+
|
|
237
|
+
## 导出格式
|
|
238
|
+
|
|
239
|
+
### JSON Full
|
|
240
|
+
|
|
241
|
+
包含完整的标注数据和笔刷 mask。
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"version": "1.0",
|
|
246
|
+
"imageUrl": "https://example.com/image.jpg",
|
|
247
|
+
"imageWidth": 1280,
|
|
248
|
+
"imageHeight": 720,
|
|
249
|
+
"pointAnnotations": [
|
|
250
|
+
{
|
|
251
|
+
"id": "point_xxx",
|
|
252
|
+
"pixel": { "x": 100, "y": 200 },
|
|
253
|
+
"normalized": { "x": 0.078, "y": 0.278 },
|
|
254
|
+
"label": "#1",
|
|
255
|
+
"createdAt": 1716960000000,
|
|
256
|
+
"updatedAt": 1716960000000
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
"brushMask": "data:image/png;base64,...",
|
|
260
|
+
"exportTime": 1716960000000
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### COCO
|
|
265
|
+
|
|
266
|
+
适用于关键点检测任务。
|
|
267
|
+
|
|
268
|
+
### YOLO
|
|
269
|
+
|
|
270
|
+
适用于 YOLO 系列模型训练。
|
|
271
|
+
|
|
272
|
+
### Mask Image
|
|
273
|
+
|
|
274
|
+
PNG/JPG 格式的二值图,前景为黑色/白色,背景透明/白色。
|
|
275
|
+
|
|
276
|
+
## 项目文档
|
|
277
|
+
|
|
278
|
+
- [需求文档](./project-docs/REQUIREMENTS.md) - 详细的功能需求说明
|
|
279
|
+
- [架构文档](./project-docs/ARCHITECTURE.md) - 系统架构设计
|
|
280
|
+
- [实现计划](./project-docs/IMPLEMENTATION_PLAN.md) - 开发任务规划
|
|
281
|
+
- [开发指南](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS 开发实战指南
|
|
282
|
+
|
|
283
|
+
## 浏览器支持
|
|
284
|
+
|
|
285
|
+
- Chrome 60+
|
|
286
|
+
- Firefox 55+
|
|
287
|
+
- Safari 12+
|
|
288
|
+
- Edge 79+
|
|
289
|
+
|
|
290
|
+
## 依赖
|
|
291
|
+
|
|
292
|
+
- Vue 3.3.0+
|
|
293
|
+
- LeaferUI 2.0.8+
|
|
294
|
+
- Tinykeys 3.0.0+
|
|
295
|
+
- @zzalai/leafer-undo-redo 1.0.3+
|
|
296
|
+
|
|
297
|
+
## 许可证
|
|
298
|
+
|
|
299
|
+
MIT License
|
|
300
|
+
|
|
301
|
+
## 贡献
|
|
302
|
+
|
|
303
|
+
欢迎提交 Issue 和 Pull Request!
|
|
304
|
+
|
|
305
|
+
## 相关项目
|
|
306
|
+
|
|
307
|
+
- [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - 多区域 ROI 标注工具
|
|
308
|
+
- [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS 撤销/重做插件
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# @zzalai/leafer-point-annotation
|
|
2
|
+
|
|
3
|
+
English | [中文](README.md)
|
|
4
|
+
|
|
5
|
+
A point annotation and brush painting tool based on Vue3 + LeaferJS, supporting COCO/YOLO/JSON export, designed for AI model training dataset annotation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 📍 **Point Annotation** - Add editable points on images
|
|
10
|
+
- 🖌️ **Brush Painting** - Freehand painting with eraser support
|
|
11
|
+
- 🎨 **Custom Styles** - Configurable brush color, size, and opacity
|
|
12
|
+
- 🔄 **Undo/Redo** - Complete history management
|
|
13
|
+
- 📤 **Multi-format Export** - JSON/COCO/YOLO/Mask Image
|
|
14
|
+
- 🔍 **Canvas Zoom** - Zoom, pan, reset support
|
|
15
|
+
- ⌨️ **Hotkeys** - V/P/B/E/Ctrl+Z/Ctrl+Y and more
|
|
16
|
+
- 📱 **Responsive Design** - Vue3 component architecture
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @zzalai/leafer-point-annotation
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### yarn
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
yarn add @zzalai/leafer-point-annotation
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### pnpm
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add @zzalai/leafer-point-annotation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<div class="demo-container">
|
|
45
|
+
<PointAnnotation
|
|
46
|
+
ref="annotationRef"
|
|
47
|
+
:imageSource="imageSource"
|
|
48
|
+
:options="options"
|
|
49
|
+
@pointChange="handlePointChange"
|
|
50
|
+
@loadSuccess="handleLoadSuccess"
|
|
51
|
+
/>
|
|
52
|
+
<div class="controls">
|
|
53
|
+
<button @click="exportJSON">Export JSON</button>
|
|
54
|
+
<button @click="exportMask">Export Mask</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { ref, computed } from 'vue'
|
|
61
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
62
|
+
|
|
63
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
64
|
+
const imageSource = computed(() => ({
|
|
65
|
+
id: 'demo-image',
|
|
66
|
+
url: 'https://example.com/image.jpg'
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
const options = ref({
|
|
70
|
+
pointStyle: {
|
|
71
|
+
circleFill: '#ff4d4f',
|
|
72
|
+
circleStroke: '#ffffff',
|
|
73
|
+
labelBackgroundColor: '#ffffff'
|
|
74
|
+
},
|
|
75
|
+
brushStyle: {
|
|
76
|
+
color: '#ff4d4f',
|
|
77
|
+
opacity: 0.55,
|
|
78
|
+
size: 100
|
|
79
|
+
},
|
|
80
|
+
maskExportFormat: 'png',
|
|
81
|
+
maskExportForeground: 'black'
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const handlePointChange = (points) => {
|
|
85
|
+
console.log('Points changed:', points)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleLoadSuccess = () => {
|
|
89
|
+
console.log('Image loaded successfully')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const exportJSON = () => {
|
|
93
|
+
const json = annotationRef.value?.exportCanvasJSON()
|
|
94
|
+
if (json) {
|
|
95
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
96
|
+
const url = URL.createObjectURL(blob)
|
|
97
|
+
const a = document.createElement('a')
|
|
98
|
+
a.href = url
|
|
99
|
+
a.download = 'annotation.json'
|
|
100
|
+
a.click()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const exportMask = async () => {
|
|
105
|
+
const mask = await annotationRef.value?.exportMaskImage('png', 'black')
|
|
106
|
+
if (mask) {
|
|
107
|
+
const a = document.createElement('a')
|
|
108
|
+
a.href = mask
|
|
109
|
+
a.download = 'mask.png'
|
|
110
|
+
a.click()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.demo-container {
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 600px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.controls {
|
|
122
|
+
margin-top: 16px;
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 12px;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API Documentation
|
|
130
|
+
|
|
131
|
+
### Props
|
|
132
|
+
|
|
133
|
+
| Property | Type | Default | Description |
|
|
134
|
+
|----------|------|---------|-------------|
|
|
135
|
+
| imageSource | `{ id?: string; url: string }` | Required | Image source configuration |
|
|
136
|
+
| options | `Object` | `{}` | Configuration options |
|
|
137
|
+
|
|
138
|
+
#### Options Configuration
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface Options {
|
|
142
|
+
pointStyle?: Partial<PointStyle>
|
|
143
|
+
brushStyle?: Partial<BrushStyle>
|
|
144
|
+
maskExportFormat?: 'png' | 'jpg' | 'jpeg'
|
|
145
|
+
maskExportForeground?: 'black' | 'white'
|
|
146
|
+
maxUndoSteps?: number
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### PointStyle Configuration
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface PointStyle {
|
|
154
|
+
circleRadius: number
|
|
155
|
+
circleFill: string
|
|
156
|
+
circleStroke: string
|
|
157
|
+
circleStrokeWidth: number
|
|
158
|
+
hoverCircleFill: string
|
|
159
|
+
hoverCircleStroke: string
|
|
160
|
+
selectedCircleFill: string
|
|
161
|
+
selectedCircleStroke: string
|
|
162
|
+
selectedCircleScale: number
|
|
163
|
+
labelBackgroundColor: string
|
|
164
|
+
labelTextColor: string
|
|
165
|
+
labelFontSize: number
|
|
166
|
+
labelPadding: number[]
|
|
167
|
+
fixedSizeOnZoom: boolean
|
|
168
|
+
fixedSizeScale: number
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### BrushStyle Configuration
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
interface BrushStyle {
|
|
176
|
+
color: string
|
|
177
|
+
opacity: number
|
|
178
|
+
size: number
|
|
179
|
+
minSize: number
|
|
180
|
+
maxSize: number
|
|
181
|
+
continuity: number
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Events
|
|
186
|
+
|
|
187
|
+
| Event Name | Parameters | Description |
|
|
188
|
+
|------------|------------|-------------|
|
|
189
|
+
| pointChange | `PointAnnotation[]` | Triggered when point annotations change |
|
|
190
|
+
| loadStart | - | Triggered when image starts loading |
|
|
191
|
+
| loadSuccess | - | Triggered when image loads successfully |
|
|
192
|
+
| loadError | `error` | Triggered when image loading fails |
|
|
193
|
+
| undoStateChange | - | Triggered when undo state changes |
|
|
194
|
+
| redoStateChange | - | Triggered when redo state changes |
|
|
195
|
+
|
|
196
|
+
### Methods
|
|
197
|
+
|
|
198
|
+
The component exposes the following methods:
|
|
199
|
+
|
|
200
|
+
| Method Name | Parameters | Return Value | Description |
|
|
201
|
+
|-------------|------------|--------------|-------------|
|
|
202
|
+
| getPointAnnotations | - | `PointAnnotation[]` | Get all point annotation data |
|
|
203
|
+
| getImageInfo | - | `Object` | Get image information |
|
|
204
|
+
| exportCanvasJSON | - | `string` | Export complete JSON data |
|
|
205
|
+
| exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | Export mask image |
|
|
206
|
+
| exportCOCO | - | `string` | Export COCO format JSON |
|
|
207
|
+
| exportYOLO | - | `{ annotations: string; classNames: string }` | Export YOLO format |
|
|
208
|
+
| importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | Import JSON data |
|
|
209
|
+
| loadImage | `url?` | `Promise<void>` | Load image |
|
|
210
|
+
| clearBrush | - | `void` | Clear brush content |
|
|
211
|
+
| zoomIn | - | `void` | Zoom in canvas |
|
|
212
|
+
| zoomOut | - | `void` | Zoom out canvas |
|
|
213
|
+
| resetZoom | - | `void` | Reset zoom |
|
|
214
|
+
| undo | - | `void` | Undo operation |
|
|
215
|
+
| redo | - | `void` | Redo operation |
|
|
216
|
+
| getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | Get current tool |
|
|
217
|
+
| setTool | `tool` | `void` | Set current tool |
|
|
218
|
+
| createPointAnnotation | `x`, `y` | `string\|null` | Create point annotation |
|
|
219
|
+
| removePointAnnotation | `id` | `boolean` | Remove specific point annotation |
|
|
220
|
+
|
|
221
|
+
## Hotkeys
|
|
222
|
+
|
|
223
|
+
| Hotkey | Function |
|
|
224
|
+
|--------|----------|
|
|
225
|
+
| V | Select tool |
|
|
226
|
+
| P | Point annotation tool |
|
|
227
|
+
| B | Brush tool |
|
|
228
|
+
| E | Eraser tool |
|
|
229
|
+
| Ctrl + Z | Undo |
|
|
230
|
+
| Ctrl + Y | Redo |
|
|
231
|
+
| Delete | Delete selected / Clear all |
|
|
232
|
+
| Ctrl + + | Zoom in |
|
|
233
|
+
| Ctrl + - | Zoom out |
|
|
234
|
+
| Ctrl + 0 | Reset zoom |
|
|
235
|
+
| Alt | Show/Hide hotkey hints |
|
|
236
|
+
|
|
237
|
+
## Export Formats
|
|
238
|
+
|
|
239
|
+
### JSON Full
|
|
240
|
+
|
|
241
|
+
Includes complete annotation data and brush mask.
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"version": "1.0",
|
|
246
|
+
"imageUrl": "https://example.com/image.jpg",
|
|
247
|
+
"imageWidth": 1280,
|
|
248
|
+
"imageHeight": 720,
|
|
249
|
+
"pointAnnotations": [
|
|
250
|
+
{
|
|
251
|
+
"id": "point_xxx",
|
|
252
|
+
"pixel": { "x": 100, "y": 200 },
|
|
253
|
+
"normalized": { "x": 0.078, "y": 0.278 },
|
|
254
|
+
"label": "#1",
|
|
255
|
+
"createdAt": 1716960000000,
|
|
256
|
+
"updatedAt": 1716960000000
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
"brushMask": "data:image/png;base64,...",
|
|
260
|
+
"exportTime": 1716960000000
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### COCO
|
|
265
|
+
|
|
266
|
+
For keypoint detection tasks.
|
|
267
|
+
|
|
268
|
+
### YOLO
|
|
269
|
+
|
|
270
|
+
For YOLO series model training.
|
|
271
|
+
|
|
272
|
+
### Mask Image
|
|
273
|
+
|
|
274
|
+
PNG/JPG format binary image, foreground in black/white, background transparent/white.
|
|
275
|
+
|
|
276
|
+
## Project Documentation
|
|
277
|
+
|
|
278
|
+
- [Requirements](./project-docs/REQUIREMENTS.md) - Detailed functional requirements
|
|
279
|
+
- [Architecture](./project-docs/ARCHITECTURE.md) - System architecture design
|
|
280
|
+
- [Implementation Plan](./project-docs/IMPLEMENTATION_PLAN.md) - Development task planning
|
|
281
|
+
- [Development Guide](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS development practical guide
|
|
282
|
+
|
|
283
|
+
## Browser Support
|
|
284
|
+
|
|
285
|
+
- Chrome 60+
|
|
286
|
+
- Firefox 55+
|
|
287
|
+
- Safari 12+
|
|
288
|
+
- Edge 79+
|
|
289
|
+
|
|
290
|
+
## Dependencies
|
|
291
|
+
|
|
292
|
+
- Vue 3.3.0+
|
|
293
|
+
- LeaferUI 2.0.8+
|
|
294
|
+
- Tinykeys 3.0.0+
|
|
295
|
+
- @zzalai/leafer-undo-redo 1.0.3+
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT License
|
|
300
|
+
|
|
301
|
+
## Contributing
|
|
302
|
+
|
|
303
|
+
Issues and Pull Requests are welcome!
|
|
304
|
+
|
|
305
|
+
## Related Projects
|
|
306
|
+
|
|
307
|
+
- [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - Multi-region ROI annotation tool
|
|
308
|
+
- [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.brush-panel-overlay[data-v-10bc9982]{z-index:1500;background:#0000004d;position:fixed;inset:0}.brush-style-panel[data-v-10bc9982]{z-index:1501;background:#fff;border-radius:10px;min-width:240px;position:fixed;overflow:visible;box-shadow:0 4px 24px #00000026}.panel-header[data-v-10bc9982]{background:var(--leafer-point-color-background-light);border-bottom:1px solid var(--leafer-point-color-border);border-radius:10px 10px 0 0;justify-content:space-between;align-items:center;padding:10px 16px;display:flex}.panel-header span[data-v-10bc9982]{color:var(--leafer-point-color-text);font-size:14px;font-weight:600}.close-btn[data-v-10bc9982]{cursor:pointer;width:24px;height:24px;color:var(--leafer-point-color-text);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:18px;transition:all .2s;display:flex}.close-btn[data-v-10bc9982]:hover{background:var(--leafer-point-color-border)}.panel-content[data-v-10bc9982]{padding:16px 16px 24px}.config-item[data-v-10bc9982]{align-items:center;margin-bottom:20px;display:flex}.config-item[data-v-10bc9982]:last-child{margin-bottom:0}.config-label[data-v-10bc9982]{color:var(--leafer-point-color-text);text-align:right;min-width:50px;padding-right:5px;font-size:12px;display:block}.config-value[data-v-10bc9982]{color:var(--leafer-point-color-text);width:30px;padding-left:5px;font-size:12px}.color-picker-wrapper[data-v-10bc9982]{width:100%;margin:-10px 0}.config-slider[data-v-10bc9982]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:3px;outline:none;width:200px;height:6px}.config-slider[data-v-10bc9982]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}.config-slider[data-v-10bc9982]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}:root{--leafer-point-color-primary:#007aff;--leafer-point-color-background:#f5f5f5;--leafer-point-color-background-light:#f0f0f0;--leafer-point-color-white:#fff;--leafer-point-color-text:#333;--leafer-point-color-text-secondary:#666;--leafer-point-color-text-tertiary:#999;--leafer-point-color-border:#ddd;--leafer-point-color-border-light:#e0e0e0;--leafer-point-color-error:#e74c3c;--leafer-point-color-button:#3498db;--leafer-point-color-button-hover:#2980b9;--leafer-point-padding-toolbar:10px;--leafer-point-padding-tool-button:8px;--leafer-point-size-tool-icon:18px;--leafer-point-size-zoom-button:36px;--leafer-point-size-zoom-value:60px;--leafer-point-font-size-hotkey:10px;--leafer-point-padding-hotkey:1px 3px;--leafer-point-padding-error:20px;--leafer-point-padding-error-button:8px 16px;--leafer-point-border-radius-tool-button:4px;--leafer-point-border-radius-hotkey:2px;--leafer-point-border-radius-overlay:8px;--leafer-point-border-radius-zoom:8px;--leafer-point-shadow-tool-button:0 2px 4px #0000001a;--leafer-point-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-point-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-point-shadow-overlay:0 4px 12px #0000001a;--leafer-point-shadow-zoom:0 2px 8px #00000026;--leafer-point-transition-time:.2s;--leafer-point-animation-gradient:2s}.point-annotation[data-v-b7ee4495]{width:100%;height:100%}.canvas-container[data-v-b7ee4495]{outline:none;width:100%;height:calc(100% - 55px);position:relative;overflow:hidden}.canvas-container[data-v-b7ee4495]:focus{outline:2px solid var(--leafer-point-color-primary);outline-offset:-2px}.loading-overlay[data-v-b7ee4495]{background-color:var(--leafer-point-color-background-light);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-b7ee4495]{animation:gradientShift-b7ee4495 var(--leafer-point-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-b7ee4495{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-b7ee4495]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:16px;font-weight:500;position:relative}.error-overlay[data-v-b7ee4495]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);padding:var(--leafer-point-padding-error);z-index:1000;flex-direction:column;justify-content:center;align-items:center;min-width:200px;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.error-overlay p[data-v-b7ee4495]{color:var(--leafer-point-color-error);margin-bottom:20px;font-size:16px}.error-overlay button[data-v-b7ee4495]{padding:var(--leafer-point-padding-error-button);background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;font-size:14px}.error-overlay button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-button-hover)}.zoom-controller[data-v-b7ee4495]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-zoom);box-shadow:var(--leafer-point-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-b7ee4495]{width:var(--leafer-point-size-zoom-button);height:var(--leafer-point-size-zoom-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.zoom-button[data-v-b7ee4495]:active{background-color:#e0e0e0}.zoom-value[data-v-b7ee4495]{min-width:var(--leafer-point-size-zoom-value);height:var(--leafer-point-size-zoom-button);line-height:var(--leafer-point-size-zoom-button);text-align:center;color:var(--leafer-point-color-text);cursor:pointer;border-left:1px solid var(--leafer-point-color-border-light);border-right:1px solid var(--leafer-point-color-border-light);transition:all var(--leafer-point-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-b7ee4495]{line-height:1}.zoom-value[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.toolbar[data-v-b7ee4495]{padding:var(--leafer-point-padding-toolbar);background-color:var(--leafer-point-color-background);border-top:1px solid var(--leafer-point-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-b7ee4495]{padding:var(--leafer-point-padding-tool-button);border-radius:var(--leafer-point-border-radius-tool-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;box-shadow:var(--leafer-point-shadow-tool-button);border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary);box-shadow:var(--leafer-point-shadow-tool-button-hover)}.tool-button[data-v-b7ee4495]:active{box-shadow:var(--leafer-point-shadow-tool-button);transform:translateY(1px)}.tool-button.active[data-v-b7ee4495]{background-color:var(--leafer-point-color-primary);color:#fff;box-shadow:var(--leafer-point-shadow-tool-button-active)}.hotkey-hint[data-v-b7ee4495]{font-size:var(--leafer-point-font-size-hotkey);color:#fff;padding:var(--leafer-point-padding-hotkey);border-radius:var(--leafer-point-border-radius-hotkey);pointer-events:none;white-space:nowrap;background-color:#0009;position:absolute;top:0;right:0}.size-control[data-v-b7ee4495]{padding:var(--leafer-point-padding-tool-button);background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-tool-button);box-shadow:var(--leafer-point-shadow-tool-button);align-items:center;gap:8px;display:flex}.size-label[data-v-b7ee4495]{color:var(--leafer-point-color-text);white-space:nowrap;font-size:12px}.size-slider[data-v-b7ee4495]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:4px;outline:none;width:120px;height:8px}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus::-webkit-slider-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-b7ee4495]::-moz-range-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-moz-range-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus::-moz-range-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus{outline:none}.size-value[data-v-b7ee4495]{text-align:center;min-width:30px;color:var(--leafer-point-color-primary);font-size:12px;font-weight:600}.app[data-v-c05a47b7]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-c05a47b7]{text-align:center;margin-bottom:30px}.editor-container[data-v-c05a47b7]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-c05a47b7]{margin-bottom:15px}label[data-v-c05a47b7]{margin-bottom:5px;font-weight:700;display:block}input[data-v-c05a47b7]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}.mask-options[data-v-c05a47b7]{gap:15px;margin-bottom:10px;display:flex}.mask-options label[data-v-c05a47b7]{align-items:center;gap:5px;font-weight:400;display:flex}.mask-options select[data-v-c05a47b7]{border:1px solid #ddd;border-radius:4px;padding:4px 8px}button[data-v-c05a47b7]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-c05a47b7]:hover{background-color:#0069d9}.output[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-c05a47b7]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;padding:20px}
|