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

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

双图版本(模板对比)

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

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.offsetXsY = em.offsetYlet 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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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)}breakcase 2:iframe = {x: mark.x,y: mark.y + (em.offsetY - sY),w: mark.w + (em.offsetX - sX),h: mark.h - (em.offsetY - sY)}breakcase 3:iframe = {x: mark.x + (em.offsetX - sX),y: mark.y,w: mark.w - (em.offsetX - sX),h: mark.h + (em.offsetY - sY)}breakcase 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 = truesX = ed.offsetXsY = 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.offsetXsY = 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.whichif ((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-uploadv-model:file-list="templateFileList"list-type="picture-card"class="content-upload":show-upload-list="false":openFileDialogOnClick="!templateUrl":before-upload="beforeUpload":maxCount="1"@change="handleTemplateChange"><divv-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-uploadv-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-uploadv-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-uploadv-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 = trueconst 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 = falseresponseData.value = res.data.dataresponseData.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.valuecav.width = contentRef.value.offsetWidth * zoom.valuecav.height = contentRef.value.offsetHeight * zoom.valueconsole.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 = trueconsole.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.valuecav.width = imgContentRef.value.offsetWidth * zoom.valuecav.height = imgContentRef.value.offsetHeight * zoom.valueimgCtx2D.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) || 100innerZoom += 10zoom.value = innerZoom / 100console.log('zoom', zoom.value)temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.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) || 100innerZoom -= 10zoom.value = innerZoom / 100console.log('zoom', zoom.value)temContentRef.value.style.zoom = innerZoom + '%'temMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.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.offsetXsY = em.offsetYlet 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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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 = indexjudgeDraw(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)}breakcase 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)}breakcase 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)}breakcase 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 = truesX = ed.offsetXsY = 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.offsetXsY = 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.whichif ((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 iradix = radix || chars.lengthif (len) {// Compact formfor (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]} else {// rfc4122, version 4 formvar r// rfc4122 requires these charactersuuid[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.5for (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 = falsefor (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 = truebreak}} else {if (unique2 === unique1) {isExist = truebreak}}}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-uploadv-model:file-list="templateFileList"list-type="picture-card"class="content-upload":show-upload-list="false":openFileDialogOnClick="!templateUrl":before-upload="beforeUpload":maxCount="1"@change="handleTemplateChange"><divv-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-uploadv-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-uploadv-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-uploadv-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.lengthconst oldLen = oldVal.lengthif (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 = trueconst 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 = falseresponseData.value = res.data.dataresponseData.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.valuecav.width = contentRef.value.offsetWidth * zoom.valuecav.height = contentRef.value.offsetHeight * zoom.valuecav.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) || 100innerZoom += 10zoom.value = innerZoom / 100if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'imgMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value - 0.1)) * zoom.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.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.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}// //缩小
const scaleSmall = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom -= 10zoom.value = innerZoom / 100if (imgContentRef.value) {imgContentRef.value.style.zoom = innerZoom + '%'imgMarkList.value.forEach((item) => {item.x = (item.x / (zoom.value + 0.1)) * zoom.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.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.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.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) || 100innerZoom += 10zoom.value = innerZoom / 100temContentRef.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.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.h = (item.h / (zoom.value - 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}// //缩小
const scaleSmall = () => {let innerZoom = parseInt(temContentRef.value.style.zoom) || 100innerZoom -= 10zoom.value = innerZoom / 100temContentRef.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.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.h = (item.h / (zoom.value + 0.1)) * zoom.value})initCanvas(temContentRef, temMarkCanvasRef, temMarkList, temShowFlag, temContentWrapClassFlag)
}...

扩展-滚轮放大缩小

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

ColorDifference.vue

  1. 在标签外层加上滚轮事件
  2. 添加滚轮事件方法
  3. 外层容器不允许出现滚动条
...<divclass="tem-content"ref="temContentRef":style="{ opacity: temShowFlag ? 1 : 0 }"@mousewheel="rollImg()"><img :src="templateUrl" /><canvas ref="temMarkCanvasRef"></canvas></div><divclass="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) || 100if ((innerZoom === 10 && event.wheelDelta < 0) || (innerZoom === 200 && event.wheelDelta > 0)) {// 最小值,最大值零界点处理return}innerZoom += event.wheelDelta / 12zoom.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.valueitem.y = (item.y / (zoom.value - 0.1)) * zoom.valueitem.w = (item.w / (zoom.value - 0.1)) * zoom.valueitem.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.valueitem.y = (item.y / (zoom.value + 0.1)) * zoom.valueitem.w = (item.w / (zoom.value + 0.1)) * zoom.valueitem.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>
...

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

相关文章:

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

双图版本&#xff08;模板对比&#xff09; 业务描述&#xff1a;模板与图片对比&#xff0c;只操作模板框选的位置进行色差对比&#xff0c;传框选坐标位置给后端&#xff0c;返回对比结果显示 draw.js文件&#xff1a; 新增了 createUuid&#xff0c;和求取两个数组差集的方…...

unity3d—demo(2d人物左右移动发射子弹)

目录 人物代码示例&#xff1a; 子弹代码示例&#xff1a; 总结上面代码&#xff1a; 注意点&#xff1a; 人物代码示例&#xff1a; using System.Collections; using System.Collections.Generic; using UnityEngine;public class PlayerTiao : MonoBehaviour {public f…...

【ETCD】【源码阅读】 深入解析 raftNode.start`函数:Raft 核心启动逻辑剖析

raftNode.start方法 是 etcd 中 Raft 模块的核心启动点&#xff0c;其职责是管理 Raft 状态机的状态变迁、日志处理及集群通信等逻辑。通过对源码的逐行分析&#xff0c;我们将全面揭示其运行机制&#xff0c;探讨其设计背后的分布式系统理念。 函数核心结构 raftNode.start 方…...

Robust Depth Enhancement via Polarization Prompt Fusion Tuning

paper&#xff1a;论文地址 code&#xff1a;github项目地址 今天给大家分享一篇2024CVPR上的文章&#xff0c;文章是用偏振做提示学习&#xff0c;做深度估计的。模型架构图如下 这篇博客不是讲这篇论文的内容&#xff0c;感兴趣的自己去看paper&#xff0c;主要是分享环境&…...

NEFTune,SFT训练阶段给Embedding加噪音

仿照CV里&#xff0c;数据增强的思路&#xff08;给图像做旋转、反转、改变亮度等&#xff09;&#xff1b;NLP里&#xff0c;SFT训练数据较少时&#xff0c;也可往embedding上加噪音&#xff0c;来增加训练数据的丰富程度。进而提升最终训练效果。 前提假设&#xff1a;Embed…...

uniapp -- 实现页面滚动触底加载数据

效果 首选,是在pages.json配置开启下拉刷新 {"path": "pages/my/document/officialDocument","style": {"navigationStyle":</...

L22.【LeetCode笔记】相交链表(新版)

目录 1.题目 代码模板 2.分析 ​编辑 算法误区 正确方法1 但不能通过所有的测试用例 修改后 提交结果 正确方法2 节省代码的技巧 1.题目 https://leetcode.cn/problems/3u1WK4/description/ 给定两个单链表的头节点 headA 和 headB &#xff0c;请找出并返回两个单…...

智能时代网络空间认知安全新观察

文章目录 前言一、历史上的四次认知革命二、人工智能革命掀起认知安全新浪潮三、人工智能技术塑造认知安全新范式四、人工智能治理应对认知安全新思考 前言 12月5日&#xff0c;在2024第三届北外滩网络安全论坛上以“智能时代网络空间认知安全新观察”为主题作主旨演讲&#x…...

游戏如何应对模拟器作弊

模拟器是指能在PC端模拟出安卓手机系统的软件&#xff0c;市面上比较常见的安卓模拟器有&#xff1a;雷电模拟器、MuMu模拟器、夜神模拟器等。 市面上常见的模拟器 模拟器既可以节省手机内存空间&#xff0c;避免长时间玩游戏手机发烫发热的尴尬&#xff0c;也可以用键盘鼠标对…...

c++ 判断一个 IP 地址(可能是 IPv6 或 IPv4)是否属于特定范围

在 C 中&#xff0c;判断一个 IP 地址&#xff08;可能是 IPv6 或 IPv4&#xff09;是否属于特定范围时&#xff0c;需要考虑两种不同的地址格式和它们的范围比较。IPv6 和 IPv4 地址结构完全不同&#xff0c;因此需要分别处理这两种地址类型。 实现思路&#xff1a; 识别 IP…...

计算机视觉——相机标定(Camera Calibration)

文章目录 1. 简介2. 原理3. 相机模型3.1 四大坐标系3.2 坐标系间的转换关系3.2.1 世界坐标系到相机坐标系3.2.2 相机坐标系到图像坐标系3.2.3 像素坐标系转换为图像坐标系3.2.4 世界坐标转换为像素坐标 3.3 畸变3.3.1 畸变类型3.3.1.1 径向畸变&#xff08;Radial Distortion&a…...

【qt环境配置】windows下的qt与vs工具集安装\版本对应关系

vs工具集安装通过vs的在线安装器勾选工具集即可 工具包下载路径&#xff1a;https://www.microsoft.com/zh-cn/download/details.aspx?id40784 配置工具集在qt中可以自动扫描到 《正确在 Windows 上配置 MSVC(2019) 作为 Qt 编译器》https://b3logfile.com/pdf/article/15922…...

GitHub使用

太久不用GitHub发现自己又有些不会了&#xff0c;突发奇想为何不把每次看到的有指导意义的博客收录一下以便下次查阅呢 如何上传文件夹到GitHub上&#xff08;配图详解&#xff09;&#xff1f;_github上傳資料夾-CSDN博客 github上如何删除自己的仓库_github删除仓库-CSDN博…...

元宇宙时代的社交平台:Facebook的愿景与实践

随着科技的不断进步&#xff0c;元宇宙&#xff08;Metaverse&#xff09;这一概念逐渐走进了人们的视野。作为全球最大的社交平台之一&#xff0c;Facebook&#xff08;现Meta&#xff09;在这场元宇宙革命中扮演着重要角色。Meta不仅在不断扩展其社交平台的边界&#xff0c;还…...

vue2中各种钩子函数的总结以及使用场景

在 Vue 2 中&#xff0c;生命周期钩子函数是 Vue 实例在不同阶段自动调用的函数。这些钩子允许开发者在组件的创建、更新和销毁的特定时刻插入自定义逻辑。以下是 Vue 2 中的各种生命周期钩子函数的总结及其使用场景。 生命周期钩子函数总结 1、beforeCreate 调用时机&#…...

软件架构:从传统单体到现代微服务的技术演变

1.引言 在软件开发中&#xff0c;架构设计不仅仅是程序员的技术任务&#xff0c;它更是一个项目成功的关键。无论是小型应用还是大型分布式系统&#xff0c;软件架构都直接影响着系统的可维护性、可扩展性、性能和稳定性。理解软件架构的必要性&#xff0c;能够帮助开发人员做…...

git新建远程分支后,无法切换

git remote # 列出所有远程主机 git remote update origin --prune # 更新远程主机origin 整理分支 git branch -r # 列出远程分支 git branch -vv # 查看本地分支和远程分支对应关系 git checkout -b gpf origin/gpf # 新建本地分支gpf与远程gpf分支相关…...

【SpringBoot】31 Session + Redis 实战

Gitee https://gitee.com/Lin_DH/system 介绍 【SpringBoot】30 Cookie、Session、Token https://blog.csdn.net/weixin_44088274/article/details/144241595 背景 Spring Session 是 Spring 的一个子项目&#xff0c;它提供了一种管理用户会话信息的方法&#xff0c;无论…...

在Windows环境下的rknn-toolkit环境搭建

首先安装好conda&#xff0c;我是用的是anaconda&#xff0c;miniconda也可以。 下载rknn_toolkit的轮子。可以直接在瑞芯微的git仓库中下载&#xff0c;地址为&#xff1a;github.com/rockchip-linux/rknn-toolkit/releases。我这里下载的是1.7.5版本的。选择rknn-toolkit-v1.…...

Facebook广告突然无消耗?原因解析与解决方案。

在Facebook广告投放中&#xff0c;广告突然无消耗是很多广告主都会遇到的难题。这种情况不仅浪费时间&#xff0c;还可能导致营销活动停滞&#xff0c;影响业务发展。那么&#xff0c;广告无消耗的原因是什么&#xff1f;又该如何解决呢&#xff1f; 一、Facebook广告无消耗的…...

Rabbitmq 镜像队列

RabbitMQ 支持高可用性队列&#xff08;HA Queues&#xff09;&#xff0c;可以在多个节点之间复制队列&#xff0c;确保即使某个节点失败&#xff0c;消息仍然可用。将 RabbitMQ 部署为集群&#xff0c;确保高可用性和负载均衡。 RabbitMQ 的镜像队列集群&#xff08;Mirrore…...

TensorBoard

1、TensorFlow的TensorBoard TensorBoard是TensorFlow的一个组件&#xff0c;它提供了一个交互式的界面&#xff0c;用于可视化TensorFlow程序的训练过程和模型结构。 使用TensorBoard&#xff0c;你可以&#xff1a; 可视化训练过程中的各种指标&#xff0c;如损失函数、准…...

运维实战:K8s 上的 Doris 高可用集群最佳实践

今天我们将深入探讨&#xff1a;&#xff1a;如何在 K8s 集群上部署 Compute storage coupled&#xff08;存算耦合&#xff09; 模式的 Doris 高可用集群&#xff1f; 本文&#xff0c;我将为您提供一份全面的实战指南&#xff0c;逐步引导您完成以下关键任务&#xff1a; 配…...

2024.12.5——攻防世界Training-WWW-Robots攻防世界baby_web

2024.12.5—攻防世界Training-WWW-Robots 知识点&#xff1a;robots协议 dirsearch工具 本题与第一道Robots协议十分类似&#xff0c;不做wp解析 大致步骤&#xff1a; step 1 打开靶机&#xff0c;发现是robots协议相关 step 2 用dirsearch进行扫描目录 step 3 url传参r…...

当 Nginx 出现连接超时问题,如何排查?

文章目录 当 Nginx 出现连接超时问题&#xff0c;如何排查&#xff1f; 一、了解 Nginx 连接超时的基本概念二、可能导致 Nginx 连接超时的原因 &#xff08;一&#xff09;服务器负载过高&#xff08;二&#xff09;上游服务响应缓慢&#xff08;三&#xff09;网络问题&…...

vue2 项目中实现动态代理,服务器上通过nginx部署 实现动态代理

一、前言&&原理 前言&#xff1a;vue2 项目中&#xff0c;请求接口是从表格的当前获取的&#xff0c;也就是接口ip:端口号:路经不确定&#xff0c;要实现点击表格当前行请求对应的接口 实现原理&#xff1a;将实际要请求的ip等信息存在请求头中&#xff0c;用的时候再…...

基于SpringBoot+Vue的民宿山庄农家乐管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…...

【数据分享】1901-2023年我国省市县三级逐年最低气温数据(Shp/Excel格式)

之前我们分享过1901-2023年1km分辨率逐月最低气温栅格数据和Excel和Shp格式的省市县三级逐月最低气温数据&#xff0c;原始的逐月最低气温栅格数据来源于彭守璋学者在国家青藏高原科学数据中心平台上分享的数据&#xff01;基于逐月栅格数据我们采用求年平均值的方法得到逐年最…...

后端API接口设计标准(Java)

Controller 层&#xff08;API接口&#xff09; 无论是传统的三层架构还是现在的COLA架构&#xff0c;Controller 层依旧有一席之地&#xff0c;说明他的必要性&#xff1b;说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现&#xff0c;但是它负责接收…...

网络安全法 -网络信息安全

第四章 网络信息安全 第四十条 网络运营者应当对其收集的用户信息严格保密&#xff0c;并建立健全用户信息保护制度。 第四十一条 网络运营者收集、使用个人信息&#xff0c;应当遵循合法、正当、必要的原则&#xff0c;公开收集、使用规则&#xff0c;明示收集、使用信息的…...

公司宣传片制作多少钱/seo专业培训班

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一.W25Q32-Flash1.官方说明2.引脚排列3.特殊引脚说明1.串行数输入输出和IOS (DI DO和IO0, IO1,IO2,IO3)2.写保护&#xff08;/WP&#xff09;3.保持端&#xff0…...

上海购物网站建设/链接搜索

maven项目,maven-install总是出现这个错误,气死了, 查阅资料终于找到解决办法: 原因 这是由于缺少maven-resources-plugin-2.4.3.jar文件。这个文件是在{user.home}\.m2\repository\org\apache\maven\plugins\maven-resources-plugin\下。{user.home}是maven的配置路径&#xf…...

大型网站开发语言框架工具/软文营销经典案例优秀软文

一&#xff1a;Spring相关API 笔记二程序源码下载 applicationContext&#xff1a;接口类型&#xff0c;代表应用上下文&#xff0c;可以通过其实例获得 Spring 容器中的 Bean 对象 ApplicationContext的实现类 1&#xff09;ClassPathXmlApplicationContext ​ 它是从类的…...

义乌做网站/重庆网站关键词排名优化

椒盐噪声 椒盐噪声也称为脉冲噪声,是图像中经常见到的一种噪声,它是一种随机出现的白点或者黑点,可能是亮的区域有黑色像素或是在暗的区域有白色像素(或是两者皆有)。椒盐噪声的成因可能是影像讯号受到突如其来的强烈干扰而产生、类比数位转换器或位元传输错误等。例如失效…...

外贸网站好做吗/网络推广是什么工作内容

介绍 此款源码是彻底解放劳动人民的双手&#xff0c;全自动采集&#xff0c;模板代码也进行了全面优化&#xff0c;更加有助于SEO 下载链接 http://www.bytepan.com/iTHwNKE6vZD 图片...

网站多久需要维护/免费的域名和网站

网上有大哥总结了一张图&#xff0c;完整地囊括了整个NLP处理企业文本数据的整个流程。挺好的&#xff0c;贴出来给大家看一下。在此也搜藏下。...