利用fabricjs 实现图片的标注,把标注像素点传入到后端

背景:我们有一个摄像的产品,拍照传统的水表盘面,我们需要框选水表读数,标注点传到后端,后端根据标注点自动去截取摄像表拍摄回来的图片,然后拿到大模型里面进行训练。由于同一只表拍摄的画面都是一样的,所以按此方法减少了人工标注的繁琐工作

一、效果图
请添加图片描述

在这里插入图片描述

二、问题记录

1、引入fabric

  <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js" referrerpolicy="no-referrer"></script>

2、实例化fabric,并根据图片大小设置canvas的背景大小

 loadImageAndSetCanvas() {
      this.fabricCanvas = new fabric.Canvas("canvasId");
      const img = new Image();
      img.src = this.images;
      img.onload = () => {
        this.canvasProp.width = img.width;
        this.canvasProp.height = img.height;
        // 设置canvas大小
        this.fabricCanvas.setWidth(img.width);
        this.fabricCanvas.setHeight(img.height);
        // 创建 Fabric 图片对象
        const fabricImage = new fabric.Image(img, {
          left: 0,
          top: 0,
          selectable: false // 防止背景图片被选择
        });
        // 设置背景图片
        this.fabricCanvas.setBackgroundImage(fabricImage);
      };
    },

3、画矩形和文字,实践了三种方式实现
3.1 、矩形+文字,放到一个分组里面

    drawTags(item, callback = () => {}) {
      //this.clear(); // 清空所有对象
      // tagsData.forEach(item => {
      // 创建一个矩形
      const rect = new fabric.Rect({
        fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
        width: item.width, // 矩形的宽度
        height: item.height, // 矩形的高度
        angle: 0, // 旋转角度
        stroke: item.color,
        strokeWidth: 2, // 边框宽度
      });

      // 创建第一个文字对象
      const text1 = new fabric.Text(item.label, {
        fontFamily: "Arial",
        fontSize: 20,
        fill: item.color, // 文字颜色
        // stroke: "#ffffff", // 文字边框颜色
        // strokeWidth: 0.5 // 文字边框宽度
      });

      // 计算文字位置以确保其在矩形的正中央
      text1.set({
        left: rect.left + rect.width / 2,
        top: rect.top + rect.height / 2,
        originX: "center",
        originY: "center",
      });

      // 使用fabric.Group将矩形和文字组合在一起
      const group = new fabric.Group([rect, text1], {
        left: item.isInit ? this.canvasProp.width / 2 - item.width / 2 : item.startX, // 矩形的左上角 x 坐标
        top: item.isInit ? this.canvasProp.height / 2 - item.height / 2 : item.startY, // 矩形的左上角 y 坐标
        cornerColor: item.color, // 控制点的颜色
        cornerSize: 10, // 控制点的大小
        borderWidth: 0, // 选中时的边框宽度
        transparentCorners: true,
        angle: item.rotate, // 组合对象的旋转角度
        id: item.id,
      });
      // 将组合添加到画布上
      console.log("添加到画布上", group);
      this.fabricCanvas.add(group);
      callback();
      //});
    },

3.2 实现Polygon+文字实现
此时的数据格式为:

{
            label: "基表数据",
            color: "#0000ff",
            type: "rectangle",
            dataPoint: [ ]
 }
 drawTags(item, callback = () => {}) {

      // 使用 fabric.Polygon 创建多边形
      let points = item.dataPoint;
      if (!points.length) {
        let x = this.canvasProp.width / 2 - this.width / 2;
        let y = this.canvasProp.height / 2 - this.height / 2;

        points = [
          [x, y],
          [x + this.width, y],
          [x + this.width, y + this.height],
          [x, y + this.height]
        ];
      }
      console.log("points", points);
      const polygon = new fabric.Polygon(
        points.map(point => ({ x: point[0], y: point[1] })),
        {
          fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
          stroke: item.color, // 边框颜色
          strokeWidth: 2 // 边框宽度
        }
      );

      // this.fabricCanvas.add(polygon);

      // 创建第一个文字对象
      const text = new fabric.Text(item.label, {
        fontFamily: "Arial",
        fontSize: 20,
        fill: item.color, // 文字颜色
        left: points[0][0] + this.width / 2,
        top: points[0][1] + this.height / 2,
        originX: "center",
        originY: "center"
        // stroke: "#ffffff", // 文字边框颜色
        // strokeWidth: 0.5 // 文字边框宽度
      });

      //this.fabricCanvas.add(text);

      // 使用fabric.Group将矩形和文字组合在一起
      const group = new fabric.Group([polygon], {
        left: points[0][0],
        top: points[1][1],
        cornerColor: item.color, // 控制点的颜色
        cornerSize: 10, // 控制点的大小
        borderWidth: 0, // 选中时的边框宽度
        transparentCorners: true,
        id: item.id
      });

      this.fabricCanvas.add(group);
    },

