vue3-canvas实现在图片上框选标记(放大,缩小,移动,删除)

news2024/12/27 22:06:53

双图版本(模板对比)

业务描述:模板与图片对比,只操作模板框选的位置进行色差对比,传框选坐标位置给后端,返回对比结果显示

draw.js文件:

  1. 新增了 createUuid,和求取两个数组差集的方法getArrayDifference
  2. 新增了两个参数:是否可删除delEnable, 是否可新增newEnable-‘1’是可,‘0’是不可
/**
 * 画布中绘制矩形
 * 参数: cav-画布对象  list-矩形数组 i-选中矩形下标
 **/
let globalZoom = 1

/* 操作执行方法分发 */
export function draw(cav, list, i, zoom,) {
  globalZoom = zoom || globalZoom
  // 画布初始化
  let ctx = cav.getContext('2d')
  ctx.strokeStyle = 'red'
  ctx.lineWidth = 2

  // 变量初始化
  let sX = 0 // 鼠标X坐标
  let sY = 0 // 鼠标Y坐标

  /*
   *鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形
   */
  cav.onmousemove = function (em) {
    sX = em.offsetX
    sY = em.offsetY
    let iem = undefined // 鼠标移动时临时存储当前鼠标所在矩形的下标

    if (list.length === 0) {
      // **** 无矩形 ****
      // 绘制新矩形
      newDraw(cav, ctx, list)
    } else if (i === undefined) {
      // **** 已有矩形无选中 ****
      // 判断鼠标位置
      list.forEach(function (value, index, array) {
        if (
          value.w > 0 &&
          value.h > 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // 鼠标在右下方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem)
        }
        if (
          value.w < 0 &&
          value.h > 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // 鼠标在左下方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem)
        }
        if (
          value.w > 0 &&
          value.h < 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // 鼠标在右上方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem)
        }
        if (
          value.w < 0 &&
          value.h < 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // 鼠标在左上方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem)
        }
        if (iem === undefined) {
          // 鼠标不在矩形中
          newDraw(cav, ctx, list)
        }
      })
    } else {
      // **** 已有选中矩形 ****
      // 判断鼠标位置
      for (let index = 0; index < list.length; index++) {
        let value = list[index]
        if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {
          // ***  鼠标在起点角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 1)
            break
          }
        } else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {
          // ***  鼠标在起点横向角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 2)
            break
          }
        } else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
          // ***  鼠标在起点纵向角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 3)
            break
          }
        } else if (
          sX < value.x + value.w + 5 &&
          sX > value.x + value.w - 5 &&
          sY < value.y + value.h + 5 &&
          sY > value.y + value.h - 5
        ) {
          // ***  鼠标在终点角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 4)
            break
          }
        } else if (
          value.w > 0 &&
          value.h > 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // ***  鼠标在右下方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index)
          break
        } else if (
          value.w < 0 &&
          value.h > 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // ***  鼠标在左下方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index)
          break
        } else if (
          value.w > 0 &&
          value.h < 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // ***  鼠标在右上方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index)
          break
        } else if (
          value.w < 0 &&
          value.h < 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // ***  鼠标在左上方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index)
          break
        } else {
          if (iem === undefined) {
            // *** 鼠标不在矩形中 ***
            newDraw(cav, ctx, list)
          }
        }
      }
    }

    /* 鼠标移出画布区域时保存选中矩形下标(如有) */
    cav.onmouseout = function (eo) {
      if (i !== undefined) {
        // 初始化
        draw(cav, list, i)
      }
    }
  }

  // console.log(cav, list, i);

  return list
}

/* 编辑矩形四个角 */
function changeDraw(cav, ctx, list, i, site) {
  cav.style.cursor = 'pointer'

  // site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点
  let mark = list[i]

  /* 按下鼠标左键 */
  cav.onmousedown = function (ed) {
    // 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来
    let sX = ed.offsetX // 起点X坐标
    let sY = ed.offsetY // 起点Y坐标

    /* 移动鼠标 */
    cav.onmousemove = function (em) {
      // 计算绘制数据
      let iframe = {}
      switch (site) {
        case 1:
          iframe = {
            x: em.offsetX,
            y: em.offsetY,
            w: mark.w - (em.offsetX - sX),
            h: mark.h - (em.offsetY - sY)
          }
          break
        case 2:
          iframe = {
            x: mark.x,
            y: mark.y + (em.offsetY - sY),
            w: mark.w + (em.offsetX - sX),
            h: mark.h - (em.offsetY - sY)
          }
          break
        case 3:
          iframe = {
            x: mark.x + (em.offsetX - sX),
            y: mark.y,
            w: mark.w - (em.offsetX - sX),
            h: mark.h + (em.offsetY - sY)
          }
          break
        case 4:
          iframe = {
            x: mark.x,
            y: mark.y,
            w: mark.w + (em.offsetX - sX),
            h: mark.h + (em.offsetY - sY)
          }
          break
      }
      list.splice(i, 1, iframe)

      // 重新绘制
      reDraw(cav, ctx, list, i)
    }

    /* 鼠标离开矩形区 */
    cav.onmouseout = function (eo) {
      // 重新绘制
      reDraw(cav, ctx, list)
      // 初始化
      draw(cav, list)
    }

    /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
    delDraw(cav, ctx, list, i)
  }
}

/* 绘制新矩形 */
function newDraw(cav, ctx, list) {
  cav.style.cursor = 'crosshair'
  // 初始化变量
  let start = false // 画框状态, false时不执行画框操作
  let sX = 0 // 起点X坐标
  let sY = 0 // 起点Y坐标

  /* 按下鼠标左键 */
  cav.onmousedown = function (ed) {
    /* 使用变量 */
    start = true
    sX = ed.offsetX
    sY = ed.offsetY

    /* 重置按键监听, 防止选中取消后仍可删除 */
    delDraw(cav, ctx, list, null)

    /* 鼠标移动 */
    cav.onmousemove = function (em) {
      if (start) {
        // 重新绘制
        reDraw(cav, ctx, list)
        // 设置边框为虚线
        ctx.beginPath()
        ctx.setLineDash([8, 4])
        ctx.rect(sX, sY, em.offsetX - sX, em.offsetY - sY)
        ctx.stroke()
      }
    }

    /* 鼠标抬起 */
    cav.onmouseup = function (eu) {
      if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {
        // 改变矩形数组
        let frame = {
          x: sX,
          y: sY,
          w: eu.offsetX - sX,
          h: eu.offsetY - sY
        }
        list.push(frame)
        // 重新绘制
        reDraw(cav, ctx, list)
        // 改变画框状态
        start = false
        // 初始化
        draw(cav, list)
      } else {
        // 重新绘制
        reDraw(cav, ctx, list)
        // 改变画框状态
        start = false
        // 初始化
        draw(cav, list)
      }
    }

    /* 鼠标离开矩形区 */
    cav.onmouseout = function (eo) {
      if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {
        // 改变矩形数组
        let frame = {
          x: sX,
          y: sY,
          w: eo.offsetX - sX,
          h: eo.offsetY - sY
        }
        list.push(frame)
        // 重新绘制
        reDraw(cav, ctx, list)
        // 改变画框状态
        start = false
        // 初始化
        draw(cav, list)
      } else {
        // 重新绘制
        reDraw(cav, ctx, list)
        // 改变画框状态
        start = false
        // 初始化
        draw(cav, list)
      }
    }
  }
}

/* 选中矩形, 重绘矩形, 并分发后续事件 */
function judgeDraw(cav, ctx, list, iem) {
  cav.style.cursor = 'default'
  // 初始化变量
  let sX = 0 // 起点X坐标
  let sY = 0 // 起点Y坐标

  /* 按下鼠标左键 */
  cav.onmousedown = function (ed) {
    sX = ed.offsetX
    sY = ed.offsetY

    // 更改选中状态, 重绘矩形
    reDraw(cav, ctx, list, iem)

    /* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */
    cav.onmouseup = function () {
      // 重绘矩形
      reDraw(cav, ctx, list, iem)

      // 初始化
      draw(cav, list, iem)
    }

    /* 按住拖动鼠标, 移动选中矩形*/
    moveDraw(cav, ctx, list, iem, sX, sY)

    /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
    delDraw(cav, ctx, list, iem)
  }
}

