通过canvas getImageData()裁剪空白区域

Aditya2024-01-29图像渲染相关Canvas

引子

用canvas绘制图像时,我们会着重于ROI区域,在一张干净的画布绘制了ROI区域,我们又在上面添加了诸多元素,当我们想要把这部分图clip下来的时候,一般都会裁剪整个画布,受限于浏览器容器的不确定性,容器有大有小,而ROI区域又是固定的,导致有时候裁剪下来的图像会有大部分留白,对于这部分了留白是不需要的,所以最终在裁剪画布的时候也要对这部分留白进行处理掉……

Canvas getImageData()

在Canvas的getImageData方法中,返回的imageData对象包含了指定矩形区域像素的信息,其中data属性是一个一维数组,包含了每个像素的红、绿、蓝和透明度信息。这个数组的长度是矩形区域像素的总数乘以4(每个像素有4个值)。

具体而言,data数组的结构如下:

  • 第一个元素是第一个像素的红色分量(0 到 255)。
  • 第二个元素是第一个像素的绿色分量(0 到 255)。
  • 第三个元素是第一个像素的蓝色分量(0 到 255)。
  • 第四个元素是第一个像素的透明度(0 到 255,0 表示完全透明,255 表示完全不透明)。
  • 后续的元素按照相同的顺序依次表示接下来的像素的信息。
const context = canvas.getContext("2d")
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);

const width  = imageData.width;
const height = imageData.height;

const pixels = _.chunk(imageData.data, 4);
const image  = _.chunk(pixels, width);

_.chunk(array, [size=1]) 将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。

将imageData从一维数组按照RGBA拆成一个一维数组,这样每个数组代表一个像素的颜色信息,然后再将像素数组按照图像的宽度分组成行,得到一个二维数组,表示整个图像。

寻找起始位置

思路

我们需要确定的是ROI的上下左右的四个边界,从左侧开始寻找,当检查的像素点不是纯黑色,即point != [0,0,0,1],那就说明遇到了有颜色的像素,即触碰到ROI的边检,那就找到了左上角……

代码

let startX = 0;
let startY = 0;
let endX   = width - 1;
let endY   = height - 1;

// 找打左侧边缘
for(let i = 0; i < width; i++) {
  for(let j = 0; j < height; j++) {
    const point = image[j][i]
    if(point[0] + point[1] + point[2] !== 0) {
      startX = i;
      break;
    }
  }

  if(startX !== 0) break;
}

……
  1. 初始化变量 startXstartYendXendY 分别表示矩形区域的左上角和右下角坐标。
  2. 使用嵌套的循环遍历图像的每一列和每一行。
  3. 获取图像中每个像素的颜色信息,存储在 point 变量中。
  4. 检查像素点的颜色是否等于0(这里是一个简单的条件,可以根据实际情况调整),如果是,说明这是有颜色的像素。
  5. 如果找到有颜色的像素,记录当前列的索引为 startX,并立即跳出内层循环。
  6. 如果 startX 不等于 0,说明找到了左侧边缘的起始位置,于是跳出外层循环。

这段代码的目的是找到图像左侧的第一个有颜色的像素,以确定左侧边缘的起始位置。

图像中含有中“杂质”

let startX = 0;
let startY = 0;
let endX   = width - 1;
let endY   = height - 1;

// 找打左侧边缘
for(let i = 0; i < width; i++) {
  for(let j = 0; j < height; j++) {
    const point = image[j][i]
    if(point[0] + point[1] + point[2] > 3) {
      startX = i;
      break;
    }
  }

  if(startX !== 0) break;
}

// 找到右侧边缘
for(let i = width - 1; i > 0; i--) {
  for(let j = 0; j < height; j++) {
    const point = image[j][i]
    if(point[0] + point[1] + point[2] > 3) {
      endX = i;
      break;
    }
  }

  if(endX !== width - 1) break;
}

// 找到顶部边缘
for(let i = 0; i < height; i++) {
  for(let j = 0; j < width; j++) {
    const point = image[i][j]
    if(point[0] + point[1] + point[2] > 3) {
      startY = i;
      break;
    }
  }

  if(startY !== 0) break;
}

 // 找到底部边缘
 for(let i = height - 1; i > 0; i--) {
  for(let j = 0; j < width; j++) {
    const point = image[i][j]
    if(point[0] + point[1] + point[2] > 3) {
      endY = i;
      break;
    }
  }

  if(endY !== height - 1) break;
}

const bound = {
  x: startX,
  y: startY,
  top: startY,
  left: startX,
  bottom: endY,
  right: endX,
  width: endX - startX,
  height: endY - startY
};

通过上述代码能简单的判断留白为纯色像素点,但实际上我们会遇到像素点中可能包含一些噪声或杂质,那我们可能需要更加复杂方式来判断了。

单个杂质

对于单个的杂质,我们可以需要判断前一个**“杂质”的位置与当前“杂质”**的位置是否连贯,不连贯那就属于杂质。

let prevPos = 0
for(let i = 0; i < width; i++) {
  for(let j = 0; j < height; j++) {
    const point = image[j][i]
    if(point[0] + point[1] + point[2] > 3) {
      startX = i;
      break;
    }
  }

  if(startX !== 0) {
    if (startX - prevPos === 1) {
      break
    }
    prevPos = startX
  };
}

灰度值

一种常见的方法是通过设置一个阈值,只有当像素的颜色超过阈值时才被认为是有颜色的像素。这可以通过计算像素的亮度或灰度值来实现。

const threshold = 50; // 设置一个阈值,根据实际情况调整

// 找到左侧边缘
for (let i = 0; i < width; i++) {
  for (let j = 0; j < height; j++) {
    const point = image[j][i];

    // 计算像素的灰度值
    const grayscale = (point[0] + point[1] + point[2]) / 3;

    // 检查灰度值是否大于阈值
    if (grayscale > threshold) {
      startX = i; // 记录左侧边缘的起始位置
      break;
    }
  }

  if (startX !== 0) break; // 如果找到了左侧边缘的起始位置,跳出循环
}

在这个例子中,grayscale 表示像素的灰度值,计算方式是将红、绿、蓝通道的值求平均。如果灰度值超过了预定的阈值 threshold,就认为这个像素是有颜色的。

裁剪

clip(canvas: HTMLCanvasElement, area: BoundingRect): Promise<string> {
  return new Promise((resolve) => {
    const context = canvas.getContext("2d")
    const imageData = context.getImageData(area.x, area.y, area.width, area.height)
    const partialCanvas = document.createElement("canvas")
    const partialContext = partialCanvas.getContext("2d")
    partialCanvas.width  = area.width
    partialCanvas.height = area.height
    partialContext.putImageData(imageData, 0, 0)
    resolve(partialCanvas.toDataURL("image/jpeg",1))
  })
}

尾声

由于我们的留白基本是纯黑色,图片里可能是rgb(1,1,1)的背景,所以在判断杂质方面没有做过多的研究,以上仅是一个思路。

Last Updated 2024/12/27 11:36:49