使用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();
  }
);

八、官方文档

ailabel官方文档

posted @ 2025-01-30 15:05  火炬冬天  阅读(1866)  评论(0)    收藏  举报