1.Fabric.js 实现图片标注

在项目中,我们需要根据后端返回的坐标对图片进行标注,比如画矩形框或多边形。本文将详细介绍如何使用 Fabric.js 实现这一功能,包括初始化 Fabric.js、加载图片、绘制标注等步骤。

1.1.初始化 Fabric.js

首先,我们需要在页面初始化时对 Fabric.js 进行初始化。需要注意的是,初始化操作应该在 nextTick 中执行,否则可能不会生效。另外,初始化只需进行一次,不要重复初始化。

const canvas = ref();
const fabricInit = () => {
  canvas.value = new fabric.Canvas('fabric'); // 初始化 fabric 画布
};

onMounted(() => {
  nextTick(() => {
    fabricInit();
  });
});

1.2.加载图片并进行标注

加载图片是进行标注的第一步。我们从后端获取数据后,开始渲染图片,并在图片加载到画布中后,进行标注绘制。

const { execute, isLoading } = useAsyncStateWithError(
  () => {
    return getAnnotatorData(props.fileId); // 获取标注数据
  },
  {},
  {
    onSuccess(data) {
      categoriesAnnotations.value = data.categories; // 存储分类标注数据
      checkedVals.value = categoriesAnnotations.value.map((item) => item.name); // 存储选中的分类名称
      allAnnotationsCount.value = categoriesAnnotations.value.reduce(
        (acc, category) => {
          return acc + category.annotations.length; // 计算所有标注的总数
        },
        0,
      );
      addDetectionImage(props.imgUrl); // 加载图片
    },
    immediate: false,
  },
);

在获取到数据后,我们需要加载图片并进行标注。这里要注意,我们需要在每次加载新图片前清除画布,以防止图片叠加:

const maxWidth = ref(1200);
const maxHeight = ref(600);
const scaleX = ref();
const scaleY = ref();
const addDetectionImage = (imageUrl) => {
  // 清除原来的画布,防止图片叠加
  if (canvas.value) {
    canvas.value.clear();
  }
  fabric.Image.fromURL(
    imageUrl,
    (fabricImg) => {
      // 计算缩放比例
      const scale = Math.min(
        maxWidth.value / fabricImg.width!, // 计算宽度缩放比例
        maxHeight.value / fabricImg.height!, // 计算高度缩放比例
      );

      // 计算缩放后的尺寸
      const scaledWidth = fabricImg.width! * scale;
      const scaledHeight = fabricImg.height! * scale;

      // 设置画布的宽度和高度为缩放后的尺寸
      canvas.value.setWidth(scaledWidth);
      canvas.value.setHeight(scaledHeight);
      
      // 设置图片属性
      fabricImg.set({
        selectable: false, // 不可选中
        hasControls: false, // 无控制点
        hasRotatingPoint: true, // 允许旋转
        lockRotation: false, // 不锁定旋转
        lockScalingX: false, // 不锁定水平缩放
        lockScalingY: false, // 不锁定垂直缩放
        scaleX: (scaleX.value = scaledWidth / fabricImg.width!), // 设置水平缩放比例
        scaleY: (scaleY.value = scaledHeight / fabricImg.height!), // 设置垂直缩放比例
      });

      // 将图片添加到画布中
      canvas.value.add(fabricImg);
      draw(categoriesAnnotations.value); // 绘制标注
    },
    { crossOrigin: 'anonymous' }, // 允许跨域
  );
};

1.2.1.计算图片的缩放比例

为了确保图片能在画布中完整显示,我们需要计算图片的缩放比例。具体步骤如下:

  1. 计算宽度和高度的缩放比例:分别为 maxWidth / fabricImg.widthmaxHeight / fabricImg.height
  2. 选择较小的缩放比例:以确保图片的宽度和高度都能在画布中显示完整。
  3. 计算缩放后的尺寸:使用较小的缩放比例,计算出缩放后的宽度和高度。
  4. 设置画布尺寸:将画布的宽度和高度设置为缩放后的尺寸。

1.3.绘制多边形标注

多边形标注是根据后端返回的坐标绘制的,我们将坐标转换为 fabric.Point 对象,并通过 fabric.Polygon 绘制多边形。

/**
 * 绘制多边形标注
 * @param {Array} polygonPoints 多边形的坐标数组
 * @param {string} borderColor 多边形的边框颜色
 * @param {string} labelText 多边形的标签文本
 */
function processPolygon(polygonPoints, borderColor, labelText) {
  // 处理多边形坐标
  const points = polygonPoints[0].reduce(
    (acc, val, index, array) => {
      if (index % 2 === 0) {
        acc.push(
          new fabric.Point(val * scaleX.value, array[index + 1] * scaleY.value), // 转换为 fabric.Point 对象
        );
      }
      return acc;
    },
    [],
  );

  // 创建多边形对象
  const polygon = new fabric.Polygon(points, {
    stroke: borderColor, // 边框颜色
    strokeWidth: 2, // 边框宽度
    fill: borderColor, // 填充颜色
    opacity: 0.6, // 透明度
    selectable: false, // 不可选中
  });

  // 创建文本对象
  const text = processText(labelText, polygon.left, polygon.top, borderColor);

  // 将多边形和文本添加到画布中
  canvas.value.add(polygon, text);
}

1.4. 绘制矩形标注

矩形标注是根据后端返回的矩形坐标绘制的,我们将坐标转换为 fabric.Rect 对象进行绘制。

这里我们使用的是COCO数据集,他的bbox表示为

