feat(heal): upgrade Fast Engine with Poisson Clean Donor Blending and AI Server with LaMa FFC
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user