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>
<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.
</p>
<section class="grid md:grid-cols-5 gap-4">
<div class="flex flex-col gap-2 md:col-span-2">
<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 class="flex flex-col gap-2">
<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">
<option value="foreground">foreground</option>
<option value="mask">mask</option>
<option value="background">background</option>
</select>
<input value="foreground (fixed)" disabled class="border border-neutral-300 rounded-lg px-3 py-2 bg-neutral-100 text-neutral-500" />
</div>
<div class="flex flex-col gap-2">
<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">
<option value="image/png">image/png</option>
<option value="image/webp">image/webp</option>
<option value="image/jpeg">image/jpeg</option>
</select>
<input value="image/png (fixed)" disabled class="border border-neutral-300 rounded-lg px-3 py-2 bg-neutral-100 text-neutral-500" />
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">Quality (0-1)</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" />
<label class="text-xs text-neutral-400 uppercase tracking-wider">Max Side (px)</label>
<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>
</section>
@@ -88,9 +80,7 @@
<script type="module">
const el = {
files: document.getElementById("inp-files"),
type: document.getElementById("sel-type"),
format: document.getElementById("sel-format"),
quality: document.getElementById("inp-quality"),
maxSide: document.getElementById("inp-max-side"),
run: document.getElementById("btn-run"),
clear: document.getElementById("btn-clear"),
clearLog: document.getElementById("btn-clear-log"),
@@ -127,13 +117,7 @@
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const toDownloadExt = (mime) => {
if (mime === "image/webp") return "webp";
if (mime === "image/jpeg") return "jpg";
return "png";
};
const makeCard = ({ name, originalUrl, outputUrl, outputMime }, idx) => {
const makeCard = ({ name, originalUrl, outputUrl }, idx) => {
const card = document.createElement("article");
card.className = "border border-neutral-200 rounded-xl p-4 flex flex-col gap-3";
@@ -162,7 +146,7 @@
const rightLabel = document.createElement("p");
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");
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);
right.append(rightLabel, checker);
wrap.append(left, right);
const actions = document.createElement("div");
@@ -181,7 +164,7 @@
const dl = document.createElement("a");
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.textContent = "download output";
@@ -250,6 +233,42 @@
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.clearLog.addEventListener("click", () => { el.log.textContent = ""; });
@@ -278,32 +297,37 @@
setStatus("loading @imgly/background-removal...");
const removeBackground = await loadLib();
const outputType = el.type.value;
const outputFormat = el.format.value;
const quality = clamp(Number.parseFloat(el.quality.value || "0.9") || 0.9, 0, 1);
appendLog("config:", { outputType, outputFormat, quality });
const maxSide = clamp(Number.parseInt(el.maxSide.value || "1024", 10) || 1024, 256, 4096);
appendLog("config:", { outputType: "foreground", outputFormat: "image/png", maxSide });
for (let i = 0; i < files.length; 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 = {
debug: true,
debug: false,
output: {
type: outputType,
format: outputFormat,
quality
type: "foreground",
format: "image/png",
quality: 1
},
progress: (key, current, total) => {
if (!total) return;
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 outBlob = await removeBackground(file, config);
const outBlob = await removeBackground(pngFile, config);
const ms = Math.round(performance.now() - started);
appendLog("done:", file.name, `in ${ms}ms`, `blob=${outBlob.type} ${outBlob.size} bytes`);
@@ -315,8 +339,7 @@
const card = makeCard({
name: file.name,
originalUrl,
outputUrl,
outputMime: outBlob.type || outputFormat
outputUrl
}, i);
el.results.appendChild(card);
@@ -334,8 +357,16 @@
});
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");
</script>
</body>
</html>