使用ailabel对图片进行标注,vue3/js+ailabel.js
一、实现效果
对方案可以添加多张图片,并在图片上进行标注。并且可以通过下方的缩略图切换方案图片
(demo)
二、效果图


三、页面元素
<div class="w-full overflow-auto p-2" style="height: calc(100% - 7rem)">
<div class="btns mb-2">
<el-button size="small" type="primary" @click="setMode('CIRCLE')">{{ Translate.i18n("圆") }}</el-button>
<el-button size="small" type="primary" @click="setMode('RECT')">{{ Translate.i18n("矩形") }}</el-button>
<el-button size="small" type="primary" @click="setMode('POLYGON')">{{ Translate.i18n("多边形") }}</el-button>
<el-button size="small" type="primary" @click="setMode('PAN')">{{ Translate.i18n("平移模式") }}</el-button>
</div>
<div v-for="(v, k) in imgs" :key="k" :id="v.id" v-show="v.id == selectedId" class="relative w-full overflow-hidden bg-gray-110" style="height: calc(100% - 2rem)"></div>
</div>
<div class="thumbnail flex h-16 w-full items-center justify-center overflow-auto border-t p-2">
<div
class="mr-2 flex h-12 w-12 flex-shrink-0 cursor-pointer items-center justify-center rounded border p-1"
v-for="(v, k) in imgs"
:key="k"
@click="changeImg(v, k)"
:class="selectedId == v.id ? 'border-green-550' : ''">
<img class="h-full w-full" :src="v.url" />
</div>
</div>
四、基础定义
npm install ailabel
import AILabel from "ailabel"
let selectedId = ref(1), // 选中的图片ID
imgs = ref([]), // 图片列表
gMap = {}, // 地图实例
drawingStyle = {}, // 绘制样式
featureLayer = {}, // 绘制图层
textLayer = {}, // 文字图层
imgLayer = {}, // 图片图层
graphCount = ref(0); // 图片计数
const textStyle = { fillStyle: "#F4A460", strokeStyle: "#D2691E", background: true, globalAlpha: 1, fontColor: "#0f0" }; // 文本样式
五、主要逻辑
5.1 添加图片
在添加图片的时候进行画布初始化
// 添加图片
const addImg = async () => {
graphCount.value++;
if (!selectedId.value) {
selectedId.value = graphCount.value;
}
const n = imgs.value.length;
//推荐以网络图片地址为例测试
const obj = {
url: `http://192.168.2.179:9100/template/img-${n + 1}.jpg`,
count: 0,
id: graphCount.value
};
imgs.value.push(obj);
await nextTick();
initGraph(graphCount.value);
};
5.2 画布初始化
//初始化画布
const initGraph = (id) => {
if (selectedId.value != id) return;
gMap[id] = new AILabel.Map(id, {
center: { x: 400, y: 250 },
zoom: 500,
mode: "PAN",
refreshDelayWhenZooming: true,
zoomWhenDrawing: true,
panWhenDrawing: true,
zoomWheelRatio: 5,
withHotKeys: true
});
// click单击事件
gMap[id].events.on("click", (point) => {
const feature = gMap[id].getTargetFeatureWithPoint(point.global) || {};
if (feature.id) {
gMap[id].setActiveFeature(feature);
addDeleteIcon(id, feature);
showItem.value = true; //可以控制详情显示
}
});
// 绘制完成事件
gMap[id].events.on("drawDone", (type, data) => {
addFeature(id, type, data);
});
// 双击编辑
gMap[id].events.on("featureSelected", (feature) => {
gMap[id].setActiveFeature(feature);
// 增加删除按钮
addDeleteIcon(id, feature);
});
// 单机空白取消编辑
gMap[id].events.on("featureUnselected", () => {
gMap[id].markerLayer.removeAllMarkers();
gMap[id].setActiveFeature(null);
});
// 更新完
gMap[id].events.on("featureUpdated", (feature, shape) => {
// 更新或者移动需要重新设置删除图标
gMap[id].markerLayer.removeAllMarkers();
// 更新text的位置
const text = textLayer[id].getTextById(feature.id);
text.updatePosition(getIndexPosition(feature, shape));
feature.updateShape(shape);
addDeleteIcon(id, feature);
});
// 删除
gMap[id].events.on("FeatureDeleted", () => {});
// 初始化图层
featureLayer[id] = new AILabel.Layer.Feature(
"featureLayer", // 图层id
{ name: "featureLayer" },
{ zIndex: 10 }
);
gMap[id].addLayer(featureLayer[id]);
textLayer[id] = new AILabel.Layer.Text(
"text",
{ name: "textLayer" }, // props
{ zIndex: 12, opacity: 1 } // style
);
gMap[id].addLayer(textLayer[id]);
const imgurl = imgs.value.find((v) => v.id == id)?.url;
imgLayer[id] = new AILabel.Layer.Image(
"img",
{
src: imgurl,
width: 800,
height: 500,
position: {
// 左上角相对中心点偏移量
x: 0,
y: 0
}
// grid: {
// // 3 * 3
// columns: [{ color: "#9370DB" }, { color: "#FF6347" }],
// rows: [{ color: "#9370DB" }, { color: "#FF6347" }]
// } // 网格线颜色
},
{ zIndex: 5 }
);
gMap[id].addLayer(imgLayer[id]);
};
5.3 增加删除按钮
// 增加删除图标
const addDeleteIcon = (id, feature) => {
// 添加delete-icon
let points = getPoints(feature);
const gFirstMarker = new AILabel.Marker(
+new Date(), // id
{
src: "http://192.168.2.179:9100/template/delete.png",
position: points[1], // 矩形右上角
offset: {
x: -16,
y: -16
}
}, // markerInfo
{ name: "delete" } // props
);
gFirstMarker.events.on("click", (marker) => {
// 首先删除当前marker
gMap[id].markerLayer.removeMarkerById(marker.id);
// 删除对应text
textLayer[id].removeTextById(feature.id);
// 删除对应feature
featureLayer[id].removeFeatureById(feature.id);
});
gMap[id].markerLayer.addMarker(gFirstMarker);
};
5.4 获取所有点
// 获取所有点
const getPoints = (feature) => {
switch (feature.type) {
case "RECT":
return feature.getPoints();
case "CIRCLE":
return feature.getEdgePoints();
case "POLYGON":
return feature.shape.points;
default:
return [];
}
};
5.5 设置模式
// 设置模式
const setMode = (mode) => {
const layers = gMap[selectedId.value].getLayers()?.filter((v) => v.type == "IMAGE") || [];
if (mode != "PAN" && !layers.length) {
elMessage(Translate.i18n("请先添加方案图片"), "warning");
return;
}
gMap[selectedId.value].setMode(mode);
if (mode == "CIRCLE") {
drawingStyle = { fillStyle: "#9370DB", strokeStyle: "#0000FF", lineWidth: 1 };
gMap[selectedId.value].setDrawingStyle(drawingStyle);
} else if (mode == "RECT") {
drawingStyle = { strokeStyle: "#0f0", lineWidth: 1 };
gMap[selectedId.value].setDrawingStyle(drawingStyle);
} else if (mode == "POLYGON") {
drawingStyle = { strokeStyle: "#00f", fillStyle: "#0f0", globalAlpha: 0.3, lineWidth: 1, fill: true, stroke: true };
gMap[selectedId.value].setDrawingStyle(drawingStyle);
}
};
5.6 添加图层元素
// 添加图层元素-元素数字标记
const addFeature = (id, type, data, name) => {
let count = imgs.value.find((v) => v.id == id)?.count;
count++;
imgs.value.forEach((v) => {
if (v.id == id) {
v.count = count;
}
});
if (type == "CIRCLE") {
const circleFeature = new AILabel.Feature.Circle(
count, // id
data, // shape
{ name }, // props
drawingStyle // style
);
featureLayer[id].addFeature(circleFeature);
addText(id, count, { x: data.cx, y: data.cy });
} else if (type == "RECT") {
const rectFeature = new AILabel.Feature.Rect(
count, // id
data, // shape
{ name }, // props
drawingStyle // style
);
featureLayer[id].addFeature(rectFeature);
addText(id, count, { x: data.x, y: data.y });
} else if (type == "POLYGON") {
const polygonFeature = new AILabel.Feature.Polygon(
count, // id
{ points: data }, // shape
{ name }, // props
drawingStyle // style
);
featureLayer[id].addFeature(polygonFeature);
addText(id, count, data[0]);
}
};
5.7 添加标记序号文本
// 添加文本
const addText = (layerId, textId, point) => {
const textPointer = new AILabel.Text(
textId, // id
{ text: textId, position: point, offset: { x: 0, y: 0 } }, // shape
{ name: "" }, // props
textStyle // style
);
textLayer[layerId].addText(textPointer);
};
5.8 获取序号文本位置
// 获取序号位置
const getIndexPosition = (feature, shape) => {
switch (feature.type) {
case "RECT":
return { x: shape.x, y: shape.y };
case "CIRCLE":
return { x: shape.cx, y: shape.cy };
case "POLYGON":
return shape.points[0];
default:
return {};
}
};
至此大致标注逻辑已全部完成,略微补充一段切换底下缩略图变化画布的逻辑
六、切换缩略图
const changeImg = async (v, k) => {
selectedId.value = v.id;
await nextTick();
if (!gMap[v.id]) {
initGraph(v.id);
}
};
七、画布resize
const resizeMap = () => {
nextTick(() => {
gMap[selectedId.value] && gMap[selectedId.value].resize();
gMap[selectedId.value] && gMap[selectedId.value].centerAndZoom({ center: { x: 400, y: 250 }, zoom: 500 });
});
};
watch(
() => showItem.value,
() => {
resizeMap();
}
);
浙公网安备 33010602011771号