3.3 自定义LabelRect

var LabeledRect = fabric.util.createClass(fabric.Rect, {
  type: "labeledRect",

  initialize: function(options) {
    options || (options = {});

    this.callSuper("initialize", options);
    this.set("label", options.label || "");
    this.set("left", options.left || 0);
    this.set("top", options.top || 0);
    this.set("width", options.width || 0);
    this.set("height", options.height || 0);
    this.set("stroke", options.stroke || 0);
  },

  toObject: function() {
    return fabric.util.object.extend(this.callSuper("toObject"), {
      label: this.get("label")
    });
  },

  _render: function(ctx) {
    // 调用父类的渲染方法以绘制矩形
    this.callSuper("_render", ctx);
    let displayText = this.label;

    ctx.font = "14px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillStyle = this.stroke;
    ctx.strokeStyle = "white";
    ctx.strokeText(displayText, 0, 0);
    ctx.fillText(this.label, 0, 0);
  }
});


 var labeledRect = new LabeledRect({
        left: 100, // 矩形的左上角 x 坐标
        top: 100, // 矩形的左上角 y 坐标
        fill: this.hexToRgba("#0000ff", 0.2), // 填充颜色,透明度为 0.2
        width: 150, // 矩形的宽度
        height: 50, // 矩形的高度
        angle: 0, // 旋转角度
        stroke: "#0000ff",
        label: "基表数据",
        strokeWidth: 2 // 边框宽度
      });
      this.fabricCanvas.add(labeledRect);

4、添加每个数据的时候,记得添加唯一的表示ID
在这里插入图片描述
5、当编辑页面后,需要计算当前矩形四个在画布上的实际坐标点


    getPointData() {
      let result = {};
      this.fabricCanvas.getObjects().forEach((rect) => {
        console.log("rect==", rect);
        const coords = [];
        const points = rect.get("aCoords"); // 获取矩形的绝对坐标

        console.log("points", points);

        const viewportTransform = this.fabricCanvas.viewportTransform;
        const zoom = this.fabricCanvas.getZoom();

        // 将内部坐标转换为实际画布坐标
        Object.keys(points).forEach((key) => {
          let point = points[key];
          const actualX = (point.x - viewportTransform[4]) / zoom;
          const actualY = (point.y - viewportTransform[5]) / zoom;
          coords.push([Math.round(actualX), Math.round(actualY)]);
        });

        result[rect.id] = coords;
      });
      console.log("result", result);
      return result;
    },

6、如果下次需要还原现场,需要保存canvas的JSON数据,一定要假如["id],因为id是自定义属性,如果不加该id,保存的数据就没有id 这个唯一标志

  getCanvasJson() {
      return JSON.stringify(this.fabricCanvas.toJSON(["id"]));
    },

7、还原现场,因为标注点没变,背景图片变了,所以我们需要重置背景图

  loadFromJSON(json) {
      this.fabricCanvas.clear();
      //背景图替换为当前的背景图
      let newjson = JSON.parse(json);

      console.log("加载的json", newjson);
      newjson.backgroundImage = backgroundImage;
      this.fabricCanvas.setBackgroundImage();
      //this.fabricCanvas.loadFromJSON(newjson);
      // 反序列化对象
      this.fabricCanvas.loadFromJSON(newjson);
    },

8、画布的放大、缩小

    zoomBig() {
      const currentZoom = this.fabricCanvas.getZoom();
      if (currentZoom < 3) {
        this.scaleCanvas(currentZoom + 0.1);
      }
    },
    zoomSmall() {
      const currentZoom = this.fabricCanvas.getZoom();
      if (currentZoom > 1) {
        this.scaleCanvas(currentZoom - 0.1);
      }
    },
    scaleCanvas(scale) {
      const center = this.getCanvasCenter();
      this.fabricCanvas.zoomToPoint({ x: center.x, y: center.y }, scale);
    },
    //获取画布的中心点
    getCanvasCenter() {
      const canvasCenter = {
        x: this.fabricCanvas.width / 2,
        y: this.fabricCanvas.height / 2,
      };
      return canvasCenter;
    },

9、画布的平移

 handleKeyDown(event) {
      console.log("event.key", event.key);
      const step = 5; // 每次移动的步长
      switch (event.key) {
        case "ArrowUp":
          this.canvasProp.translateY -= step;![请添加图片描述](https://i-blog.csdnimg.cn/direct/36dc2234522d46f0a2d37206ea00799c.gif)

          break;
        case "ArrowDown":
          this.canvasProp.translateY += step;
          break;
        case "ArrowLeft":
          this.canvasProp.translateX -= step;
          break;
        case "ArrowRight":
          this.canvasProp.translateX += step;
          break;
      }
      this.panCanvas(this.canvasProp.translateX, this.canvasProp.translateY);
    },
   panCanvas(translateX, translateY) {
      // 获取当前的 viewportTransform
      const viewportTransform = this.fabricCanvas.viewportTransform.slice(); // 创建一个副本,以免直接修改原始数组

      // 更新平移值
      viewportTransform[4] = translateX;
      viewportTransform[5] = translateY;

      // 设置新的 viewportTransform
      this.fabricCanvas.setViewportTransform(viewportTransform);
    },

三、效果源代码

<template>
  <div
    :style="{
      width: canvasProp.width + 'px',
      height: canvasProp.height + 'px',
      border: '1px solid #ccc',
    }"
  >
    <canvas
      :width="canvasProp.width"
      :height="canvasProp.height"
      :style="{
        width: canvasProp.width + 'px',
        height: canvasProp.height + 'px',
      }"
      id="canvasId"
    ></canvas>
    <div class="muane">
      <span @click="zoomBig">
        <i class="el-icon-zoom-in"></i>
      </span>
      <span @click="zoomSmall">
        <i class="el-icon-zoom-out"></i>
      </span>
    </div>
  </div>
