Spaces:
Running
Running
File size: 3,983 Bytes
9de7b70 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | // 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色)
// 所有结果都用 Typed Array 表示,不再生成 HEX 字符串再反解析
const PAPER = [247, 241, 230]
function getImageDimensions(image) {
const width = Number(image.width) || Number(image.naturalWidth) || Number(image._w)
const height = Number(image.height) || Number(image.naturalHeight) || Number(image._h)
if (!width || !height) throw new Error('IMAGE_DIMENSIONS_UNAVAILABLE')
return { width, height }
}
// 把图片按裁切参数画到 context 上(cover 语义,zoom≥1)
function drawCropped(context, image, width, height, crop) {
const source = getImageDimensions(image)
const scale = Math.max(width / source.width, height / source.height) * (crop.zoom || 1)
const w = source.width * scale
const h = source.height * scale
const offsetX = (width - w) * ((crop.focusX == null ? 50 : crop.focusX) / 100)
const offsetY = (height - h) * ((crop.focusY == null ? 50 : crop.focusY) / 100)
context.fillStyle = `rgb(${PAPER[0]},${PAPER[1]},${PAPER[2]})`
context.fillRect(0, 0, width, height)
context.save()
context.translate(crop.flipH ? width : 0, crop.flipV ? height : 0)
context.scale(crop.flipH ? -1 : 1, crop.flipV ? -1 : 1)
context.drawImage(image, offsetX, offsetY, w, h)
context.restore()
}
// 返回 { size, rgb: Uint8Array(size*size*3) }
function sampleImage(canvas, image, size, crop) {
// 超采样到约 700px 再按块平均,等价于高质量面积缩放
const factor = Math.max(1, Math.min(24, Math.floor(720 / size)))
const big = size * factor
canvas.width = big
canvas.height = big
const context = canvas.getContext('2d')
context.imageSmoothingEnabled = true
try { context.imageSmoothingQuality = 'high' } catch (e) { /* 部分基础库不支持 */ }
drawCropped(context, image, big, big, crop || {})
const pixels = context.getImageData(0, 0, big, big).data
const rgb = new Uint8Array(size * size * 3)
const area = factor * factor
for (let gy = 0; gy < size; gy += 1) {
for (let gx = 0; gx < size; gx += 1) {
let r = 0; let g = 0; let b = 0
for (let sy = 0; sy < factor; sy += 1) {
let p = ((gy * factor + sy) * big + gx * factor) * 4
for (let sx = 0; sx < factor; sx += 1) {
r += pixels[p]; g += pixels[p + 1]; b += pixels[p + 2]
p += 4
}
}
const o = (gy * size + gx) * 3
rgb[o] = (r / area + 0.5) | 0
rgb[o + 1] = (g / area + 0.5) | 0
rgb[o + 2] = (b / area + 0.5) | 0
}
}
return { size, rgb }
}
// 去除简单纯色背景:四角均色为背景色,从边缘洪泛填充相近颜色(主体内部不受影响)
// 返回 Uint8Array 掩码:1 = 背景(空钉位,不放豆)
function removeSimpleBackground(rgb, size, tolerance) {
const corners = [0, size - 1, (size - 1) * size, size * size - 1]
let br = 0; let bg = 0; let bb = 0
corners.forEach((i) => { br += rgb[i * 3]; bg += rgb[i * 3 + 1]; bb += rgb[i * 3 + 2] })
br /= 4; bg /= 4; bb /= 4
const threshold = (tolerance / 100) * 441.673
const limit = threshold * threshold
const mask = new Uint8Array(size * size)
const isBg = (i) => {
const dr = rgb[i * 3] - br; const dg = rgb[i * 3 + 1] - bg; const db = rgb[i * 3 + 2] - bb
return dr * dr + dg * dg + db * db <= limit
}
const stack = []
const push = (x, y) => {
if (x < 0 || y < 0 || x >= size || y >= size) return
const i = y * size + x
if (mask[i] || !isBg(i)) return
mask[i] = 1
stack.push(i)
}
for (let i = 0; i < size; i += 1) { push(i, 0); push(i, size - 1); push(0, i); push(size - 1, i) }
while (stack.length) {
const i = stack.pop()
const x = i % size
const y = (i - x) / size
push(x + 1, y); push(x - 1, y); push(x, y + 1); push(x, y - 1)
}
return mask
}
module.exports = { PAPER, getImageDimensions, drawCropped, sampleImage, removeSimpleBackground }
|