/* 移动矩形 */
function moveDraw(cav, ctx, list, i, sX, sY) {
  let mark = list[i]
  cav.onmousemove = function (em) {
    let iframe = {
      x: mark.x + (em.offsetX - sX),
      y: mark.y + (em.offsetY - sY),
      w: mark.w,
      h: mark.h
    }
    list.splice(i, 1, iframe)
    /* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */
    delDraw(cav, ctx, list, i)
    // 重新绘制
    reDraw(cav, ctx, list, i)
  }

  cav.onmouseup = function () {
    // 重绘矩形
    reDraw(cav, ctx, list, i)

    // 初始化
    draw(cav, list, i)
  }
}

/* 删除矩形 */
function delDraw(cav, ctx, list, i) {
  /* 按键事件 */
  if (i === null) {
    // i为null时阻止按键监听事件冒泡
    document.onkeydown = function (k) {
      return false
    }
  } else {
    // 监听按键事件
    document.onkeydown = function (k) {
      let key = k.keyCode || k.which
      if ((key == 46 || key == 8) && i !== null) {
        if (list.length >= 1) {
          // 删除数组元素
          list.splice(i, 1)
          // 重绘矩形
          reDraw(cav, ctx, list)
        } else {
          /* 矩形数组长度为0, 已将矩形框全部删除 */
          ctx.clearRect(0, 0, cav.width, cav.height)
        }
        // 重置监听状态, 防止删除完毕后, 按键监听不消失
        delDraw(cav, ctx, list, null)
        // 重绘矩形
        reDraw(cav, ctx, list)
        // 初始化
        draw(cav, list)
      }
    }
  }
}

/* 重绘所有矩形 */
function reDraw(cav, ctx, list, i) {
  ctx.setLineDash([8, 0]) // 设置边框为实线
  ctx.clearRect(0, 0, cav.width, cav.height)
  // 绘制未选中部分
  list.forEach(function (value, index, array) {
    if (i === undefined || index != i) {
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.rect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'red'
      if (globalZoom <= 0.5) {
        ctx.font = '14px sans-serif'
      } else {
        ctx.font = '20px sans-serif'
      }
      ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)
      ctx.stroke()
    }
  })
  // 绘制已选中部分
  list.forEach(function (value, index, array) {
    if (index === i) {
      /* 绘制方框 */
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.rect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'RGBA(102,102,102,0.2)'
      ctx.fillRect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'red'
      if (globalZoom <= 0.5) {
        ctx.font = '14px sans-serif'
      } else {
        ctx.font = '20px sans-serif'
      }
      ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)
      ctx.stroke()
      // 绘制四个角的圆圈
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.arc(value.x, value.y, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点纵向实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点横向实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画终点实心圆
      ctx.stroke()
    }
  })
}

ColorDifference.vue文件

<template>
  <a-spin tip="Loading..." :spinning="spinning">
    <a-card title="色差差异检测" :bordered="false">
      <a-checkbox v-model:checked="checked" style="margin-bottom: 15px">是否进行全局色差检测</a-checkbox>
      <div class="difference-wrap">
        <a-upload
          v-model:file-list="templateFileList"
          list-type="picture-card"
          class="content-upload"
          :show-upload-list="false"
          :openFileDialogOnClick="!templateUrl"
          :before-upload="beforeUpload"
          :maxCount="1"
          @change="handleTemplateChange"
        >
          <div
            v-if="templateUrl"
            class="content-wrap"
            :class="[temContentWrapClassFlag ? 'content-wrap-flag' : '']"
            ref="contentWrapRef"
          >
            <div class="tem-content" ref="temContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }">
              <img :src="templateUrl" />
              <canvas ref="temMarkCanvasRef"></canvas>
            </div>
          </div>
          <div v-else>
            <plus-outlined style="font-size: 28px"></plus-outlined>
            <div class="ant-upload-text">上传模板</div>
          </div>
        </a-upload>
        <a-upload
          v-model:file-list="imgFileList"
          list-type="picture-card"
          class="content-upload"
          :maxCount="1"
          :show-upload-list="false"
          :openFileDialogOnClick="!imgUrl"
          :before-upload="beforeUpload"
          @change="handleImageChange"
        >
          <div v-if="imgUrl" class="content-wrap" :class="[imgContentWrapClassFlag ? 'content-wrap-flag' : '']">
            <div class="img-content" ref="imgContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }">
              <img :src="imgUrl" />
              <canvas ref="imgMarkCanvasRef"></canvas>
            </div>
          </div>
          <div v-else>
            <plus-outlined style="font-size: 28px"></plus-outlined>
            <div class="ant-upload-text">上传图片</div>
          </div>
        </a-upload>
      </div>
      <div class="actionBar">
        <a-button class="btn" :disabled="zoom === 2" type="primary" @click="scaleLarge">放大</a-button>
        <a-button class="btn" :disabled="zoom === 0.1" type="primary" @click="scaleSmall">缩小</a-button>
        <a-upload
          v-if="templateUrl"
          v-model:file-list="templateFileList"
          :maxCount="1"
          :before-upload="beforeUpload"
          @change="renewTemplate"
        >
          <a-button class="btn" type="primary">上传模板</a-button>
        </a-upload>
        <a-upload
          v-if="imgUrl"
          v-model:file-list="imgFileList"
          :before-upload="beforeUpload"
          :maxCount="1"
          @change="renewImg"
        >
          <a-button class="btn" type="primary">上传图片</a-button>
        </a-upload>
        <a-button class="btn" type="primary" @click="handleTest">开始检测</a-button>
      </div>
    </a-card>
    <a-card title="检测结果" :bordered="false" v-if="responseData" style="margin-top: 20px">
      <div style="width: 80vw">
        <div v-if="responseData.image_template" style="margin-bottom: 20px">
          <img :src="responseData.image_template" style="zoom: 50%" alt="Loaded Image" />
        </div>
        <div v-if="responseData.check_all" class="result">
          <span>全局检测结果:</span>
          <pre style="white-space: pre-wrap">{{ responseData.all_content }}</pre>
        </div>
        <div v-for="item in responseData.contents" :key="item.id">
          <span>序号{{ item.id }}:</span>
          <pre style="white-space: pre-wrap">{{ item.result }}</pre>
        </div>
      </div>
    </a-card>
  </a-spin>
</template>
<script setup>
import { draw } from '../lib/draw' // 矩形绘制方法
import { PlusOutlined } from '@ant-design/icons-vue'
import debounce from 'xe-utils/debounce'
import { testColorDiff } from '../api/index'
import { message } from 'ant-design-vue'

const checked = ref(false)
const spinning = ref(false)
const responseData = ref(null) //接口成功响应数据
const contentWrapRef = ref() // 用来获取最外层的div高度
const zoom = ref(1) //缩放比例

