@zzalai/leafer-point-annotation 1.1.0 → 1.1.2
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 +502 -205
- package/README_EN.md +507 -205
- package/docs/assets/{index-Dqqq7qvI.css → index-BgRWhBwU.css} +1 -1
- package/docs/assets/index-D7LeHdrN.js +1 -0
- package/docs/index.html +2 -2
- package/package.json +8 -2
- 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 +637 -217
- package/src/elements/PointAnnotationElement.ts +115 -17
- package/src/types/index.ts +22 -5
- package/src/utils/CanvasBrush.ts +20 -0
- package/tsconfig.json +5 -1
- package/vite.config.ts +6 -1
- package/docs/assets/index-CPn8AE3g.js +0 -1
package/README_EN.md
CHANGED
|
@@ -2,19 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
A point annotation and brush painting tool based on
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- 🎨 **
|
|
12
|
-
-
|
|
13
|
-
- 📤 **Multi-format Export** - JSON/COCO/YOLO/Mask
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
5
|
+
> A point annotation and brush painting tool based on **Vue 3 + LeaferJS**, supporting COCO/YOLO/JSON/Mask export, designed for AI model training dataset annotation.
|
|
6
|
+
|
|
7
|
+
- 📍 **Point Annotation** - Draggable, editable points with auto-renumbering and hover/selected states
|
|
8
|
+
- 🖌️ **Multi-layer Brush Painting** - Painting and erasing with adjustable color, opacity, size, and continuity
|
|
9
|
+
- 🔀 **Brush Polygon from Points** - One-click generation of brush polygon area from point annotation trajectory
|
|
10
|
+
- 🖼️ **Local Image Upload** - Click to select or drag-and-drop local images, or use remote image URLs
|
|
11
|
+
- 🎨 **Complete Style Customization** - Full point and brush style configuration
|
|
12
|
+
- ⬅️ **Undo / Redo** - Complete command-based history management
|
|
13
|
+
- 📤 **Multi-format Export** - JSON / COCO / YOLO / Mask (dataURL / Blob / File)
|
|
14
|
+
- 📱 **Custom Toolbar Support** - Hide built-in toolbar and build custom UI via ref API
|
|
15
|
+
- 🔒 **Brush Disabling** - Use `enableBrush: false` to disable all brush functionality for point-only annotation
|
|
16
|
+
- ⌨️ **Rich Keyboard Shortcuts** - `v`, `p`, `b`, `e`, `Ctrl+Z`, `Ctrl+Y`, `Delete`, and more
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Props Configuration](#props-configuration)
|
|
25
|
+
- [imageSource](#imagesource)
|
|
26
|
+
- [options](#options)
|
|
27
|
+
- [currentLayer (v-model:currentLayer)](#currentlayer-v-modelcurrentlayer)
|
|
28
|
+
- [Events](#events)
|
|
29
|
+
- [Ref API (Parent Component Calls)](#ref-api-parent-component-calls)
|
|
30
|
+
- [Point Annotation](#point-annotation)
|
|
31
|
+
- [Image & Canvas](#image--canvas)
|
|
32
|
+
- [Tool Switching](#tool-switching)
|
|
33
|
+
- [Delete & Clear](#delete--clear)
|
|
34
|
+
- [Brush Layers](#brush-layers)
|
|
35
|
+
- [Brush Style](#brush-style)
|
|
36
|
+
- [Point Trajectory to Brush Area](#point-trajectory-to-brush-area)
|
|
37
|
+
- [Zoom](#zoom)
|
|
38
|
+
- [Undo / Redo](#undo--redo)
|
|
39
|
+
- [Import / Export](#import--export)
|
|
40
|
+
- [Usage Examples](#usage-examples)
|
|
41
|
+
- [Minimal Example](#minimal-example)
|
|
42
|
+
- [Full Customization (Hide Built-in Toolbar)](#full-customization-hide-built-in-toolbar)
|
|
43
|
+
- [Multi-layer Brush](#multi-layer-brush)
|
|
44
|
+
- [Backend Upload Mask (Blob/File)](#backend-upload-mask-blobfile)
|
|
45
|
+
- [Point-Only Annotation (Disable Brush)](#point-only-annotation-disable-brush)
|
|
46
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
47
|
+
- [Development & Build](#development--build)
|
|
48
|
+
- [License](#license)
|
|
49
|
+
|
|
50
|
+
---
|
|
18
51
|
|
|
19
52
|
## Installation
|
|
20
53
|
|
|
@@ -36,274 +69,543 @@ yarn add @zzalai/leafer-point-annotation
|
|
|
36
69
|
pnpm add @zzalai/leafer-point-annotation
|
|
37
70
|
```
|
|
38
71
|
|
|
39
|
-
|
|
72
|
+
> ⚠️ **Note**: `vue@^3.3.0` is a peer dependency (not automatically installed, must exist in the host project).
|
|
73
|
+
|
|
74
|
+
> ⚠️ **Important**: You must manually import the CSS:
|
|
75
|
+
> ```ts
|
|
76
|
+
> import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
77
|
+
> ```
|
|
78
|
+
|
|
79
|
+
---
|
|
40
80
|
|
|
41
|
-
|
|
81
|
+
## Quick Start
|
|
42
82
|
|
|
43
83
|
```vue
|
|
44
84
|
<template>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/>
|
|
53
|
-
<div class="controls">
|
|
54
|
-
<button @click="exportJSON">Export JSON</button>
|
|
55
|
-
<button @click="exportMask">Export Mask</button>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
85
|
+
<PointAnnotation
|
|
86
|
+
ref="annotationRef"
|
|
87
|
+
:image-source="imageSource"
|
|
88
|
+
:options="options"
|
|
89
|
+
@point-change="handlePointChange"
|
|
90
|
+
@load-success="handleLoadSuccess"
|
|
91
|
+
/>
|
|
58
92
|
</template>
|
|
59
93
|
|
|
60
94
|
<script setup lang="ts">
|
|
61
95
|
import { ref, computed } from 'vue'
|
|
62
96
|
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
63
97
|
|
|
98
|
+
// 👇 Must manually import styles
|
|
99
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
100
|
+
|
|
64
101
|
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
102
|
+
|
|
103
|
+
// Method 1: Remote image
|
|
65
104
|
const imageSource = computed(() => ({
|
|
66
|
-
|
|
67
|
-
url: 'https://example.com/image.jpg'
|
|
105
|
+
url: 'https://example.com/sample.jpg'
|
|
68
106
|
}))
|
|
69
107
|
|
|
70
|
-
|
|
108
|
+
// Method 2: Don't pass imageSource - user can upload locally
|
|
109
|
+
// const imageSource = null
|
|
110
|
+
|
|
111
|
+
const options = {
|
|
112
|
+
enableBrush: true,
|
|
71
113
|
pointStyle: {
|
|
72
114
|
circleFill: '#ff4d4f',
|
|
73
|
-
circleStroke: '#ffffff'
|
|
74
|
-
labelBackgroundColor: '#ffffff'
|
|
115
|
+
circleStroke: '#ffffff'
|
|
75
116
|
},
|
|
76
117
|
brushStyle: {
|
|
77
|
-
color: '#
|
|
118
|
+
color: '#1890ff',
|
|
78
119
|
opacity: 0.55,
|
|
79
120
|
size: 100
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
maskExportForeground: 'black'
|
|
83
|
-
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
84
123
|
|
|
85
|
-
|
|
124
|
+
function handlePointChange(points: any[]) {
|
|
86
125
|
console.log('Points changed:', points)
|
|
87
126
|
}
|
|
88
127
|
|
|
89
|
-
|
|
90
|
-
console.log('Image loaded successfully')
|
|
128
|
+
function handleLoadSuccess(info: any) {
|
|
129
|
+
console.log('Image loaded successfully:', info)
|
|
91
130
|
}
|
|
131
|
+
</script>
|
|
132
|
+
```
|
|
92
133
|
|
|
93
|
-
|
|
94
|
-
const json = annotationRef.value?.exportCanvasJSON()
|
|
95
|
-
if (json) {
|
|
96
|
-
const blob = new Blob([json], { type: 'application/json' })
|
|
97
|
-
const url = URL.createObjectURL(blob)
|
|
98
|
-
const a = document.createElement('a')
|
|
99
|
-
a.href = url
|
|
100
|
-
a.download = 'annotation.json'
|
|
101
|
-
a.click()
|
|
102
|
-
}
|
|
103
|
-
}
|
|
134
|
+
---
|
|
104
135
|
|
|
105
|
-
|
|
106
|
-
const mask = await annotationRef.value?.exportMaskImage('png', 'black')
|
|
107
|
-
if (mask) {
|
|
108
|
-
const a = document.createElement('a')
|
|
109
|
-
a.href = mask
|
|
110
|
-
a.download = 'mask.png'
|
|
111
|
-
a.click()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
</script>
|
|
136
|
+
## Props Configuration
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
.demo-container {
|
|
118
|
-
width: 100%;
|
|
119
|
-
height: 600px;
|
|
120
|
-
}
|
|
138
|
+
### imageSource
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```
|
|
140
|
+
| Field | Type | Description |
|
|
141
|
+
|-------|------|-------------|
|
|
142
|
+
| `url` | `string` | Image URL (remote URL or dataURL) |
|
|
143
|
+
| `id` | `string` | Optional, business identifier |
|
|
144
|
+
|
|
145
|
+
> When `imageSource` is not provided, the component displays a large upload area supporting **click to select** and **drag-and-drop** file loading.
|
|
129
146
|
|
|
130
|
-
|
|
147
|
+
### options
|
|
131
148
|
|
|
132
|
-
|
|
149
|
+
Complete `OptionsSource` interface:
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
```ts
|
|
152
|
+
interface OptionsSource {
|
|
153
|
+
// ============ Feature Toggles ============
|
|
154
|
+
enableBrush?: boolean // Whether to enable brush (default: true);
|
|
155
|
+
// When false, brush buttons/panel are not rendered,
|
|
156
|
+
// and brushTool/eraserTool/mask export methods are disabled
|
|
138
157
|
|
|
139
|
-
|
|
158
|
+
// ============ UI Toggles ============
|
|
159
|
+
showToolbar?: boolean // Whether to show built-in toolbar (default: true)
|
|
160
|
+
showZoomController?: boolean // Whether to show built-in zoom controller (default: true)
|
|
161
|
+
canvasBackground?: string // Canvas background color (default: '#f6f6f6')
|
|
140
162
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
// ============ Zoom ============
|
|
164
|
+
zoomMin?: number // Minimum zoom ratio (default: 0.2)
|
|
165
|
+
zoomMax?: number // Maximum zoom ratio (default: 4)
|
|
166
|
+
|
|
167
|
+
// ============ Point Annotation ============
|
|
168
|
+
pointStyle?: Partial<PointStyle> // Point annotation style (overrides defaults)
|
|
169
|
+
maxPoints?: number // Maximum number of points (optional)
|
|
170
|
+
|
|
171
|
+
// ============ Brush ============
|
|
172
|
+
brushStyle?: Partial<BrushStyle> // Brush style (overrides defaults)
|
|
173
|
+
brushLayers?: BrushLayerConfig[] // Brush layer configuration (defaults to single layer if not provided)
|
|
174
|
+
maxBrushLayers?: number // Maximum number of layers (optional)
|
|
175
|
+
|
|
176
|
+
// ============ History ============
|
|
177
|
+
maxUndoSteps?: number // Maximum undo steps (default: 100)
|
|
178
|
+
|
|
179
|
+
// ============ Mask Export ============
|
|
180
|
+
maskExportFormat?: 'png' | 'jpeg' | 'jpg' // Default mask export format (default: png)
|
|
181
|
+
maskExportForeground?: 'black' | 'white' // Default mask foreground color (default: black)
|
|
148
182
|
}
|
|
149
183
|
```
|
|
150
184
|
|
|
151
|
-
#### PointStyle
|
|
185
|
+
#### PointStyle (Point Annotation Style)
|
|
152
186
|
|
|
153
|
-
```
|
|
187
|
+
```ts
|
|
154
188
|
interface PointStyle {
|
|
155
189
|
circleRadius: number
|
|
156
190
|
circleFill: string
|
|
157
191
|
circleStroke: string
|
|
158
192
|
circleStrokeWidth: number
|
|
193
|
+
|
|
194
|
+
// hover
|
|
159
195
|
hoverCircleFill: string
|
|
160
196
|
hoverCircleStroke: string
|
|
197
|
+
|
|
198
|
+
// selected
|
|
161
199
|
selectedCircleFill: string
|
|
162
200
|
selectedCircleStroke: string
|
|
163
201
|
selectedCircleScale: number
|
|
202
|
+
|
|
203
|
+
// text
|
|
204
|
+
circleTextFontSize: number
|
|
205
|
+
circleTextFontFamily: string
|
|
206
|
+
circleTextFill: string
|
|
207
|
+
|
|
208
|
+
// label
|
|
164
209
|
labelBackgroundColor: string
|
|
165
210
|
labelTextColor: string
|
|
166
211
|
labelFontSize: number
|
|
167
|
-
labelPadding: number[]
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
labelPadding: number | number[]
|
|
213
|
+
|
|
214
|
+
// fixed size
|
|
215
|
+
fixedSizeOnZoom?: boolean // When enabled, points don't grow with canvas zoom
|
|
216
|
+
fixedSizeScale?: number // Fixed size scale factor
|
|
170
217
|
}
|
|
171
218
|
```
|
|
172
219
|
|
|
173
|
-
|
|
220
|
+
Refer to [`src/types/index.ts`](src/types/index.ts) for default values.
|
|
221
|
+
|
|
222
|
+
#### BrushStyle (Brush Style)
|
|
174
223
|
|
|
175
|
-
```
|
|
224
|
+
```ts
|
|
176
225
|
interface BrushStyle {
|
|
177
|
-
color: string
|
|
178
|
-
opacity: number
|
|
179
|
-
size: number
|
|
180
|
-
minSize: number
|
|
181
|
-
maxSize: number
|
|
182
|
-
continuity: number
|
|
226
|
+
color: string // Brush color (hex)
|
|
227
|
+
opacity: number // Opacity 0~1 (controlled via Group.opacity)
|
|
228
|
+
size: number // Brush size (pixels)
|
|
229
|
+
minSize: number // Slider minimum
|
|
230
|
+
maxSize: number // Slider maximum
|
|
231
|
+
continuity: number // Maximum distance threshold for continuous strokes
|
|
183
232
|
}
|
|
184
233
|
```
|
|
185
234
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
| redoStateChange | - | Triggered when redo state changes |
|
|
196
|
-
|
|
197
|
-
### Methods
|
|
198
|
-
|
|
199
|
-
The component exposes the following methods:
|
|
200
|
-
|
|
201
|
-
| Method Name | Parameters | Return Value | Description |
|
|
202
|
-
|-------------|------------|--------------|-------------|
|
|
203
|
-
| getPointAnnotations | - | `PointAnnotation[]` | Get all point annotation data |
|
|
204
|
-
| getImageInfo | - | `Object` | Get image information |
|
|
205
|
-
| exportCanvasJSON | - | `string` | Export complete JSON data |
|
|
206
|
-
| exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | Export mask image |
|
|
207
|
-
| exportCOCO | - | `string` | Export COCO format JSON |
|
|
208
|
-
| exportYOLO | - | `{ annotations: string; classNames: string }` | Export YOLO format |
|
|
209
|
-
| importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | Import JSON data |
|
|
210
|
-
| loadImage | `url?` | `Promise<void>` | Load image |
|
|
211
|
-
| clearBrush | - | `void` | Clear brush content |
|
|
212
|
-
| zoomIn | - | `void` | Zoom in canvas |
|
|
213
|
-
| zoomOut | - | `void` | Zoom out canvas |
|
|
214
|
-
| resetZoom | - | `void` | Reset zoom |
|
|
215
|
-
| undo | - | `void` | Undo operation |
|
|
216
|
-
| redo | - | `void` | Redo operation |
|
|
217
|
-
| getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | Get current tool |
|
|
218
|
-
| setTool | `tool` | `void` | Set current tool |
|
|
219
|
-
| createPointAnnotation | `x`, `y` | `string\|null` | Create point annotation |
|
|
220
|
-
| removePointAnnotation | `id` | `boolean` | Remove specific point annotation |
|
|
221
|
-
|
|
222
|
-
## Hotkeys
|
|
223
|
-
|
|
224
|
-
| Hotkey | Function |
|
|
225
|
-
|--------|----------|
|
|
226
|
-
| V | Select tool |
|
|
227
|
-
| P | Point annotation tool |
|
|
228
|
-
| B | Brush tool |
|
|
229
|
-
| E | Eraser tool |
|
|
230
|
-
| Ctrl + Z | Undo |
|
|
231
|
-
| Ctrl + Y | Redo |
|
|
232
|
-
| Delete | Delete selected / Clear all |
|
|
233
|
-
| Ctrl + + | Zoom in |
|
|
234
|
-
| Ctrl + - | Zoom out |
|
|
235
|
-
| Ctrl + 0 | Reset zoom |
|
|
236
|
-
| Alt | Show/Hide hotkey hints |
|
|
237
|
-
|
|
238
|
-
## Export Formats
|
|
239
|
-
|
|
240
|
-
### JSON Full
|
|
241
|
-
|
|
242
|
-
Includes complete annotation data and brush mask.
|
|
243
|
-
|
|
244
|
-
```json
|
|
245
|
-
{
|
|
246
|
-
"version": "1.0",
|
|
247
|
-
"imageUrl": "https://example.com/image.jpg",
|
|
248
|
-
"imageWidth": 1280,
|
|
249
|
-
"imageHeight": 720,
|
|
250
|
-
"pointAnnotations": [
|
|
251
|
-
{
|
|
252
|
-
"id": "point_xxx",
|
|
253
|
-
"pixel": { "x": 100, "y": 200 },
|
|
254
|
-
"normalized": { "x": 0.078, "y": 0.278 },
|
|
255
|
-
"label": "#1",
|
|
256
|
-
"createdAt": 1716960000000,
|
|
257
|
-
"updatedAt": 1716960000000
|
|
258
|
-
}
|
|
259
|
-
],
|
|
260
|
-
"brushMask": "data:image/png;base64,...",
|
|
261
|
-
"exportTime": 1716960000000
|
|
235
|
+
#### BrushLayerConfig (Multi-layer Configuration)
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
interface BrushLayerConfig {
|
|
239
|
+
label: string // Layer display name
|
|
240
|
+
value: string // Layer unique identifier
|
|
241
|
+
color?: string // Default color for this layer
|
|
242
|
+
opacity?: number // Default opacity for this layer
|
|
243
|
+
size?: number // Default brush size for this layer
|
|
262
244
|
}
|
|
263
245
|
```
|
|
264
246
|
|
|
265
|
-
|
|
247
|
+
When `brushLayers` is not provided, defaults to a single layer `{label:'Default Layer', value:'default'}`.
|
|
248
|
+
|
|
249
|
+
### currentLayer (v-model:currentLayer)
|
|
250
|
+
|
|
251
|
+
Controlled layer switching. For example:
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<PointAnnotation
|
|
255
|
+
:options="{ brushLayers: [
|
|
256
|
+
{ label: 'Foreground', value: 'foreground' },
|
|
257
|
+
{ label: 'Occlusion', value: 'occlusion' }
|
|
258
|
+
]}"
|
|
259
|
+
v-model:current-layer="activeLayer"
|
|
260
|
+
/>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Events
|
|
266
|
+
|
|
267
|
+
| Event | Parameters | Triggered When |
|
|
268
|
+
|-------|-----------|----------------|
|
|
269
|
+
| `point-change` | `(points: PointAnnotation[])` | Point added / deleted / modified / renumbered |
|
|
270
|
+
| `load-start` | - | Image starts loading |
|
|
271
|
+
| `load-success` | `{ url, width, height }` | Image loads successfully |
|
|
272
|
+
| `load-error` | `{ error }` | Image loading fails |
|
|
273
|
+
| `undo-state-change` | `{ canUndo }` | Undo stack state changes |
|
|
274
|
+
| `redo-state-change` | `{ canRedo }` | Redo stack state changes |
|
|
275
|
+
| `update:currentLayer` | `layerValue` | Current brush layer changes (use with v-model) |
|
|
276
|
+
| `layer-change` | `layerValue` | Same as update:currentLayer |
|
|
266
277
|
|
|
267
|
-
|
|
278
|
+
---
|
|
268
279
|
|
|
269
|
-
|
|
280
|
+
## Ref API (Parent Component Calls)
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
After accessing the component instance via `ref`, you can call the following methods.
|
|
272
283
|
|
|
273
|
-
|
|
284
|
+
```ts
|
|
285
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
274
286
|
|
|
275
|
-
|
|
287
|
+
// Example:
|
|
288
|
+
annotationRef.value?.pointTool() // Switch to point annotation tool
|
|
289
|
+
annotationRef.value?.createBrushFromPoints() // Generate brush area from point trajectory
|
|
290
|
+
annotationRef.value?.getMaskBlob() // Export current layer as Blob (for backend upload)
|
|
291
|
+
```
|
|
276
292
|
|
|
277
|
-
|
|
293
|
+
### Point Annotation
|
|
278
294
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
| Method | Description |
|
|
296
|
+
|--------|-------------|
|
|
297
|
+
| `getPointAnnotations(): PointAnnotation[]` | Get all current points |
|
|
298
|
+
| `createPointAnnotation(x: number, y: number, label?: string): boolean` | Programmatically add a point |
|
|
299
|
+
| `removePointAnnotation(id: string): boolean` | Programmatically delete a point |
|
|
300
|
+
| `updatePointAnnotationLabel(id: string, label: string): boolean` | Modify a point's label text |
|
|
283
301
|
|
|
284
|
-
|
|
302
|
+
### Image & Canvas
|
|
285
303
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
304
|
+
| Method | Description |
|
|
305
|
+
|--------|-------------|
|
|
306
|
+
| `getImageInfo()` | `{ url, width, height }` |
|
|
307
|
+
| `loadImage(url: string)` | Dynamically load a new image |
|
|
290
308
|
|
|
291
|
-
|
|
309
|
+
### Tool Switching
|
|
292
310
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
-
|
|
296
|
-
|
|
311
|
+
| Method | Description |
|
|
312
|
+
|--------|-------------|
|
|
313
|
+
| `getCurrentTool(): 'select' \| 'point' \| 'brush' \| 'eraser'` | - |
|
|
314
|
+
| `setTool(tool)` | Switch to specified tool (brush/eraser blocked when `enableBrush=false`) |
|
|
315
|
+
| `selectTool()` | Select tool |
|
|
316
|
+
| `pointTool()` | Point annotation tool |
|
|
317
|
+
| `brushTool(openPanel?: boolean)` | Brush tool |
|
|
318
|
+
| `eraserTool()` | Eraser tool |
|
|
297
319
|
|
|
298
|
-
|
|
320
|
+
### Delete & Clear
|
|
321
|
+
|
|
322
|
+
| Method | Description |
|
|
323
|
+
|--------|-------------|
|
|
324
|
+
| `deleteSelected()` | Delete currently selected point (with confirm dialog) |
|
|
325
|
+
| `clearAllAnnotationsAndBrush()` | Clear all points + brush (with confirm dialog) |
|
|
326
|
+
| `clearBrush()` | Clear current layer's brush |
|
|
327
|
+
| `clearAllBrushLayers()` | Clear all layers' brush |
|
|
328
|
+
|
|
329
|
+
### Brush Layers
|
|
330
|
+
|
|
331
|
+
| Method | Description |
|
|
332
|
+
|--------|-------------|
|
|
333
|
+
| `getCurrentLayer(): string` | Current active layer value |
|
|
334
|
+
| `setActiveLayer(value: string): boolean` | Switch to specified layer |
|
|
335
|
+
| `getAllLayers(): BrushLayerConfig[]` | All layer configurations |
|
|
336
|
+
|
|
337
|
+
### Brush Style
|
|
338
|
+
|
|
339
|
+
| Method | Description |
|
|
340
|
+
|--------|-------------|
|
|
341
|
+
| `getBrushStyle(): BrushStyle` | Returns copy of current style |
|
|
342
|
+
| `updateBrushStyle(partial: Partial<BrushStyle>): void` | Dynamically update (e.g., color/opacity/size) |
|
|
343
|
+
|
|
344
|
+
### Point Trajectory to Brush Area
|
|
345
|
+
|
|
346
|
+
| Method | Description |
|
|
347
|
+
|--------|-------------|
|
|
348
|
+
| `createBrushFromPoints(): boolean` | Connects pixel coordinates of points in `sequenceNumber` order to form closed polygon, fills with current brush style; no action when point count < 3 |
|
|
349
|
+
|
|
350
|
+
### Zoom
|
|
351
|
+
|
|
352
|
+
| Method | Description |
|
|
353
|
+
|--------|-------------|
|
|
354
|
+
| `zoomIn()` | Zoom in |
|
|
355
|
+
| `zoomOut()` | Zoom out |
|
|
356
|
+
| `resetZoom()` | Reset to 100% |
|
|
357
|
+
|
|
358
|
+
### Undo / Redo
|
|
359
|
+
|
|
360
|
+
| Method | Description |
|
|
361
|
+
|--------|-------------|
|
|
362
|
+
| `undo()` | Undo last action |
|
|
363
|
+
| `redo()` | Redo last action |
|
|
299
364
|
|
|
300
|
-
|
|
365
|
+
### Import / Export
|
|
301
366
|
|
|
302
|
-
|
|
367
|
+
| Method | Description |
|
|
368
|
+
|--------|-------------|
|
|
369
|
+
| `exportCanvasJSON(): string` | Full export (points + brush snapshot + image info) |
|
|
370
|
+
| `importCanvasJSON(data: string \| object): boolean` | Restore canvas from exported JSON |
|
|
371
|
+
| `exportMaskImage(format?, fg?)`: `Promise<string \| null>` | Current layer mask (dataURL) |
|
|
372
|
+
| `exportMaskImageByLayer(layerValue, format?, fg?)`: `Promise<string \| null>` | Specified layer mask |
|
|
373
|
+
| `exportAllMaskImages(format?, fg?)`: `Promise<Record<string, string>>` | All layer masks |
|
|
374
|
+
| `getMaskBlob(layerValue?, format?, fg?)`: `Promise<Blob \| null>` | Current/specified layer Blob (for backend upload) |
|
|
375
|
+
| `getMaskFile(layerValue?, filename?, format?, fg?)`: `Promise<File \| null>` | Current/specified layer File |
|
|
376
|
+
| `getAllMaskBlobs(format?, fg?)`: `Promise<Record<string, Blob>>` | All layer Blob collection |
|
|
377
|
+
| `exportCOCO(): string` | Export COCO JSON (points = keypoints) |
|
|
378
|
+
| `exportYOLO(): string` | Export YOLO annotations |
|
|
303
379
|
|
|
304
|
-
|
|
380
|
+
> Parameter notes: `format` = `'png' \| 'jpeg' \| 'jpg'`; `fg` = `'black' \| 'white'` (mask foreground color).
|
|
381
|
+
> Note: All Mask/Blob/File related methods require browser environment, and return null/{} when `enableBrush=false`.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Usage Examples
|
|
386
|
+
|
|
387
|
+
### Minimal Example
|
|
388
|
+
|
|
389
|
+
```vue
|
|
390
|
+
<template>
|
|
391
|
+
<PointAnnotation
|
|
392
|
+
ref="annotationRef"
|
|
393
|
+
:image-source="{ url: 'https://example.com/image.jpg' }"
|
|
394
|
+
:options="{ enableBrush: false }"
|
|
395
|
+
/>
|
|
396
|
+
</template>
|
|
397
|
+
|
|
398
|
+
<script setup lang="ts">
|
|
399
|
+
import { ref } from 'vue'
|
|
400
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
401
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
402
|
+
|
|
403
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
404
|
+
</script>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Full Customization (Hide Built-in Toolbar)
|
|
408
|
+
|
|
409
|
+
```vue
|
|
410
|
+
<template>
|
|
411
|
+
<div class="my-toolbar">
|
|
412
|
+
<button @click="() => annotationRef.value?.pointTool()">Point</button>
|
|
413
|
+
<button @click="() => annotationRef.value?.brushTool()">Brush</button>
|
|
414
|
+
<button @click="() => annotationRef.value?.eraserTool()">Eraser</button>
|
|
415
|
+
<button @click="() => annotationRef.value?.deleteSelected()">Delete</button>
|
|
416
|
+
<button @click="() => annotationRef.value?.clearAllAnnotationsAndBrush()">Clear All</button>
|
|
417
|
+
<button @click="() => annotationRef.value?.undo()">Undo</button>
|
|
418
|
+
<button @click="() => annotationRef.value?.redo()">Redo</button>
|
|
419
|
+
<button @click="() => annotationRef.value?.createBrushFromPoints()">Points→Polygon</button>
|
|
420
|
+
<button @click="uploadMask">Upload Mask</button>
|
|
421
|
+
</div>
|
|
305
422
|
|
|
306
|
-
|
|
423
|
+
<PointAnnotation
|
|
424
|
+
ref="annotationRef"
|
|
425
|
+
:image-source="{ url: 'https://example.com/image.jpg' }"
|
|
426
|
+
:options="{ showToolbar: false, showZoomController: false }"
|
|
427
|
+
@point-change="handlePoints"
|
|
428
|
+
/>
|
|
429
|
+
</template>
|
|
430
|
+
|
|
431
|
+
<script setup lang="ts">
|
|
432
|
+
import { ref } from 'vue'
|
|
433
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
434
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
435
|
+
|
|
436
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
437
|
+
|
|
438
|
+
function handlePoints(points: any[]) {
|
|
439
|
+
console.log('points:', points)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function uploadMask() {
|
|
443
|
+
const blob = await annotationRef.value?.getMaskBlob()
|
|
444
|
+
if (!blob) return
|
|
445
|
+
const fd = new FormData()
|
|
446
|
+
fd.append('file', blob, 'mask.png')
|
|
447
|
+
await fetch('/api/upload', { method: 'POST', body: fd })
|
|
448
|
+
}
|
|
449
|
+
</script>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Multi-layer Brush
|
|
453
|
+
|
|
454
|
+
```vue
|
|
455
|
+
<template>
|
|
456
|
+
<PointAnnotation
|
|
457
|
+
ref="annotationRef"
|
|
458
|
+
:image-source="{ url: 'https://example.com/image.jpg' }"
|
|
459
|
+
:options="{
|
|
460
|
+
brushLayers: [
|
|
461
|
+
{ label: 'Foreground', value: 'foreground', color: '#1890ff', opacity: 0.55 },
|
|
462
|
+
{ label: 'Occlusion', value: 'occlusion', color: '#faad14', opacity: 0.55 },
|
|
463
|
+
{ label: 'Background', value: 'background', color: '#52c41a', opacity: 0.55 }
|
|
464
|
+
]
|
|
465
|
+
}"
|
|
466
|
+
v-model:current-layer="activeLayer"
|
|
467
|
+
/>
|
|
468
|
+
</template>
|
|
469
|
+
|
|
470
|
+
<script setup lang="ts">
|
|
471
|
+
import { ref } from 'vue'
|
|
472
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
473
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
474
|
+
|
|
475
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
476
|
+
const activeLayer = ref('foreground')
|
|
477
|
+
</script>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Backend Upload Mask (Blob/File)
|
|
481
|
+
|
|
482
|
+
```vue
|
|
483
|
+
<template>
|
|
484
|
+
<PointAnnotation ref="annotationRef" :image-source="{ url: '...' }" />
|
|
485
|
+
<button @click="uploadAllMasks">Upload All Layer Masks</button>
|
|
486
|
+
</template>
|
|
487
|
+
|
|
488
|
+
<script setup lang="ts">
|
|
489
|
+
import { ref } from 'vue'
|
|
490
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
491
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
492
|
+
|
|
493
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
494
|
+
|
|
495
|
+
async function uploadAllMasks() {
|
|
496
|
+
const blobs = await annotationRef.value?.getAllMaskBlobs('png', 'black')
|
|
497
|
+
if (!blobs) return
|
|
498
|
+
for (const [layerValue, blob] of Object.entries(blobs)) {
|
|
499
|
+
const fd = new FormData()
|
|
500
|
+
fd.append('file', blob, `${layerValue}.png`)
|
|
501
|
+
await fetch('/api/mask-upload', { method: 'POST', body: fd })
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
</script>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Point-Only Annotation (Disable Brush)
|
|
508
|
+
|
|
509
|
+
```vue
|
|
510
|
+
<template>
|
|
511
|
+
<PointAnnotation
|
|
512
|
+
ref="annotationRef"
|
|
513
|
+
:image-source="{ url: '...' }"
|
|
514
|
+
:options="{ enableBrush: false }"
|
|
515
|
+
@point-change="handlePoints"
|
|
516
|
+
/>
|
|
517
|
+
</template>
|
|
518
|
+
|
|
519
|
+
<script setup lang="ts">
|
|
520
|
+
import { ref } from 'vue'
|
|
521
|
+
import { PointAnnotation } from '@zzalai/leafer-point-annotation'
|
|
522
|
+
import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
|
|
523
|
+
|
|
524
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
525
|
+
function handlePoints(points: any[]) {
|
|
526
|
+
console.log('Points:', points)
|
|
527
|
+
}
|
|
528
|
+
</script>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Keyboard Shortcuts
|
|
534
|
+
|
|
535
|
+
> Effective when: **Canvas has focus** or **mouse hovers over canvas**
|
|
536
|
+
|
|
537
|
+
| Key | Function | Restriction |
|
|
538
|
+
|-----|----------|-------------|
|
|
539
|
+
| `v` | Select tool | - |
|
|
540
|
+
| `p` | Point annotation tool | - |
|
|
541
|
+
| `b` | Brush tool | Requires `enableBrush=true` |
|
|
542
|
+
| `e` | Eraser tool | Requires `enableBrush=true` |
|
|
543
|
+
| `Ctrl + Z` | Undo | - |
|
|
544
|
+
| `Ctrl + Y` | Redo | - |
|
|
545
|
+
| `Delete` | Delete selected point | - |
|
|
546
|
+
| `Ctrl + +` | Zoom in | - |
|
|
547
|
+
| `Ctrl + -` | Zoom out | - |
|
|
548
|
+
| `Ctrl + 0` | Reset zoom | - |
|
|
549
|
+
| `Alt` | Show/Hide shortcut hint overlay | - |
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Development & Build
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
# Install dependencies
|
|
557
|
+
pnpm install
|
|
558
|
+
|
|
559
|
+
# Local development (App.vue as demo entry)
|
|
560
|
+
pnpm dev
|
|
561
|
+
|
|
562
|
+
# Build library output (dist/)
|
|
563
|
+
pnpm build
|
|
564
|
+
|
|
565
|
+
# Build demo site (docs/)
|
|
566
|
+
pnpm docs:build
|
|
567
|
+
|
|
568
|
+
# Build library + demo site simultaneously
|
|
569
|
+
pnpm build:all
|
|
570
|
+
|
|
571
|
+
# Type check
|
|
572
|
+
pnpm tsc --noEmit
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Project Structure
|
|
576
|
+
|
|
577
|
+
```
|
|
578
|
+
src/
|
|
579
|
+
├── components/
|
|
580
|
+
│ ├── PointAnnotation.vue # Core main component (integrates all capabilities)
|
|
581
|
+
│ ├── BrushSizeSlider.vue # Brush size slider
|
|
582
|
+
│ └── BrushStylePanel.vue # Brush style configuration panel
|
|
583
|
+
├── elements/
|
|
584
|
+
│ └── PointAnnotationElement.ts # Custom point element (Group + Ellipse + Text)
|
|
585
|
+
├── utils/
|
|
586
|
+
│ ├── CanvasBrush.ts # Brush core (canvas + drawing snapshot)
|
|
587
|
+
│ ├── BrushCommands.ts # Brush undo commands
|
|
588
|
+
│ ├── PointCommands.ts # Point undo commands
|
|
589
|
+
│ ├── BrushStroke.ts # Brush stroke data
|
|
590
|
+
│ ├── COCOExporter.ts # COCO export
|
|
591
|
+
│ └── YOLOExporter.ts # YOLO export
|
|
592
|
+
├── types/
|
|
593
|
+
│ └── index.ts # All public types and default values
|
|
594
|
+
├── App.vue # Dev demo page
|
|
595
|
+
├── index.ts # Public export
|
|
596
|
+
└── main.ts # Dev entry
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Release Process
|
|
600
|
+
|
|
601
|
+
1. `pnpm install` → `pnpm build:all`
|
|
602
|
+
2. Confirm `dist/` and `docs/` are up to date
|
|
603
|
+
3. Update `version` in `package.json`
|
|
604
|
+
4. `npm publish`
|
|
605
|
+
5. `git push` to GitHub (triggers Pages redeployment)
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## License
|
|
307
610
|
|
|
308
|
-
|
|
309
|
-
- [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
|
|
611
|
+
MIT © zzalai
|