Vue前端实现在图片上画线, 并将端点坐标传给后端

一. 业务需求

        需求背景是自研设备需要一个后台画线的功能, 在摄像头返回的画面上画线, 然后把坐标, 以及勾选的相关配置传给后端, 后端进行保存, 后续的机器识别基于这个配置, 对通过车辆进行车牌, 颜色等信息识别.

        具体需求是:

        1. 接收后台传递的图片、现有画线坐标和参数, 展示已有画线和勾选参数;

        2.可重新划线进行修改并保存; 

        3. 点击测试按钮, 可以在下方列表展示测试结果,并展示更新后的划线图片.

        本文主要介绍图片划线的模块.

二. 技术借鉴

基于本站fruge大佬的方案: vue2 canvas实现在图片上选点,画区域并将 坐标传递给后端

非常感谢fruge大佬, 您的方案是本文需求实现的基础.

原方案是绘制多边形区域的, 我做了相应的修改, 用于绘制线段.

扫描二维码关注公众号,回复: 17368136 查看本文章

三. 业务需求实现

1. 页面布局

画布:

      <div class="canvas-wrap">
        <canvas id="imgCanvas" ref="canvaxbox"></canvas>
        <!--用来和鼠标进行交互操作的canvas-->
        <canvas
          id="drawCanvas"
          ref="canvas"
          :style="{ cursor: isDrawing ? 'crosshair' : 'default' }"
        >
        </canvas>
        <!--存储已生成的点线,避免被清空-->
        <canvas id="saveCanvas" ref="canvasSave"></canvas>
      </div>

画三条线的按钮, 和清除标线的按钮:

          <div class="button-group">
            <a-button
              class="upper-button"
              :type="isDrawing ? 'warning' : 'primary'"
              :disabled="submitData.length != 0 || drawedPoints.length != 0"
              @click="startDraw"
              >绘制左侧标线</a-button
            >
            <a-button
              class="upper-button"
              @click="startDraw"
              :type="isDrawing ? 'warning' : 'primary'"
              :disabled="drawedPoints.length != 1"
              >绘制中间标线</a-button
            >
            <a-button
              class="upper-button"
              @click="startDraw"
              :type="isDrawing ? 'warning' : 'primary'"
              :disabled="drawedPoints.length != 2"
              >绘制右侧标线</a-button
            >
          </div>

          <div class="button-group">
            <a-button
              class="upper-button"
              :disabled="isDrawing || drawedPoints.length == 0"
              @click="clearAll"
              type="danger"
              >清除标线</a-button
            >
          </div>
2. 核心逻辑
<script>
// 我的接口引入
import { reqPic, reqPostConfig, reqTestConfig } from '@/api/camera'