/* 模板 */
const templateUrl = ref()
const templateFileList = ref([])
const temContentRef = ref(null) //模板外层
const temMarkCanvasRef = ref(null) //模板画布
const temMarkList = ref([]) //模板标记数组
const temShowFlag = ref(false) //模板初始化完成标识
const temContentWrapClassFlag = ref(false) // 模板外层样式生效标识
// 模版上传
const handleTemplateChange = (info) => {
  templateUrl.value = URL.createObjectURL(info.file)
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// 重新上传模板
const renewTemplate = (info) => {
  temMarkList.value = []
  handleTemplateChange(info)
}

/* 图片 */
const imgUrl = ref('')
const imgFileList = ref([])
const imgContentRef = ref(null) //图片外层
const imgMarkCanvasRef = ref(null) //图片画布
const imgShowFlag = ref(false) //图片初始化完成标识
const imgContentWrapClassFlag = ref(false) // 图片外层样式生效标识
const imgCtx2D = ref() // 图片画布实例
// 图片上传
const handleImageChange = (info) => {
  imgUrl.value = URL.createObjectURL(info.file)
  InitImgDrawRect()
}
// 重新上传图片
const renewImg = (info) => {
  handleImageChange(info)
}

watch(
  () => temMarkList.value,
  (newVal, oldVal) => {
    if (imgUrl.value) {
      debounce(InitImgDrawRect(), 1000)
    }
  },
  { deep: true }
)

// 关闭自动上传
const beforeUpload = (file) => {
  return false
}
// 处理坐标数据给后端接口
const handleMarkPoint = () => {
  const pointArr = []
  temMarkList.value.forEach((item, index) => {
    pointArr.push({
      id: index + 1,
      left_x: item.x/zoom.value,
      left_y: item.y/zoom.value,
      right_x: item.x/zoom.value + item.w/zoom.value,
      right_y: item.y/zoom.value + item.h/zoom.value,
      result: ''
    })
  })
  console.log('pointArr',pointArr)
  return JSON.stringify(pointArr)
}
// 开始检测
const handleTest = () => {
  console.log('temMarkList.value', temMarkList.value, temMarkList.value.length)
  if (!templateUrl.value) {
    message.error('请上传模板')
    return
  } else if (!imgUrl.value) {
    message.error('请上传图片')
    return
  } else if (temMarkList.value.length === 0 && !checked.value) {
    message.error('请进行框选')
    return
  }
//   spinning.value = true
  const formData = new FormData()
  formData.append('template', templateFileList.value[0].originFileObj)
  formData.append('file', imgFileList.value[0].originFileObj)
  formData.append('points_json', handleMarkPoint())
  // 模版色差
  /*   testColorDiff(formData, { check_all: checked.value ? 1 : 0 })
    .then((res) => {
      spinning.value = false
      responseData.value = res.data.data
      responseData.value.image_template = `data:image/png;base64,${res.data.data.image_template}`
    })
    .catch(() => {
      spinning.value = false
    }) */
}

/* 画布操作 */
// 标记内容数组

//画布初始化
const initCanvas = (contentRef, canvasRef, markListName, showFlag, contentWrapClassFlag) => {
  setTimeout(() => {
    nextTick(() => {
      const contentWrapHeight = contentWrapRef.value.offsetHeight
      // 初始化canvas宽高
      let cav = canvasRef.value
      cav.width = contentRef.value.offsetWidth * zoom.value
      cav.height = contentRef.value.offsetHeight * zoom.value
      console.log('cav.width', zoom, cav.width)
      let ctx = cav.getContext('2d')
      ctx.strokeStyle = 'red'
      cav.style.cursor = 'crosshair'
      if (contentWrapHeight > cav.height) {
        // 说明图片高度小于容器高度
        contentWrapClassFlag.value = true
      } else {
        contentWrapClassFlag.value = false
      }
      showFlag.value = true
      console.log('markListName.value ', markListName.value)
      // 计算使用变量
      let list = markListName.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存

      // 若list长度不为0, 则显示已标记框
      if (list.length !== 0) {
        list.forEach(function (value, index, array) {
          // 遍历绘制所有标记框
          ctx.rect(value.x, value.y, value.w, value.h)
          if (zoom.value <= 0.5) {
            ctx.font = '14px sans-serif'
          } else {
            ctx.font = '20px sans-serif'
          }
          ctx.fillStyle = 'red'
          ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)

          ctx.stroke()
        })
      }

      // 调用封装的绘制方法
      draw(cav, list, undefined,zoom.value)
    })
  }, 500)
}
// 图片不让操作,初始化显示矩形框
const InitImgDrawRect = () => {
  setTimeout(() => {
    nextTick(() => {
      const contentWrapHeight = contentWrapRef.value.offsetHeight
      // 初始化canvas宽高
      let cav = imgMarkCanvasRef.value
      cav.width = imgContentRef.value.offsetWidth * zoom.value
      cav.height = imgContentRef.value.offsetHeight * zoom.value
      imgCtx2D.value = cav.getContext('2d')
      imgCtx2D.value.strokeStyle = 'red'
      // cav.style.cursor = 'crosshair'
      if (contentWrapHeight > cav.height) {
        // 说明图片高度小于容器高度
        imgContentWrapClassFlag.value = true
      } else {
        imgContentWrapClassFlag.value = false
      }
      imgShowFlag.value = true

      // 计算使用变量
      let list = temMarkList.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存
      if (list.length !== 0) {
        list.forEach(function (value, index, array) {
          // 遍历绘制所有标记框
          imgCtx2D.value.rect(value.x, value.y, value.w, value.h)
          if (zoom.value <= 0.5) {
            imgCtx2D.value.font = '14px sans-serif'
          } else {
            imgCtx2D.value.font = '20px sans-serif'
          }
          imgCtx2D.value.fillStyle = 'red'
          imgCtx2D.value.fillText(index + 1, value.x + value.w / 2, value.y - 5)
          imgCtx2D.value.stroke()
        })
      }
    })
  }, 500)
}

//放大
const scaleLarge = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom += 10
  zoom.value = innerZoom / 100
  console.log('zoom', zoom.value)
  temContentRef.value.style.zoom = innerZoom + '%'
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value - 0.1)) * zoom.value
    item.y = (item.y / (zoom.value - 0.1)) * zoom.value
    item.w = (item.w / (zoom.value - 0.1)) * zoom.value
    item.h = (item.h / (zoom.value - 0.1)) * zoom.value
  })
  if (imgContentRef.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
  }
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

// //缩小
const scaleSmall = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom -= 10
  zoom.value = innerZoom / 100
  console.log('zoom', zoom.value)
  temContentRef.value.style.zoom = innerZoom + '%'
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value + 0.1)) * zoom.value
    item.y = (item.y / (zoom.value + 0.1)) * zoom.value
    item.w = (item.w / (zoom.value + 0.1)) * zoom.value
    item.h = (item.h / (zoom.value + 0.1)) * zoom.value
  })
  if (imgContentRef.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
  }
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

//滚轮缩放(TODO:未实现缩放后再画矩形鼠标位置发生变化)
// const rollImg = () => {
//   /* 获取当前页面的缩放比
//            若未设置zoom缩放比,则为默认100%,即1,原图大小
//        */
//   console.log(document.getElementById('bigImg').style)
//   var zoom = parseInt(document.getElementById('bigImg').style.zoom) || 100
//   /* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom    wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动  */
//   zoom += event.wheelDelta / 12
//   /* 最小范围 和 最大范围 的图片缩放尺度 */
//   if (zoom >= 100 && zoom < 250) {
//     document.getElementById('bigImg').style.zoom = zoom + '%'
//   }
//   return false
// }
</script>
<style lang="less" scoped>
.difference-wrap {
  width: 80vw;
  height: 56vh;
  display: flex;
  .content-upload {
    width: calc(50% - 5px);
    :deep(.ant-upload) {
      width: 100%;
      height: 100%;
    }
  }
  .content-upload:first-child {
    margin-right: 5px;
  }
  .content-upload:last-child {
    margin-left: 5px;
  }
  .content-wrap {
    height: 100%;
    width: 100%;
    overflow: auto;
  }
  .content-wrap-flag::after {
    content: '';
    /*让伪元素撑起高度*/
    height: 100%;
    display: inline-block;
    vertical-align: middle;
  }

  .tem-content,
  .img-content {
    display: inline-block;
    position: relative;
    height: auto;
    width: auto;
    vertical-align: middle;
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 10;
      width: 100%;
      height: 100%;
    }
  }
}
.actionBar {
  display: flex;
  justify-content: flex-end;
  .btn {
    margin: 20px 10px 0;
  }
  .btn:last-child {
    margin-right: 0;
  }
}
.result {
  span {
    display: inline-block;
    width: 120px;
  }
}
</style>

结果显示

结果显示

扩展,可在图片上进行拖拽操作,模板和图片的点位不一定一一对应

业务上,在之前的基础上进行扩展,可在图片上进行拖拽操作,不可在图片上进行新增和删除操作,若在图片上拖拽新的位置,则以自身为准,模板上对应的框不在可控图片上对应的框,将两组数据传给后端

draw.js

/**
 * 画布中绘制矩形
 * 参数: cav-画布对象  list-矩形数组 i-选中矩形下标
 **/
let globalZoom = 1 //缩放