COCO数据集bbox

在COCO (Common Objects in Context)数据集中,bbox (boundingbox)表示的是对象的边界框坐标。具体来说:

  1. bbox是一个包含4个元素的列表或元组,表示为[x, y, width,height]。
  2. x, y表示边界框左上角的坐标。
  3. width和height分别表示边界框的宽度和高度。

例如,一个边界框的坐标为[100,200,50,80],表示:

  • 左上角的x坐标为100
  • 左上角的y坐标为200
  • 边界框的宽度为50
  • 边界框的高度为80

这样的坐标和尺寸信息可以用来定位和描述图像中的对象位置。COCO格式中的bbox表示的是对象在图像中的边界框坐标和尺寸信息,可用于目标检测等计算机视觉任务。

/**
 * 绘制矩形标注
 * @param {Array} rectangleCoords 矩形的坐标数组 [x1, y1, x2, y2]
 * @param {string} borderColor 矩形的边框颜色
 * @returns {fabric.Rect} 返回绘制的矩形对象
 */
function processRectangle(rectangleCoords, borderColor) {
  const [x1, y1, x2, y2] = rectangleCoords; // 提取矩形的四个顶点坐标

  // 创建矩形对象
  return new fabric.Rect({
    left: x1 * scaleX.value, // 左上角 x 坐标
    top: y1 * scaleY.value, // 左上角 y 坐标
    width: x2 * scaleX.value, // 宽度
    height: y2 * scaleY.value, // 高度
    stroke: borderColor, // 边框颜色
    strokeWidth: 2, // 边框宽度
    fill: borderColor, // 填充颜色
    opacity: 0.6, // 透明度
    selectable: false, // 不可选中
    evented: false, // 事件不可用
  });
}

1.5. 绘制文本

文本标注用于显示标注的名称,我们使用 fabric.Text 对象进行绘制。

/**
 * 绘制文本标注
 * @param {string} labelText 文本内容
 * @param {number} leftPos 文本的左侧位置
 * @param {number} topPos 文本的顶部位置
 * @param {string} bgColor 文本背景颜色
 * @returns {fabric.Text} 返回绘制的文本对象
 */
function processText(labelText, leftPos, topPos, bgColor) {
  // 创建文本对象
  return new fabric.Text(labelText, {
    fontSize: 14, // 字体大小
    fill: 'white', // 字体颜色
    left: leftPos, // 左侧位置
    top: topPos - 14, // 顶部位置(略微向上偏移)
    backgroundColor: bgColor, // 背景颜色
    padding: 5, // 内边距
    selectable: false, // 不可选中
    evented: false, // 事件不可用
  });
}

1.6. 绘制标注集合

根据分类标注数据进行遍历,并根据模型类型绘制相应的标注。

/**
 * 绘制矩形和文本标注
 * @param {Array} rectangleCoords 矩形的坐标数组 [x1, y1, x2, y2]
 * @param {string} borderColor 矩形的边框颜色
 * @param {string} labelText 文本内容
 */
function drawRectAndText(rectangleCoords, borderColor, labelText) {
  const rect = processRectangle(rectangleCoords, borderColor); // 处理矩形
  const text = processText(labelText, rect.left, rect.top, borderColor); // 处理文本
  canvas.value.add(rect, text); // 将矩形和文本添加到画布中
}
/**
 * 绘制标注集合
 * @param {Array} categoryAnnotations 分类标注数据
 */
const draw = (categoryAnnotations) => {
  // 清除之前的图形和文本,防止叠加
  canvas.value.getObjects().forEach((obj) => {
    const typeArr = ['rect', 'text', 'polygon']; // 标注对象类型
    if (typeArr.includes(obj.type)) {
      canvas.value.remove(obj); // 移除标注对象
    }
  });

  // 遍历分类标注数据
  categoryAnnotations.forEach((category) => {
    if (category.annotations && category.annotations.length) {
      category.annotations.forEach((annotation) => {
        if (props.modelType === 'IMAGE_SEGMENTATION' && annotation.segmentation.length)
          processPolygon(annotation.segmentation, category.color, category.name); // 绘制多边形标注
        if (props.modelType === 'OBJECT_DETECTION' && annotation.bbox.length)
          drawRectAndText(annotation.bbox, category.color, category.name); // 绘制矩形标注
      });
    }
  });
};

1.7. 注意事项

  1. 初始化时机:Fabric.js 的初始化需要在 nextTick 中执行,以确保 DOM 元素已经加载完毕。这是因为 Fabric.js 需要操作页面中的 DOM 元素,如果这些元素还没有加载完毕,初始化操作将无法成功。
  2. 防止图片叠加:每次加载新图片前需要清除画布,否则会导致图片叠加。这是因为 Fabric.js 的画布是持久化的,即上一次绘制的内容会保留在画布上。如果不清除画布,新的图片将叠加在旧的图片上,导致显示混乱。
  3. 标注绘制时机:标注绘制应在图片加载后进行,以避免标注被图片覆盖。由于图片加载是异步的,如果在图片加载前就进行标注绘制,图片加载完成后会将标注覆盖。因此,确保图片加载完成后再进行标注绘制。

结论

希望本文能帮助你更好地使用 Fabric.js 进行图片标注。如果你有任何问题或建议,欢迎在评论区留言讨论。通过本文的详细步骤和注意事项,你可以轻松地在项目中实现基于 Fabric.js 的图片标注功能。

文章转自:https://juejin.cn/post/7372070947819733019

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