export default {
  data() {
    return {
      // 这里可以先网上找个图做测试  
      imgUrl: '',
      isDrawing: false, // 是否正在绘制
      ratio: 1,
      // 配置画布的大小, 需要跟后台商量返回图片的大小和比例, 便于后续的坐标计算  
      imgWidth: '863px',
      imgHeight: '360px',
      wrapWidth: '863px',
      wrapHeight: '360px',
      canvasWidth: '863px',
      canvasHeight: '360px',
      drawingPoints: [],
      drawedPoints: [],
      imgCanvas: null,
      imgCtx: null,
      drawCanvas: null,
      drawCtx: null,
      saveCanvas: null,
      saveCtx: null,
      // 暂且写死的坐标用于测试效果
      submitData: [
        {
          polygon: {
            x1: 330,
            y1: 200,
            x2: 190,
            y2: 350,
          },
        },
        {
          polygon: {
            x1: 440,
            y1: 200,
            x2: 430,
            y2: 350,
          },
        },
        {
          polygon: {
            x1: 535,
            y1: 200,
            x2: 638,
            y2: 350,
          },
        },
      ],
      loadData: [],
      loading: false,
      picData: {},
      submitPoints: {},
      parameter: {},
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.initCanvas()
      // this.initImgCanvas() // Call initImgCanvas here
      this.getImage()
    })
  },
  methods: {
    initCanvas() {
      // 初始化canvas画布

      let canvasWrap = document.getElementsByClassName('canvas-wrap')
      console.log(canvasWrap)
      this.wrapWidth = canvasWrap[0].clientWidth
      this.wrapHeight = canvasWrap[0].clientHeight

      this.imgCanvas = document.getElementById('imgCanvas')
      this.imgCtx = this.imgCanvas.getContext('2d')

      // 绘制canvas
      this.drawCanvas = document.getElementById('drawCanvas')
      this.drawCtx = this.drawCanvas.getContext('2d')

      // 保存绘制区域 saveCanvas
      this.saveCanvas = document.getElementById('saveCanvas')
      this.saveCtx = this.saveCanvas.getContext('2d')
      // this.initImgCanvas()
    },
    initImgCanvas() {
      // 计算宽高比
      let ww = this.wrapWidth // 画布宽度
      let wh = this.wrapHeight // 画布高度
      let iw = this.imgWidth // 图片宽度
      let ih = this.imgHeight // 图片高度

      if (iw / ih < ww / wh) {
        // 以高为主
        this.ratio = ih / wh
        this.canvasHeight = wh
        this.canvasWidth = (wh * iw) / ih
      } else {
        // 以宽为主
        this.ratio = iw / ww
        this.canvasWidth = ww
        this.canvasHeight = (ww * ih) / iw
      }
      // 初始化画布大小
      this.imgCanvas.width = this.canvasWidth
      this.imgCanvas.height = this.canvasHeight
      this.drawCanvas.width = this.canvasWidth
      this.drawCanvas.height = this.canvasHeight
      this.saveCanvas.width = this.canvasWidth
      this.saveCanvas.height = this.canvasHeight

      // 图片加载绘制
      let img = document.createElement('img')
      img.src = this.imgUrl
      img.onload = () => {
        console.log('图片已加载')
        this.imgCtx.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight)
        this.renderDatas() // 渲染原有数据
      }
    },
    startDraw() {
      // 绘制区域
      if (this.isDrawing) return
      this.isDrawing = true
      // 绘制逻辑
      this.drawCanvas.addEventListener('click', this.drawImageClickFn)
      this.drawCanvas.addEventListener('dblclick', this.drawImageDblClickFn)
      this.drawCanvas.addEventListener('mousemove', this.drawImageMoveFn)
    },
    clearAll() {
      // 清空所有绘制区域
      this.saveCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      this.drawedPoints = []
      this.submitData = []
    },
    // 从后台获取图片并整理坐标的逻辑
    async getImage() {
      const res = await reqPic()

      console.log('pic data', res)

      // 后台给我的是base64格式, 这里加上必要的前缀, 图片即可显示  
      this.imgUrl = 'data:image/png;base64,' + res.scribing_image

      const reqPoints = res.coordinate

      // 遍历 reqPoints 中的对象
      for (let key in reqPoints) {
        // 获取子对象或数组
        let subObjectOrArray = reqPoints[key]

        // 打印子对象或数组
        // console.log(subObjectOrArray)

        // 检查是否为数组
        if (Array.isArray(subObjectOrArray) && subObjectOrArray.length === 4) {
          // 将数组中的每个数字除以6
          let modifiedArray = subObjectOrArray.map((num) =>
            (Number(num) / 6).toFixed()
          )

          // 在 reqPoints 对象中更新数组
          reqPoints[key] = modifiedArray
        }
      }

      // 打印修改后的 reqPoints 对象
      console.log('drawline points', reqPoints)
      
      // 整理从后台获取的坐标进行绘制
      this.submitData = [
        {
          polygon: {
            x1: reqPoints.lineone[0],
            y1: reqPoints.lineone[1],
            x2: reqPoints.lineone[2],
            y2: reqPoints.lineone[3],
          },
        },
        {
          polygon: {
            x1: reqPoints.linetwo[0],
            y1: reqPoints.linetwo[1],
            x2: reqPoints.linetwo[2],
            y2: reqPoints.linetwo[3],
          },
        },
        {
          polygon: {
            x1: reqPoints.linethree[0],
            y1: reqPoints.linethree[1],
            x2: reqPoints.linethree[2],
            y2: reqPoints.linethree[3],
          },
        },
      ]
      // console.log('base64 file', this.imgUrl)
      this.imgWidth = 683
      this.imgHeight = 360
      this.imgUrl && this.initImgCanvas()
    },
    
    // 这里对用户行为进行了限制, 只允许单击一次设置起点, 然后双击设置终点, 完成一条线的绘制
    drawImageClickFn(e) {
      let drawCtx = this.drawCtx
      if (e.offsetX || e.layerX) {
        let pointX = e.offsetX == undefined ? e.layerX : e.offsetX
        let pointY = e.offsetY == undefined ? e.layerY : e.offsetY
        // Check if there are fewer than 1 endpoints
        if (this.drawingPoints.length < 1) {
          let lastPoint =
            this.drawingPoints[this.drawingPoints.length - 1] || []

          // Check if the current point is different from the last recorded point
          if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
            this.drawingPoints.push([pointX, pointY])
          }
        }
      }
    },
    drawImageMoveFn(e) {
      let drawCtx = this.drawCtx
      if (e.offsetX || e.layerX) {
        let pointX = e.offsetX == undefined ? e.layerX : e.offsetX
        let pointY = e.offsetY == undefined ? e.layerY : e.offsetY
        // 绘制
        drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

        // 绘制点
        drawCtx.fillStyle = 'yellow'
        this.drawingPoints.forEach((item, i) => {
          drawCtx.beginPath()
          drawCtx.arc(...item, 6, 0, 180)
          drawCtx.fill() //填充
        })

        // 绘制动态区域
        drawCtx.save()
        drawCtx.beginPath()
        this.drawingPoints.forEach((item, i) => {
          drawCtx.lineTo(...item)
        })
        drawCtx.lineTo(pointX, pointY)
        drawCtx.lineWidth = '3'
        drawCtx.strokeStyle = 'yellow'
        drawCtx.fillStyle = 'rgba(255, 0, 0, 0.3)'
        drawCtx.stroke()
        drawCtx.fill() //填充
        drawCtx.restore()
      }
    },
    drawImageDblClickFn(e) {
      let drawCtx = this.drawCtx
      let saveCtx = this.saveCtx
      if (e.offsetX || e.layerX) {
        let pointX = e.offsetX == undefined ? e.layerX : e.offsetX
        let pointY = e.offsetY == undefined ? e.layerY : e.offsetY
        let lastPoint = this.drawingPoints[this.drawingPoints.length - 1] || []
        if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
          this.drawingPoints.push([pointX, pointY])
        }
      }
      // 清空绘制图层
      drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      // 绘制区域至保存图层
      this.drawSaveArea(this.drawingPoints)

      this.drawedPoints.push(this.drawingPoints)
      this.drawingPoints = []
      this.isDrawing = false

      // 绘制结束逻辑
      this.drawCanvas.removeEventListener('click', this.drawImageClickFn)
      this.drawCanvas.removeEventListener('dblclick', this.drawImageDblClickFn)
      this.drawCanvas.removeEventListener('mousemove', this.drawImageMoveFn)
    },
    drawSaveArea(points) {
      // console.log(points, "points");
      if (points.length === 0) return
      this.saveCtx.save()
      this.saveCtx.beginPath()
      points.forEach((item, i) => {
        this.saveCtx.lineTo(...item)
      })
      this.saveCtx.closePath()
      this.saveCtx.lineWidth = '5'
      this.saveCtx.fillStyle = 'rgba(255,0, 255, 0.3)'
      this.saveCtx.strokeStyle = 'yellow'
      this.saveCtx.stroke()
      this.saveCtx.fill() //填充
      this.saveCtx.restore()
    },

    savePoints() {
      // 将画布坐标数据转换成提交数据
      let objectPoints = []

      objectPoints = this.drawedPoints.map((area) => {
        let polygon = {}
        area.forEach((point, i) => {
          polygon[`x${i + 1}`] = Math.round(point[0] * this.ratio)
          polygon[`y${i + 1}`] = Math.round(point[1] * this.ratio)
        })
        return {
          polygon: polygon,
        }
      })

      const coordinates = {
        lineone: [],
        linetwo: [],
        linethree: [],
      }
      
      // 与后台商议, 把原图缩放6倍, 所以提交给后台之前, 也要相应地乘以6
      objectPoints.forEach((data, index) => {
        const { x1, y1, x2, y2 } = data.polygon
        coordinates[
          `${
            index + 1 === 1
              ? 'lineone'
              : index + 1 === 2
              ? 'linetwo'
              : 'linethree'
          }`
        ] = [x1 * 6, y1 * 6, x2 * 6, y2 * 6]
      })

      console.log('converted points', coordinates)
      this.submitPoints = coordinates
      console.log('最终提交坐标', this.submitPoints)

      // 我在这里做了一些对用户行为限制的判断, 包括上下颠倒, 三条线的顺序等, 各位可以根据实际需要进行配置

      // 构建要发送到后台的数据
      let dataToSend = {}

      dataToSend = {
        ...dataToSend,
        coordinates: coordinates,
      }
      console.log('combined value', dataToSend)

      this.parameter = dataToSend

      this.postConfig()
    },
    async postConfig() {
      const res = await reqPostConfig(this.parameter)
      if (res.result_code === 200) {
        this.$message.success('保存配置成功')
      } else {
        this.$message.error('配置失败')
      }
    },
    renderDatas() {
      // 将提交数据数据转换成画布坐标
      this.drawedPoints = this.submitData.map((item) => {
        let polygon = item.polygon
        let points = []
        for (let i = 1; i < Object.keys(polygon).length / 2 + 1; i++) {
          if (!isNaN(polygon[`x${i}`]) && !isNaN(polygon[`y${i}`])) {
            points.push([
              polygon[`x${i}`] / this.ratio,
              polygon[`y${i}`] / this.ratio,
            ])
          }
        }
        this.drawSaveArea(points)
        return points
      })
    },
  },
}
</script>
3. 样式

