match-crops.cjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. const fs = require('fs');
  2. const Jimp = require('jimp');
  3. const [referencePath, mosaicPath, outputPrefix, scalesText] = process.argv.slice(2);
  4. const scales = scalesText.split(',').map(Number);
  5. const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
  6. const gray = (image) => {
  7. const { data, width, height } = image.bitmap;
  8. const result = new Float32Array(width * height);
  9. for (let p = 0, j = 0; p < result.length; p++, j += 4)
  10. result[p] = .299 * data[j] + .587 * data[j + 1] + .114 * data[j + 2];
  11. return result;
  12. };
  13. const rank = (scores, hit) => {
  14. if (scores.length >= 20 && hit.score < scores[scores.length - 1].score) return;
  15. 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;
  16. scores.push(hit);
  17. scores.sort((a, b) => b.score - a.score);
  18. if (scores.length > 20) scores.pop();
  19. };
  20. (async () => {
  21. const ref = await Jimp.read(fs.readFileSync(referencePath));
  22. const source = await Jimp.read(mosaicPath);
  23. const tw = ref.bitmap.width, th = ref.bitmap.height;
  24. const step = tw < 120 ? 4 : 7;
  25. const pts = [];
  26. const refPixels = gray(ref);
  27. for (let y = 1; y < th - 1; y += step)
  28. for (let x = 1; x < tw - 1; x += step) pts.push({ x, y, val: refPixels[y * tw + x] });
  29. const tm = pts.reduce((sum, p) => sum + p.val, 0) / pts.length;
  30. let tss = 0;
  31. for (const p of pts) { p.val -= tm; tss += p.val * p.val; }
  32. const scores = [];
  33. for (const scale of scales) {
  34. const resized = source.clone().resize(Math.round(source.bitmap.width * scale), Math.round(source.bitmap.height * scale));
  35. const sw = resized.bitmap.width, sh = resized.bitmap.height;
  36. const pix = gray(resized);
  37. let best = { score: -2 };
  38. const dx = sw > 2000 ? 5 : sw > 1200 ? 3 : 2;
  39. const dy = dx;
  40. for (let y = 0; y <= sh - th; y += dy) {
  41. for (let x = 0; x <= sw - tw; x += dx) {
  42. let sum = 0, squares = 0, dot = 0;
  43. for (const p of pts) {
  44. const v = pix[(y + p.y) * sw + x + p.x];
  45. sum += v; squares += v * v; dot += v * p.val;
  46. }
  47. const denom = Math.sqrt(tss * (squares - sum * sum / pts.length));
  48. const score = denom > 0 ? dot / denom : -1;
  49. if (score > best.score) best = { score, x, y, scale, sourceX: x / scale, sourceY: y / scale };
  50. }
  51. }
  52. console.log(JSON.stringify({ scale, ...best }));
  53. rank(scores, best);
  54. }
  55. console.log('BEST ' + JSON.stringify(scores));
  56. const top = scores[0];
  57. const crop = source.clone().crop(
  58. clamp(Math.round(top.sourceX), 0, source.bitmap.width - 1),
  59. clamp(Math.round(top.sourceY), 0, source.bitmap.height - 1),
  60. clamp(Math.round(tw / top.scale), 1, source.bitmap.width - Math.round(top.sourceX)),
  61. clamp(Math.round(th / top.scale), 1, source.bitmap.height - Math.round(top.sourceY))
  62. );
  63. await crop.writeAsync(outputPrefix + '-best.jpg');
  64. await ref.clone().resize(tw * 4, th * 4, Jimp.RESIZE_NEAREST_NEIGHBOR).writeAsync(outputPrefix + '-reference-upscaled.png');
  65. })().catch(e => { console.error(e); process.exitCode = 1; });