Files
RecipesCam/docker/frontend/scripts/test-all-raws-accuracy.mjs
T

174 lines
6.8 KiB
JavaScript

import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import ts from 'typescript';
const transpile = (path) =>
ts.transpileModule(readFileSync(new URL(path, import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText;
const LIBRAW_NODE_SRC = `
import { Worker } from 'node:worker_threads';
const SHIM = \`
import { parentPort, workerData } from 'node:worker_threads';
import { readFile } from 'node:fs/promises';
globalThis.self = globalThis;
globalThis.postMessage = (m, t) => parentPort.postMessage(m, t);
Object.defineProperty(globalThis, 'onmessage', {
configurable: true,
set(fn) { parentPort.on('message', (data) => fn({ data })); },
});
const base = new URL('.', workerData.url);
globalThis.fetch = async (u) =>
new Response(await readFile(new URL(u, base)), { headers: { 'content-type': 'application/wasm' } });
await import(workerData.url);
\`;
export default class LibRaw {
constructor() {
const url = new URL('worker.js', ${JSON.stringify(import.meta.resolve('libraw-wasm/dist/index.js'))}).href;
this.worker = new Worker(SHIM, { eval: true, workerData: { url } });
this.pending = new Map();
this.next = 0;
this.worker.on('message', ({ id, out, error }) => {
const p = this.pending.get(id);
if (!p) return;
this.pending.delete(id);
if (error) p.reject(new Error(error));
else p.resolve(out);
});
}
run(fn, ...args) {
return new Promise((resolve, reject) => {
const id = this.next++;
this.pending.set(id, { resolve, reject });
const transfer = args.filter((a) => ArrayBuffer.isView(a)).map((a) => a.buffer);
this.worker.postMessage({ id, fn, args }, transfer);
});
}
open(bytes, settings) {
const copy = Uint8Array.from(bytes);
return this.run('open', copy, settings);
}
metadata(full) { return this.run('metadata', !!full); }
imageData() { return this.run('imageData'); }
thumbnailData() { return this.run('thumbnailData'); }
dispose() { return this.worker.terminate(); }
}
`;
const dir = join(tmpdir(), 'test-all-raws-acc');
import { mkdtempSync } from 'node:fs';
const testDir = mkdtempSync(dir);
const ckEntry = import.meta.resolve('canvaskit-wasm/full');
writeFileSync(
join(testDir, 'skiaShim.mjs'),
transpile('../src/engine/skiaShim.ts').replace(
/^import CanvasKitInit from ['"]canvaskit-wasm\/full['"];$/m,
`import CanvasKitInit from ${JSON.stringify(ckEntry)};`,
),
);
writeFileSync(join(testDir, 'sensorWhite.mjs'), transpile('../src/engine/sensorWhite.ts'));
writeFileSync(join(testDir, 'halfFloat.mjs'), transpile('../src/engine/halfFloat.ts'));
writeFileSync(join(testDir, 'previewMatch.mjs'), transpile('../src/engine/previewMatch.ts'));
writeFileSync(
join(testDir, 'tiffDecode.mjs'),
transpile('../src/engine/tiffDecode.ts').replace(/['"]\.\/skiaShim['"]/, '"./skiaShim.mjs"')
);
// Transpile rawDevelop.ts with fix
let rawDevTs = readFileSync(new URL('../src/engine/rawDevelop.ts', import.meta.url), 'utf8');
// Fix WB multiplier order in rawDevelop.ts code before transpiling
rawDevTs = rawDevTs.replace(
` if (cd?.cam_mul && Array.isArray(cd.cam_mul) && cd.cam_mul.some((v: number) => v > 0 && v !== 1)) {
return cd.cam_mul;
}
// 2. Try pre-calculated WB multipliers from LibRaw (pre_mul)
if (cd?.pre_mul && Array.isArray(cd.pre_mul) && cd.pre_mul.some((v: number) => v > 0 && v !== 1)) {
return cd.pre_mul;
}`,
` if (cd?.pre_mul && Array.isArray(cd.pre_mul) && cd.pre_mul.some((v: number) => v > 0 && v !== 1)) {
return cd.pre_mul;
}
// 2. Try camera WB multipliers from LibRaw (cam_mul)
if (cd?.cam_mul && Array.isArray(cd.cam_mul) && cd.cam_mul.some((v: number) => v > 0 && v !== 1)) {
return cd.cam_mul;
}`
);
const rawDevelopCode = ts.transpileModule(rawDevTs, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText
.replace(/^import LibRaw from ['"]libraw-wasm['"];$/m, 'import LibRaw from "./librawNode.mjs";')
.replace(/['"]\.\/sensorWhite['"]/, '"./sensorWhite.mjs"')
.replace(/['"]\.\/halfFloat['"]/, '"./halfFloat.mjs"')
.replace(/['"]\.\/previewMatch['"]/, '"./previewMatch.mjs"')
.replace(/['"]\.\/skiaShim['"]/, '"./skiaShim.mjs"')
.replace(/['"]\.\/tiffDecode['"]/, '"./tiffDecode.mjs"');
writeFileSync(join(testDir, 'rawDevelop.mjs'), rawDevelopCode);
writeFileSync(join(testDir, 'librawNode.mjs'), LIBRAW_NODE_SRC);
const { developRaw, extractEmbeddedJpeg } = await import(pathToFileURL(join(testDir, 'rawDevelop.mjs')).href);
const Skia = await (await import(pathToFileURL(join(testDir, 'skiaShim.mjs')).href)).initSkia();
const getAvgRgb = (img) => {
const w = 64, h = 64;
const surface = Skia.Surface.MakeOffscreen(w, h);
const canvas = surface.getCanvas();
canvas.drawImageRectCubic(img, Skia.XYWHRect(0, 0, img.width(), img.height()), Skia.XYWHRect(0, 0, w, h), 1/3, 1/3);
surface.flush();
const px = canvas.readPixels(0, 0, { width: w, height: h, colorType: Skia.ColorType.RGBA_8888, alphaType: Skia.AlphaType.Unpremul, colorSpace: Skia.ColorSpace.SRGB });
surface.dispose();
let r = 0, g = 0, b = 0;
for (let i = 0; i < px.length; i += 4) {
r += px[i]; g += px[i+1]; b += px[i+2];
}
const count = px.length / 4;
return [ (r/count).toFixed(2), (g/count).toFixed(2), (b/count).toFixed(2) ];
};
const sampleDir = '../sample';
const files = readdirSync(sampleDir).filter(f => /\.(raf|nef|dng|rw2|orf|arw|cr2|cr3)$/i.test(f));
for (const f of files) {
const filePath = join(sampleDir, f);
const bytes = new Uint8Array(readFileSync(filePath));
console.log(`\n=== Testing ${f} ===`);
try {
const devJpeg = await developRaw(bytes, f);
const embJpeg = extractEmbeddedJpeg(bytes);
const devImg = Skia.Image.MakeImageFromEncoded(devJpeg);
const embImg = embJpeg ? Skia.Image.MakeImageFromEncoded(embJpeg) : null;
const devRgb = getAvgRgb(devImg);
console.log(`Dev dim: ${devImg.width()}x${devImg.height()} RGB: [ ${devRgb.join(', ')} ]`);
if (embImg) {
const embRgb = getAvgRgb(embImg);
console.log(`Emb dim: ${embImg.width()}x${embImg.height()} RGB: [ ${embRgb.join(', ')} ]`);
const rDiff = Math.abs(parseFloat(devRgb[0]) - parseFloat(embRgb[0]));
const gDiff = Math.abs(parseFloat(devRgb[1]) - parseFloat(embRgb[1]));
const bDiff = Math.abs(parseFloat(devRgb[2]) - parseFloat(embRgb[2]));
console.log(`RGB Abs Diffs: R=${rDiff.toFixed(2)}, G=${gDiff.toFixed(2)}, B=${bDiff.toFixed(2)}`);
const pass = rDiff < 15 && gDiff < 15 && bDiff < 15;
console.log(`COLOR ACCURACY PASS: ${pass ? 'YES ✅' : 'NO ❌'}`);
}
devImg.delete();
if (embImg) embImg.delete();
} catch (err) {
console.error(`FAILED ${f}:`, err.message);
}
}