背景:我们有一个摄像的产品,拍照传统的水表盘面,我们需要框选水表读数,标注点传到后端,后端根据标注点自动去截取摄像表拍摄回来的图片,然后拿到大模型里面进行训练。由于同一只表拍摄的画面都是一样的,所以按此方法减少了人工标注的繁琐工作
一、效果图
二、问题记录
1、引入fabric
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js" referrerpolicy="no-referrer"></script>
2、实例化fabric,并根据图片大小设置canvas的背景大小
loadImageAndSetCanvas() {
this.fabricCanvas = new fabric.Canvas("canvasId");
const img = new Image();
img.src = this.images;
img.onload = () => {
this.canvasProp.width = img.width;
this.canvasProp.height = img.height;
// 设置canvas大小
this.fabricCanvas.setWidth(img.width);
this.fabricCanvas.setHeight(img.height);
// 创建 Fabric 图片对象
const fabricImage = new fabric.Image(img, {
left: 0,
top: 0,
selectable: false // 防止背景图片被选择
});
// 设置背景图片
this.fabricCanvas.setBackgroundImage(fabricImage);
};
},
3、画矩形和文字,实践了三种方式实现
3.1 、矩形+文字,放到一个分组里面
drawTags(item, callback = () => {
}) {
//this.clear(); // 清空所有对象
// tagsData.forEach(item => {
// 创建一个矩形
const rect = new fabric.Rect({
fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
width: item.width, // 矩形的宽度
height: item.height, // 矩形的高度
angle: 0, // 旋转角度
stroke: item.color,
strokeWidth: 2, // 边框宽度
});
// 创建第一个文字对象
const text1 = new fabric.Text(item.label, {
fontFamily: "Arial",
fontSize: 20,
fill: item.color, // 文字颜色
// stroke: "#ffffff", // 文字边框颜色
// strokeWidth: 0.5 // 文字边框宽度
});
// 计算文字位置以确保其在矩形的正中央
text1.set({
left: rect.left + rect.width / 2,
top: rect.top + rect.height / 2,
originX: "center",
originY: "center",
});
// 使用fabric.Group将矩形和文字组合在一起
const group = new fabric.Group([rect, text1], {
left: item.isInit ? this.canvasProp.width / 2 - item.width / 2 : item.startX, // 矩形的左上角 x 坐标
top: item.isInit ? this.canvasProp.height / 2 - item.height / 2 : item.startY, // 矩形的左上角 y 坐标
cornerColor: item.color, // 控制点的颜色
cornerSize: 10, // 控制点的大小
borderWidth: 0, // 选中时的边框宽度
transparentCorners: true,
angle: item.rotate, // 组合对象的旋转角度
id: item.id,
});
// 将组合添加到画布上
console.log("添加到画布上", group);
this.fabricCanvas.add(group);
callback();
//});
},
3.2 实现Polygon+文字实现
此时的数据格式为:
{
label: "基表数据",
color: "#0000ff",
type: "rectangle",
dataPoint: [ ]
}
drawTags(item, callback = () => {
}) {
// 使用 fabric.Polygon 创建多边形
let points = item.dataPoint;
if (!points.length) {
let x = this.canvasProp.width / 2 - this.width / 2;
let y = this.canvasProp.height / 2 - this.height / 2;
points = [
[x, y],
[x + this.width, y],
[x + this.width, y + this.height],
[x, y + this.height]
];
}
console.log("points", points);
const polygon = new fabric.Polygon(
points.map(point => ({
x: point[0], y: point[1] })),
{
fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
stroke: item.color, // 边框颜色
strokeWidth: 2 // 边框宽度
}
);
// this.fabricCanvas.add(polygon);
// 创建第一个文字对象
const text = new fabric.Text(item.label, {
fontFamily: "Arial",
fontSize: 20,
fill: item.color, // 文字颜色
left: points[0][0] + this.width / 2,
top: points[0][1] + this.height / 2,
originX: "center",
originY: "center"
// stroke: "#ffffff", // 文字边框颜色
// strokeWidth: 0.5 // 文字边框宽度
});
//this.fabricCanvas.add(text);
// 使用fabric.Group将矩形和文字组合在一起
const group = new fabric.Group([polygon], {
left: points[0][0],
top: points[1][1],
cornerColor: item.color, // 控制点的颜色
cornerSize: 10, // 控制点的大小
borderWidth: 0, // 选中时的边框宽度
transparentCorners: true,
id: item.id
});
this.fabricCanvas.add(group);
},
3.3 自定义LabelRect
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: "labeledRect",
initialize: function(options) {
options || (options = {
});
this.callSuper("initialize", options);
this.set("label", options.label || "");
this.set("left", options.left || 0);
this.set("top", options.top || 0);
this.set("width", options.width || 0);
this.set("height", options.height || 0);
this.set("stroke", options.stroke || 0);
},
toObject: function() {
return fabric.util.object.extend(this.callSuper("toObject"), {
label: this.get("label")
});
},
_render: function(ctx) {
// 调用父类的渲染方法以绘制矩形
this.callSuper("_render", ctx);
let displayText = this.label;
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = this.stroke;
ctx.strokeStyle = "white";
ctx.strokeText(displayText, 0, 0);
ctx.fillText(this.label, 0, 0);
}
});
var labeledRect = new LabeledRect({
left: 100, // 矩形的左上角 x 坐标
top: 100, // 矩形的左上角 y 坐标
fill: this.hexToRgba("#0000ff", 0.2), // 填充颜色,透明度为 0.2
width: 150, // 矩形的宽度
height: 50, // 矩形的高度
angle: 0, // 旋转角度
stroke: "#0000ff",
label: "基表数据",
strokeWidth: 2 // 边框宽度
});
this.fabricCanvas.add(labeledRect);
4、添加每个数据的时候,记得添加唯一的表示ID
5、当编辑页面后,需要计算当前矩形四个在画布上的实际坐标点
getPointData() {
let result = {
};
this.fabricCanvas.getObjects().forEach((rect) => {
console.log("rect==", rect);
const coords = [];
const points = rect.get("aCoords"); // 获取矩形的绝对坐标
console.log("points", points);
const viewportTransform = this.fabricCanvas.viewportTransform;
const zoom = this.fabricCanvas.getZoom();
// 将内部坐标转换为实际画布坐标
Object.keys(points).forEach((key) => {
let point = points[key];
const actualX = (point.x - viewportTransform[4]) / zoom;
const actualY = (point.y - viewportTransform[5]) / zoom;
coords.push([Math.round(actualX), Math.round(actualY)]);
});
result[rect.id] = coords;
});
console.log("result", result);
return result;
},
6、如果下次需要还原现场,需要保存canvas的JSON数据,一定要假如["id],因为id是自定义属性,如果不加该id,保存的数据就没有id 这个唯一标志
getCanvasJson() {
return JSON.stringify(this.fabricCanvas.toJSON(["id"]));
},
7、还原现场,因为标注点没变,背景图片变了,所以我们需要重置背景图
loadFromJSON(json) {
this.fabricCanvas.clear();
//背景图替换为当前的背景图
let newjson = JSON.parse(json);
console.log("加载的json", newjson);
newjson.backgroundImage = backgroundImage;
this.fabricCanvas.setBackgroundImage();
//this.fabricCanvas.loadFromJSON(newjson);
// 反序列化对象
this.fabricCanvas.loadFromJSON(newjson);
},
8、画布的放大、缩小
zoomBig() {
const currentZoom = this.fabricCanvas.getZoom();
if (currentZoom < 3) {
this.scaleCanvas(currentZoom + 0.1);
}
},
zoomSmall() {
const currentZoom = this.fabricCanvas.getZoom();
if (currentZoom > 1) {
this.scaleCanvas(currentZoom - 0.1);
}
},
scaleCanvas(scale) {
const center = this.getCanvasCenter();
this.fabricCanvas.zoomToPoint({
x: center.x, y: center.y }, scale);
},
//获取画布的中心点
getCanvasCenter() {
const canvasCenter = {
x: this.fabricCanvas.width / 2,
y: this.fabricCanvas.height / 2,
};
return canvasCenter;
},
9、画布的平移
handleKeyDown(event) {
console.log("event.key", event.key);
const step = 5; // 每次移动的步长
switch (event.key) {
case "ArrowUp":
this.canvasProp.translateY -= step;
break;
case "ArrowDown":
this.canvasProp.translateY += step;
break;
case "ArrowLeft":
this.canvasProp.translateX -= step;
break;
case "ArrowRight":
this.canvasProp.translateX += step;
break;
}
this.panCanvas(this.canvasProp.translateX, this.canvasProp.translateY);
},
panCanvas(translateX, translateY) {
// 获取当前的 viewportTransform
const viewportTransform = this.fabricCanvas.viewportTransform.slice(); // 创建一个副本,以免直接修改原始数组
// 更新平移值
viewportTransform[4] = translateX;
viewportTransform[5] = translateY;
// 设置新的 viewportTransform
this.fabricCanvas.setViewportTransform(viewportTransform);
},
三、效果源代码
<template>
<div
:style="{
width: canvasProp.width + 'px',
height: canvasProp.height + 'px',
border: '1px solid #ccc',
}"
>
<canvas
:width="canvasProp.width"
:height="canvasProp.height"
:style="{
width: canvasProp.width + 'px',
height: canvasProp.height + 'px',
}"
id="canvasId"
></canvas>
<div class="muane">
<span @click="zoomBig">
<i class="el-icon-zoom-in"></i>
</span>
<span @click="zoomSmall">
<i class="el-icon-zoom-out"></i>
</span>
</div>
</div>
</template>
<script>
let backgroundImage = null;
export default {
name: "images-tags",
props: {
// 矩形标注的数据
tagsData: {
type: Array,
default: () => {
return [
{
label: "基表数据",
color: "#0000ff",
type: "rectangle",
width: 150,
height: 50,
rotate: 0,
isInit: true,
startX: 185,
startY: 235,
},
{
label: "数据点2",
color: "#0000ff",
type: "rectangle",
width: 150,
height: 50,
rotate: 0,
isInit: false,
startX: 100,
startY: 100,
},
];
},
},
// 图片路径
images: {
type: String,
default: "/img/yejing1.jpg",
},
},
data() {
return {
fabricCanvas: null,
canvasProp: {
width: 0, // canvas的宽度
height: 0, // canvas的高度
translateX: 0,
translateY: 0,
},
};
},
mounted() {
this.loadImageAndSetCanvas();
window.addEventListener("keydown", this.handleKeyDown);
},
beforeDestroy() {
window.removeEventListener("keydown", this.handleKeyDown);
},
methods: {
zoomBig() {
const currentZoom = this.fabricCanvas.getZoom();
if (currentZoom < 3) {
this.scaleCanvas(currentZoom + 0.1);
}
},
zoomSmall() {
const currentZoom = this.fabricCanvas.getZoom();
if (currentZoom > 1) {
this.scaleCanvas(currentZoom - 0.1);
}
},
scaleCanvas(scale) {
const center = this.getCanvasCenter();
this.fabricCanvas.zoomToPoint({
x: center.x, y: center.y }, scale);
},
//获取画布的中心点
getCanvasCenter() {
const canvasCenter = {
x: this.fabricCanvas.width / 2,
y: this.fabricCanvas.height / 2,
};
return canvasCenter;
},
handleKeyDown(event) {
console.log("event.key", event.key);
const step = 5; // 每次移动的步长
switch (event.key) {
case "ArrowUp":
this.canvasProp.translateY -= step;
break;
case "ArrowDown":
this.canvasProp.translateY += step;
break;
case "ArrowLeft":
this.canvasProp.translateX -= step;
break;
case "ArrowRight":
this.canvasProp.translateX += step;
break;
}
this.panCanvas(this.canvasProp.translateX, this.canvasProp.translateY);
},
loadFromJSON(json) {
this.fabricCanvas.clear();
//背景图替换为当前的背景图
let newjson = JSON.parse(json);
console.log("加载的json", newjson);
newjson.backgroundImage = backgroundImage;
this.fabricCanvas.setBackgroundImage();
//this.fabricCanvas.loadFromJSON(newjson);
// 反序列化对象
this.fabricCanvas.loadFromJSON(newjson);
},
getCanvasJson() {
console.log("在线获取json", this.fabricCanvas.toJSON(["id"]));
return JSON.stringify(this.fabricCanvas.toJSON(["id"]));
},
hexToRgba(hex, alpha) {
const bigint = parseInt(hex.replace("#", ""), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `rgba(${
r},${
g},${
b},${
alpha})`;
},
// ...其他方法
panCanvas(translateX, translateY) {
// 获取当前的 viewportTransform
const viewportTransform = this.fabricCanvas.viewportTransform.slice(); // 创建一个副本,以免直接修改原始数组
// 更新平移值
viewportTransform[4] = translateX;
viewportTransform[5] = translateY;
// 设置新的 viewportTransform
this.fabricCanvas.setViewportTransform(viewportTransform);
},
loadImageAndSetCanvas() {
this.fabricCanvas = new fabric.Canvas("canvasId");
const img = new Image();
img.src = this.images;
img.onload = () => {
console.log("图片加载完毕", img);
this.canvasProp.width = img.width;
this.canvasProp.height = img.height;
// 设置canvas大小
this.fabricCanvas.setWidth(img.width);
this.fabricCanvas.setHeight(img.height);
// 创建 Fabric 图片对象
const fabricImage = new fabric.Image(img, {
left: 0,
top: 0,
selectable: false, // 防止背景图片被选择
});
// 设置背景图片
let backgroundImageObj = this.fabricCanvas.setBackgroundImage(fabricImage);
backgroundImage = backgroundImageObj.backgroundImage;
this.drawTags(this.tagsData[0])
};
},
clear() {
this.fabricCanvas.getObjects().forEach((obj) => {
if (obj !== this.fabricCanvas.backgroundImage) {
this.fabricCanvas.remove(obj);
}
});
},
getGroupById(id) {
let result = null;
this.fabricCanvas.getObjects().forEach((obj) => {
console.log(obj);
if (obj.id === id) {
result = obj;
}
});
return result;
},
remove(item) {
console.log("id", item.id);
let result = this.getGroupById(item.id);
console.log("result", result);
if (result) {
this.fabricCanvas.remove(result);
}
},
drawTags(item, callback = () => {
}) {
//this.clear(); // 清空所有对象
// tagsData.forEach(item => {
// 创建一个矩形
const rect = new fabric.Rect({
fill: this.hexToRgba(item.color, 0.2), // 填充颜色,透明度为 0.2
width: item.width, // 矩形的宽度
height: item.height, // 矩形的高度
angle: 0, // 旋转角度
stroke: item.color,
strokeWidth: 2, // 边框宽度
});
// 创建第一个文字对象
const text1 = new fabric.Text(item.label, {
fontFamily: "Arial",
fontSize: 20,
fill: item.color, // 文字颜色
// stroke: "#ffffff", // 文字边框颜色
// strokeWidth: 0.5 // 文字边框宽度
});
// 计算文字位置以确保其在矩形的正中央
text1.set({
left: rect.left + rect.width / 2,
top: rect.top + rect.height / 2,
originX: "center",
originY: "center",
});
// 使用fabric.Group将矩形和文字组合在一起
const group = new fabric.Group([rect, text1], {
left: item.isInit ? this.canvasProp.width / 2 - item.width / 2 : item.startX, // 矩形的左上角 x 坐标
top: item.isInit ? this.canvasProp.height / 2 - item.height / 2 : item.startY, // 矩形的左上角 y 坐标
cornerColor: item.color, // 控制点的颜色
cornerSize: 10, // 控制点的大小
borderWidth: 0, // 选中时的边框宽度
transparentCorners: true,
angle: item.rotate, // 组合对象的旋转角度
id: item.id,
});
// 将组合添加到画布上
console.log("添加到画布上", group);
this.fabricCanvas.add(group);
callback();
//});
},
saveData() {
this.getPointData();
},
getPointData() {
let result = {
};
this.fabricCanvas.getObjects().forEach((rect) => {
console.log("rect==", rect);
const coords = [];
const points = rect.get("aCoords"); // 获取矩形的绝对坐标
console.log("points", points);
const viewportTransform = this.fabricCanvas.viewportTransform;
const zoom = this.fabricCanvas.getZoom();
// 将内部坐标转换为实际画布坐标
Object.keys(points).forEach((key) => {
let point = points[key];
const actualX = (point.x - viewportTransform[4]) / zoom;
const actualY = (point.y - viewportTransform[5]) / zoom;
coords.push([Math.round(actualX), Math.round(actualY)]);
});
result[rect.id] = coords;
});
console.log("result", result);
return result;
},
},
};
</script>
<style lang="scss" scoped>
.muane {
height: 36px;
background: #fafafa;
display: flex;
align-items: center;
span{
width: 50%;
text-align: center;
}
}
</style>