const fs = require('fs'); const Jimp = require('jimp'); const [referencePath, mosaicPath, outputPrefix, scalesText] = process.argv.slice(2); const scales = scalesText.split(',').map(Number); const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const gray = (image) => { const { data, width, height } = image.bitmap; const result = new Float32Array(width * height); for (let p = 0, j = 0; p < result.length; p++, j += 4) result[p] = .299 * data[j] + .587 * data[j + 1] + .114 * data[j + 2]; return result; }; const rank = (scores, hit) => { if (scores.length >= 20 && hit.score < scores[scores.length - 1].score) return; if (scores.some(v => Math.abs(v.x - hit.x) < 8 && Math.abs(v.y - hit.y) < 8 && Math.abs(v.scale - hit.scale) < .04)) return; scores.push(hit); scores.sort((a, b) => b.score - a.score); if (scores.length > 20) scores.pop(); }; (async () => { const ref = await Jimp.read(fs.readFileSync(referencePath)); const source = await Jimp.read(mosaicPath); const tw = ref.bitmap.width, th = ref.bitmap.height; const step = tw < 120 ? 4 : 7; const pts = []; const refPixels = gray(ref); for (let y = 1; y < th - 1; y += step) for (let x = 1; x < tw - 1; x += step) pts.push({ x, y, val: refPixels[y * tw + x] }); const tm = pts.reduce((sum, p) => sum + p.val, 0) / pts.length; let tss = 0; for (const p of pts) { p.val -= tm; tss += p.val * p.val; } const scores = []; for (const scale of scales) { const resized = source.clone().resize(Math.round(source.bitmap.width * scale), Math.round(source.bitmap.height * scale)); const sw = resized.bitmap.width, sh = resized.bitmap.height; const pix = gray(resized); let best = { score: -2 }; const dx = sw > 2000 ? 5 : sw > 1200 ? 3 : 2; const dy = dx; for (let y = 0; y <= sh - th; y += dy) { for (let x = 0; x <= sw - tw; x += dx) { let sum = 0, squares = 0, dot = 0; for (const p of pts) { const v = pix[(y + p.y) * sw + x + p.x]; sum += v; squares += v * v; dot += v * p.val; } const denom = Math.sqrt(tss * (squares - sum * sum / pts.length)); const score = denom > 0 ? dot / denom : -1; if (score > best.score) best = { score, x, y, scale, sourceX: x / scale, sourceY: y / scale }; } } console.log(JSON.stringify({ scale, ...best })); rank(scores, best); } console.log('BEST ' + JSON.stringify(scores)); const top = scores[0]; const crop = source.clone().crop( clamp(Math.round(top.sourceX), 0, source.bitmap.width - 1), clamp(Math.round(top.sourceY), 0, source.bitmap.height - 1), clamp(Math.round(tw / top.scale), 1, source.bitmap.width - Math.round(top.sourceX)), clamp(Math.round(th / top.scale), 1, source.bitmap.height - Math.round(top.sourceY)) ); await crop.writeAsync(outputPrefix + '-best.jpg'); await ref.clone().resize(tw * 4, th * 4, Jimp.RESIZE_NEAREST_NEIGHBOR).writeAsync(outputPrefix + '-reference-upscaled.png'); })().catch(e => { console.error(e); process.exitCode = 1; });