feat(heal): upgrade Fast Engine with Poisson Clean Donor Blending and AI Server with LaMa FFC

This commit is contained in:
2026-10-16 21:54:01 +07:00
parent 6df3cec7f8
commit 996564dbde
4 changed files with 339 additions and 499 deletions
+101 -6
View File
@@ -1,7 +1,7 @@
import * as ort from 'onnxruntime-node';
import { existsSync } from 'node:fs';
const MODEL_PATH = process.env.INPAINT_MODEL_PATH || '/models/migan_pipeline_v2.onnx';
const MODEL_PATH = process.env.INPAINT_MODEL_PATH || '/models/lama_fp32.onnx';
let sessionPromise: Promise<ort.InferenceSession | null> | null = null;
@@ -10,9 +10,11 @@ export async function getInpaintSession(): Promise<ort.InferenceSession | null>
sessionPromise = (async () => {
const candidates = [
MODEL_PATH,
'/home/locpham/RecipesCam/docker/frontend/public/models/migan_pipeline_v2.onnx',
'../frontend/public/models/migan_pipeline_v2.onnx',
'./models/migan_pipeline_v2.onnx',
'/home/locpham/RecipesCam/docker/frontend/public/models/lama_fp32.onnx',
'../frontend/public/models/lama_fp32.onnx',
'./models/lama_fp32.onnx',
'/models/lama_fp32.onnx',
'/models/migan_pipeline_v2.onnx',
];
let foundPath: string | null = null;
for (const p of candidates) {
@@ -33,6 +35,7 @@ export async function getInpaintSession(): Promise<ort.InferenceSession | null>
graphOptimizationLevel: 'all',
});
console.log('[inpaintService] Model loaded in ' + (Date.now() - t0) + 'ms');
console.log('[inpaintService] Inputs:', session.inputNames, 'Outputs:', session.outputNames);
return session;
})().catch((err) => {
console.error('[inpaintService] Failed to load ONNX model:', err);
@@ -43,6 +46,48 @@ export async function getInpaintSession(): Promise<ort.InferenceSession | null>
return sessionPromise;
}
function resizeBilinear(
src: Float32Array,
sw: number,
sh: number,
dw: number,
dh: number,
channels: number
): Float32Array {
const dst = new Float32Array(channels * dw * dh);
const xRatio = sw / dw;
const yRatio = sh / dh;
for (let dy = 0; dy < dh; dy++) {
const sy = dy * yRatio;
const y0 = Math.floor(sy);
const y1 = Math.min(sh - 1, y0 + 1);
const yLerp = sy - y0;
for (let dx = 0; dx < dw; dx++) {
const sx = dx * xRatio;
const x0 = Math.floor(sx);
const x1 = Math.min(sw - 1, x0 + 1);
const xLerp = sx - x0;
for (let c = 0; c < channels; c++) {
const cOffsetSrc = c * sw * sh;
const v00 = src[cOffsetSrc + y0 * sw + x0];
const v01 = src[cOffsetSrc + y0 * sw + x1];
const v10 = src[cOffsetSrc + y1 * sw + x0];
const v11 = src[cOffsetSrc + y1 * sw + x1];
const top = v00 + (v01 - v00) * xLerp;
const bot = v10 + (v11 - v10) * xLerp;
const val = top + (bot - top) * yLerp;
dst[c * dw * dh + dy * dw + dx] = val;
}
}
}
return dst;
}
export async function runServerInpaint(
srcBytes: Uint8Array,
maskBytes: Uint8Array,
@@ -52,6 +97,57 @@ export async function runServerInpaint(
const session = await getInpaintSession();
if (!session) return null;
const isLama = session.inputNames.includes('image') || session.inputNames.includes('mask');
const plane = w * h;
if (isLama) {
const rawRgb = new Float32Array(3 * plane);
for (let i = 0; i < 3 * plane; i++) {
rawRgb[i] = srcBytes[i] / 255.0;
}
const rawMask = new Float32Array(plane);
for (let i = 0; i < plane; i++) {
rawMask[i] = maskBytes[i] > 128 ? 1.0 : 0.0;
}
// Bilinear resize to 512x512 for LaMa
const inImage512 = resizeBilinear(rawRgb, w, h, 512, 512, 3);
const inMask512 = resizeBilinear(rawMask, w, h, 512, 512, 1);
for (let i = 0; i < 512 * 512; i++) {
inMask512[i] = inMask512[i] > 0.15 ? 1.0 : 0.0;
}
const imageInputName = session.inputNames.includes('image') ? 'image' : session.inputNames[0];
const maskInputName = session.inputNames.includes('mask') ? 'mask' : session.inputNames[1];
const out = await session.run({
[imageInputName]: new ort.Tensor('float32', inImage512, [1, 3, 512, 512]),
[maskInputName]: new ort.Tensor('float32', inMask512, [1, 1, 512, 512]),
});
const outTensor = out[session.outputNames[0]];
const out512 = outTensor.data as Float32Array;
// Resize output back to w x h
const outSw = resizeBilinear(out512, 512, 512, w, h, 3);
const patch = new Uint8Array(4 * plane);
const plane2 = 2 * plane;
for (let i = 0; i < plane; i++) {
const r = Math.round(outSw[i]);
const g = Math.round(outSw[plane + i]);
const b = Math.round(outSw[plane2 + i]);
patch[i * 4] = r < 0 ? 0 : r > 255 ? 255 : r;
patch[i * 4 + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
patch[i * 4 + 2] = b < 0 ? 0 : b > 255 ? 255 : b;
patch[i * 4 + 3] = 255;
}
return patch;
}
// Fallback for uint8 legacy models (e.g. MiGAN)
const in0 = new ort.Tensor('uint8', srcBytes, [1, 3, h, w]);
const in1 = new ort.Tensor('uint8', maskBytes, [1, 1, h, w]);
@@ -62,8 +158,7 @@ export async function runServerInpaint(
const tensor = out[session.outputNames[0]];
const data = tensor.data as Uint8Array;
const patch = new Uint8Array(4 * w * h);
const plane = w * h;
const patch = new Uint8Array(4 * plane);
for (let i = 0; i < plane; i++) {
patch[i * 4] = data[i];
patch[i * 4 + 1] = data[plane + i];