/* 操作执行方法分发 */
export function draw(cav, list, i, delEnable, newEnable, zoom) {
  globalZoom = zoom || globalZoom

  // 画布初始化
  let ctx = cav.getContext('2d')
  ctx.strokeStyle = 'red'
  ctx.lineWidth = 2

  // 变量初始化
  let sX = 0 // 鼠标X坐标
  let sY = 0 // 鼠标Y坐标

  /*
   *鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形
   */
  cav.onmousemove = function (em) {
    sX = em.offsetX
    sY = em.offsetY
    let iem = undefined // 鼠标移动时临时存储当前鼠标所在矩形的下标

    if (list.length === 0) {
      // **** 无矩形 ****
      // 绘制新矩形
      newDraw(cav, ctx, list, delEnable, newEnable, zoom)
    } else if (i === undefined) {
      // **** 已有矩形无选中 ****
      // 判断鼠标位置
      list.forEach(function (value, index, array) {
        if (
          value.w > 0 &&
          value.h > 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // 鼠标在右下方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)
        }
        if (
          value.w < 0 &&
          value.h > 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // 鼠标在左下方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)
        }
        if (
          value.w > 0 &&
          value.h < 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // 鼠标在右上方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)
        }
        if (
          value.w < 0 &&
          value.h < 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // 鼠标在左上方向生成的矩形中
          iem = index
          judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)
        }
        if (iem === undefined) {
          // 鼠标不在矩形中
          newDraw(cav, ctx, list, delEnable, newEnable, zoom)
        }
      })
    } else {
      // **** 已有选中矩形 ****
      // 判断鼠标位置
      for (let index = 0; index < list.length; index++) {
        let value = list[index]
        if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {
          // ***  鼠标在起点角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 1, delEnable, newEnable, zoom)
            break
          }
        } else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {
          // ***  鼠标在起点横向角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 2, delEnable, newEnable, zoom)
            break
          }
        } else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
          // ***  鼠标在起点纵向角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 3, delEnable, newEnable, zoom)
            break
          }
        } else if (
          sX < value.x + value.w + 5 &&
          sX > value.x + value.w - 5 &&
          sY < value.y + value.h + 5 &&
          sY > value.y + value.h - 5
        ) {
          // ***  鼠标在终点角  ***
          if (index === i) {
            changeDraw(cav, ctx, list, i, 4, delEnable, newEnable, zoom)
            break
          }
        } else if (
          value.w > 0 &&
          value.h > 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // ***  鼠标在右下方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)
          break
        } else if (
          value.w < 0 &&
          value.h > 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY > value.y &&
          sY < value.y + value.h
        ) {
          // ***  鼠标在左下方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)
          break
        } else if (
          value.w > 0 &&
          value.h < 0 &&
          sX > value.x &&
          sX < value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // ***  鼠标在右上方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)
          break
        } else if (
          value.w < 0 &&
          value.h < 0 &&
          sX < value.x &&
          sX > value.x + value.w &&
          sY < value.y &&
          sY > value.y + value.h
        ) {
          // ***  鼠标在左上方向生成的矩形中  ***
          iem = index
          judgeDraw(cav, ctx, list, index, delEnable, newEnable, zoom)
          break
        } else {
          if (iem === undefined) {
            // *** 鼠标不在矩形中 ***
            newDraw(cav, ctx, list, delEnable, newEnable, zoom)
          }
        }
      }
    }

    /* 鼠标移出画布区域时保存选中矩形下标(如有) */
    cav.onmouseout = function (eo) {
      if (i !== undefined) {
        // 初始化
        draw(cav, list, i, delEnable, newEnable, zoom)
      }
    }
  }

  return list
}

/* 编辑矩形四个角 */
function changeDraw(cav, ctx, list, i, site, delEnable, newEnable, zoom) {
  cav.style.cursor = 'pointer'

  // site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点
  let mark = list[i]

  /* 按下鼠标左键 */
  cav.onmousedown = function (ed) {
    // 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来
    let sX = ed.offsetX // 起点X坐标
    let sY = ed.offsetY // 起点Y坐标

    /* 移动鼠标 */
    cav.onmousemove = function (em) {
      // 计算绘制数据
      let iframe = {}
      switch (site) {
        case 1:
          iframe = {
            uuid: mark.uuid,
            x: em.offsetX,
            y: em.offsetY,
            w: mark.w - (em.offsetX - sX),
            h: mark.h - (em.offsetY - sY)
          }
          break
        case 2:
          iframe = {
            uuid: mark.uuid,
            x: mark.x,
            y: mark.y + (em.offsetY - sY),
            w: mark.w + (em.offsetX - sX),
            h: mark.h - (em.offsetY - sY)
          }
          break
        case 3:
          iframe = {
            uuid: mark.uuid,
            x: mark.x + (em.offsetX - sX),
            y: mark.y,
            w: mark.w - (em.offsetX - sX),
            h: mark.h + (em.offsetY - sY)
          }
          break
        case 4:
          iframe = {
            uuid: mark.uuid,
            x: mark.x,
            y: mark.y,
            w: mark.w + (em.offsetX - sX),
            h: mark.h + (em.offsetY - sY)
          }
          break
      }
      list.splice(i, 1, iframe)

      // 重新绘制
      reDraw(cav, ctx, list, i)
    }

    /* 鼠标离开矩形区 */
    cav.onmouseout = function (eo) {
      // 重新绘制
      reDraw(cav, ctx, list)
      // 初始化
      draw(cav, list, undefined, delEnable, newEnable, zoom)
    }

    /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
    delDraw(cav, ctx, list, i, delEnable, newEnable, zoom)
  }
}

/* 绘制新矩形 */
function newDraw(cav, ctx, list, delEnable, newEnable, zoom) {
  if (newEnable === '1') {
    cav.style.cursor = 'crosshair'
    // 初始化变量
    let start = false // 画框状态, false时不执行画框操作
    let sX = 0 // 起点X坐标
    let sY = 0 // 起点Y坐标

    /* 按下鼠标左键 */
    cav.onmousedown = function (ed) {
      /* 使用变量 */
      start = true
      sX = ed.offsetX
      sY = ed.offsetY

      /* 重置按键监听, 防止选中取消后仍可删除 */
      delDraw(cav, ctx, list, null, delEnable, newEnable, zoom)

      /* 鼠标移动 */
      cav.onmousemove = function (em) {
        if (start) {
          // 重新绘制
          reDraw(cav, ctx, list)
          // 设置边框为虚线
          ctx.beginPath()
          ctx.setLineDash([8, 4])
          ctx.rect(sX, sY, em.offsetX - sX, em.offsetY - sY)
          ctx.stroke()
        }
      }

      /* 鼠标抬起 */
      cav.onmouseup = function (eu) {
        if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {
          // 改变矩形数组
          let frame = {
            uuid: createUuid(),
            x: sX,
            y: sY,
            w: eu.offsetX - sX,
            h: eu.offsetY - sY
          }
          list.push(frame)
          // 重新绘制
          reDraw(cav, ctx, list)
          // 改变画框状态
          start = false
          // 初始化
          draw(cav, list, undefined, delEnable, newEnable, zoom)
        } else {
          // 重新绘制
          reDraw(cav, ctx, list)
          // 改变画框状态
          start = false
          // 初始化
          draw(cav, list, undefined, delEnable, newEnable, zoom)
        }
      }

      /* 鼠标离开矩形区 */
      cav.onmouseout = function (eo) {
        if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {
          // 改变矩形数组
          let frame = {
            uuid: createUuid(),
            x: sX,
            y: sY,
            w: eo.offsetX - sX,
            h: eo.offsetY - sY
          }
          list.push(frame)
          // 重新绘制
          reDraw(cav, ctx, list)
          // 改变画框状态
          start = false
          // 初始化
          draw(cav, list, undefined, delEnable, newEnable, zoom)
        } else {
          // 重新绘制
          reDraw(cav, ctx, list)
          // 改变画框状态
          start = false
          // 初始化
          draw(cav, list, undefined, delEnable, newEnable, zoom)
        }
      }
    }
  }
}

/* 选中矩形, 重绘矩形, 并分发后续事件 */
function judgeDraw(cav, ctx, list, iem, delEnable, newEnable, zoom) {
  cav.style.cursor = 'default'
  // 初始化变量
  let sX = 0 // 起点X坐标
  let sY = 0 // 起点Y坐标

  /* 按下鼠标左键 */
  cav.onmousedown = function (ed) {
    sX = ed.offsetX
    sY = ed.offsetY

    // 更改选中状态, 重绘矩形
    reDraw(cav, ctx, list, iem)

    /* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */
    cav.onmouseup = function () {
      // 重绘矩形
      reDraw(cav, ctx, list, iem)

      // 初始化
      draw(cav, list, iem, delEnable, newEnable, zoom)
    }

    /* 按住拖动鼠标, 移动选中矩形*/
    moveDraw(cav, ctx, list, iem, sX, sY, delEnable, newEnable, zoom)

    /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
    delDraw(cav, ctx, list, iem, delEnable, newEnable, zoom)
  }
}

