前端分割实现2-连通域优化

29 阅读3分钟

前言

在上一篇文章《前端分割实现》中,初步实现了前端分割碎片的功能。但是UI给过来的图片在蓝湖上出现了破损,导致边缘和圆角的RGBA不精确,导致采样失败。

虽然后续让UI直接把PS的图片导出发给前端,解决了图片质量问题。

但中间还是通过连通域优化,解决了问题。

需要记录一下。

解决的思路来自这篇文章:《# OpenCV 笔记(13):连通域分析》

思路

解决思路很简单:

1、在根据RGBA的值,得到目标图片数据

2、进行连通域标记

3、如果连通域面积小于指定阈值,则把该连通域的RGBA数据移除,比如把不透明度设置为0

4、剩下的是有效数据,对有效数据进行裁剪。

代码如下

/**
     * 裁剪
     * @param index 序号
     * @param list 存储列表
     */
    cut(index: number, list: any[]) {
      const ctx = this.ctx
      const canvas = this.canvas
      const newData = ctx.createImageData(this.width, this.height);
      for (let i = 0; i < newData.data.length; i += 4) {
        const temp = this.tempData.data.slice(i, i + 4)
        if (Math.abs(temp[3] - index * 13) < 1) {
          newData.data[i + 0] = this.imgData.data[i + 0]
          newData.data[i + 1] = this.imgData.data[i + 1]
          newData.data[i + 2] = this.imgData.data[i + 2]
          newData.data[i + 3] = this.imgData.data[i + 3]
        }
      }
      // 进行连通域标记
      const labels = this.connectedComponentLabeling(newData.data, this.width, this.height);
      const areaCounts = this.countAreas(labels, this.width, this.height);

      // 设定一个阈值来移除小的连通域
      const thresholdArea = 100; // 设定一个合理的阈值

      // 应用面积过滤
      for (let y = 0; y < this.height; y++) {
        for (let x = 0; x < this.width; x++) {
          const index = y * this.width + x;
          const label = labels[index];
          if (areaCounts[label] < thresholdArea) {
            newData.data[index * 4 + 3] = 0; // 移除小面积区域
          }
        }
      }

      // 重新确定边界
      let minX = this.width;
      let minY = this.height;
      let maxX = -1;
      let maxY = -1;

      // 遍历所有像素,查找非透明像素的位置
      for (let y = 0; y < this.height; y++) {
        for (let x = 0; x < this.width; x++) {
          const pixelIndex = (y * this.width + x) * 4;
          if (newData.data[pixelIndex + 3] > 0) { // Alpha通道大于0表示非透明
            minX = Math.min(minX, x);
            maxX = Math.max(maxX, x);
            minY = Math.min(minY, y);
            maxY = Math.max(maxY, y);
          }
        }
      }

      // 如果没有找到非透明像素,则直接返回
      if (minX === this.width && minY === this.height) {
        console.log('No non-transparent pixels found.');
        return;
      }

      const width = maxX - minX + 1;
      const height = maxY - minY + 1;
      canvas.width = width;
      canvas.height = height;

      // 创建新的图像数据对象用于裁剪后的图像
      const croppedImage = ctx.createImageData(width, height);

      // 复制非透明像素到新的图像数据中
      for (let y = minY; y <= maxY; y++) {
        for (let x = minX; x <= maxX; x++) {
          const srcIndex = ((y * this.width) + x) * 4;
          const destIndex = ((y - minY) * (maxX - minX + 1) + (x - minX)) * 4;
          croppedImage.data.set(newData.data.subarray(srcIndex, srcIndex + 4), destIndex);
        }
      }

      // 清除画布并绘制裁剪后的图像
      ctx.clearRect(0, 0, width, height);
      ctx.putImageData(croppedImage, 0, 0);

      const dataUrl = canvas.toDataURL('image/png');
      list.push({
        x: minX,
        y: minY,
        width,
        height,
        dataUrl
      });
    },
    /**
     * 对给定的图像数据执行连通域标记算法。
     * @param {Uint8ClampedArray} data 图像数据。
     * @param {number} width 图像宽度。
     * @param {number} height 图像高度。
     * @returns {Array<number>} 包含每个像素所属连通域标签的一维数组。
     */
    connectedComponentLabeling(data: Uint8ClampedArray, width: number, height: number) {
      const labels = new Array(width * height).fill(0); // 初始化标签数组
      let nextLabel = 1; // 下一个可用的标签值

      function dfs(x: number, y: number, label: any) {
        const stack: any = [[x, y]];
        while (stack.length > 0) {
          const [cx, cy] = stack.pop();
          if (cx < 0 || cy < 0 || cx >= width || cy >= height || data[cy * width * 4 + cx * 4 + 3] === 0 || labels[cy * width + cx] !== 0) {
            continue;
          }
          labels[cy * width + cx] = label;
          stack.push([cx - 1, cy]); // 左
          stack.push([cx + 1, cy]); // 右
          stack.push([cx, cy - 1]); // 上
          stack.push([cx, cy + 1]); // 下
        }
      }

      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const index = y * width + x;
          if (data[index * 4 + 3] > 0 && labels[index] === 0) { // 如果当前像素不是透明且未标记
            dfs(x, y, nextLabel); // 标记连通域
            nextLabel++;
          }
        }
      }

      return labels;
    },

    /**
    * 计算每个连通域的面积。
    * @param {Array<number>} labels 每个像素所属连通域的标签数组。
    * @param {number} width 图像宽度。
    * @param {number} height 图像高度。
    * @returns {Object} 包含每个连通域面积的对象。
    */
    countAreas(labels: Array<number>, width: number, height: number) {
      const areaCounts: any = {};
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const index = y * width + x;
          const label = labels[index];
          if (label > 0) {
            areaCounts[label] = (areaCounts[label] || 0) + 1;
          }
        }
      }
      return areaCounts;
    }