请参考即可, 为了使上述第一小节的模版布局更清晰, 在第一小节我删掉了不必要的父元素

<style lang="less" scoped>
.main {
  margin: 0;
  padding: 0;
}
.upper {
  display: flex;
  height: 400px;
  width: 100%;
  margin-bottom: -25px;
  .upper-left {
    background-color: skyblue;
    width: 683px;
    min-width: 683px;
    height: 360px;
    min-height: 360px;
    position: relative;
  }

  .upper-right {
    margin: 0 0 0 20px;
    display: flex;
    flex-direction: column;

    .draw-buttons {
      display: flex;
      flex-direction: column;
      .button-group {
        margin-bottom: 10px;
        .upper-button {
          margin: 0 10px 5px 0;
          height: 5.5vh;
          width: 8.5vw;
          font-size: 1vw;
          font-weight: bold;
        }
      }
    }

    .function {
      width: 27vw;
      margin: -20px 0;
      .submit-button {
        height: 5.5vh;
        width: 8vw;
        font-size: 16px;
        margin: 0 0 0 2.5vw;
        font-weight: bold;
      }
    }
  }
}


.canvas-wrap {
  width: 683px;
  height: 360px;
  min-width: 683px;
  min-height: 360px;
  // margin: 0px auto;
  background-color: skyblue;
  position: relative;
}
#imgCanvas,
#drawCanvas,
#saveCanvas {
  background: rgba(255, 0, 255, 0);
  position: absolute;
  // top: 50%;
  // left: 50%;
  // transform: translate(-50%, -50%);
  width: 683px;
  height: 360px;
}
#drawCanvas {
  z-index: 2;
}
</style>

四. 总结

        本项目的目标用户主要是本公司其他部门的调试人员, 加上后边还排着其他的项目(其他前端同事都离职了, 我必须都得顶起来哇), 所以没有过多优化, 更多的是对用户行为进行了限制, 一定程度上影响了用户体验. 划线的动作应该是有优化空间的.

        最后祝各位开发顺利~~

猜你喜欢

转载自blog.csdn.net/hero8011/article/details/135080810