/* 移动矩形 */
function moveDraw(cav, ctx, list, i, sX, sY, delEnable, newEnable, zoom) {
  let mark = list[i]
  cav.onmousemove = function (em) {
    let iframe = {
      uuid: mark.uuid,
      x: mark.x + (em.offsetX - sX),
      y: mark.y + (em.offsetY - sY),
      w: mark.w,
      h: mark.h
    }
    list.splice(i, 1, iframe)
    /* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */
    delDraw(cav, ctx, list, i, delEnable, newEnable, zoom)
    // 重新绘制
    reDraw(cav, ctx, list, i)
  }

  cav.onmouseup = function () {
    // 重绘矩形
    reDraw(cav, ctx, list, i)

    // 初始化
    draw(cav, list, i, delEnable, newEnable, zoom)
  }
}

/* 删除矩形 */
function delDraw(cav, ctx, list, i, delEnable, newEnable, zoom) {
  if (delEnable == 1) {
    /* 按键事件 */
    if (i === null) {
      // i为null时阻止按键监听事件冒泡
      document.onkeydown = function (k) {
        return false
      }
    } else {
      // 监听按键事件
      document.onkeydown = function (k) {
        let key = k.keyCode || k.which
        if ((key == 46 || key == 8) && i !== null) {
          if (list.length >= 1) {
            // 删除数组元素
            list.splice(i, 1)
            // 重绘矩形
            reDraw(cav, ctx, list)
          } else {
            /* 矩形数组长度为0, 已将矩形框全部删除 */
            ctx.clearRect(0, 0, cav.width, cav.height)
          }
          // 重置监听状态, 防止删除完毕后, 按键监听不消失
          delDraw(cav, ctx, list, null, delEnable, newEnable, zoom)
          // 重绘矩形
          reDraw(cav, ctx, list)
          // 初始化
          draw(cav, list, undefined, delEnable, newEnable, zoom)
        }
      }
    }
  }
}

/* 重绘所有矩形 */
function reDraw(cav, ctx, list, i) {
  ctx.setLineDash([8, 0]) // 设置边框为实线
  ctx.clearRect(0, 0, cav.width, cav.height)
  // 绘制未选中部分
  list.forEach(function (value, index, array) {
    if (i === undefined || index != i) {
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.rect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'red'
      if (globalZoom <= 0.5) {
        ctx.font = '14px sans-serif'
      } else {
        ctx.font = '20px sans-serif'
      }
      ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)
      ctx.stroke()
    }
  })
  // 绘制已选中部分
  list.forEach(function (value, index, array) {
    if (index === i) {
      /* 绘制方框 */
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.rect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'RGBA(102,102,102,0.2)'
      ctx.fillRect(value.x, value.y, value.w, value.h)
      ctx.fillStyle = 'red'
      if (globalZoom <= 0.5) {
        ctx.font = '14px sans-serif'
      } else {
        ctx.font = '20px sans-serif'
      }
      ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)
      ctx.stroke()
      // 绘制四个角的圆圈
      ctx.beginPath()
      ctx.strokeStyle = 'red'
      ctx.arc(value.x, value.y, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点纵向实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画起点横向实心圆
      ctx.stroke()
      ctx.beginPath()
      ctx.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2)
      ctx.fillStyle = 'red'
      ctx.fill() // 画终点实心圆
      ctx.stroke()
    }
  })
}
/**
 * 生成 通用唯一编码
 * @param len 指定长度
 * @param radix 基数
 */
function createUuid(len, radix) {
  var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
  var uuid = []
  var i
  radix = radix || chars.length

  if (len) {
    // Compact form
    for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]
  } else {
    // rfc4122, version 4 form
    var r

    // rfc4122 requires these characters
    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
    uuid[14] = '4'

    // Fill in random data.  At i==19 set the high bits of clock sequence as
    // per rfc4122, sec. 4.1.5
    for (i = 0; i < 36; i++) {
      if (!uuid[i]) {
        r = 0 | (Math.random() * 16)
        uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r]
      }
    }
  }

  return uuid.join('')
}

// 根据某一字段求arr2对象数组的差集,arr2的长度要比arr1的长度长,扩展,需要比对多组字段key2,key3,key4
export const getArrayDifference = function (arr1, arr2, ...keys) {
  const result = []
  for (let i = 0; i < arr2.length; i++) {
    const obj = arr2[i]
    const unique1 = obj[keys[0]]
    let isExist = false
    for (let j = 0; j < arr1.length; j++) {
      const aj = arr1[j]
      const unique2 = aj[keys[0]]
      if (keys.length > 1) {
        const flag = keys.reduce((pre, cur) => pre && obj[cur] === aj[cur], true)
        if (flag) {
          isExist = true
          break
        }
      } else {
        if (unique2 === unique1) {
          isExist = true
          break
        }
      }
    }
    if (!isExist) {
      result.push(obj)
    }
  }
  return result
}

ColorDifference.vue文件

  1. 添加temMarkList watch监听事件
  2. 放大、缩小事件添加图片逻辑
  3. 多处细小改动
<template>
  <a-spin tip="Loading..." :spinning="spinning">
    <a-card title="色差差异检测" :bordered="false">
      <a-checkbox v-model:checked="checked" style="margin-bottom: 15px">是否进行全局色差检测</a-checkbox>
      <div class="difference-wrap">
        <a-upload
          v-model:file-list="templateFileList"
          list-type="picture-card"
          class="content-upload"
          :show-upload-list="false"
          :openFileDialogOnClick="!templateUrl"
          :before-upload="beforeUpload"
          :maxCount="1"
          @change="handleTemplateChange"
        >
          <div
            v-if="templateUrl"
            class="content-wrap"
            :class="[temContentWrapClassFlag ? 'content-wrap-flag' : '']"
            ref="contentWrapRef"
          >
            <div class="tem-content" ref="temContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }">
              <img :src="templateUrl" />
              <canvas ref="temMarkCanvasRef"></canvas>
            </div>
          </div>
          <div v-else>
            <plus-outlined style="font-size: 28px"></plus-outlined>
            <div class="ant-upload-text">上传模板</div>
          </div>
        </a-upload>
        <a-upload
          v-model:file-list="imgFileList"
          list-type="picture-card"
          class="content-upload"
          :maxCount="1"
          :show-upload-list="false"
          :openFileDialogOnClick="!imgUrl"
          :before-upload="beforeUpload"
          @change="handleImageChange"
        >
          <div v-if="imgUrl" class="content-wrap" :class="[imgContentWrapClassFlag ? 'content-wrap-flag' : '']">
            <div class="img-content" ref="imgContentRef" :style="{ opacity: temShowFlag ? 1 : 0 }">
              <img :src="imgUrl" />
              <canvas ref="imgMarkCanvasRef"></canvas>
            </div>
          </div>
          <div v-else>
            <plus-outlined style="font-size: 28px"></plus-outlined>
            <div class="ant-upload-text">上传图片</div>
          </div>
        </a-upload>
      </div>
      <div class="actionBar">
        <a-button class="btn" :disabled="zoom === 2" type="primary" @click="scaleLarge">放大</a-button>
        <a-button class="btn" :disabled="zoom === 0.1" type="primary" @click="scaleSmall">缩小</a-button>
        <a-upload
          v-if="templateUrl"
          v-model:file-list="templateFileList"
          :maxCount="1"
          :before-upload="beforeUpload"
          @change="renewTemplate"
        >
          <a-button class="btn" type="primary">上传模板</a-button>
        </a-upload>
        <a-upload
          v-if="imgUrl"
          v-model:file-list="imgFileList"
          :before-upload="beforeUpload"
          :maxCount="1"
          @change="renewImg"
        >
          <a-button class="btn" type="primary">上传图片</a-button>
        </a-upload>
        <a-button class="btn" type="primary" @click="handleTest">开始检测</a-button>
      </div>
    </a-card>
    <a-card title="检测结果" :bordered="false" v-if="responseData" style="margin-top: 20px">
      <div style="width: 80vw">
        <div v-if="responseData.image_template" style="margin-bottom: 20px">
          <img :src="responseData.image_template" style="zoom: 50%" alt="Loaded Image" />
        </div>
        <div v-if="responseData.check_all" class="result">
          <span>全局检测结果:</span>
          <pre style="white-space: pre-wrap">{{ responseData.all_content }}</pre>
        </div>
        <div v-for="item in responseData.contents" :key="item.id">
          <span>序号{{ item.id }}:</span>
          <pre style="white-space: pre-wrap">{{ item.result }}</pre>
        </div>
      </div>
    </a-card>
  </a-spin>
