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

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

可关注,参考另外一篇文章:利用fabricjs 实现图片的标注,把标注像素点传入到后端

解锁前端难题:亲手实现一个图片标注工具

遗留问题:
1、矩形框旋转后,鼠标悬浮在缩放标注点的位置上,鼠标的样式无法旋转角度
2、矩形框旋转后,拖动缩放的标准变了
备注:经测试,不管怎么变化,传入到后端的像素点是对的

一、效果图

请添加图片描述

二、问题分解

三、源代码

<template>
  <div
    :style="{
      width: canvasProp.width + 'px',
      height: canvasProp.height + 'px',
      border: '1px solid #ccc'
    }"
  >
    <canvas
      ref="canvas"
      :width="canvasProp.width"
      :height="canvasProp.height"
      @mousedown="onMouseDown"
      @mousemove="onMouseMove"
      @mouseup="onMouseUp"
      :style="{
        width: canvasProp.width + 'px',
        height: canvasProp.height + 'px'
      }"
    ></canvas>
    <div @click="saveData">保存数据</div>
    <div @click="zoomBig">放大</div>
    <div @click="zoomSmall">缩小</div>
  </div>
</template>
  
  <script>
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
          }
        ];
      }
    },
    // 图片路径
    images: {
      type: String,
      default: "/img/yejing1.jpg"
    }
  },
  data() {
    return {
      ctx: null,
      cursorClass: "",
      initCenterX: 0,
      initCenterY: 0,
      rotateImages: null, //旋转图标是否加载
      bgImage: null, //背景图是否加载
      canvasProp: {
        width: 0, // canvas的宽度
        height: 0, // canvas的高度
        scale: 1, // canvas的缩放比例
        scaleX: 0,
        scaleY: 0,
        translateX: 0,
        translateY: 0
      },
      selectedTag: null, // 当前选中的矩形框
      isResizing: false,
      isDragging: false,
      isRotating: false,
      resizeHandle: null,
      dragOffsetX: 0,
      dragOffsetY: 0,
      mouseDownX: 0,
      mouseDownY: 0,
      initialRotation: 0,
      isCanvasDraging: false
    };
  },
  mounted() {
    this.loadImageAndSetCanvas();
    window.addEventListener("keydown", this.handleKeyDown);
    window.addEventListener("wheel", this.onWheel, { passive: false });

    console.log("保存的数据===", this.tagsData);
  },
  beforeDestroy() {
    window.removeEventListener("keydown", this.handleKeyDown);
    window.removeEventListener("wheel", this.onWheel);
  },
  methods: {
    zoomBig() {
      this.zoom(true, this.initCenterX, this.initCenterY);
    },
    zoomSmall() {
      this.zoom(false, this.initCenterX, this.initCenterY);
    },
    onWheel(event) {
      if (event.ctrlKey) {
        // detect pinch
        event.preventDefault(); // prevent zoom
        this.zoom(event.deltaY < 0, event.offsetX, event.offsetY);
      }
    },
    zoom(iszoomBig, zoomCenterX, zoomCenterY) {
      if (iszoomBig) {
        console.log("Pinching 放大");
        if (this.canvasProp.scale < 3) {
          this.canvasProp.scaleX = zoomCenterX;
          this.canvasProp.scaleY = zoomCenterY;
          this.canvasProp.scale = Math.min(this.canvasProp.scale + 0.1, 3);
        }
        this.drawTags();
      } else {
        if (this.canvasProp.scale > 1) {
          this.canvasProp.scaleX = zoomCenterX;
          this.canvasProp.scaleY = zoomCenterY;
          this.canvasProp.scale = Math.max(this.canvasProp.scale - 0.1, 1);
          this.drawTags();
        }
      }
    },
    computexy(x, y) {
      let { scaleX, scale, scaleY, translateX, translateY } = this.canvasProp;
      const xy = {
        // x: x / scale - translateX,
        // y: y / scale - translateY,
        offsetX: (x - scaleX * (1 - scale) - translateX * scale) / scale,
        offsetY: (y - scaleY * (1 - scale) - translateY * scale) / scale
      };
      return xy;
    },
    computewh(width, height) {
      return {
        width: width / scale,
        height: height / scale
      };
    },
    handleKeyDown(event) {
      console.log("event.key", event.key);
      const step = 10; // 每次移动的步长
      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.drawTags(); // 重新绘制画布
    },
    saveData() {
      console.log("保存的数据", this.tagsData);
      let pointData = this.getPointData();
      console.log("pointData", pointData);
      this.setPointData(pointData);
      // this.$emit("saveData",this.setPointData(pointData));
    },
    getPointData() {
      const result = this.tagsData.map(tag => {
        const { startX, startY, width, height, rotate } = tag;
        const centerX = startX + width / 2;
        const centerY = startY + height / 2;

        const points = [
          { x: startX, y: startY }, // Top-left
          { x: startX + width, y: startY }, // Top-right
          { x: startX + width, y: startY + height }, // Bottom-right
          { x: startX, y: startY + height } // Bottom-left
        ];

        const rotatedPoints = points.map(point => {
          const dx = point.x - centerX;
          const dy = point.y - centerY;
          const rotatedX =
            centerX +
            dx * Math.cos((rotate * Math.PI) / 180) -
            dy * Math.sin((rotate * Math.PI) / 180);
          const rotatedY =
            centerY +
            dy * Math.cos((rotate * Math.PI) / 180) +
            dx * Math.sin((rotate * Math.PI) / 180);
          return [Math.round(rotatedX), Math.round(rotatedY)];
        });

        return rotatedPoints;
      });
      return result;
    },
    setPointData(result) {
      const newTagData = result.map(points => {
        const [p1, p2, p3, p4] = points;

        const centerX = (p1[0] + p3[0]) / 2;
        const centerY = (p1[1] + p3[1]) / 2;

        const width = Math.sqrt(
          Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2)
        );
        const height = Math.sqrt(
          Math.pow(p4[0] - p1[0], 2) + Math.pow(p4[1] - p1[1], 2)
        );

        const rotate =
          Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * (180 / Math.PI);

        return {
          label: "新矩形", // 可以根据需要更改标签
          color: "#0000ff", // 可以根据需要更改颜色
          type: "rectangle",
          startX: centerX - width / 2,
          startY: centerY - height / 2,
          width: width,
          height: height,
          rotate: rotate,
          isInit: false
        };
      });
      console.log("newTagData", newTagData);

      return newTagData;

      //this.tagsData = newTagData;
      //this.drawTags();
    },
    loadImageAndSetCanvas() {
      const img = new Image();
      img.src = this.images;
      img.onload = () => {
        this.bgImage = img;
        this.canvasProp.width = img.width;
        this.canvasProp.height = img.height;
        this.initCenterX = img.width / 2;
        this.initCenterY = img.height / 2;
        this.ctx = this.$refs.canvas.getContext("2d");
        this.$nextTick(() => {
          this.drawTags();
        });
      };
    },
    drawTags() {
      this.ctx.clearRect(0, 0, this.canvasProp.width, this.canvasProp.height);
      this.ctx.save();
      if (this.bgImage) {
        //画布缩放
        this.ctx.translate(this.canvasProp.scaleX, this.canvasProp.scaleY);
        this.ctx.scale(this.canvasProp.scale, this.canvasProp.scale);
        this.ctx.translate(-this.canvasProp.scaleX, -this.canvasProp.scaleY);
        //画布平移
        this.ctx.translate(
          this.canvasProp.translateX,
          this.canvasProp.translateY
        );

        this.ctx.drawImage(
          this.bgImage,
          0,
          0,
          this.bgImage.width,
          this.bgImage.height
        );

        this.tagsData.forEach(tag => {
          if (tag.type === "rectangle") {
            this.drawRectangle(tag);
          }
        });
      }
      this.ctx.restore();
    },
    rotateExec(tag) {
      let { startX, startY, width, height, rotate } = tag;
      this.ctx.translate(startX + width / 2, startY + height / 2);
      this.ctx.rotate((rotate * Math.PI) / 180);
      this.ctx.translate(-(startX + width / 2), -(startY + height / 2));
    },
    //手动添加输入框的时候
    drawRectangle(tag) {
      const { label, color, width, height, rotate, isInit } = tag;
      if (isInit) {
        tag.startX = this.initCenterX - width / 2;
        tag.startY = this.initCenterY - height / 2;
      }
      // 旋转矩形框,平移-旋转-平移到原来
      this.rotateExec(tag);
      this.ctx.save();
      // Draw the rectangle
      this.ctx.beginPath();
      this.ctx.rect(tag.startX, tag.startY, width, height);
      this.ctx.fillStyle = this.hexToRgba(color, 0.2);
      this.ctx.fill();
      this.ctx.lineWidth = 2;
      this.ctx.strokeStyle = color;
      this.ctx.stroke();
      //旋转矩形框

      // Draw the label text
      this.ctx.font = "14px Arial";
      this.ctx.textAlign = "center";
      this.ctx.textBaseline = "middle";
      let textX = tag.startX + width / 2;
      let textY = tag.startY + height / 2;
      let displayText = label;
      if (this.ctx.measureText(label).width > width) {
        displayText = this.truncateText(label, width);
      }
      this.ctx.fillStyle = color;
      this.ctx.strokeStyle = "white";
      this.ctx.lineWidth = 1;
      this.ctx.strokeText(displayText, textX, textY);
      this.ctx.fillText(displayText, textX, textY);

      this.drawResizeHandles(tag);
      this.drawRotateHandle(tag);
      this.ctx.restore();
      tag.isInit = false;
    },
    drawResizeHandles(tag) {
      const { startX, startY, width, height, color, rotate } = tag;
      const handles = [
        { x: startX, y: startY },
        { x: startX + width / 2, y: startY },
        { x: startX + width, y: startY },
        { x: startX, y: startY + height / 2 },
        { x: startX + width, y: startY + height / 2 },
        { x: startX, y: startY + height },
        { x: startX + width / 2, y: startY + height },
        { x: startX + width, y: startY + height }
      ];
      this.ctx.save();
      //this.rotateExec(tag);
      handles.forEach(handle => {
        this.ctx.beginPath();
        this.ctx.rect(handle.x - 2.5, handle.y - 2.5, 5, 5);
        this.ctx.fillStyle = "white";
        this.ctx.fill();
        this.ctx.lineWidth = 1;
        this.ctx.strokeStyle = color;
        this.ctx.stroke();
        //添加鼠标悬浮事件,如果鼠标悬浮在矩形框上,则鼠标样式显示为resize样式,否则显示为默认样式
      });
      this.ctx.restore();
    },
    drawRotateHandle(tag) {
      const { startX, startY, width, height, color, rotate } = tag;
      const handleX = startX + width;
      const handleY = startY - 12 - 5;
      this.ctx.save();
      // this.rotateExec(tag);
      this.ctx.beginPath();
      if (!this.rotateImages) {
        console.log("记载旋1转图片");
        var img = new Image();
        img.src = "/img/tagRotate.png";
        img.onload = () => {
          this.rotateImages = img;
          this.ctx.drawImage(img, handleX, handleY, 24, 24);
          this.ctx.restore();
        };
      } else {
        this.ctx.drawImage(this.rotateImages, handleX, handleY, 24, 24);
        this.ctx.restore();
      }
    },
    truncateText(text, maxWidth) {
      const ellipsis = "...";
      let truncated = text;
      while (this.ctx.measureText(truncated + ellipsis).width > maxWidth) {
        truncated = truncated.slice(0, -1);
      }
      return truncated + ellipsis;
    },
    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})`;
    },
    onMouseDown(e) {
      const { offsetX, offsetY } = this.computexy(e.offsetX, e.offsetY);
      this.mouseDownX = offsetX;
      this.mouseDownY = offsetY;
      this.tagsData.forEach(tag => {
        const handle = this.getHandleUnderMouse(tag, offsetX, offsetY);
        if (handle) {
          this.isResizing = true; //缩放
          this.resizeHandle = handle;
          this.selectedTag = tag;
          return;
        }
        const rotateHandle = this.getRotateHandleUnderMouse(
          tag,
          offsetX,
          offsetY
        );
        if (rotateHandle) {
          this.isRotating = true; //旋转
          this.selectedTag = tag;
          this.initialRotation = this.selectedTag.rotate; // 保存初始旋转角度
          return;
        }
        if (this.isMouseInsideRectangle(tag, offsetX, offsetY)) {
          this.isDragging = true;
          this.selectedTag = tag;
          this.dragOffsetX = offsetX - tag.startX;
          this.dragOffsetY = offsetY - tag.startY;
        }
      });

      // if (!this.isDragging && !this.isResizing && !this.isRotating) {
      //   console.log("拖动canvas大小");
      //   this.$refs.canvas.style.cursor = "hand";
      //   this.isCanvasDraging = true;
      // }
    },
    onMouseUp() {
      this.isDragging = false;
      this.isResizing = false;
      this.isRotating = false;
      this.selectedTag = null;
      this.resizeHandle = null;
      this.isCanvasDraging = false;
    },
    onMouseMove(e) {
      // console.log("鼠标移动事件", e);
      const { offsetX, offsetY } = this.computexy(e.offsetX, e.offsetY);

      // if (this.isCanvasDraging) {

      //   this.canvasProp.translateX -= offsetX - this.mouseDownX;
      //   this.canvasProp.translateY -= offsetY - this.mouseDownY;
      //   this.drawTags();
      //   return;
      // }
      if (this.isDragging && this.selectedTag) {
        //矩形框拖动
        this.selectedTag.startX = offsetX - this.dragOffsetX;
        this.selectedTag.startY = offsetY - this.dragOffsetY;
        this.drawTags();
      } else if (this.isResizing && this.selectedTag) {
        //矩形框缩放
        const handle = this.resizeHandle;
        switch (handle.position) {
          case "top-left":
            this.selectedTag.width += this.selectedTag.startX - offsetX;
            this.selectedTag.height += this.selectedTag.startY - offsetY;
            this.selectedTag.startX = offsetX;
            this.selectedTag.startY = offsetY;
            break;
          case "top":
            this.selectedTag.height += this.selectedTag.startY - offsetY;
            this.selectedTag.startY = offsetY;
            break;
          case "top-right":
            this.selectedTag.width = offsetX - this.selectedTag.startX;
            this.selectedTag.height += this.selectedTag.startY - offsetY;
            this.selectedTag.startY = offsetY;
            break;
          case "left":
            this.selectedTag.width += this.selectedTag.startX - offsetX;
            this.selectedTag.startX = offsetX;
            break;
          case "right":
            this.selectedTag.width = offsetX - this.selectedTag.startX;
            break;
          case "bottom-left":
            this.selectedTag.width += this.selectedTag.startX - offsetX;
            this.selectedTag.height = offsetY - this.selectedTag.startY;
            this.selectedTag.startX = offsetX;
            break;
          case "bottom":
            this.selectedTag.height = offsetY - this.selectedTag.startY;
            break;
          case "bottom-right":
            this.selectedTag.width = offsetX - this.selectedTag.startX;
            this.selectedTag.height = offsetY - this.selectedTag.startY;
            break;
        }
        this.drawTags();
      } else if (this.isRotating && this.selectedTag) {
        //矩形旋转
        const centerX = this.selectedTag.startX + this.selectedTag.width / 2;
        const centerY = this.selectedTag.startY + this.selectedTag.height / 2;

        const initDeg = Math.atan2(
          this.mouseDownY - centerY,
          this.mouseDownX - centerX
        );
        const currentDeg = Math.atan2(offsetY - centerY, offsetX - centerX);
        // this.selectedTag.rotate = ((currentDeg - initDeg) * 180) / Math.PI;
        const rotationChange = ((currentDeg - initDeg) * 180) / Math.PI;
        this.selectedTag.rotate = this.initialRotation + rotationChange; // 根据初始旋转角度调整
        this.drawTags();
      } else {
        let cursorSet = false;

        this.tagsData.some(tag => {
          const handle = this.getHandleUnderMouse(tag, offsetX, offsetY);
          if (handle) {
            let cursor = this.getCursorStyle(handle);
            this.$refs.canvas.style.cursor = cursor;
            cursorSet = true;
            return true;
          }
          const rotateHandle = this.getRotateHandleUnderMouse(
            tag,
            offsetX,
            offsetY
          );
          if (rotateHandle) {
            this.$refs.canvas.style.cursor = "crosshair";
            cursorSet = true;
            return true;
          }
          if (this.isMouseInsideRectangle(tag, offsetX, offsetY)) {
            this.$refs.canvas.style.cursor = "move";
            cursorSet = true;
            return true;
          }
          return false;
        });

        if (!cursorSet) {
          this.$refs.canvas.style.cursor = "default";
        }
      }
    },
    getCursorCustomStyle(handle) {
      if (handle.position == "left" || handle.position == "right") {
        return `h-cursor`;
      } else if (handle.position === "top" || handle.position == "bottom") {
        return `s-cursor`;
      } else if (
        handle.position === "top-left" ||
        handle.position == "top-right"
      ) {
        return `lx-cursor`;
      } else if (
        handle.position === "bottom-left" ||
        handle.position == "bottom-right"
      ) {
        return `-cursor`;
      }
    },
    getCursorStyle(handle) {
      if (handle.position == "left") {
        return `w-resize`;
      } else if (handle.position === "top") {
        return `n-resize`;
      } else if (handle.position === "top-left") {
        return `nw-resize`;
      } else if (handle.position === "top-right") {
        return `ne-resize`;
      } else if (handle.position === "right") {
        return `e-resize`;
      } else if (handle.position === "bottom") {
        return `s-resize`;
      } else if (handle.position == "bottom-left") {
        return `sw-resize`;
      } else if (handle.position === "bottom-right") {
        return `se-resize`;
      }
    },

    getHandleUnderMouse(tag, x, y) {
      const handles = [
        {
          x: tag.startX,
          y: tag.startY,
          position: "top-left"
        },
        {
          x: tag.startX + tag.width / 2,
          y: tag.startY,
          position: "top"
        },
        {
          x: tag.startX + tag.width,
          y: tag.startY,
          position: "top-right"
        },
        {
          x: tag.startX,
          y: tag.startY + tag.height / 2,
          position: "left"
        },
        {
          x: tag.startX + tag.width,
          y: tag.startY + tag.height / 2,
          position: "right"
        },
        {
          x: tag.startX,
          y: tag.startY + tag.height,
          position: "bottom-left"
        },
        {
          x: tag.startX + tag.width / 2,
          y: tag.startY + tag.height,
          position: "bottom"
        },
        {
          x: tag.startX + tag.width,
          y: tag.startY + tag.height,
          position: "bottom-right"
        }
      ];
      return handles.find(handle => {
        let { rotatedX, rotatedY } = this.rotateAfterPoint(tag, x, y);
        return this.isMouseOverHandle(handle, rotatedX, rotatedY);
      });
    },
    isMouseOverHandle(handle, x, y) {
      return (
        x >= handle.x - 2.5 &&
        x <= handle.x + 2.5 &&
        y >= handle.y - 2.5 &&
        y <= handle.y + 2.5
      );
    },
    getRotateHandleUnderMouse(tag, x, y) {
      let { rotatedX, rotatedY } = this.rotateAfterPoint(tag, x, y);

      const handleX = tag.startX + tag.width;
      const handleY = tag.startY - 12 - 5;
      if (
        rotatedX > handleX &&
        rotatedX <= handleX + 24 &&
        rotatedY > handleY &&
        rotatedY <= handleY + 24
      ) {
        return true;
      } else {
        return false;
      }
    },
    isMouseInsideRectangle(tag, x, y) {
      const { startX, startY, width, height, rotate } = tag;
      this.ctx.save();
      //this.rotateExec(tag);
      let { rotatedX, rotatedY } = this.rotateAfterPoint(tag, x, y);
      const isInside =
        rotatedX >= startX &&
        rotatedX <= startX + width &&
        rotatedY >= startY &&
        rotatedY <= startY + height;
      this.ctx.restore();
      return isInside;
    },
    //解决这个问题有两个思路,一个是将旋转后矩形的四个点坐标计算出来,这种方法比较麻烦。另一个思路是逆向的,将要判断的点,以矩形的中点为中心,做逆向旋转,计算出其在 canvas 中的坐标,这个坐标,可以继续参与我们之前点在矩形内的计算
    rotateAfterPoint(tag, x, y) {
      const { startX, startY, width, height, rotate } = tag;
      const centerX = startX + width / 2;
      const centerY = startY + height / 2;

      let dx = x - centerX;
      let dy = y - centerY;
      // // 将鼠标点旋转回矩形未旋转前的坐标
      let rotatedX =
        dx * Math.cos((-rotate * Math.PI) / 180) -
        dy * Math.sin((-rotate * Math.PI) / 180) +
        centerX;
      let rotatedY =
        dy * Math.cos((-rotate * Math.PI) / 180) +
        dx * Math.sin((-rotate * Math.PI) / 180) +
        centerY;

      return { rotatedX: rotatedX, rotatedY: rotatedY };
    }
  }
};
</script>
  
  <style scoped>
.h-cursor {
  cursor: url("/img/h-custor"), auto !important;
}
.s-cursor {
  cursor: url("/img/s-custor"), auto !important;
}
.lx-cursor {
  cursor: url("/img/lx-custor"), auto !important;
}
.yx-cursor {
  cursor: url("/img/yx-custor"), auto !important;
}
.rotate-cursor {
  cursor: url("/img/yx-custor"), auto !important;
}
</style>
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值