当前位置: 首页 > news >正文

canvas实现图片标注之Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

canvas实现图片标注之Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

文章目录

  • canvas实现图片标注之Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】
    • 前言
    • 主体功能
    • 效果预览
    • 实现步骤
      • 第一步、布局实现
      • 第二步、下载并引入fabric.js对象及功能方法
      • 第三步、初始化画布
      • 第四步、标注图片初始化
      • 第五步、设置画布及其他事件监听器
      • 第六步、 矩形标注实现
      • 第七步、多边形标注实现
      • 第八步、坐标转换系统
      • 第九步、标签管理
      • 第十步、十字辅助线创建
      • 第十一步、自定义控制点
      • 第十二步、基于标签文本生成固定颜色
      • 第十三步、进行矩形的回显绘制
      • 第十四步、矩形及多边形绘制结束label弹窗
      • 第十五步、根据数据源回显多边形
      • 第十六步、导出数据格式及其数据
      • 完结~
    • 优化进行中~ 关注我不定时进行代码优化与功能添加
    • 需要代码私信我即发~

前言

经过Fabric.js从入门学习到实现labelImg矩形多边形标注工具【上】 的学习,开始进入前端标注工具的实战阶段,该工具可以进行源码二次开发或者根据自己的标注需求进行拓展,结尾有获取源码的方式~

主体功能

当前工具实现了labelImg的标注功能,其中有:

  1. 辅助线功能,方便查看标注过程中的参照位置。【可开关控制】
  2. 鼠标指向点滚轮缩放或点击缩放按钮进行缩放功能。
  3. 进行大图片自适应窗口及自适应缩放按钮功能。
  4. 进行上一张,下一张图片标注切换功能。
  5. 可通过”w“键进行矩形框的拖拽创建绘制。
  6. 可通过按钮进行多边形框的多点绘制功能。
  7. 可进行矩形框的拖拽修改大小以及双击修改label标签内容。
  8. 多边形双击进入编辑,再双击进入预览状态,可对顶点进行拖拽编辑功能。
  9. 可根据定义好的矩形数据以及多边形数据进行矩形多边形的回显功能。
  10. 可进行数据的精确的导出,矩形【导出上下左右点位及中心点】,多边形【导出各个点的位置】
  11. 可同时标注多边形矩形且可以层叠绘制,鼠标移入高亮等可视化功能。
  12. 固定字符固定随机颜色。

Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

效果预览

矩形标注:
Fabric.js从入门学习到实现labelImg矩形多边形标注工具
多边形标注:
Fabric.js从入门学习到实现labelImg矩形多边形标注工具
十字辅助线
Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

导出数据格式:
Fabric.js从入门学习到实现labelImg矩形多边形标注工具
标签修改:
Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

多边形回显
canvas实现图片标注之Fabric.js从入门学习到实现labelImg矩形多边形标注工具

canvas标注

矩形标注

多边形标注工具web端

实现步骤

第一步、布局实现

  • 左侧工具栏:包含各种工具按钮,如辅助线、保存、图片切换按钮等。
  • 中间图像区域:显示需要标注的图像,并且有绿色线条辅助标注。
  • 右侧标签列表:显示文件列表和标签信息

Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】
我比较直接,使用了绝对定位实现的圣杯布局:

  • .left-menu 类设置了左边栏,其宽度固定为122px
  • .right-list 类设置了右边栏,其宽度固定为230px
  • .canvas-box 类代表中央内容区,它的左右位置分别由左边栏和右边栏的宽度决定,并且通过设置 left:122px; right: 230px; 来确保中央内容区不会与两边栏重叠。