</template>
<script setup>
import { draw, getArrayDifference } from '../lib/draw' // 矩形绘制方法
import { PlusOutlined } from '@ant-design/icons-vue'
import debounce from 'xe-utils/debounce'
import clone from 'xe-utils/clone'
import { testColorDiff } from '../api/index'
import { message } from 'ant-design-vue'

const checked = ref(false)
const spinning = ref(false)
const responseData = ref(null) //接口成功响应数据
const contentWrapRef = ref() // 用来获取最外层的div高度
const zoom = ref(1) //缩放比例

/* 模板 */
const templateUrl = ref()
const templateFileList = ref([])
const temContentRef = ref(null) //模板外层
const temMarkCanvasRef = ref(null) //模板画布
const temMarkList = ref([]) //模板标记数组
const oldTemMarkList = ref([]) //模板标记旧值
const temShowFlag = ref(false) //模板初始化完成标识
const temContentWrapClassFlag = ref(false) // 模板外层样式生效标识
// 模版上传
const handleTemplateChange = (info) => {
  templateUrl.value = URL.createObjectURL(info.file)
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}
// 重新上传模板
const renewTemplate = (info) => {
  temMarkList.value = []
  handleTemplateChange(info)
}

/* 图片 */
const imgUrl = ref('')
const imgFileList = ref([])
const imgContentRef = ref(null) //图片外层
const imgMarkCanvasRef = ref(null) //图片画布
const imgMarkList = ref([]) //图片标记数组
const imgShowFlag = ref(false) //图片初始化完成标识
const imgContentWrapClassFlag = ref(false) // 图片外层样式生效标识
// 图片上传
const handleImageChange = (info) => {
  imgUrl.value = URL.createObjectURL(info.file)
  InitImgDrawRect(temMarkList.value)
}
// 重新上传图片
const renewImg = (info) => {
  handleImageChange(info)
}

watch(
  () => clone(temMarkList.value, true),
  debounce((newVal, oldVal) => {
    // console.log('newVal', newVal)
    // console.log('oldVal', oldVal)
    // console.log('oldTemMarkList', oldTemMarkList.value)
    // console.log('temMarkList.value', temMarkList.value)
    // console.log('imgMarkList.value', imgMarkList.value)
    let newMarkList = [...imgMarkList.value]
    const newLen = newVal.length
    const oldLen = oldVal.length
    if (newLen > oldLen) {
      // 新增新的矩形
      newMarkList.push(newVal[newLen - 1])
    } else if (newLen < oldLen) {
      // 删除矩形
      const resArr = getArrayDifference(newVal, oldTemMarkList.value, 'uuid') //找到删除的那个矩形
      const dealArr = []
      newMarkList.forEach((item) => {
        if (item.uuid !== resArr[0].uuid) {
          dealArr.push(item)
        }
      })
      newMarkList = [...dealArr]
    } else if (newLen === oldLen) {
      // 移动矩形或者放大缩小矩形
      const resArr = getArrayDifference(newVal, oldTemMarkList.value, 'x', 'y', 'w', 'h') //找到移动的那个矩形旧值
      console.log('resArr', resArr)
      if (resArr.length === 0) {
        // 可能是放大,缩小操作
        return
      }
      const newRes = newVal.find((item) => item.uuid === resArr[0].uuid) //找到移动的那个矩形新值
      const dealArr = []
      newMarkList.forEach((item) => {
        if (item.uuid !== resArr[0].uuid) {
          dealArr.push(item)
        } else {
          console.log('resArr[0]', resArr[0], item)
          if (resArr[0].x === item.x && resArr[0].y === item.y && resArr[0].w === item.w && resArr[0].h === item.h) {
            // 如果图片上的框与模板上框的旧值相等,说明,图片上的框没有移动过,则,同步模板上的框
            dealArr.push(newRes)
          } else {
            // 不相等,说明,图片上的框没有移动过,则,保持图片上的框
            dealArr.push(item)
          }
        }
      })
      newMarkList = [...dealArr]
    }

    if (imgUrl.value) {
      InitImgDrawRect(newMarkList)
    }
  }, 500),
  { deep: true }
)

// 关闭自动上传
const beforeUpload = (file) => {
  return false
}
// 处理坐标数据给后端接口
const handleMarkPoint = () => {
  // 这时候需要传两组数据,看后端如何定义,图片和模板一一对应
  const pointArr = []
  temMarkList.value.forEach((item, index) => {
    const imgItem = imgMarkList.value[index]
    pointArr.push({
      id: index + 1,
      template: {
        left_x: item.x / zoom.value,
        left_y: item.y / zoom.value,
        right_x: item.x / zoom.value + item.w / zoom.value,
        right_y: item.y / zoom.value + item.h / zoom.value
      },
      image: {
        left_x: imgItem.x / zoom.value,
        left_y: imgItem.y / zoom.value,
        right_x: imgItem.x / zoom.value + imgItem.w / zoom.value,
        right_y: imgItem.y / zoom.value + imgItem.h / zoom.value
      },
      result: ''
    })
  })
  return JSON.stringify(pointArr)
}
// 开始检测
const handleTest = () => {
  if (!templateUrl.value) {
    message.error('请上传模板')
    return
  } else if (!imgUrl.value) {
    message.error('请上传图片')
    return
  } else if (temMarkList.value.length === 0 && !checked.value) {
    message.error('请进行框选')
    return
  }
  //   spinning.value = true
  const formData = new FormData()
  formData.append('template', templateFileList.value[0].originFileObj)
  formData.append('file', imgFileList.value[0].originFileObj)
  formData.append('points_json', handleMarkPoint())
  // 模版色差
  /*   testColorDiff(formData, { check_all: checked.value ? 1 : 0 })
    .then((res) => {
      spinning.value = false
      responseData.value = res.data.data
      responseData.value.image_template = `data:image/png;base64,${res.data.data.image_template}`
    })
    .catch(() => {
      spinning.value = false
    }) */
}

/* 画布操作 */
// 标记内容数组

//画布初始化
const initCanvas = (
  contentRef,
  canvasRef,
  markListName,
  showFlag,
  contentWrapClassFlag,
  delEnable = '1',
  newEnable = '1'
) => {
  setTimeout(() => {
    nextTick(() => {
      const contentWrapHeight = contentWrapRef.value.offsetHeight
      // 初始化canvas宽高
      let cav = canvasRef.value
      cav.width = contentRef.value.offsetWidth * zoom.value
      cav.height = contentRef.value.offsetHeight * zoom.value
      cav.style.cursor = 'crosshair'
      let ctx = cav.getContext('2d')
      ctx.strokeStyle = 'red'
      if (contentWrapHeight > cav.height) {
        // 说明图片高度小于容器高度
        contentWrapClassFlag.value = true
      } else {
        contentWrapClassFlag.value = false
      }
      showFlag.value = true
      // 计算使用变量
      let list = markListName.value // 画框数据集合, 用于服务端返回的数据显示和绘制的矩形保存
      // 若list长度不为0, 则显示已标记框
      if (list.length !== 0) {
        list.forEach(function (value, index, array) {
          // 遍历绘制所有标记框
          ctx.rect(value.x, value.y, value.w, value.h)
          if (zoom.value <= 0.5) {
            ctx.font = '14px sans-serif'
          } else {
            ctx.font = '20px sans-serif'
          }
          ctx.fillStyle = 'red'
          ctx.fillText(index + 1, value.x + value.w / 2, value.y - 5)

          ctx.stroke()
        })
      }

      // 调用封装的绘制方法
      draw(cav, list, undefined, delEnable, newEnable, zoom.value)
    })
  }, 500)
}
// 图片不让操作,初始化显示矩形框
const InitImgDrawRect = (markList) => {
  oldTemMarkList.value = clone(temMarkList.value) //在这里存上模板标记的旧值,好在移动缩小放大时进行比较
  imgMarkList.value = clone(markList)
  initCanvas(imgContentRef, imgMarkCanvasRef, imgMarkList, imgShowFlag, imgContentWrapClassFlag, '0', '0')
}

