Fix: PNG preprocess + faster bg remove

This commit is contained in:
2026-03-21 00:26:23 -07:00
parent a6a6485087
commit d83aafbc89

115
test.html
View File

@@ -34,37 +34,29 @@
</header> </header>
<p class="text-neutral-500"> <p class="text-neutral-500">
Upload image(s), run <code>@imgly/background-removal</code>, inspect output + logs. Upload image(s), convert to PNG, remove background, inspect output + logs.
First run can take longer due model download. First run can take longer due model download.
</p> </p>
<section class="grid md:grid-cols-5 gap-4"> <section class="grid md:grid-cols-5 gap-4">
<div class="flex flex-col gap-2 md:col-span-2"> <div class="flex flex-col gap-2 md:col-span-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">Files</label> <label class="text-xs text-neutral-400 uppercase tracking-wider">Files</label>
<input id="inp-files" type="file" multiple class="border border-neutral-300 rounded-lg px-3 py-2" /> <input id="inp-files" type="file" multiple accept="image/*" class="border border-neutral-300 rounded-lg px-3 py-2" />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">Output Type</label> <label class="text-xs text-neutral-400 uppercase tracking-wider">Output Type</label>
<select id="sel-type" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white"> <input value="foreground (fixed)" disabled class="border border-neutral-300 rounded-lg px-3 py-2 bg-neutral-100 text-neutral-500" />
<option value="foreground">foreground</option>
<option value="mask">mask</option>
<option value="background">background</option>
</select>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">Format</label> <label class="text-xs text-neutral-400 uppercase tracking-wider">Format</label>
<select id="sel-format" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white"> <input value="image/png (fixed)" disabled class="border border-neutral-300 rounded-lg px-3 py-2 bg-neutral-100 text-neutral-500" />
<option value="image/png">image/png</option>
<option value="image/webp">image/webp</option>
<option value="image/jpeg">image/jpeg</option>
</select>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">Quality (0-1)</label> <label class="text-xs text-neutral-400 uppercase tracking-wider">Max Side (px)</label>
<input id="inp-quality" type="number" min="0" max="1" step="0.05" value="0.9" class="border border-neutral-300 rounded-lg px-3 py-2" /> <input id="inp-max-side" type="number" min="256" max="4096" step="32" value="1024" class="border border-neutral-300 rounded-lg px-3 py-2" />
</div> </div>
</section> </section>
@@ -88,9 +80,7 @@
<script type="module"> <script type="module">
const el = { const el = {
files: document.getElementById("inp-files"), files: document.getElementById("inp-files"),
type: document.getElementById("sel-type"), maxSide: document.getElementById("inp-max-side"),
format: document.getElementById("sel-format"),
quality: document.getElementById("inp-quality"),
run: document.getElementById("btn-run"), run: document.getElementById("btn-run"),
clear: document.getElementById("btn-clear"), clear: document.getElementById("btn-clear"),
clearLog: document.getElementById("btn-clear-log"), clearLog: document.getElementById("btn-clear-log"),
@@ -127,13 +117,7 @@
const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const toDownloadExt = (mime) => { const makeCard = ({ name, originalUrl, outputUrl }, idx) => {
if (mime === "image/webp") return "webp";
if (mime === "image/jpeg") return "jpg";
return "png";
};
const makeCard = ({ name, originalUrl, outputUrl, outputMime }, idx) => {
const card = document.createElement("article"); const card = document.createElement("article");
card.className = "border border-neutral-200 rounded-xl p-4 flex flex-col gap-3"; card.className = "border border-neutral-200 rounded-xl p-4 flex flex-col gap-3";
@@ -162,7 +146,7 @@
const rightLabel = document.createElement("p"); const rightLabel = document.createElement("p");
rightLabel.className = "text-xs text-neutral-400 uppercase tracking-wider"; rightLabel.className = "text-xs text-neutral-400 uppercase tracking-wider";
rightLabel.textContent = "background removed (checkerboard)"; rightLabel.textContent = "background removed (transparent png)";
const checker = document.createElement("div"); const checker = document.createElement("div");
checker.className = "checker rounded-lg border border-neutral-200 h-56 flex items-center justify-center overflow-hidden"; checker.className = "checker rounded-lg border border-neutral-200 h-56 flex items-center justify-center overflow-hidden";
@@ -173,7 +157,6 @@
checker.appendChild(rightImg); checker.appendChild(rightImg);
right.append(rightLabel, checker); right.append(rightLabel, checker);
wrap.append(left, right); wrap.append(left, right);
const actions = document.createElement("div"); const actions = document.createElement("div");
@@ -181,7 +164,7 @@
const dl = document.createElement("a"); const dl = document.createElement("a");
dl.href = outputUrl; dl.href = outputUrl;
dl.download = `${name.replace(/\.[^.]+$/, "")}-nobg.${toDownloadExt(outputMime)}`; dl.download = `${name.replace(/\.[^.]+$/, "")}-nobg.png`;
dl.className = "bg-neutral-800 text-white rounded-lg px-4 py-2 hover:bg-neutral-700 transition no-underline"; dl.className = "bg-neutral-800 text-white rounded-lg px-4 py-2 hover:bg-neutral-700 transition no-underline";
dl.textContent = "download output"; dl.textContent = "download output";
@@ -250,6 +233,42 @@
try { return JSON.stringify(err); } catch { return String(err); } try { return JSON.stringify(err); } catch { return String(err); }
} }
async function fileToPng(file, maxSide) {
const srcUrl = URL.createObjectURL(file);
try {
const img = new Image();
img.decoding = "async";
img.src = srcUrl;
await img.decode();
let w = img.naturalWidth || img.width;
let h = img.naturalHeight || img.height;
const longest = Math.max(w, h);
const scale = longest > maxSide ? (maxSide / longest) : 1;
w = Math.max(1, Math.round(w * scale));
h = Math.max(1, Math.round(h * scale));
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not create 2D canvas context");
ctx.clearRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, w, h);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Failed converting to PNG")), "image/png", 1);
});
const pngName = `${file.name.replace(/\.[^.]+$/, "")}.png`;
return new File([blob], pngName, { type: "image/png" });
} finally {
URL.revokeObjectURL(srcUrl);
}
}
el.clear.addEventListener("click", clearResults); el.clear.addEventListener("click", clearResults);
el.clearLog.addEventListener("click", () => { el.log.textContent = ""; }); el.clearLog.addEventListener("click", () => { el.log.textContent = ""; });
@@ -278,32 +297,37 @@
setStatus("loading @imgly/background-removal..."); setStatus("loading @imgly/background-removal...");
const removeBackground = await loadLib(); const removeBackground = await loadLib();
const outputType = el.type.value; const maxSide = clamp(Number.parseInt(el.maxSide.value || "1024", 10) || 1024, 256, 4096);
const outputFormat = el.format.value; appendLog("config:", { outputType: "foreground", outputFormat: "image/png", maxSide });
const quality = clamp(Number.parseFloat(el.quality.value || "0.9") || 0.9, 0, 1);
appendLog("config:", { outputType, outputFormat, quality });
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
setStatus(`processing ${i + 1}/${files.length}: ${file.name} ...`); setStatus(`preparing ${i + 1}/${files.length}: ${file.name} ...`);
appendLog("converting to PNG:", file.name);
const startedPrep = performance.now();
const pngFile = await fileToPng(file, maxSide);
const prepMs = Math.round(performance.now() - startedPrep);
appendLog("png ready:", pngFile.name, `${pngFile.size} bytes`, `in ${prepMs}ms`);
const config = { const config = {
debug: true, debug: false,
output: { output: {
type: outputType, type: "foreground",
format: outputFormat, format: "image/png",
quality quality: 1
}, },
progress: (key, current, total) => { progress: (key, current, total) => {
if (!total) return; if (!total) return;
const pct = Math.round((current / total) * 100); const pct = Math.round((current / total) * 100);
el.status.textContent = `downloading model assets (${key}) ${pct}% — file ${i + 1}/${files.length}`; el.status.textContent = `loading model assets (${key}) ${pct}% — file ${i + 1}/${files.length}`;
} }
}; };
appendLog("calling removeBackground for:", file.name); appendLog("calling removeBackground for:", pngFile.name);
setStatus(`processing ${i + 1}/${files.length}: ${file.name} ...`);
const started = performance.now(); const started = performance.now();
const outBlob = await removeBackground(file, config); const outBlob = await removeBackground(pngFile, config);
const ms = Math.round(performance.now() - started); const ms = Math.round(performance.now() - started);
appendLog("done:", file.name, `in ${ms}ms`, `blob=${outBlob.type} ${outBlob.size} bytes`); appendLog("done:", file.name, `in ${ms}ms`, `blob=${outBlob.type} ${outBlob.size} bytes`);
@@ -315,8 +339,7 @@
const card = makeCard({ const card = makeCard({
name: file.name, name: file.name,
originalUrl, originalUrl,
outputUrl, outputUrl
outputMime: outBlob.type || outputFormat
}, i); }, i);
el.results.appendChild(card); el.results.appendChild(card);
@@ -334,8 +357,16 @@
}); });
window.addEventListener("beforeunload", revokeAllUrls); window.addEventListener("beforeunload", revokeAllUrls);
const idleWarmup = () => loadLib().then(
() => appendLog("warmup: library ready"),
(err) => appendLog("warmup failed:", formatErr(err))
);
if ("requestIdleCallback" in window) requestIdleCallback(idleWarmup, { timeout: 2500 });
else setTimeout(idleWarmup, 800);
appendLog("test page booted"); appendLog("test page booted");
</script> </script>
</body> </body>
</html> </html>