luo-image-annotator 0.0.2 → 0.0.4
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 +160 -29
- package/dist/luo-image-annotator.css +1 -1
- package/dist/luo-image-annotator.es.js +829 -703
- package/dist/luo-image-annotator.umd.js +33 -1
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -102,13 +102,7 @@ npm publish --otp=你的恢复码字符串
|
|
|
102
102
|
npm install luo-image-annotator
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
npm install element-plus @element-plus/icons-vue
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 2. 全局引入 (推荐)
|
|
105
|
+
### 2. 引入样式与组件
|
|
112
106
|
|
|
113
107
|
在 `main.ts` 或 `main.js` 中引入样式和组件:
|
|
114
108
|
|
|
@@ -116,51 +110,89 @@ npm install element-plus @element-plus/icons-vue
|
|
|
116
110
|
import { createApp } from 'vue'
|
|
117
111
|
import App from './App.vue'
|
|
118
112
|
|
|
119
|
-
// 1.
|
|
120
|
-
import ElementPlus from 'element-plus'
|
|
121
|
-
import 'element-plus/dist/index.css'
|
|
122
|
-
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|
123
|
-
|
|
124
|
-
// 2. 引入本组件库样式
|
|
113
|
+
// 1. 引入本组件库样式
|
|
125
114
|
import 'luo-image-annotator/dist/style.css'
|
|
126
115
|
|
|
127
|
-
//
|
|
116
|
+
// 2. 引入组件
|
|
128
117
|
import { BatchAnnotator } from 'luo-image-annotator'
|
|
129
118
|
|
|
130
119
|
const app = createApp(App)
|
|
131
120
|
|
|
132
|
-
app.use(ElementPlus)
|
|
133
|
-
// 注册图标
|
|
134
|
-
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
135
|
-
app.component(key, component)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
121
|
// 全局注册 BatchAnnotator 组件
|
|
139
122
|
app.component('BatchAnnotator', BatchAnnotator)
|
|
140
123
|
|
|
141
124
|
app.mount('#app')
|
|
142
125
|
```
|
|
143
126
|
|
|
144
|
-
### 3.
|
|
127
|
+
### 3. 平台嵌入流程(推荐)
|
|
128
|
+
|
|
129
|
+
#### 第一步:平台下发任务与图片
|
|
130
|
+
|
|
131
|
+
平台侧准备以下数据并传入组件:
|
|
132
|
+
|
|
133
|
+
- `batchImages`:图片与历史标注
|
|
134
|
+
- `labels`:标签体系
|
|
135
|
+
- `session`:`projectId/taskId/userId/role`
|
|
136
|
+
- `requestId`:一次页面会话追踪 ID
|
|
137
|
+
|
|
138
|
+
#### 第二步:监听标准化事件
|
|
139
|
+
|
|
140
|
+
组件会发出两类事件:
|
|
141
|
+
|
|
142
|
+
- 兼容事件:`annotationChange`、`batchChange`、`labelChange`
|
|
143
|
+
- 标准事件:`annotation:add`、`annotation:update`、`annotation:delete`、`annotation:select`、`prediction:*`、`review:action`、`qa:issue`
|
|
144
|
+
|
|
145
|
+
平台建议仅消费标准事件做业务编排,兼容事件用于平滑迁移。
|
|
146
|
+
|
|
147
|
+
#### 第三步:自动标注闭环
|
|
148
|
+
|
|
149
|
+
1. 用户点击自动标注按钮,调用组件实例 `requestPrediction(threshold)`
|
|
150
|
+
2. 组件触发 `prediction:request` 事件
|
|
151
|
+
3. 平台调用算法服务,拿到候选结果后调用组件实例 `loadPredictionCandidates(candidates)`
|
|
152
|
+
4. 标注员筛选后调用 `applyPredictions(ids)` 或 `rejectPredictions(ids, reason)`
|
|
153
|
+
5. 平台监听 `prediction:apply` / `prediction:reject` 持久化结果
|
|
154
|
+
|
|
155
|
+
#### 第四步:审阅质检闭环
|
|
156
|
+
|
|
157
|
+
1. 审阅员调用 `reviewAction('pass'|'reject'|'revise', annotationIds, comment)`
|
|
158
|
+
2. 组件更新标注 `reviewStatus` 并触发 `review:action`
|
|
159
|
+
3. 质检员调用 `reportQaIssue(issueType, annotationIds, detail)` 触发 `qa:issue`
|
|
160
|
+
4. 平台决定状态流转与权限校验
|
|
161
|
+
|
|
162
|
+
### 4. 局部引入示例
|
|
145
163
|
|
|
146
164
|
你也可以在单个 `.vue` 文件中局部引入:
|
|
147
165
|
|
|
148
166
|
```vue
|
|
149
167
|
<template>
|
|
150
168
|
<div style="height: 100vh;">
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
:
|
|
154
|
-
|
|
169
|
+
<ImageAnnotator
|
|
170
|
+
ref="annotatorRef"
|
|
171
|
+
:batchImages="images"
|
|
172
|
+
:labels="labels"
|
|
173
|
+
:annotationTypes="['rectangle', 'polygon', 'point', 'rotatedRect']"
|
|
174
|
+
:session="session"
|
|
175
|
+
:requestId="requestId"
|
|
176
|
+
:predictionCandidates="predictionCandidates"
|
|
177
|
+
@annotation:add="onAnnotationAdd"
|
|
178
|
+
@annotation:update="onAnnotationUpdate"
|
|
179
|
+
@annotation:delete="onAnnotationDelete"
|
|
180
|
+
@prediction:request="onPredictionRequest"
|
|
181
|
+
@prediction:apply="onPredictionApply"
|
|
182
|
+
@prediction:reject="onPredictionReject"
|
|
183
|
+
@review:action="onReviewAction"
|
|
184
|
+
@qa:issue="onQaIssue"
|
|
155
185
|
/>
|
|
156
186
|
</div>
|
|
157
187
|
</template>
|
|
158
188
|
|
|
159
189
|
<script setup lang="ts">
|
|
160
190
|
import { ref } from 'vue';
|
|
161
|
-
import {
|
|
191
|
+
import { ImageAnnotator } from 'luo-image-annotator';
|
|
162
192
|
import 'luo-image-annotator/dist/style.css'; // 别忘了引入样式
|
|
163
193
|
|
|
194
|
+
const annotatorRef = ref<InstanceType<typeof ImageAnnotator> | null>(null);
|
|
195
|
+
|
|
164
196
|
// 定义标签
|
|
165
197
|
const labels = [
|
|
166
198
|
{ id: '1', name: 'Person', color: '#FF0000', visible: true },
|
|
@@ -176,9 +208,59 @@ const images = ref([
|
|
|
176
208
|
// ... 更多图片
|
|
177
209
|
]);
|
|
178
210
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
const session = {
|
|
212
|
+
projectId: 'proj-1',
|
|
213
|
+
taskId: 'task-1001',
|
|
214
|
+
userId: 'u-01',
|
|
215
|
+
role: 'annotator'
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const requestId = 'req-20260318-001';
|
|
219
|
+
const predictionCandidates = ref([]);
|
|
220
|
+
|
|
221
|
+
const onAnnotationAdd = (payload) => {
|
|
222
|
+
console.log('新增标注', payload);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const onAnnotationUpdate = (payload) => {
|
|
226
|
+
console.log('更新标注', payload);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const onAnnotationDelete = (payload) => {
|
|
230
|
+
console.log('删除标注', payload);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const onPredictionRequest = async () => {
|
|
234
|
+
const fakeCandidates = [
|
|
235
|
+
{
|
|
236
|
+
id: 'pred-1',
|
|
237
|
+
confidence: 0.92,
|
|
238
|
+
modelRunId: 'run-1',
|
|
239
|
+
annotation: {
|
|
240
|
+
id: 'a-pred-1',
|
|
241
|
+
type: 'rectangle',
|
|
242
|
+
label: 'Person',
|
|
243
|
+
coordinates: { x1: 120, y1: 80, x2: 260, y2: 320 }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
annotatorRef.value?.loadPredictionCandidates(fakeCandidates);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const onPredictionApply = (payload) => {
|
|
251
|
+
console.log('接受预测', payload);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const onPredictionReject = (payload) => {
|
|
255
|
+
console.log('拒绝预测', payload);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const onReviewAction = (payload) => {
|
|
259
|
+
console.log('审阅动作', payload);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const onQaIssue = (payload) => {
|
|
263
|
+
console.log('质检问题', payload);
|
|
182
264
|
};
|
|
183
265
|
</script>
|
|
184
266
|
```
|
|
@@ -199,6 +281,55 @@ const handleExport = (data) => {
|
|
|
199
281
|
| `export` | `data: Array` | 点击“导出”按钮时触发,返回最新的图片和标注数据 |
|
|
200
282
|
| `update:images` | `data: Array` | 当标注数据发生变化时触发,支持 `v-model:images` |
|
|
201
283
|
|
|
284
|
+
### ImageAnnotator Props(新增能力)
|
|
285
|
+
|
|
286
|
+
| 属性名 | 类型 | 必填 | 描述 |
|
|
287
|
+
| --- | --- | --- | --- |
|
|
288
|
+
| `batchImages` | `Array` | 否 | 批量图片与标注 |
|
|
289
|
+
| `image` | `Object` | 否 | 单图模式输入,结构 `{ id, url, meta }` |
|
|
290
|
+
| `labels` | `Array` | 否 | 标签定义 |
|
|
291
|
+
| `annotationTypes` | `Array` | 否 | 工具类型列表 |
|
|
292
|
+
| `predictionCandidates` | `Array` | 否 | 自动标注候选结果 |
|
|
293
|
+
| `reviewMode` | `string` | 否 | `off/reviewer/qa` |
|
|
294
|
+
| `session` | `Object` | 否 | 会话上下文 `{ projectId, taskId, userId, role }` |
|
|
295
|
+
| `requestId` | `string` | 否 | 平台链路追踪 ID |
|
|
296
|
+
|
|
297
|
+
### ImageAnnotator 标准事件
|
|
298
|
+
|
|
299
|
+
| 事件名 | 描述 |
|
|
300
|
+
| --- | --- |
|
|
301
|
+
| `ready` | 组件初始化完成 |
|
|
302
|
+
| `tool:change` | 工具切换 |
|
|
303
|
+
| `viewport:change` | 画布缩放/视口变化 |
|
|
304
|
+
| `annotation:add` | 新增标注 |
|
|
305
|
+
| `annotation:update` | 更新标注 |
|
|
306
|
+
| `annotation:delete` | 删除标注 |
|
|
307
|
+
| `annotation:select` | 选中标注 |
|
|
308
|
+
| `prediction:request` | 请求自动标注 |
|
|
309
|
+
| `prediction:loaded` | 自动标注候选已加载 |
|
|
310
|
+
| `prediction:apply` | 接受候选结果 |
|
|
311
|
+
| `prediction:reject` | 拒绝候选结果 |
|
|
312
|
+
| `review:action` | 审阅动作 |
|
|
313
|
+
| `qa:issue` | 质检问题上报 |
|
|
314
|
+
| `error` | 错误事件 |
|
|
315
|
+
|
|
316
|
+
### ImageAnnotator 实例方法
|
|
317
|
+
|
|
318
|
+
| 方法名 | 参数 | 描述 |
|
|
319
|
+
| --- | --- | --- |
|
|
320
|
+
| `jumpTo` | `(index: number)` | 批量模式跳转指定图片 |
|
|
321
|
+
| `setImage` | `(image, annotations?)` | 设置单图及标注 |
|
|
322
|
+
| `setAnnotations` | `(annotations)` | 覆盖当前标注 |
|
|
323
|
+
| `getAnnotations` | `()` | 获取当前标注 |
|
|
324
|
+
| `selectTool` | `(tool)` | 切换工具 |
|
|
325
|
+
| `requestPrediction` | `(threshold?)` | 触发自动标注请求事件 |
|
|
326
|
+
| `loadPredictionCandidates` | `(candidates)` | 加载自动标注候选 |
|
|
327
|
+
| `applyPredictions` | `(candidateIds, threshold?)` | 批量接受候选 |
|
|
328
|
+
| `rejectPredictions` | `(candidateIds, reason?)` | 批量拒绝候选 |
|
|
329
|
+
| `reviewAction` | `(action, annotationIds, comment?)` | 执行审阅动作 |
|
|
330
|
+
| `reportQaIssue` | `(issueType, annotationIds, detail?)` | 上报质检问题 |
|
|
331
|
+
| `exportAnnotations` | `(format?)` | 导出当前结果 |
|
|
332
|
+
|
|
202
333
|
## 许可证
|
|
203
334
|
|
|
204
335
|
MIT
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.annotation-container[data-v-
|
|
1
|
+
.svg-icon[data-v-3928607b]{display:inline-flex;align-items:center;justify-content:center;width:1em;height:1em;fill:currentColor;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.svg-icon[data-v-3928607b] svg{width:1em;height:1em;fill:currentColor}.size-small[data-v-3928607b]{font-size:12px}.size-large[data-v-3928607b]{font-size:20px}.annotation-container[data-v-7b013204]{display:flex;height:100%;width:100%;border:1px solid #e0e0e0;background:#f5f5f5;overflow:hidden}.left-sidebar[data-v-7b013204]{width:50px;background:#fff;border-right:1px solid #e0e0e0;display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:8px;z-index:10}.tool-btn[data-v-7b013204]{width:36px;height:36px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:18px;transition:all .2s}.tool-btn[data-v-7b013204]:hover{background:#f0f0f0}.tool-btn.active[data-v-7b013204]{background:#e3f2fd;border-color:#2196f3;color:#2196f3}.divider[data-v-7b013204]{width:80%;height:1px;background:#ddd;margin:4px 0}.center-area[data-v-7b013204]{flex:1;display:flex;flex-direction:column;position:relative;overflow:hidden}.top-bar[data-v-7b013204]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px}.label-selector[data-v-7b013204]{display:flex;align-items:center;gap:12px;width:100%;overflow-x:auto}.label-text[data-v-7b013204]{font-size:14px;font-weight:700;color:#555;white-space:nowrap}.tags-row[data-v-7b013204]{display:flex;gap:8px}.tag-chip[data-v-7b013204]{padding:4px 12px;border-radius:16px;font-size:12px;color:#fff;cursor:pointer;border:2px solid transparent;opacity:.7;transition:all .2s;white-space:nowrap}.tag-chip.active[data-v-7b013204]{opacity:1;transform:scale(1.05);box-shadow:0 2px 4px #0003}.canvas-wrapper[data-v-7b013204]{flex:1;background:#333;position:relative;overflow:hidden}.batch-nav[data-v-7b013204]{height:48px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;gap:16px}.batch-nav button[data-v-7b013204]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer}.batch-nav button[data-v-7b013204]:hover:not(:disabled){background:#e0e0e0}.right-sidebar[data-v-7b013204]{width:250px;background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;z-index:10}.sidebar-header[data-v-7b013204]{padding:16px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.sidebar-header h3[data-v-7b013204]{margin:0;font-size:16px}.add-btn[data-v-7b013204]{padding:4px 8px;background:#2196f3;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}.label-list[data-v-7b013204]{flex:1;overflow-y:auto;padding:8px}.label-item[data-v-7b013204]{margin-bottom:8px;padding:8px;background:#f9f9f9;border-radius:4px;border:1px solid #eee}.label-row[data-v-7b013204]{display:flex;align-items:center;gap:8px}.eye-icon[data-v-7b013204],.delete-icon[data-v-7b013204]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;opacity:.7}.eye-icon[data-v-7b013204]:hover,.delete-icon[data-v-7b013204]:hover{opacity:1}.color-picker[data-v-7b013204]{width:24px;height:24px;padding:0;border:none;cursor:pointer;background:none}.name-input[data-v-7b013204]{flex:1;border:1px solid transparent;background:transparent;font-size:14px;padding:2px 4px}.name-input[data-v-7b013204]:focus{border-color:#2196f3;background:#fff;outline:none}.color-wrapper[data-v-7b013204]{width:16px;height:16px;border-radius:50%;cursor:pointer;border:1px solid rgba(0,0,0,.1);flex-shrink:0}.label-name[data-v-7b013204]{flex:1;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.action-icon[data-v-7b013204]{cursor:pointer;font-size:16px;-webkit-user-select:none;user-select:none;width:24px;text-align:center;opacity:.6}.action-icon[data-v-7b013204]:hover{opacity:1}.more-actions[data-v-7b013204]{position:relative;display:flex;justify-content:center;align-items:center}.more-actions .delete-btn[data-v-7b013204]{display:none;font-size:14px}.more-actions:hover .dots[data-v-7b013204]{display:none}.more-actions:hover .delete-btn[data-v-7b013204]{display:inline-block;color:#f44336}.modal-overlay[data-v-7b013204]{position:fixed;top:0;left:0;width:100%;height:100%;background:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}.modal-content[data-v-7b013204]{background:#fff;padding:20px;border-radius:8px;width:300px;box-shadow:0 4px 12px #00000026}.modal-content h3[data-v-7b013204]{margin:0 0 16px;font-size:18px;color:#333}.form-group[data-v-7b013204]{margin-bottom:16px}.form-group label[data-v-7b013204]{display:block;margin-bottom:8px;font-size:14px;color:#666}.modal-input[data-v-7b013204]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.color-input-wrapper[data-v-7b013204]{display:flex;align-items:center;gap:8px}.modal-color-picker[data-v-7b013204]{width:40px;height:30px;padding:0;border:none;background:none;cursor:pointer}.modal-actions[data-v-7b013204]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.cancel-btn[data-v-7b013204]{padding:6px 16px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer;color:#666}.confirm-btn[data-v-7b013204]{padding:6px 16px;background:#2196f3;border:none;border-radius:4px;cursor:pointer;color:#fff}.cancel-btn[data-v-7b013204]:hover{background:#e0e0e0}.confirm-btn[data-v-7b013204]:hover{background:#1976d2}.thumbnail-wrapper[data-v-78bcbe0c]{position:relative;width:100%;height:100%;overflow:hidden;border-radius:4px;background:#f0f0f0}.thumbnail-image[data-v-78bcbe0c]{width:100%;height:100%;object-fit:cover;display:block}.annotation-overlay[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.loading-placeholder[data-v-78bcbe0c]{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#999;font-size:12px}.anno-label[data-v-78bcbe0c]{paint-order:stroke;stroke:#fff;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.batch-annotator[data-v-87f3e002]{width:100%;height:100vh;display:flex;flex-direction:column;background:#f5f5f5}.gallery-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:20px;position:relative}.gallery-header[data-v-87f3e002]{margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.gallery-header h3[data-v-87f3e002]{margin:0;font-size:20px;color:#333}.label-summary[data-v-87f3e002]{display:flex;gap:8px}.label-badge[data-v-87f3e002]{padding:4px 10px;border-radius:12px;color:#fff;font-size:12px;font-weight:700}.gallery-grid[data-v-87f3e002]{flex:1;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px;overflow-y:auto;padding-bottom:80px}.gallery-item[data-v-87f3e002]{background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px #0000001a;cursor:pointer;transition:transform .2s,box-shadow .2s;display:flex;flex-direction:column;height:240px}.gallery-item[data-v-87f3e002]:hover{transform:translateY(-4px);box-shadow:0 4px 12px #00000026}.thumbnail-wrapper[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}.img-meta[data-v-87f3e002]{padding:8px;font-size:12px;color:#666;display:flex;justify-content:space-between;background:#fff;border-top:1px solid #eee;height:32px;align-items:center}.bottom-bar[data-v-87f3e002]{position:absolute;bottom:0;left:0;width:100%;height:60px;background:#fff;border-top:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center;padding:0 40px;box-shadow:0 -2px 10px #0000000d;z-index:100}.action-btn[data-v-87f3e002]{display:flex;align-items:center;gap:8px;padding:10px 24px;border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background .2s}.action-btn.primary[data-v-87f3e002]{background:#2196f3;color:#fff}.action-btn.primary[data-v-87f3e002]:hover{background:#1976d2}.action-btn.success[data-v-87f3e002]{background:#4caf50;color:#fff}.action-btn.success[data-v-87f3e002]:hover{background:#388e3c}.editor-view[data-v-87f3e002]{flex:1;display:flex;flex-direction:column;height:100%}.editor-header[data-v-87f3e002]{height:50px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;padding:0 16px;justify-content:space-between;flex-shrink:0}.header-left[data-v-87f3e002]{display:flex;align-items:center;gap:16px}.back-btn[data-v-87f3e002]{display:flex;align-items:center;gap:4px;background:transparent;border:1px solid #ddd;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:14px;color:#666}.back-btn[data-v-87f3e002]:hover{background:#f5f5f5;color:#333}.editor-content[data-v-87f3e002]{flex:1;overflow:hidden;position:relative}
|