</template>
<script>
let backgroundImage = null;
export default {
  name: "images-tags",
  props: {
    // 矩形标注的数据
    tagsData: {
      type: Array,
      default: () => {
        return [
          {
            label: "基表数据",
            color: "#0000ff",
            type: "rectangle",
            width: 150,
            height: 50,
            rotate: 0,
            isInit: true,
            startX: 185,
            startY: 235,
          },
          {
            label: "数据点2",
            color: "#0000ff",
            type: "rectangle",
            width: 150,
            height: 50,
            rotate: 0,
            isInit: false,
            startX: 100,
            startY: 100,
          },
        ];
      },
    },
    // 图片路径
    images: {
      type: String,
      default: "/img/yejing1.jpg",
    },
  },
  data() {
    return {
      fabricCanvas: null,
      canvasProp: {
        width: 0, // canvas的宽度
        height: 0, // canvas的高度
        translateX: 0,
        translateY: 0,
      },
    };
  },
  mounted() {
    this.loadImageAndSetCanvas();
    window.addEventListener("keydown", this.handleKeyDown);
  },
  beforeDestroy() {
    window.removeEventListener("keydown", this.handleKeyDown);
  },
  methods: {
    zoomBig() {
      const currentZoom = this.fabricCanvas.getZoom();
      if (currentZoom < 3) {
        this.scaleCanvas(currentZoom + 0.1);
      }
    },
    zoomSmall() {
      const currentZoom = this.fabricCanvas.getZoom();
      if (currentZoom > 1) {
        this.scaleCanvas(currentZoom - 0.1);
      }
    },
    scaleCanvas(scale) {
      const center = this.getCanvasCenter();
      this.fabricCanvas.zoomToPoint({ x: center.x, y: center.y }, scale);
    },
    //获取画布的中心点
    getCanvasCenter() {
      const canvasCenter = {
        x: this.fabricCanvas.width / 2,
        y: this.fabricCanvas.height / 2,
      };
      return canvasCenter;
    },

    handleKeyDown(event) {
      console.log("event.key", event.key);
      const step = 5; // 每次移动的步长
      switch (event.key) {
        case "ArrowUp":
          this.canvasProp.translateY -= step;
          break;
        case "ArrowDown":
          this.canvasProp.translateY += step;
          break;
        case "ArrowLeft":
          this.canvasProp.translateX -= step;
          break;
        case "ArrowRight":
          this.canvasProp.translateX += step;
          break;
      }
      this.panCanvas(this.canvasProp.translateX, this.canvasProp.translateY);
    },
    loadFromJSON(json) {
      this.fabricCanvas.clear();
      //背景图替换为当前的背景图
      let newjson = JSON.parse(json);

      console.log("加载的json", newjson);
      newjson.backgroundImage = backgroundImage;
      this.fabricCanvas.setBackgroundImage();
      //this.fabricCanvas.loadFromJSON(newjson);
      // 反序列化对象
      this.fabricCanvas.loadFromJSON(newjson);
    },
    getCanvasJson() {
      console.log("在线获取json", this.fabricCanvas.toJSON(["id"]));
      return JSON.stringify(this.fabricCanvas.toJSON(["id"]));
    },

    hexToRgba(hex, alpha) {
      const bigint = parseInt(hex.replace("#", ""), 16);
      const r = (bigint >> 16) & 255;
      const g = (bigint >> 8) & 255;
      const b = bigint & 255;
      return `rgba(${r},${g},${b},${alpha})`;
    },
    // ...其他方法
    panCanvas(translateX, translateY) {
      // 获取当前的 viewportTransform
      const viewportTransform = this.fabricCanvas.viewportTransform.slice(); // 创建一个副本,以免直接修改原始数组

      // 更新平移值
      viewportTransform[4] = translateX;
      viewportTransform[5] = translateY;

      // 设置新的 viewportTransform
      this.fabricCanvas.setViewportTransform(viewportTransform);
    },
    loadImageAndSetCanvas() {
      this.fabricCanvas = new fabric.Canvas("canvasId");
      const img = new Image();
      img.src = this.images;
      img.onload = () => {
        console.log("图片加载完毕", img);
        this.canvasProp.width = img.width;
        this.canvasProp.height = img.height;

        // 设置canvas大小
        this.fabricCanvas.setWidth(img.width);
        this.fabricCanvas.setHeight(img.height);

        // 创建 Fabric 图片对象
        const fabricImage = new fabric.Image(img, {
          left: 0,
          top: 0,
          selectable: false, // 防止背景图片被选择
        });

        // 设置背景图片
        let backgroundImageObj = this.fabricCanvas.setBackgroundImage(fabricImage);
        backgroundImage = backgroundImageObj.backgroundImage;

        this.drawTags(this.tagsData[0])
      };
    },
    clear() {
      this.fabricCanvas.getObjects().forEach((obj) => {
        if (obj !== this.fabricCanvas.backgroundImage) {
          this.fabricCanvas.remove(obj);
        }
      });
    },
    getGroupById(id) {
      let result = null;
      this.fabricCanvas.getObjects().forEach((obj) => {
        console.log(obj);
        if (obj.id === id) {
          result = obj;
        }
      });
      return result;
    },
    remove(item) {
      console.log("id", item.id);

      let result = this.getGroupById(item.id);

      console.log("result", result);

      if (result) {
        this.fabricCanvas.remove(result);
      }
    },
    drawTags(item, callback = () => {}) {
      //this.clear(); // 清空所有对象
      // tagsData.forEach(item => {
      // 创建一个矩形
      const rect = new fabric.Rect({
        fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
        width: item.width, // 矩形的宽度
        height: item.height, // 矩形的高度
        angle: 0, // 旋转角度
        stroke: item.color,
        strokeWidth: 2, // 边框宽度
      });

      // 创建第一个文字对象
      const text1 = new fabric.Text(item.label, {
        fontFamily: "Arial",
        fontSize: 20,
        fill: item.color, // 文字颜色
        // stroke: "#ffffff", // 文字边框颜色
        // strokeWidth: 0.5 // 文字边框宽度
      });

      // 计算文字位置以确保其在矩形的正中央
      text1.set({
        left: rect.left + rect.width / 2,
        top: rect.top + rect.height / 2,
        originX: "center",
        originY: "center",
      });

      // 使用fabric.Group将矩形和文字组合在一起
      const group = new fabric.Group([rect, text1], {
        left: item.isInit ? this.canvasProp.width / 2 - item.width / 2 : item.startX, // 矩形的左上角 x 坐标
        top: item.isInit ? this.canvasProp.height / 2 - item.height / 2 : item.startY, // 矩形的左上角 y 坐标
        cornerColor: item.color, // 控制点的颜色
        cornerSize: 10, // 控制点的大小
        borderWidth: 0, // 选中时的边框宽度
        transparentCorners: true,
        angle: item.rotate, // 组合对象的旋转角度
        id: item.id,
      });
      // 将组合添加到画布上
      console.log("添加到画布上", group);
      this.fabricCanvas.add(group);
      callback();
      //});
    },
    saveData() {
      this.getPointData();
    },
    getPointData() {
      let result = {};
      this.fabricCanvas.getObjects().forEach((rect) => {
        console.log("rect==", rect);
        const coords = [];
        const points = rect.get("aCoords"); // 获取矩形的绝对坐标

        console.log("points", points);

        const viewportTransform = this.fabricCanvas.viewportTransform;
        const zoom = this.fabricCanvas.getZoom();

        // 将内部坐标转换为实际画布坐标
        Object.keys(points).forEach((key) => {
          let point = points[key];
          const actualX = (point.x - viewportTransform[4]) / zoom;
          const actualY = (point.y - viewportTransform[5]) / zoom;
          coords.push([Math.round(actualX), Math.round(actualY)]);
        });

        result[rect.id] = coords;
      });
      console.log("result", result);
      return result;
    },
  },
};
</script>
<style lang="scss" scoped>
.muane {
  height: 36px;
  background: #fafafa;
  display: flex;
  align-items: center;
  span{
    width: 50%;
    text-align: center;
  }
}
</style>

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值