LeetCode的一个算法帮我解决了工作棘手问题
起因是这样的, 我工作中碰到了一个问题, 允许我用一张图来表达这个问题是什么
我所维护的一个图片编辑器有这么一个功能, 就是我的编辑器提供一些模板, 然后用户会上传图片, 我需要把用户的图片缩放到模板抠出来的填图框那个大小, 从而避免让用户自己去手动缩放. 因此我们的设计师会给我一个填图框相对于整个模板的XY和宽高数据, 例如这样
这样我们就能够通过模板本身的宽高, 再乘以给定的数据就能计算得出, 我要把用户的图片缩放到哪个大小及位置, 从而让这个图片正好适应模板所抠出来的部分
但问题是我们的模板里面的填图框是大小不一, 的很难提前去预设一个位置宽高数据, 这就需要我去通过模板去运行时解析计算他这个填图框的x,y,w,h信息
起初这个问题是很容易解决的, 我们只需要找出图片的所有透明像素(即alpha通道为0), 然后向左取最小值, 向上取最小值, 向右取最大, 向下取最大即可:
{
x: minX,
y: minY,
w: maxX - minX,
h: maxY - minY
}
正当我沾沾自喜时, 我们的设计师给我整了个大招出来
之前那个简单算法只能求一个简单矩形的位置信息, 但现如今有这么多形态各异位置不同的透明区域, 该如何计算它们各自的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食用