//放大
const scaleLarge = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom += 10
  zoom.value = innerZoom / 100

  if (imgContentRef.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
    imgMarkList.value.forEach((item) => {
      item.x = (item.x / (zoom.value - 0.1)) * zoom.value
      item.y = (item.y / (zoom.value - 0.1)) * zoom.value
      item.w = (item.w / (zoom.value - 0.1)) * zoom.value
      item.h = (item.h / (zoom.value - 0.1)) * zoom.value
    })
  }
  temContentRef.value.style.zoom = innerZoom + '%'
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value - 0.1)) * zoom.value
    item.y = (item.y / (zoom.value - 0.1)) * zoom.value
    item.w = (item.w / (zoom.value - 0.1)) * zoom.value
    item.h = (item.h / (zoom.value - 0.1)) * zoom.value
  })
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

// //缩小
const scaleSmall = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom -= 10
  zoom.value = innerZoom / 100

  if (imgContentRef.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
    imgMarkList.value.forEach((item) => {
      item.x = (item.x / (zoom.value + 0.1)) * zoom.value
      item.y = (item.y / (zoom.value + 0.1)) * zoom.value
      item.w = (item.w / (zoom.value + 0.1)) * zoom.value
      item.h = (item.h / (zoom.value + 0.1)) * zoom.value
    })
    // InitImgDrawRect(imgMarkList.value) 此处不需要调用,会在temMarkList的watch监听里更新图片标记点
  }
  temContentRef.value.style.zoom = innerZoom + '%'
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value + 0.1)) * zoom.value
    item.y = (item.y / (zoom.value + 0.1)) * zoom.value
    item.w = (item.w / (zoom.value + 0.1)) * zoom.value
    item.h = (item.h / (zoom.value + 0.1)) * zoom.value
  })
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

//滚轮缩放(TODO:未实现缩放后再画矩形鼠标位置发生变化)
// const rollImg = () => {
//   /* 获取当前页面的缩放比
//            若未设置zoom缩放比,则为默认100%,即1,原图大小
//        */
//   console.log(document.getElementById('bigImg').style)
//   var zoom = parseInt(document.getElementById('bigImg').style.zoom) || 100
//   /* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom    wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动  */
//   zoom += event.wheelDelta / 12
//   /* 最小范围 和 最大范围 的图片缩放尺度 */
//   if (zoom >= 100 && zoom < 250) {
//     document.getElementById('bigImg').style.zoom = zoom + '%'
//   }
//   return false
// }
</script>
<style lang="less" scoped>
.difference-wrap {
  width: 80vw;
  height: 56vh;
  display: flex;
  .content-upload {
    width: calc(50% - 5px);
    :deep(.ant-upload) {
      width: 100%;
      height: 100%;
    }
  }
  .content-upload:first-child {
    margin-right: 5px;
  }
  .content-upload:last-child {
    margin-left: 5px;
  }
  .content-wrap {
    height: 100%;
    width: 100%;
    overflow: auto;
  }
  .content-wrap-flag::after {
    content: '';
    /*让伪元素撑起高度*/
    height: 100%;
    display: inline-block;
    vertical-align: middle;
  }

  .tem-content,
  .img-content {
    display: inline-block;
    position: relative;
    height: auto;
    width: auto;
    vertical-align: middle;
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 10;
      width: 100%;
      height: 100%;
    }
  }
}
.actionBar {
  display: flex;
  justify-content: flex-end;
  .btn {
    margin: 20px 10px 0;
  }
  .btn:last-child {
    margin-right: 0;
  }
}
.result {
  span {
    display: inline-block;
    width: 120px;
  }
}
</style>

结果显示
在这里插入图片描述

双向操作同步

在上一个功能基础上,修改以下内容

ColorDifference.vue

  1. watch函数
  2. 图片画布操作函数
  3. 放大、缩小、数据保存的处理函数,只需要处理temMarkList
...

watch(
  () => clone(temMarkList.value, true),
  debounce((newVal, oldVal) => {
    if (imgUrl.value) {
      InitImgDrawRect()
    }
    initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
  }, 500),
  { deep: true }
)
...

const InitImgDrawRect = (markList) => {
//   oldTemMarkList.value = clone(temMarkList.value) //在这里存上模板标记的旧值,好在移动缩小放大时进行比较
//   imgMarkList.value = clone(markList)
  initCanvas(imgContentRef, imgMarkCanvasRef, temMarkList, imgShowFlag, imgContentWrapClassFlag, '1', '1')
}
const handleMarkPoint = () => {
  // 这时候需要传两组数据,看后端如何定义,图片和模板一一对应
  const pointArr = []
  temMarkList.value.forEach((item, index) => {
    const imgItem = imgMarkList.value[index]
    pointArr.push({
      id: index + 1,
      left_x: item.x / zoom.value,
      left_y: item.y / zoom.value,
      right_x: item.x / zoom.value + item.w / zoom.value,
      right_y: item.y / zoom.value + item.h / zoom.value,
      result: ''
    })
  })
  console.log('pointArr', pointArr)
  return JSON.stringify(pointArr)
}
//放大
const scaleLarge = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom += 10
  zoom.value = innerZoom / 100
  temContentRef.value.style.zoom = innerZoom + '%'
  if (imgUrl.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
  }
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value - 0.1)) * zoom.value
    item.y = (item.y / (zoom.value - 0.1)) * zoom.value
    item.w = (item.w / (zoom.value - 0.1)) * zoom.value
    item.h = (item.h / (zoom.value - 0.1)) * zoom.value
  })

  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

// //缩小
const scaleSmall = () => {
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  innerZoom -= 10
  zoom.value = innerZoom / 100
  temContentRef.value.style.zoom = innerZoom + '%'
  if (imgUrl.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
  }
  temMarkList.value.forEach((item) => {
    item.x = (item.x / (zoom.value + 0.1)) * zoom.value
    item.y = (item.y / (zoom.value + 0.1)) * zoom.value
    item.w = (item.w / (zoom.value + 0.1)) * zoom.value
    item.h = (item.h / (zoom.value + 0.1)) * zoom.value
  })
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}

...

扩展-滚轮放大缩小

在上一个功能基础上,修改以下内容

ColorDifference.vue

  1. 在标签外层加上滚轮事件
  2. 添加滚轮事件方法
  3. 外层容器不允许出现滚动条
...
  			<div
              class="tem-content"
              ref="temContentRef"
              :style="{ opacity: temShowFlag ? 1 : 0 }"
              @mousewheel="rollImg()"
            >
              <img :src="templateUrl" />
              <canvas ref="temMarkCanvasRef"></canvas>
            </div>
            <div
              class="img-content"
              ref="imgContentRef"
              :style="{ opacity: temShowFlag ? 1 : 0 }"
              @mousewheel="rollImg()"
            >
              <img :src="imgUrl" />
              <canvas ref="imgMarkCanvasRef"></canvas>
           </div>
//滚轮缩放
const rollImg = () => {
  if (!templateUrl.value) {
    return
  }
  /* 获取当前页面的缩放比若未设置zoom缩放比,则为默认100%,即1,原图大小*/
  let innerZoom = parseInt(temContentRef.value.style.zoom) || 100
  if ((innerZoom === 10 && event.wheelDelta < 0) || (innerZoom === 200 && event.wheelDelta > 0)) {
    // 最小值,最大值零界点处理
    return
  }
  innerZoom += event.wheelDelta / 12
  zoom.value = innerZoom / 100

  /* event.wheelDelta 获取滚轮滚动值并将滚动值叠加给缩放比zoom    wheelDelta统一为±120,其中正数表示为向上滚动,负数表示向下滚动  */

  /* 最小范围 和 最大范围 的图片缩放尺度 */
  if (event.wheelDelta > 0 && innerZoom <= 200) {
    // 放大
    temMarkList.value.forEach((item) => {
      item.x = (item.x / (zoom.value - 0.1)) * zoom.value
      item.y = (item.y / (zoom.value - 0.1)) * zoom.value
      item.w = (item.w / (zoom.value - 0.1)) * zoom.value
      item.h = (item.h / (zoom.value - 0.1)) * zoom.value
    })
  } else if (event.wheelDelta < 0 && innerZoom >= 10) {
    // 缩小
    temMarkList.value.forEach((item) => {
      item.x = (item.x / (zoom.value + 0.1)) * zoom.value
      item.y = (item.y / (zoom.value + 0.1)) * zoom.value
      item.w = (item.w / (zoom.value + 0.1)) * zoom.value
      item.h = (item.h / (zoom.value + 0.1)) * zoom.value
    })
  }
  temContentRef.value.style.zoom = innerZoom + '%'
  if (imgUrl.value) {
    imgContentRef.value.style.zoom = innerZoom + '%'
  }
  initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)

  return false
}
<style lang="less" scoped>
  .content-wrap {
    height: 100%;
    width: 100%;
    // overflow: auto;
    overflow: hidden;
  }
