Spaces:
Running
Running
File size: 8,273 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | // 配色引擎:把底图 + 二维码合成为 MARD 色号网格
// 性能要点:
// 1. 5bit 量化桶 → 候选 Top-K 查找表(懒加载),每格只精算约 10 个候选,而非整套 291 色
// 2. OKLab 感知距离 + 色间距离矩阵,全部 Typed Array,无对象分配
// 3. “颜色连贯度”参考已选中的左/上邻居色号,减少零碎杂色,拼起来更省事
// 4. 定位/时序/对齐等功能区使用更严格的深浅阈值,显著提升扫码成功率
const { rgbToOklabInto } = require('./color')
const { MATCH_MODES } = require('./palette')
const { PAPER } = require('./imaging')
const TOP_K = 10
const C_ANY = 0
const C_DARK = 1
const C_LIGHT = 2
const C_FDARK = 3
const C_FLIGHT = 4
const C_QUIET = 5
const C_EMPTY = 6
function subsetFor(pal, mode, kind) {
const key = `${mode.id}:${kind}`
if (pal.subsets[key]) return pal.subsets[key]
const { n, lum } = pal
let test
if (kind === C_DARK) test = (i) => lum[i] <= mode.darkMax
else if (kind === C_LIGHT) test = (i) => lum[i] >= mode.lightMin
else if (kind === C_FDARK) test = (i) => lum[i] <= Math.min(mode.darkMax, 0.3)
else if (kind === C_FLIGHT) test = (i) => lum[i] >= Math.max(mode.lightMin, 0.82)
else test = () => true
let list = []
for (let i = 0; i < n; i += 1) if (test(i)) list.push(i)
if (!list.length) {
// 色库太小时退化为最深/最浅的 3 个颜色
const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => lum[a] - lum[b])
list = kind === C_DARK || kind === C_FDARK ? order.slice(0, 3) : order.slice(-3)
}
const member = new Uint8Array(n)
list.forEach((i) => { member[i] = 1 })
const subset = { key, list: Int16Array.from(list), member, lut: new Map() }
pal.subsets[key] = subset
return subset
}
const tmpLab = new Float32Array(3)
function topKForBucket(pal, subset, bucket) {
let hit = subset.lut.get(bucket)
if (hit) return hit
const r = ((bucket >> 10) & 31) * 8 + 4
const g = ((bucket >> 5) & 31) * 8 + 4
const b = (bucket & 31) * 8 + 4
rgbToOklabInto(r, g, b, tmpLab, 0)
const { lab } = pal
const list = subset.list
const k = Math.min(TOP_K, list.length)
const bestIdx = new Int16Array(k).fill(-1)
const bestDist = new Float32Array(k).fill(Infinity)
for (let t = 0; t < list.length; t += 1) {
const i = list[t]
const dl = lab[i * 3] - tmpLab[0]
const da = lab[i * 3 + 1] - tmpLab[1]
const db = lab[i * 3 + 2] - tmpLab[2]
const d = dl * dl + da * da + db * db
if (d >= bestDist[k - 1]) continue
let p = k - 1
while (p > 0 && bestDist[p - 1] > d) { bestDist[p] = bestDist[p - 1]; bestIdx[p] = bestIdx[p - 1]; p -= 1 }
bestDist[p] = d
bestIdx[p] = i
}
hit = bestIdx
subset.lut.set(bucket, hit)
return hit
}
/**
* @param base { size, rgb: Uint8Array, mask?: Uint8Array } 或 null(无底图)
* @param qr makeQr() 结果
* @param placement createPlacement() 结果
* @param pal preparePalette() 结果
* @param options { modeId, coherence(0-100), cleanup(bool) }
*/
function computeArtwork(base, qr, placement, pal, options = {}) {
const mode = MATCH_MODES.find((m) => m.id === options.modeId) || MATCH_MODES[1]
const size = placement.size
const total = size * size
const lab = new Float32Array(total * 3)
const bucket = new Int32Array(total)
const kind = new Uint8Array(total)
const { left, top, quiet } = placement
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const i = y * size + x
const qx = x - left
const qy = y - top
const inQr = qx >= 0 && qy >= 0 && qx < qr.size && qy < qr.size
const inQuiet = !inQr && x >= left - quiet && x < left + qr.size + quiet && y >= top - quiet && y < top + qr.size + quiet
let r; let g; let b
if (inQr) {
const q = qy * qr.size + qx
const isDark = qr.dark[q]
const isFunc = qr.func[q]
kind[i] = isFunc ? (isDark ? C_FDARK : C_FLIGHT) : (isDark ? C_DARK : C_LIGHT)
if (base && !(base.mask && base.mask[i])) {
r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
} else if (base) {
r = PAPER[0]; g = PAPER[1]; b = PAPER[2]
} else {
r = g = b = isDark ? 0 : 255
}
} else if (inQuiet || !base) {
kind[i] = C_QUIET
continue
} else if (base.mask && base.mask[i]) {
kind[i] = C_EMPTY
continue
} else {
kind[i] = C_ANY
r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2]
}
rgbToOklabInto(r, g, b, lab, i * 3)
bucket[i] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
}
}
const n = pal.n
const plab = pal.lab
const pair = pal.pair
const weight = (Math.max(0, Math.min(100, options.coherence == null ? 40 : options.coherence)) / 100) * 0.6
const subsets = [C_ANY, C_DARK, C_LIGHT, C_FDARK, C_FLIGHT].map((k) => subsetFor(pal, mode, k))
const cells = new Int16Array(total).fill(-1)
const distTo = (c, i) => {
const dl = plab[c * 3] - lab[i * 3]
const da = plab[c * 3 + 1] - lab[i * 3 + 1]
const db = plab[c * 3 + 2] - lab[i * 3 + 2]
return Math.sqrt(dl * dl + da * da + db * db)
}
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const i = y * size + x
const k = kind[i]
if (k >= C_QUIET) continue
const subset = subsets[k]
const cands = topKForBucket(pal, subset, bucket[i])
const nl = x > 0 ? cells[i - 1] : -1
const nu = y > 0 ? cells[i - size] : -1
const nbCount = (nl >= 0 ? 1 : 0) + (nu >= 0 ? 1 : 0)
let best = -1
let bestScore = Infinity
const consider = (c) => {
let score = distTo(c, i)
if (weight > 0 && nbCount) {
let s = 0
if (nl >= 0) s += pair[c * n + nl]
if (nu >= 0) s += pair[c * n + nu]
score += weight * (s / nbCount)
}
if (score < bestScore) { bestScore = score; best = c }
}
for (let t = 0; t < cands.length; t += 1) if (cands[t] >= 0) consider(cands[t])
if (weight > 0) {
if (nl >= 0 && subset.member[nl]) consider(nl)
if (nu >= 0 && nu !== nl && subset.member[nu]) consider(nu)
}
cells[i] = best
}
}
// 合并零散色:只用了 1~2 颗的色号并入最接近的常用色,减少需要购买的色号
const counts = new Int32Array(n)
for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
if (options.cleanup !== false) {
const kept = []
for (let c = 0; c < n; c += 1) if (counts[c] >= 3) kept.push(c)
if (kept.length) {
for (let i = 0; i < total; i += 1) {
const c = cells[i]
if (c < 0 || counts[c] >= 3) continue
const member = subsets[kind[i]].member
let best = -1
let bestD = Infinity
for (let t = 0; t < kept.length; t += 1) {
const kc = kept[t]
if (!member[kc]) continue
const d = distTo(kc, i)
if (d < bestD) { bestD = d; best = kc }
}
// 与原色差别过大时保留原色,避免破坏画面
if (best >= 0 && bestD <= distTo(c, i) + 12) cells[i] = best
}
counts.fill(0)
for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1
}
}
// 二维码留白区:使用画面中最常用的浅色;没有则用色库里最浅的颜色
let quietColor = -1
let quietCount = -1
for (let c = 0; c < n; c += 1) {
if (pal.lum[c] >= Math.max(0.8, mode.lightMin) && counts[c] > quietCount) { quietColor = c; quietCount = counts[c] }
}
if (quietCount <= 0) {
quietColor = 0
for (let c = 1; c < n; c += 1) if (pal.lum[c] > pal.lum[quietColor]) quietColor = c
}
for (let i = 0; i < total; i += 1) {
if (kind[i] === C_QUIET) { cells[i] = quietColor; counts[quietColor] += 1 }
}
const usage = []
let beads = 0
for (let c = 0; c < n; c += 1) {
if (counts[c] > 0) {
usage.push({ index: c, id: pal.ids[c], hex: pal.hexes[c], count: counts[c] })
beads += counts[c]
}
}
usage.sort((a, b) => b.count - a.count)
return { size, cells, usage, beads, pal, qr, placement }
}
module.exports = { computeArtwork }
|