LeetCode的一个算法帮我解决了工作棘手问题

920 阅读5分钟

LeetCode的一个算法帮我解决了工作棘手问题

起因是这样的, 我工作中碰到了一个问题, 允许我用一张图来表达这个问题是什么

image.png 我所维护的一个图片编辑器有这么一个功能, 就是我的编辑器提供一些模板, 然后用户会上传图片, 我需要把用户的图片缩放到模板抠出来的填图框那个大小, 从而避免让用户自己去手动缩放. 因此我们的设计师会给我一个填图框相对于整个模板的XY和宽高数据, 例如这样

image.png

这样我们就能够通过模板本身的宽高, 再乘以给定的数据就能计算得出, 我要把用户的图片缩放到哪个大小及位置, 从而让这个图片正好适应模板所抠出来的部分

但问题是我们的模板里面的填图框是大小不一, 的很难提前去预设一个位置宽高数据, 这就需要我去通过模板去运行时解析计算他这个填图框的x,y,w,h信息

image.png

起初这个问题是很容易解决的, 我们只需要找出图片的所有透明像素(即alpha通道为0), 然后向左取最小值, 向上取最小值, 向右取最大, 向下取最大即可:

{
	x: minX,
	y: minY,
	w: maxX - minX,
	h: maxY - minY
}

正当我沾沾自喜时, 我们的设计师给我整了个大招出来

image.png

之前那个简单算法只能求一个简单矩形的位置信息, 但现如今有这么多形态各异位置不同的透明区域, 该如何计算它们各自的x,y,w,h信息呢? 于是我翻来覆去废寝忘食苦思冥想

2000 years later..................................................................

我发现每一个像素点对于我的区别来说就是它是否透明, 那我可以用1来表示透明, 0表示不透明, 那一张图片实际上可以表达成由1和0组成的二维数组, 那这不就跟LeetCode上面的**200. 最大岛屿数量**很像吗

Input: grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
Output: 3

最大岛屿数量和我这个问题有着相似的逻辑, 比如循环遍历每一个1, 在每一个1上向它四周扩散, 这个扩散的过程会被记录, 如果他碰到了1就继续这个扩散的过程, 如果不是那就立刻停止, 直到在上下左右四个方向都没有碰到1或者超界, 就停止这个扩散的过程

在这个扩散的过程中我记录我经过了哪些位置, 即这个1在二维数组中的索引, 也意味着透明像素在模板中的相对位置

每一个1都有这么一个扩散的过程, 不过我们可以缓存之前哪些1被遍历过了, 那就不需要重新扩散

这就是我们解决这个问题的一个比较形象的思维逻辑, 好了接下来可以展示代码了

type IRect = {
  x: number
  y: number
  w: number
  h: number
}

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })!

//判断像素透明
function isEmptyPixel(
  data: Uint8ClampedArray,
  x: number,
  y: number,
  width: number
): boolean {
  const index = (y * width + x) * 4
  if (data[index + 3] === 0) return true
  return false
}

//表示扩散过程的函数
function findEmptyArea(
  data: Uint8ClampedArray,
  visited: boolean[],
  startX: number,
  startY: number,
  width: number,
  height: number
  //这个x,y表示二维数组索引, 也是模板像素点的坐标
): { x: number; y: number }[] {
  //收集扩散的 1
  const area: { x: number; y: number }[] = []
  //这个栈用来存储需要经过扩散的 1
  const stack: { x: number; y: number }[] = [{ x: startX, y: startY }]

  // 直到没有 1 需要扩散了
  while (stack.length > 0) {
    const { x, y } = stack.pop()!

    // 越界判断
    if (x < 0 || x >= width || y < 0 || y >= height) continue
    // 是否已经访问过
    if (visited[y * width + x]) continue

    // 标记为已访问
    visited[y * width + x] = true

    // 是否是透明像素
    if (!isEmptyPixel(data, x, y, width)) continue

    // 收集扩散到的 1
    area.push({ x, y })
    // 扩散到相邻的 1 ,并加入栈中
    stack.push({ x: x + 1, y })
    stack.push({ x: x - 1, y })
    stack.push({ x, y: y + 1 })
    stack.push({ x, y: y - 1 })
  }

  return area
}

// 将点集转换为矩形
function pointsToRect(
  points: { x: number; y: number }[],
  width: number,
  height: number
): IRect | null {
  // 如果点集数量小于总像素数的 3.5%,则返回 null, 即忽略那些过于小的区域
  if (points.length / (width * height) < 0.035) return null

  let minX = Infinity
  let minY = Infinity
  let maxX = -Infinity
  let maxY = -Infinity

  for (const point of points) {
    minX = Math.min(minX, point.x)
    minY = Math.min(minY, point.y)
    maxX = Math.max(maxX, point.x)
    maxY = Math.max(maxY, point.y)
  }

  return {
    x: minX,
    y: minY,
    w: maxX - minX + 1,
    h: maxY - minY + 1,
  }
}

export async function autoGrids(url: string) {
  const img = new Image()
  img.src = url
  img.crossOrigin = 'Anonymous'
  return await new Promise<IRect[]>((resolve) => {
    img.onload = () => {
      canvas.width = img.width
      canvas.height = img.height
      ctx.drawImage(img, 0, 0)
      const imageData = ctx.getImageData(0, 0, img.width, img.height)
      // 这个data是rgba的数组, 每个像素点有4个值, 分别是r,g,b,a
      const data = imageData.data
      // 这个visited是用来记录哪些像素点已经访问过
      const visited = new Array(img.width * img.height).fill(false)
      let emptyRects: IRect[] = []

      // 遍历每个 "1" 即透明像素点
      for (let y = 0; y < img.height; y++) {
        for (let x = 0; x < img.width; x++) {
          // 如果这个1已经访问过, 则跳过
          // 如果这个像素点是透明像素, 即是 "1", 则进行扩散
          if (!visited[y * img.width + x] && isEmptyPixel(data, x, y, img.width)) {
            // 进行扩散
            const area = findEmptyArea(data, visited, x, y, img.width, img.height)
            // 将扩散到的点集转换为矩形
            const rect = pointsToRect(area, img.width, img.height)
            if (rect) {
              emptyRects.push(rect)
            }
          }
        }
      }
      resolve(emptyRects)
    }
  })
}

大概就是这样, 如果有不清楚的地方可以配合ChatGPT食用

最后, 非常欢迎批评与建议