</style>
...

结果显示
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2257482.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

StarRocks-hive数据类型导致的分区问题

背景&#xff1a; 有个hive的表&#xff0c;是月分区的&#xff08;month_id&#xff09;&#xff0c;分区字段用的是string类型。数据量比较大&#xff0c;为了保证计算性能&#xff0c;所以把数据导入到SR里&#xff0c;构建一个内部表。但是在建表的时候想使用月分区使用pa…

Flume——进阶(agent特性+三种结构:串联,多路复用,聚合)

目录 agent特性ChannelSelector描述&#xff1a; SinkProcessor描述&#xff1a; 串联架构结构图解定义与描述配置示例Flume1&#xff08;监测端node1&#xff09;Flume3&#xff08;接收端node3&#xff09;启动方式 复制和多路复用结构图解定义描述配置示例node1node2node3启…

嵌入式学习(15)-stm32通用GPIO模拟串口发送数据

一、概述 在项目开发中可能会遇到串口不够用的情况这时候可以用通过GPIO来模拟串口的通信方式。 二、协议格式 按照1位起始位8位数据位1位停止位的方式去编写发送端的程序。起始位拉低一个波特率的时间&#xff1b;发送8位数据&#xff1b;拉高一个波特率的时间。 三、代码 …

【Go系列】:全面掌握 Sentinel Go —— 构建高可用微服务的流量控制、熔断、降级与系统防护体系

前言 在现代分布式系统架构中&#xff0c;服务的稳定性和可用性是至关重要的。随着微服务和云原生技术的发展&#xff0c;如何有效地进行流量控制、熔断降级以及系统保护成为了一个关键课题。Sentinel 是阿里巴巴开源的一款面向分布式服务架构的流量控制组件&#xff0c;它不仅…

多模态RAG:通用框架方案调研汇总

阅读原文 多模态检索增强生成是一种新兴的设计范式&#xff0c;允许AI模型与文本、图像、视频等存储进行交互。在介绍多模态 RAG 之前&#xff0c;我们先简单了解一下传统的检索增强生成 (RAG)。 标准 RAG RAG 的理念是找到与用户查询相关的核心信息&#xff0c;然后将该信息…

《HTML 的变革之路:从过去到未来》

一、HTML 的发展历程 图片: HTML 从诞生至今&#xff0c;经历了多个版本的迭代。 &#xff08;一&#xff09;早期版本 HTML 3.2 在 1997 年 1 月 14 日成为 W3C 推荐标准&#xff0c;提供了表格、文字绕排和复杂数学元素显示等新特性&#xff0c;但因实现复杂且缺乏浏览器…

游戏交易系统设计与实现

文末获取源码和万字论文&#xff0c;制作不易&#xff0c;感谢点赞支持。 题目&#xff1a;游戏交易系统设计与实现 摘 要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询…

Mac mini m4本地跑大模型(ollama + llama + ComfyUI + Stable Diffusion | flux)

安装chat大模型&#xff08;不推荐&#xff0c;本地运行的大模型只能聊废话&#xff0c;不如网页版使用openAI等高效&#xff09; 首先下载ollama的安装包 https://ollama.com/ 点击启动访问&#xff1a;http://localhost:11434 Ollama is running 代表已经运行起来了&#x…

精品C++项目推荐:分布式kv存储系统

项目代码直接开源到Github&#xff1a;https://github.com/youngyangyang04/KVstorageBaseRaft-cpp 欢迎去star&#xff0c;fork&#xff01; 项目背景相关 背景 在当今大规模分布式系统的背景下&#xff0c;需要可靠、高可用性的分布式数据存储系统。 传统的集中式数据库在…

Milvus中如何实现全文检索(Full Text Seach)?

在前两篇文章中&#xff08;Milvus python库 pymilvus 常用操作详解之Collection&#xff08;上&#xff09; 和 Milvus python库 pymilvus 常用操作详解之Collection&#xff08;下&#xff09;&#xff09;&#xff0c;我们了解了Milvus基于dense vector和sparse vector实现的…

unity打包web,如何减小文件体积,特别是 Build.wasm.gz

unity打包WebGL&#xff0c;使用的是wasw&#xff0c;最终生成的Build.wasm.gz体积很大&#xff0c;有6.5M&#xff0c;有几个方法可以稍微减小这个文件的大小 1. 裁剪引擎代码&#xff1a; 此步可将大小从6.5减小到 6.2&#xff08;此项默认开启&#xff0c;只是改了裁剪等级…

STM32 CubeMx HAL库 独立看门狗IWDG配置使用

看门狗这里我就不多介绍了&#xff0c;能搜到这篇文章说明你了解 总之就是一个单片机重启程序&#xff0c;设定好超时时间&#xff0c;在超时时间内没有喂狗&#xff0c;单片机就会复位 主要应用在单片机异常重启方面&#xff0c;比如程序跑飞&#xff08;注意程序跑飞时你就…

Selenium:强大的 Web 自动化测试工具

Selenium&#xff1a;强大的 Web 自动化测试工具 在当今的软件开发和测试领域&#xff0c;自动化工具的重要性日益凸显。Selenium 就是一款备受欢迎的 Web 自动化测试工具&#xff0c;它为开发者和测试人员提供了强大的功能和便利。本文将详细介绍 Selenium 是什么&#xff0c…

幼儿园学校养老院供电安全解决方案

一、 电气火灾每年以30%的比例高居各类火灾原因。以50%到80%的比例高居重特大火灾。已成为业界重点关注的对象并为此进行着孜孜不倦的努力。2021年“119”消防日&#xff0c;国家应急管理部消防救援局公布了2021年1至10月份全国火灾形势报告。数据显示&#xff0c;从火灾种类来…

UnityShaderLab-实现沿y轴溶解效果

实现思路&#xff1a; 实现思路同UnityShaderLab-实现溶解效果-CSDN博客 ShaderGraph实现&#xff1a; ShaderLab实现&#xff1a; 效果&#xff1a; 未完待续。。。

5G Multi-TRP R16~R18演进历程

提升小区边缘用户的性能&#xff0c;在覆盖范围内提供更为均衡的服务质量&#xff0c;NR中引入了多TRP协作传输的方案。多TRP协作传输通过多个TRP之间进行非相干联合传输&#xff08;Non Coherent-Joint Transmission&#xff0c;NC-JT&#xff09;、重复传输&#xff0f;接收或…

deepin 搭建 hadoop singlenode

deepin 搭建 hadoop singlenode 一、准备 1、升级软件 sudo apt updatesudo apt -y dist-upgrade2、安装常用软件 sudo apt -y install gcc make openssl libssl-dev libpcre3 libpcre3-dev libgd-dev \rsync openssh-server vim man zip unzip net-tools tcpdump lrzsz ta…

计算机毕业设计Python中华古诗词知识图谱可视化 古诗词智能问答系统 古诗词数据分析 古诗词情感分析模型 自然语言处理NLP 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

pythonbug修复案例----修复 Python数据分析程序中的持续 Bug

在 Python 编程的世界里&#xff0c;Bug 就像隐藏在暗处的幽灵&#xff0c;时不时地跳出来捣乱。而持续出现的 Bug&#xff0c;则更是让人头疼不已。今天&#xff0c;就让我们一同踏上修复一个 Python 持续 Bug 的征程。 假设我们正在开发一个简单的数据分析程序&#xff0c;其…

YOLOv8改进,YOLOv8引入U-Netv2分割网络中SDI信息融合模块+GSConv卷积,助力小目标

理论介绍 完成本篇需要参考以下两篇文章,并已添加到YOLOv8代码中 YOLOv8改进,YOLOv8引入GSConv卷积+Slim-neck,助力小目标检测,二次创新C2f结构YOLOv8改进,YOLOv8引入U-Netv2分割网络中SDI信息融合模块,助力小目标检测下文都是手把手教程,跟着操作即可添加成功 目录 理…