@microclear/label-image
v0.1.1
Published
Pure frontend JS library with Vue 3 demo and Vite build.
Readme
ImageLabel
一个纯前端标注引擎,提供:
- 基础标注能力:
dot/line/polygon/circle/ellipse - 编辑能力:选中、拖点、拖整图形、插点、删点、删除、清空
- 批量删点能力:
line/polygon支持单击切换端点、框选端点、Enter 确认删除、Esc 退出回滚 - 视口能力:缩放、平移、resize 后保持标注相对图片位置不变
- 历史能力:撤销/重做(双栈模型)
- 数据能力:按图片归一化坐标导入/导出
- 可见性能力:单个/批量/全量显示隐藏(默认不直接写入 history)
项目同时包含一个 Vue 3 Demo 页面(src/demo),用于联调和功能验证。
当前状态(TS Migration)
- 库核心与特性模块已迁移为 TypeScript(含
core/model/features)。 - Demo 已启用 TS:
src/demo/App.vue使用<script setup lang="ts">src/demo/main.ts为入口文件
- 类型声明通过
vite-plugin-dts生成,类型入口为dist/lib/types/index.d.ts。
环境与命令
安装
npm install本地开发
npm run dev构建
npm run build:lib
npm run build:demo
npm run build类型检查与类型覆盖率
npm run typecheck
npm run type-coverage
npm run type-coverage:check代码格式化
npm run prettier
npm run prettier:check目录结构
src/
lib/
image-label/ # 标注引擎核心实现
demo/ # Vue 3 Demo
dist/ # 构建产物
vite.lib.config.ts
vite.demo.config.ts快速开始
import { createImageLabel, EVENTS } from '@microclear/label-image';
const image = new Image();
image.src = '/demo.jpg';
await image.decode();
const engine = createImageLabel({
layout: {
container: document.getElementById('host'),
width: 960,
height: 560,
},
image: {
image, // 必填
},
viewport: {
viewportHotkey: 'ctrl-meta',
},
interaction: {
rightClickActivateMarkItem: true,
},
});
engine.on(EVENTS.COMPLETE_EDIT_ITEM, () => {
console.log('编辑完成', engine.getMarkData());
});初始化配置
createImageLabel(options) 仅接受结构化配置(EngineInitOptions),不再兼容旧平铺字段。
| 命名空间 | 关键字段 | 默认值 | 说明 |
| --- | --- | --- | --- |
| layout | container(必填), width, height | 800/600 | 画布挂载容器与尺寸 |
| image | image(必填), imageRect, imageBrightness, imageContrast | null/100/100 | 底图与视觉参数 |
| viewport | minScale, maxScale, zoomFactor, viewportHotkey, keyPanStep | 0.2/5/1.1/ctrl-meta/20 | 视口缩放平移参数 |
| edit | minCircleRadius, minEllipseA, minEllipseB, dotRadius, magneticPolicy | 5/5/5/5/{} | 创建约束与磁性策略 |
| interaction | rightClickActivateMarkItem | true | 右键命中标注本体时是否自动激活 |
| style | markDefaultStyle, highlightVisualStyle | 见内置默认值 | 全局绘制与高亮样式 |
| history | historyMaxLength | 100 | 撤销/重做栈最大长度 |
对外 API
生命周期与事件
on(eventName, handler)off(eventName, handler)destroy()
模式与编辑权限
setMode(mode)// 建议配合MODES常量:CREATE/EDIT/READONLYenableEdit()disableEdit()
视口与坐标
resize(width, height)zoomIn(anchor?)zoomOut(anchor?)move(dx, dy)toCanvasPos(point)toCanvasPosInner(point)toGlobalPos(point)canvasToImagePoint(point)imageToCanvasPoint(point)
图片相关
refreshImage({ image?, imageBrightness?, imageContrast?, forceRefresh? })initImageSession({ image?, cvScriptUrl? })// 预热磁力剪刀 image session(对应init-image-session)updateOpt(nextOpt)isImageReady()getImageRect()getImageSourceSize()
自动识别与框选
pickRect(options?)// 启动一次框选会话,返回 canvas/global/image 三套坐标结果autoDetectBorderAtPoint(point, options?)// 点击点附近自动识别边框并创建 polygonautoDetectBorderInRect(rect, options?)// 框选区域内自动识别边框并创建 polygon
标注创建与编辑
createMarkItem(type, options?)activateMarkItem(id)deActivateMarkItem()getMarkItemById(id, options?)updateMarkItem(id, patch, options?)updateCurrentPoint(pointIndex, point)removeCurrentPoint(pointIndex)moveCurrentItem(dx, dy)completeCurrentItem()exitCreate()deleteMarkItem(id)deleteAllMarkItem()reset()getState()activateBatchDeletePoints()// 激活批量删点模式(仅 line/polygon)deactivateBatchDeletePoints(options?)// 退出批量删点,{ restore: true }可回滚isBatchDeletePointsActive()// 查询批量删点模式状态confirmBatchDeletePoints()// 确认删除已激活端点
createMarkItem 中与磁力剪刀相关的 options:
magnetic?: boolean对type="line"/type="polygon"生效,开启后使用磁性吸附路径创建。magneticPolicy?: { maxPathPoints?: number; cvScriptUrl?: string; preset?: string }maxPathPoints:预览路径最大采样点数,默认128。cvScriptUrl:OpenCV.js 地址,默认'/vendor/opencv/opencv.js'。
批量删点补充说明:
- 仅在“非创建态 + 当前激活项是 line/polygon”时可激活。
- 激活态端点为红色高亮;支持单击切换与框选激活。
confirmBatchDeletePoints()返回{ ok, reason, removedCount },失败时可根据reason做提示。- 删除过程会执行最小端点数量校验与自交校验,校验失败不会落库。
- 调用
createMarkItem(...)开始新建时,会自动退出批量删点(回滚未确认修改)。 - 可配合
BATCH_DELETE_POINTS_STATE_CHANGE/BATCH_DELETE_POINTS_CONFIRM做 UI 与提示联动。
标注可见性
setMarkVisible(id, visible)setMarkVisibleBatch(ids, visible)setAllMarkVisible(visible)bringMarkItemToFront(id)// 置顶sendMarkItemToBack(id)// 置底
层级规则备注:
- 标注层级由数组顺序决定:
index越大,层级越高(越靠上显示)。 - 命中查找按层级从高到低执行,重叠区域优先选中上层标注。
数据导入导出
getMarkData()reShow(serialized)getExportCanvas()// 返回当前可导出的画布(分层渲染时会先合成)exportImage(options?)exportImageOrigin(options?)exportImageWithMarksOrigin(options?)// 原图尺寸 + 含标注
导出备注:
exportImage:导出当前画布(含标注),输出尺寸=当前画布尺寸。exportImageOrigin:导出原图(不含标注),仅当底图对象支持toDataURL时可用。exportImageWithMarksOrigin:导出原图尺寸且包含标注。exportImageWithMarksOrigin默认会按“当前画布显示比例(含 zoom)”自动调整标注线宽/点半径。- 如需手动调整导出粗细,可传
styleScale覆盖自动比例:exportImageWithMarksOrigin({ styleScale: 1.5 })。
磁力剪刀(Magnetic Line/Polygon)
磁力剪刀基于 OpenCV IntelligentScissors,用于沿图像边缘自动吸附线段/多边形路径。
使用前准备:
- 确保 OpenCV 资源可访问(默认路径):
public/vendor/opencv/opencv.jspublic/vendor/opencv/opencv_js.wasm
- 若你使用自定义静态资源路径,请在
magneticPolicy.cvScriptUrl传入对应地址。
创建示例:
engine.createMarkItem('polygon', {
magnetic: true,
magneticPolicy: {
maxPathPoints: 128,
cvScriptUrl: '/vendor/opencv/opencv.js',
},
props: {
lineWidth: 2,
},
});也可用于 line:
engine.createMarkItem('line', {
magnetic: true,
magneticPolicy: {
maxPathPoints: 128,
cvScriptUrl: '/vendor/opencv/opencv.js',
},
});交互说明:
- 首次点击设置锚点。
- 鼠标移动时显示“锚点 -> 当前点”的吸附预览路径。
- 再次点击会按预览路径追加多段点。
- 靠近首点点击可闭合;也可按
Enter完成当前标注,按Esc取消当前创建。
降级行为:
- 若当前环境不支持或 OpenCV 初始化失败,路径会自动回退为“锚点到目标点直线”,不阻断标注流程。
历史(Undo/Redo)
canUndo()canRedo()undo()redo()clearHistory()
模式常量(MODES)
MODES.CREATEMODES.EDITMODES.READONLY
事件列表(EVENTS)
输入与交互
CLICKMOUSEDOWNMOUSEMOVEMOUSEUPMOUSELEAVECONTEXTMENUWHEELESCAPEENTER_KEYDELETE_KEYUNDO_REQUESTREDO_REQUESTARROW_PANWINDOW_BLURVIEWPORT_HOTKEY_CHANGECANVAS_DRAG_STATE_CHANGEITEM_DRAG_STATE_CHANGE
状态与渲染
RESIZEMODE_CHANGETRANSFORMSCALE_CHANGECURSOR_CHANGEHOVER_TARGET_CHANGEUPDATED_OPTIMAGE_READYIMAGE_CONFIG_APPLIEDIMAGE_REQUIREDDESTROY
标注业务事件
CURRENT_MARK_ITEM_CHANGEIS_CREATE_MARKING_CHANGECOMPLETE_CREATE_ITEMCOMPLETE_EDIT_ITEMLINE_CROSSPOLYGON_CLOSE_HINTDELETE_MARKING_ITEMDELETE_ALL_MARKING_ITEMBATCH_DELETE_POINTS_STATE_CHANGEBATCH_DELETE_POINTS_CONFIRM
历史事件
HISTORY_CHANGEUNDOREDO
框选事件
RECT_PICK_STARTRECT_PICK_CHANGERECT_PICK_ENDRECT_PICK_CANCEL
新增:批量删点事件
BATCH_DELETE_POINTS_STATE_CHANGE- 触发时机:进入/退出批量删点模式
- 事件载荷:
active: 是否处于批量删点模式restore: 退出时是否执行了回滚reason: 状态变化原因(如activate/api/create-item/escape/window-blur/mode-change/disable-edit/confirm-success)itemId: 对应标注 id(若可解析)selectedCount: 当前激活端点数量
BATCH_DELETE_POINTS_CONFIRM- 触发时机:调用
confirmBatchDeletePoints()后(成功/失败都会触发) - 事件载荷:
ok: 是否确认删除成功reason: 结果原因(如ok/empty-selection/min-point/invalid-shape/session-inactive)removedCount: 实际删除端点数量itemId: 对应标注 id(若可解析)
- 触发时机:调用
新增:激活态标注右击事件
ACTIVE_MARK_ITEM_CONTEXTMENU- 触发条件:仅在“当前激活标注”上右击命中时触发(命中本体与端点都会触发)
- 事件载荷:
markItem: 当前激活标注(序列化对象)canvasPoint: 鼠标的 canvas 坐标{ x, y }hit: 命中信息{ pointIndex, segmentIndex }(命中端点时pointIndex >= 0)
示例:
engine.on(EVENTS.ACTIVE_MARK_ITEM_CONTEXTMENU, ({ markItem, canvasPoint, hit }) => {
// 可用于区分“命中端点”和“命中图形本体”,在菜单中按需显示“删除端点”
console.log('右击激活标注', markItem.id, canvasPoint, hit);
});标注数据格式
getMarkData() 返回数组,每个元素结构如下。
type MarkData = {
id: string | number;
type: 'dot' | 'line' | 'polygon' | 'circle' | 'ellipse';
pointArr: Array<{ x: number; y: number }>; // 图片归一化坐标(0~1)
magnetic?: boolean; // 可选,line / polygon 可能出现
magneticPolicy?: { maxPathPoints?: number; cvScriptUrl?: string; preset?: string } | null; // 可选
props: Record<string, any>;
data: Record<string, any>;
visible: boolean; // 必填
};说明:
pointArr在导出时是图片归一化坐标,不是运行时画布像素坐标。reShow仅接受包含visible的数据;缺失会抛错。- 不兼容不带
visible的旧数据结构。
历史行为说明
- 历史采用双栈(
undoStack/redoStack)。 image切换时会清空历史栈(避免跨图回退污染)。resize不进入 undo/redo。setMarkVisible*方法默认不直接调用recordHistory。- 但历史快照当前包含
visible字段,后续其他会入栈操作发生时,visible状态会随快照进入历史。
坐标与 resize 约定
- 运行时编辑坐标基于画布内部坐标。
- 导入导出坐标基于图片归一化坐标(0~1)。
- 画布
resize后会按图片归一化位置重映射标注,保持“标注在图片上的相对位置不变”。
Demo
- Demo 入口:
src/demo/main.ts - Demo 主页面:
src/demo/App.vue - 样式:
src/demo/style.css - 功能覆盖:创建/编辑、批量删点、右键菜单(置顶/置底/删除)、视口、自动识别、JSON 回显、导出、可见性测试、事件联动日志
包入口导出
从 @microclear/label-image 入口可直接使用:
createImageLabelImageLabelEngineEVENTSMODESinitImageSession// 包级预热能力(不依赖 engine 实例)isMagneticScissorsSupportedisWasmSupportedisWgpuSupportedformatLabelcreateLabelStats
类型入口
- npm 包
package.json中:"types": "./dist/lib/types/index.d.ts"
- 生成声明前建议先执行:
npm run build:lib