.right-list {position: absolute;top: 0;right: 0;height: 100%;width: 230px;background: #f0f0f0;}.left-menu {position: absolute;left: 0;height: 100%;width: 122px;padding-top: 5px;background: #f0f0f0;position: relative; /* 确保伪元素相对于这个元素定位 */}.canvas-box {position: absolute;left: 122px;right: 230px;top: 0;bottom: 0;overflow: hidden;}

使用绝对定位来安排页面元素的方式是实现圣杯布局的一种方法。也可以不使用我的这种布局,自行使用【flex布局,grid布局,float浮动布局】等很多方法都可实现。

第二步、下载并引入fabric.js对象及功能方法

下载:

npm install fabric
或者
yarn add fabric

引入:

import {Canvas, // 这是 Fabric.js 的核心类,代表整个画布(Canvas)实例。Line,  // 用于创建直线对象Group,  // 将多个 Fabric 对象组合成一个组(Group),可以统一进行移动、缩放、旋转等操作。FabricImage,  // 用于加载和操作图片对象。controlsUtils, // 提供与控件相关的实用工具函数,用于自定义对象的控制点(如缩放、旋转手柄)。Rect, // 创建矩形对象。Point, // 表示一个二维坐标点(x, y)。Textbox, // 可编辑的多行文本框对象。Control, // 定义画布对象上的控制点(小方块手柄),比如缩放、旋转、移动等util, // Fabric.js 提供的通用工具函数集合。Circle, // 创建圆形对象。Text, // 创建静态文本对象(不可编辑)。Polygon, // 创建多边形对象。
} from "fabric";

第三步、初始化画布

使用Fabric.js创建画布,并加载当前图片。注意设置画布尺寸为父容器大小,并处理图片的缩放和居中。

// 初始化 Fabric 画布
function initFabricCanvas() {// 确认canvas元素是否存在,不存在returnif (!canvas.value) return;// 获取画布的父容器元素const container = canvas.value.parentElement;// 确认父容器元素存在,不存在returnif (!container) return;// 创建新的Fabric布实例,并设宽高背景颜色属性fCanvas = new Canvas(canvas.value, {width: container.clientWidth, // 设置Fabric的宽度为容器的宽度height: container.clientHeight, // 设置Fabric的高度为容器的高度// backgroundColor: "#ffff00", // 设置背景颜色为黄色selection: true, // 关闭选择功能preserveObjectStacking: true, // 允许对象堆栈改变altSelectionKey: "altKey",});
}

第四步、标注图片初始化

  1. 检查 Fabric 画布是否初始化:在开始任何操作之前,首先检查 fCanvas 是否已经初始化。如果没有,则输出警告并退出函数,防止后续操作出错。
  2. 加载图片:通过调用 FabricImage.fromURL 方法(这里假设是 Fabric.js 提供的用于加载图片的方法),从指定的 URL 加载图片。加载时设置了跨域资源共享(CORS)为匿名模式,以便处理跨域图片资源的问题。
  3. 计算缩放比例和调整图片尺寸:根据父容器的高度来计算图片需要缩放的比例,使得图片能适应容器的高度。同时保存这个缩放比例,可能用于后续的操作。
  4. 配置并添加图片到画布:将加载的图片应用上述计算好的缩放比例,并设置其在画布中的位置为居中。然后更新画布尺寸使其匹配容器尺寸,并将图片对象添加到画布中显示出来。
  5. 绘制已有标签数据:如果有针对当前图片的标签数据(如标注信息),则调用 drawLabels 函数进行绘制,这通常用于图像标注工具中。
  6. 实现交互式十字辅助线:为了增强用户体验,函数还创建了一个额外的透明 Canvas 层,用于在用户鼠标移动时动态绘制十字辅助线,从而提供精确的位置指示。
  7. 事件监听与处理:通过监听容器的鼠标移动和离开事件,实现在用户与画布交互时实时更新十字辅助线的位置,或者在鼠标离开时清除这些辅助线。
// 异步加载图片
async function loadImage(url: string) {// 如果fabric未初始化,则输出警告并返回if (!fCanvas) {console.warn("fCanvas 未初始化");return;}// "http://192.168.80.32:8888/Image_20250306140749112.png"try {// 使用Fabric加载图片,并设置跨域资源共享(CORS)为匿名const imgObj = await FabricImage.fromURL(url, { crossOrigin: "anonymous" });console.log("图片加载完成", imgObj);// 获取画布容器的Dom元素,并计算缩放比例const container = fCanvas.getElement().parentElement as HTMLElement;const scale = container.clientHeight / imgObj.height!;imgScale = container.clientHeight / imgObj.height!;// imgObj.scale(imgScale);// 应用缩放比例,并设置图片在画布中的位置imgObj.scale(scale);// 设置图片的居中位置imgObj.set({originX: "center",originY: "center",left: fCanvas.getWidth() / 2,top: fCanvas.getHeight() / 2,selectable: false,evented: false,});// 更新画布尺寸,并添加图片对象到画布中fCanvas.setDimensions({width: container.clientWidth,height: container.clientHeight,});// 刷新画布以显示图片fCanvas.add(imgObj);// 重置视口变换,并请求重新渲染整个画布fCanvas.setViewportTransform([1, 0, 0, 1, 0, 0]);fCanvas.requestRenderAll();drawLabels(imageLabelData[currentIndex.value].labelInfo, imgObj);// 创建或获取顶层canvasconst crossCanvas = createCrossCanvas(container);crossCanvasDom.value = createCrossCanvas(container);const ctx = crossCanvas.getContext("2d")!;// 绑定鼠标移动事件绘制十字线(注意要先解绑避免重复绑定)container.removeEventListener("mousemove", onMouseMove);container.addEventListener("mousemove", onMouseMove);container.removeEventListener("mouseleave", onMouseLeave);container.addEventListener("mouseleave", onMouseLeave);function onMouseMove(e: MouseEvent) {const rect = crossCanvas.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;if (!isShowCrosshairImpont.value) return; // 显示优先级大于isCrosshairif (!isCrosshair.value) return;ctx.clearRect(0, 0, crossCanvas.width, crossCanvas.height);ctx.beginPath();ctx.strokeStyle = "#00ff01";ctx.lineWidth = 0.8;// 横线ctx.moveTo(0, y);ctx.lineTo(crossCanvas.width, y);// 纵线ctx.moveTo(x, 0);ctx.lineTo(x, crossCanvas.height);ctx.stroke();}function onMouseLeave() {ctx.clearRect(0, 0, crossCanvas.width, crossCanvas.height);}} catch (err) {// 捕获任何可能发生的错误,并输出错误信息console.error("图片加载失败", err);}
}

第五步、设置画布及其他事件监听器

实现setupEventListeners事件监听器,用于实现统一管理所有用户交互行为,包括:

  • 按 W 键进入矩形框绘制模式
  • 鼠标拖拽进行矩形标注
  • 支持多边形绘制
  • 双击修改标签
  • 拖拽平移画布(配合 Ctrl)
  • 鼠标滚轮缩放(配合 Ctrl)
  • 实时更新标注坐标(移动/缩放后自动转换坐标)

我这边再做监听之前先检查一下事件是否重复监听去重操作

if (!fCanvas) return;
fCanvas.off("mouse:down"); // 解绑旧的事件,防止重复绑定

大概事件监听代码如下:


// 绑定事件:平移、缩放
function setupEventListeners() {// 如果Fabric画布未初始化,则直接返回if (!fCanvas) return;// 解绑旧的 mouse:down 监听,避免切换图片的时候叠加上一张图的事件fCanvas.off("mouse:down");// 键盘监听window.addEventListener("keydown", (e) => {// 如果按下W键,则开始绘制});window.addEventListener("keyup", (e) => {if (e.key === "w" || drawing) {// drawing = false;fCanvas.selection = false;}});// 鼠标按下fCanvas.on("mouse:down", (opt) => {const obj = opt.target as any;const target = opt.target as any; const e = opt.e as MouseEvent;if (!opt.target) {// 用户点击画布空白处} else {const obj = opt.target as any;if (obj.boxId) {// 点击矩形框}}if (!drawingPolygon.value) return;const pointer = fCanvas.getPointer(opt.e);const point = { x: pointer.x, y: pointer.y };currentPolygonPoints.push(point);// 创建并添加临时圆点...fCanvas.add(circle);tempCirclePoints.push(circle);// 进行临时连线操作...fCanvas.add(line);tempLinePoints.push(line);}// 检测是否闭合(点击第一个点附近)if (currentPolygonPoints.length >= 3) {const first = currentPolygonPoints[0];const dx = point.x - first.x;const dy = point.y - first.y;if (Math.sqrt(dx * dx + dy * dy) < 10) {finalizePolygon();}}});// 全部可选项进行双击修改标签内容fCanvas.on("mouse:dblclick", (e) => {// 修改标签弹窗});// 对象单选时也触发fCanvas.on("object:modified", (e) => {// 当对象进行平移后根据缩放和平移距离精确计算出各个点的位置});// 监听鼠标按下事件fCanvas.on("mouse:down", (opt) => {...});// 监听鼠标移动事件fCanvas.on("mouse:move", (opt) => {});// 监听鼠标抬起事件fCanvas.on("mouse:up", (opt) => {});// 监听鼠标滚轮事件且与ctrl配合使用才会生效fCanvas.on("mouse:wheel", (opt) => {});
}

第六步、 矩形标注实现

拖拽结束后,记录鼠标最后出现位置,弹出标注框。

// 创建矩形标注
function createRectangles() {drawing = true;fCanvas.selection = false;fCanvas.defaultCursor = "crosshair";
}// 绘制矩形逻辑
fCanvas.on("mouse:down", (opt) => {if (drawing) {const e = opt.e as MouseEvent;const p = fCanvas.getPointer(e);startX = p.x;startY = p.y;// 创建临时矩形tempRect = new Rect({left: startX,top: startY,width: 0,height: 0,fill: "rgba(123, 217, 31,0.05)",stroke: "#7BD91F",strokeWidth: 1,});fCanvas.add(tempRect);}
});// 鼠标移动时更新矩形尺寸
fCanvas.on("mouse:move", (opt) => {if (drawing && tempRect) {const e = opt.e as MouseEvent;const p = fCanvas.getPointer(e);tempRect.set({width: Math.abs(p.x - startX),height: Math.abs(p.y - startY),left: p.x < startX ? p.x : startX,top: p.y < startY ? p.y : startY,});tempRect.setCoords();fCanvas.requestRenderAll();}
});

第七步、多边形标注实现

我当前是通过点击每个点进行连成线绘制多边形,三个点即可变成一个多边形,当前点数>3的时候且第一个点被二次点击的时候代表多边形标注完成,即可进行多边形标注结束。

// 多边形绘制模式切换
function createPolygon() {drawingPolygon.value = !drawingPolygon.value;if (!drawingPolygon.value) {clearTempDraw();}
}// 多边形点添加逻辑
fCanvas.on("mouse:down", (opt) => {if (!drawingPolygon.value) return;const pointer = fCanvas.getPointer(opt.e);const point = { x: pointer.x, y: pointer.y };currentPolygonPoints.push(point);// 添加控制点const circle = new Circle({left: point.x,top: point.y,radius: 3,fill: "red",});fCanvas.add(circle);tempCirclePoints.push(circle);// 添加连线if (currentPolygonPoints.length > 1) {const prev = currentPolygonPoints[currentPolygonPoints.length - 2];const line = new Line([prev.x, prev.y, point.x, point.y], {stroke: "red",strokeWidth: 1,});fCanvas.add(line);tempLinePoints.push(line);}
});// 多边形闭合逻辑
function finalizePolygon() {currentPolygonPoints.pop();// 创建多边形对象const polygon = new Polygon(currentPolygonPoints, {stroke: "red",strokeWidth: 1,fill: `rgba(149, 204, 100,0.2)`,objectCaching: false,selectable: true,});fCanvas.add(polygon);// 添加标签const center = polygon.getCenterPoint();const tb = new Textbox(label, {left: center.x,top: center.y,fontSize: 16,fill: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,});fCanvas.add(tb);// 保存多边形数据imageLabelData[currentIndex.value].labelPolygonInfo.push({labelText: label,id: polygonId,circlePoints: [...pointCacheData],});
}

第八步、坐标转换系统

// 画布坐标转图像原始坐标
function toImageOriginalPos(point: Point) {const zoom = fCanvas.getZoom();const vpt = fCanvas.viewportTransform || [1, 0, 0, 1, 0, 0];// 移除画布变换const canvasX = (point.x - vpt[4]) / zoom;const canvasY = (point.y - vpt[5]) / zoom;// 转换为相对于图像左上角的坐标const relativeX = canvasX - imgLeft;const relativeY = canvasY - imgTop;// 转换为原始图像像素坐标return {x: (relativeX / img.scaleX!) * zoom,y: (relativeY / img.scaleY!) * zoom,};
}

第九步、标签管理

// 标签编辑
function startEditLabel(id: string, type?: any) {if (type == "rect") {const label = imageLabelData[currentIndex.value].labelInfo.find((b: any) => b.id === id);editLabelText.value = label.labelText;editingLabelId.value = id;editingLabel.value = true;}
}// 标签删除
function removeBox(id: string) {const targets = fCanvas.getObjects().filter((o) => (o as any).boxId === id);targets.forEach((o) => {fCanvas.remove(o);});imageLabelData[currentIndex.value].labelInfo = imageLabelData[currentIndex.value].labelInfo.filter((b) => b.id !== id);
}

第十步、十字辅助线创建

当时实现的时候都在统一图层里面进行实现的,当完成后发现,我的十字辅助线的功能与我的图片标注一层画布出现了事件冲突,各种冲突问题很难结局,最后决定在Fabric.js画布之上创建一个透明的Canvas覆盖层,专门用于绘制跟随鼠标的十字线。不会干扰底层Fabric.js的画布内容,可以轻松控制十字线的显示/隐藏,独立于Fabric.js的渲染机制,性能开销小。具体实现代码如下

  1. 创建辅助canvas层
function createCrossCanvas(container: HTMLElement) {// 检查是否已存在辅助Canvaslet crossCanvas = container.querySelector<HTMLCanvasElement>(".cross-canvas");if (crossCanvas) return crossCanvas;// 创建新的Canvas元素crossCanvas = document.createElement("canvas");crossCanvas.classList.add("cross-canvas");// 设置Canvas样式crossCanvas.style.position = "absolute";crossCanvas.style.top = "0";crossCanvas.style.left = "0";crossCanvas.style.width = "100%";crossCanvas.style.height = "100%";crossCanvas.style.pointerEvents = "none"; // 关键:允许事件穿透crossCanvas.style.zIndex = "9999"; // 确保在最上层// 添加到容器container.appendChild(crossCanvas);// 设置Canvas物理尺寸(防止模糊)crossCanvas.width = container.clientWidth;crossCanvas.height = container.clientHeight;return crossCanvas;
}
  1. 在图像加载时初始化辅助Canvas
async function loadImage(url: string) {// ...其他图像加载逻辑...// 创建或获取辅助Canvasconst crossCanvas = createCrossCanvas(container);// 获取2D绘图上下文const ctx = crossCanvas.getContext("2d")!;// 绑定鼠标移动事件container.removeEventListener("mousemove", onMouseMove);container.addEventListener("mousemove", onMouseMove);// 绑定鼠标离开事件container.removeEventListener("mouseleave", onMouseLeave);container.addEventListener("mouseleave", onMouseLeave);// ...其他逻辑...
}
  1. 实现鼠标移动事件处理
function onMouseMove(e: MouseEvent) {// 计算鼠标在Canvas中的相对位置const rect = crossCanvas.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;// 检查是否应该显示十字线if (!isShowCrosshairImpont.value) return;if (!isCrosshair.value) return;// 清除上一帧的绘制ctx.clearRect(0, 0, crossCanvas.width, crossCanvas.height);// 开始绘制新的十字线ctx.beginPath();ctx.strokeStyle = "#00ff01"; // 亮绿色ctx.lineWidth = 0.8; // 细线// 绘制横线ctx.moveTo(0, y);ctx.lineTo(crossCanvas.width, y);// 绘制纵线ctx.moveTo(x, 0);ctx.lineTo(x, crossCanvas.height);// 完成绘制ctx.stroke();
}
  1. 实现鼠标离开事件处理
function onMouseLeave() {// 鼠标离开时清除十字线ctx.clearRect(0, 0, crossCanvas.width, crossCanvas.height);
}

第十一步、自定义控制点

function createCustomControls() {return {tl: new Control({ /* 左上角控制点配置 */ }),tr: new Control({ /* 右上角控制点配置 */ }),bl: new Control({ /* 左下角控制点配置 */ }),br: new Control({ /* 右下角控制点配置 */ }),ml: new Control({ /* 左边中点控制点配置 */ }),mr: new Control({ /* 右边中点控制点配置 */ }),mt: new Control({ /* 上边中点控制点配置 */ }),mb: new Control({ /* 下边中点控制点配置 */ }),};
}// 应用自定义控制点到矩形
rect.controls = createCustomControls();

第十二步、基于标签文本生成固定颜色

function stringToColour(str, isRgb = true) {// 生成MD5哈希值const hash = CryptoJS.MD5(str).toString();const colour = "#" + hash.substr(0, 6);// 转换为RGB格式if (isRgb) {return {r: parseInt(colour.substring(1, 3), 16),g: parseInt(colour.substring(3, 5), 16),b: parseInt(colour.substring(5, 7), 16)};}return colour;
}

第十三步、进行矩形的回显绘制

// 绘制矩形标注框
function drawLabels(labelInfo: typeof labelData.labelInfo, imgObj: any) {const imgLeft = imgObj.left!;const imgTop = imgObj.top!;// 获取偏移量const originOffsetX = imgObj.getScaledWidth() / 2;const originOffsetY = imgObj.getScaledHeight() / 2;labelInfo.forEach((item) => {const [x1, y1] = item.pos1.map((v) => v * imgScale);const [x2, y2] = item.pos2.map((v) => v * imgScale);const left = imgLeft - originOffsetX + x1;const top = imgTop - originOffsetY + y1;const width = x2 - x1;const height = y2 - y1;// 获取字符串随机且固定颜色const rgbTempColor: any = stringToColour(item.labelText);console.log("RGB", rgbTempColor.r, rgbTempColor.g, rgbTempColor.b);const rect = new Rect({left,top,width,height,fill: `rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 0)`, // 填充颜色stroke: `rgb(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b})`, // 边框颜色strokeWidth: 1,selectable: true,cornerStrokeWidth: 1,strokeUniform: true, // 确保缩放时描边宽度不变originX: "left",originY: "top",});// 创建文字标签,不加入 groupconst label = new Textbox(item.labelText, {left: left,top: top, // 👈 显示在矩形上方fontSize: 26,fill: `rgb(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b})`,editable: false,borderColor: `rgb(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b})`,selectable: false,evented: false, // 不参与事件});// 自定义控制点rect.controls = createCustomControls();// 添加元素fCanvas.add(rect);fCanvas.add(label);// 保存 label 引用在 rect 上(rect as any).labelRef = label;(rect as any).boxId = item.id;(label as any).boxId = item.id;// 拖动时同步 label 位置rect.on("moving", () => {const r = rect as any;r.labelRef.set({left: r.left! + 5,top: r.top!,});});// rect.set({ hoverCursor: "pointer" });rect.on("mouseover", () => {// "rgba(0, 255, 0, 0.2)"rect.set("fill",`rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b},0.2)`);fCanvas.renderAll();});rect.on("mouseout", () => {rect.set("fill",`rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b},0)`);fCanvas.renderAll();});rect.on("mousedown", (e) => {// 选中当前 rectSmall 对象fCanvas.setActiveObject(rect);// 重新渲图——必要,否则选中框不会出现fCanvas.requestRenderAll();console.log("已选中小矩形", rect);const obj: any = e.target as any;if (obj && obj.boxId) {selectedLabelId.value = obj.boxId;}});// 缩放时同步 label 位置rect.on("scaling", () => {console.log("scaling被一直执行了");const r = rect as any;const boundingBox = rect.getBoundingRect(); // 获取绝对左上角r.labelRef.set({left: r.left! + r.getScaledWidth() / 2 - r.labelRef.width! / 2,top: r.top!,});});// 初始化坐标rect.setCoords();});fCanvas.requestRenderAll();
}

第十四步、矩形及多边形绘制结束label弹窗


// 矩形弹出新建标签输入
function promptLabelEditor(rect: Rect) {// 创建一个inputconst wrapper = canvas.value!.parentElement! as HTMLElement;// 创建一个divconst div = document.createElement("div");// / 设置div的样式div.style.position = "absolute";div.style.left = `${startPos.x! - 122}px`;div.style.top = `${startPos.y!}px`;div.style.background = "white";div.style.padding = "4px";div.innerHTML = `<input type="text" placeholder="请输入Label" style="width:100px"/><button data-action="ok">创建</button><button data-action="cancel">取消</button>`;// 添加到画布wrapper.append(div);// 选中input输入框const input = div.querySelector("input") as HTMLInputElement;// 让输入框获取焦点input.focus();// 移除临时的矩形const cleanup = () => {// 移除临时的矩形wrapper.removeChild(div);tempRect = null;};// 监听点击事件div.addEventListener("click", (e) => {const btn = (e.target as HTMLElement).dataset.action;if (btn === "ok") {const label = input.value.trim();if (label) addLabeledRect(rect, label);// 移除临时的矩形cleanup();} else if (btn === "cancel") {fCanvas.remove(rect);// 移除临时的矩形cleanup();// 打开十字控制线isCrosshair.value = true;}});
}

第十五步、根据数据源回显多边形


let imageLabelData: any = reactive([{imgurl: "http://192.168.80.32:8888/zq.jpeg",labelInfo: [] as LabelInfo[],labelPolygonInfo: [{labelText: "10号球员",id: "box_1754903425594",circlePoints: [[{ x: 869.9902698710292, y: 2355.0001879442193 },{ x: 748.9829665322839, y: 2396.397423296948 },{ x: 586.5784278408101, y: 2323.1561607498134 },{ x: 280.8757667745062, y: 2233.9928846054736 },{ x: 233.10972598289615, y: 1921.921418100289 },{ x: 287.2445722133874, y: 1667.1692005450361 },{ x: 350.93262660220086, y: 1498.3958564146808 },{ x: 430.54269458821756, y: 1310.5160959676812 },{ x: 519.7059707325561, y: 1129.0051409595635 },{ x: 659.8196903879452, y: 1052.5794756929874 },{ x: 787.195799165572, y: 969.7850049875302 },{ x: 793.5646046044533, y: 794.6428554182937 },{ x: 860.4370617127074, y: 638.6071221657011 },{ x: 971.8911568931306, y: 597.2098868129724 },{ x: 1070.607641195791, y: 597.2098868129724 },{ x: 1166.1397227790112, y: 654.5291357629046 },{ x: 1217.0901662900615, y: 727.7703983100399 },{ x: 1201.1681526928583, y: 816.9336744543782 },{ x: 1201.1681526928583, y: 909.2813533181577 },{ x: 1185.246139095655, y: 998.4446294624961 },{ x: 1159.7709173401297, y: 1065.31708657075 },{ x: 1127.926890145723, y: 1148.1115572762076 },{ x: 1032.3948085625032, y: 1189.5087926289361 },{ x: 1000.5507813680963, y: 1224.5372225427836 },{ x: 994.181975929215, y: 1355.0977340398508 },{ x: 1022.8416004041812, y: 1482.473842817477 },{ x: 1035.5792112819438, y: 1603.4811461562226 },{ x: 1045.132419440266, y: 1673.5380059839174 },{ x: 1108.8204738290792, y: 1740.4104630921713 },{ x: 1204.3525554122991, y: 1778.623295725459 },{ x: 1382.679107700976, y: 1874.155377308679 },{ x: 1481.395592003637, y: 1899.630599064204 },{ x: 1624.6937143784667, y: 1899.630599064204 },{ x: 1720.2257959616863, y: 1953.7654452946954 },{ x: 1761.6230313144154, y: 2007.9002915251872 },{ x: 1767.9918367532969, y: 2093.879164950085 },{ x: 1710.6725878033646, y: 2151.198413900017 },{ x: 1627.8781170979075, y: 2163.93602477778 },{ x: 1430.4451484925862, y: 2065.219540475119 },{ x: 1408.1543294565015, y: 2135.2764003028137 },{ x: 1382.679107700976, y: 2288.127730835965 },{ x: 1290.331428837197, y: 2504.6671157579303 },{ x: 1150.2177091818075, y: 2676.6248626077263 },{ x: 1086.5296547929943, y: 2791.2633605075907 },{ x: 1182.0617363762142, y: 2899.533052968573 },{ x: 1283.9626233983156, y: 3042.831175343403 },{ x: 1293.515831556638, y: 3131.994451487741 },{ x: 1137.480098304045, y: 3157.4696732432667 },{ x: 809.4866182016564, y: 3141.5476596460635 },{ x: 717.1389393378772, y: 3109.703632451657 },{ x: 752.1673692517244, y: 3014.171550868437 },{ x: 717.1389393378772, y: 2915.455066565776 },{ x: 803.1178127627751, y: 2800.816568665912 },{ x: 809.4866182016564, y: 2698.9156816438112 },{ x: 812.6710209210972, y: 2536.511142952337 },{ x: 847.6994508349445, y: 2450.532269527439 },],],},] as any,},{imgurl: "http://192.168.80.32:8888/286_Coax_67480501_SourceImg.jpg",labelInfo: [{labelText: "猫",pos1: [0, 0],pos2: [500, 500],id: "box-1",},] as LabelInfo[],labelPolygonInfo: [],},
]);

绘制多边形代码:

// 绘制多边形标注框
function drawPolygons(labelPolygonInfo: any[], imgObj: any) {const imgLeft = imgObj.left!;const imgTop = imgObj.top!;const imgDisplayWidth = imgObj.getScaledWidth();const imgDisplayHeight = imgObj.getScaledHeight();const imgBaseLeft = imgLeft - imgDisplayWidth / 2;const imgBaseTop = imgTop - imgDisplayHeight / 2;labelPolygonInfo.forEach((poly) => {const points = poly.circlePoints[0].map((p: any) => ({x: imgBaseLeft + p.x * imgObj.scaleX,y: imgBaseTop + p.y * imgObj.scaleY,}));const rgbTempColor: any = stringToColour(poly.labelText);const polygon = new Polygon(points, {fill: `rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 0.01)`,stroke: `rgb(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b})`,strokeWidth: 1,selectable: true,objectCaching: false,strokeUniform: true,evented: true,hasControls: false,hasBorders: false,perPixelTargetFind: true,noScaleCache: true,});const label = new Textbox(poly.labelText, {fontSize: 16,fill: `rgb(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b})`,editable: false,selectable: false,evented: false,originX: "center",originY: "center",});(polygon as any).labelRef = label;(polygon as any).boxId = poly.id;(label as any).boxId = poly.id;(polygon as any).id = poly.id;(label as any).id = poly.id;// 绑定多边形和标签的关系(polygon as any).label = label;// 更新标签位置函数function updateLabelPosition() {const center = polygon.getCenterPoint();label.set({left: center.x,top: center.y,angle: polygon.angle || 0,});label.setCoords();fCanvas.requestRenderAll();}// 绑定标签位置同步事件polygon.on("moving", updateLabelPosition);polygon.on("scaling", updateLabelPosition);polygon.on("rotating", updateLabelPosition);// 监听多边形修改事件,实时更新数据polygon.on("modified", () => {console.log("modified", polygon);updatePolygonData(polygon);});// 双击进入/退出顶点编辑模式polygon.on("mousedblclick", () => {if (editingPolygon === polygon) {exitVertexEditing(polygon);} else {if (editingPolygon) exitVertexEditing(editingPolygon);enterVertexEditing(polygon);}});// 鼠标事件,设置激活对象和选中IDpolygon.on("mousedown", (e) => {fCanvas.setActiveObject(polygon);fCanvas.requestRenderAll();const obj: any = e.target;if (obj && obj.boxId) {selectedLabelId.value = obj.boxId;}});// 设置默认样式(你之前代码里重复设置了多次,这里统一放)polygon.set({fill: `rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 0.2)`,borderColor: `rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 1)`,stroke: `rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 1)`,});// 鼠标悬浮改变填充透明度,提升交互体验polygon.on("mouseover", () => {polygon.set("fill",`rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 0.2)`);fCanvas.renderAll();});polygon.on("mouseout", () => {polygon.set("fill",`rgba(${rgbTempColor.r}, ${rgbTempColor.g}, ${rgbTempColor.b}, 0.01)`);fCanvas.renderAll();});// 添加多边形和标签到画布fCanvas.add(polygon);fCanvas.add(label);polygon.setCoords();updateLabelPosition();});fCanvas.requestRenderAll();
}

在这里插入图片描述

第十六步、导出数据格式及其数据

let imageLabelData: any = reactive([{imgurl: "http://192.168.80.32:8888/zq.jpeg",labelInfo: [] as LabelInfo[],labelPolygonInfo: [] as any,},{imgurl: "http://192.168.80.32:8888/286_Coax_67480501_SourceImg.jpg",labelInfo: [{labelText: "猫",pos1: [0, 0],pos2: [500, 500],id: "box-1",},] as LabelInfo[],labelPolygonInfo: [],},
]);

Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

完结~

Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

Fabric.js从入门学习到实现labelImg矩形多边形标注工具【下】

优化进行中~ 关注我不定时进行代码优化与功能添加

需要代码私信我即发~

http://www.dtcms.com/a/326237.html

相关文章:

  • 河北邢台数控滑台与机器人行走轨道的内在联系
  • 煤矿工地运煤卡车的4G远程视频监控解决方案
  • QT通过qputenv设置环境变量与使用(AI生成)
  • vue2中this.$createElement()在vue3中应该如何改造
  • 开闭原则代码示例
  • Spring Framework源码解析——BeanPostProcessor
  • 进程的理解
  • 无人机航拍数据集|第12期 无人机停车场车辆计数目标检测YOLO数据集1568张yolov11/yolov8/yolov5可训练
  • 数字图像处理4
  • Spring Framework源码解析——InitializingBean
  • 线程池ThreadPoolExecutor源码剖笔记
  • 对自己的 app 进行分析, 诊断,审视
  • pcl完成halcon3d中的下采样(按对角个数)
  • 网络资源模板--基于Android Studio 实现的手绘板App
  • DNS(域名系统)详解与 BIND 服务搭建
  • C# 异步编程(BeginInvoke和EndInvoke)
  • 【Java后端】Quartz任务调度核心机制详解:从基础编排到动态控制
  • Qwen 3 架构深度解析:混合推理、MoE创新与开源生态的全面突破
  • CSPOJ:1561: 【提高】买木头
  • 智能小e-智能办公文档
  • OCAD for Orienteering 20Crack 定向越野:工作流程
  • Chrome插件开发【Service Worker练手小项目】
  • MySQL 运算符
  • [CSP-J 2021] 小熊的果篮
  • Oracle数据库Library cache lock阻塞问题排查
  • 银河麒麟V10配置KVM的Ubuntu虚机GPU直通实战
  • AI测试平台实战:深入解析自动化评分和多模型对比评测
  • 人工智能-python-机器学习-逻辑回归与K-Means算法:理论与应用
  • 机器学习之DBSCAN
  • Redis中的AOF原理详解