最近公司项目需要对图片进行标注,然后把标注信息通过接口存到后台,下次进入的时候获取标注信息并展示在前端图片上。功能简单描述一下:三个按钮开始绘制,清除,保存;编辑模式下才能清除和保存;点击某个矩形敲击键盘Backspace可以删除对应的矩形;画一个矩形框后会出现输入框,输入的文字会展示在矩形框上面;非编辑模式清除和保存禁用;矩形信息通过数组存储起来,方便存到服务器,下次进入可以复现标注信息。
话不多说,直接上最初版本的,最基础版本,各位可以根据自己的需求对初始版本进行加工成适合自己项目的功能;初级版本功能有瑕疵,可以根据自己项目进行修改。
可以扩展很多功能例如:
1、按钮区域增加上传图片,底部右侧展示一个图片列表;选中哪个图片就标注哪个图片就给哪个图片画红色虚线矩形边框;这个图片列表也可以来自远程服务器;
2、底部右侧有许多文字描述,选中文字描述,绘制矩形框的时候就把文字描述同时绘制在图片上
样式结构部分
<template>
<div id="markBox">
<div class="box-top">
<button class="btn-item" @click="toggleEditing">{{ isEditing ? "退出编辑" : "开始绘制" }}</button>
<button class="btn-item" @click="clearRectangles" :disabled="!isEditing">清除</button>
<button class="btn-item" @click="saveRectangles" :disabled="!isEditing">保存</button>
</div>
<div class="box-bottom">
<input
v-if="selectedRectIndex !== null"
type="text"
v-model="rectangles[selectedRectIndex].description"
@input="updateDescription"
placeholder="输入描述文字"
/>
<canvas
ref="canvas"
:width="imageWidth"
:height="imageHeight"
@mousedown="onMouseDown"
@mousemove="drawRect"
@mouseup="endDrawing"
></canvas>
<img :src="imageSrc" alt="Image" @load="initializeCanvas" style="display: none"/>
</div>
</div>
</template>
JavaScript部分
export default {
name:'mark',
data() {
return {
imageSrc: require("@/assets/images/1.jpg"), // 确保图片路径正确
imageWidth: 800, // 根据你的图片宽度调整
imageHeight: 600, // 根据你的图片高度调整
isDrawing: false,
startX: 0,
startY: 0,
ctx: null,
img: new Image(),
rectangles: [], // 用于存储矩形框信息
selectedRectIndex: null, // 选中的矩形框索引
isEditing: false, // 编辑模式状态
};
},
methods: {
initializeCanvas() {
const canvas = this.$refs.canvas;
this.ctx = canvas.getContext("2d");
this.img.src = this.imageSrc;
this.img.onload = () => {
this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
this.redrawRectangles();
};
},
//切换编辑模式
toggleEditing() {
this.isEditing = !this.isEditing;
},
//鼠标事件
onMouseDown(event) {
if (!this.isEditing) return;
const x = event.offsetX;
const y = event.offsetY;
// 检查点击位置是否在某个矩形框内
this.selectedRectIndex = this.rectangles.findIndex((rect) => {
return (
x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
);
});
if (this.selectedRectIndex === -1) {
// 如果没有选中矩形框,开始绘制新矩形框
this.isDrawing = true;
this.startX = x;
this.startY = y;
} else {
// 如果选中矩形框,监听键盘事件
window.addEventListener("keydown", this.onKeyDown);
}
},
//绘制矩形
drawRect(event) {
if (!this.isDrawing) return;
const canvas = this.$refs.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
// 绘制已有的矩形框
this.redrawRectangles();
// 绘制当前矩形框
const currentX = event.offsetX;
const currentY = event.offsetY;
const width = currentX - this.startX;
const height = currentY - this.startY;
this.ctx.strokeStyle = "red";
this.ctx.setLineDash([5, 3]);
this.ctx.lineWidth = 2;
this.ctx.strokeRect(this.startX, this.startY, width, height);
},
endDrawing(event) {
if (!this.isDrawing) return;
this.isDrawing = false;
const currentX = event.offsetX;
const currentY = event.offsetY;
const width = currentX - this.startX;
const height = currentY - this.startY;
// 保存当前矩形框信息
this.rectangles.push({
x: this.startX,
y: this.startY,
width: width,
height: height,
description: "", // 初始化描述文字
});
},
//键盘按键事件删除选中的矩形
onKeyDown(event) {
if (event.key === "Backspace" && this.selectedRectIndex !== null) {
// 删除选中的矩形框
this.rectangles.splice(this.selectedRectIndex, 1);
this.selectedRectIndex = null;
// 重新绘制矩形框
this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
this.redrawRectangles();
// 取消键盘事件监听
window.removeEventListener("keydown", this.onKeyDown);
}
},
clearRectangles() {
this.rectangles = [];
this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
},
//保存信息
saveRectangles() {
localStorage.setItem("savedRectangles", JSON.stringify(this.rectangles));
alert("矩形框已保存到 localStorage");
},
//复现标注信息于图片
loadRectangles() {
const savedRectangles = localStorage.getItem("savedRectangles");
if (savedRectangles) {
this.rectangles = JSON.parse(savedRectangles);
this.redrawRectangles();
}
},
updateDescription() {
// 重新绘制矩形框,更新描述文字
this.ctx.clearRect(0, 0, this.imageWidth, this.imageHeight);
this.ctx.drawImage(this.img, 0, 0, this.imageWidth, this.imageHeight);
this.redrawRectangles();
},
redrawRectangles() {
this.rectangles.forEach((rect) => {
this.ctx.strokeStyle = "red";
this.ctx.setLineDash([5, 3]);
this.ctx.lineWidth = 2;
this.ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
// 绘制描述文字
if (rect.description) {
this.ctx.font = "16px Arial";
this.ctx.fillStyle = "red";
this.ctx.fillText(rect.description, rect.x, rect.y - 5);
}
});
},
},
mounted() {
this.loadRectangles(); // 加载保存的矩形框信息
},
};
css样式部分
#markBox {
height: 100%;
width: 100%;
text-align: center;
padding-top:20px ;
.box-top{
display: flex;
align-items: center;
justify-content: center;
.btn-item{
height: 30px;
width: 100px;
margin: 5px;
}
button[disabled]{
cursor: not-allowed;
}
}
.box-bottom{
input {
display: block;
margin: 10px auto;
}
canvas {
border: 1px solid black;
}
}
}