Il problema di partenza: quale rimozione sfondo è davvero fattibile gratis?
La richiesta iniziale era ampia: un tool per rimuovere sfondi e, se possibile, anche oggetti indesiderati dalle foto. Le due cose hanno difficoltà molto diverse. Rimuovere oggetti richiede inpainting — ricostruire in modo plausibile l'area cancellata — un compito che nel 2026 è ancora dominio di modelli generativi molto pesanti, poco pratici da far girare nel browser con buona qualità su un dispositivo medio. Rimuovere lo sfondo, invece, è un problema di segmentazione: separare un soggetto da uno sfondo, un compito per cui esistono modelli molto più leggeri, eseguibili interamente via WebAssembly senza alcun server.
Ho quindi deciso di partire solo dalla rimozione sfondo, scartando per ora l'idea della rimozione oggetti. La domanda successiva è stata: quale libreria usare per eseguire un modello di segmentazione interamente lato client, mantenendo il modello a costo zero già seguito per il resto del sito?
The starting problem: which background removal is actually feasible for free?
The initial request was broad: a tool to remove backgrounds and, if possible, unwanted objects from photos too. The two tasks have very different difficulty levels. Removing objects requires inpainting — plausibly reconstructing the erased area — a task that in 2026 is still the domain of heavy generative models, impractical to run in the browser with good quality on an average device. Removing a background, on the other hand, is a segmentation problem: separating a subject from a background, a task with much lighter models available, runnable entirely via WebAssembly with no server at all.
So I decided to start with background removal only, shelving the object-removal idea for now. The next question was: which library should run a segmentation model entirely client-side, while keeping the zero-cost model I already follow for the rest of the site?
La trappola della licenza AGPL
La prima libreria che sembrava perfetta per lo scopo si è rivelata distribuita con licenza AGPL, una licenza copyleft forte: gratuita da usare, ma con un vincolo importante per chi la incorpora in un servizio web pubblico non open source. In sintesi, l'AGPL può obbligare a rilasciare il codice sorgente completo del progetto che la incorpora, sotto la stessa licenza — un vincolo non da poco per un sito che non è open source. "Gratis per l'utente finale" e "sicuro da integrare in un prodotto commerciale closed-source" sono due cose diverse, ed è meglio scoprirlo prima di scrivere codice, non dopo il deploy.
Prima di integrare qualsiasi libreria IA "gratuita" in un progetto commerciale vale sempre la pena controllare la licenza esatta, non solo il prezzo. AGPL, GPL e altre licenze copyleft forti sono compatibili con l'uso personale o interno, ma rischiose per un prodotto pubblico closed-source.
Ho quindi cercato un'alternativa con licenza permissiva (Apache-2.0 o MIT), basata su Transformers.js — la libreria di Hugging Face per eseguire modelli di machine learning nel browser sopra ONNX Runtime Web — invece del wrapper proprietario scartato. Stesso principio (modello ONNX scaricato una volta dal CDN, poi cachato dal browser), licenza pulita.
The AGPL license trap
The first library that seemed perfect for the job turned out to be distributed under an AGPL license, a strong copyleft license: free to use, but with a significant catch for anyone embedding it in a public, non-open-source web service. In short, AGPL can require releasing the full source code of the project that embeds it, under the same license — a real constraint for a site that isn't open source. "Free for the end user" and "safe to integrate into a closed-source commercial product" are two different things, and it's better to find that out before writing code, not after deploying.
Before integrating any "free" AI library into a commercial project, it's always worth checking the exact license, not just the price. AGPL, GPL, and other strong copyleft licenses are fine for personal or internal use, but risky for a public closed-source product.
So I looked for a permissively licensed alternative (Apache-2.0 or MIT), built on Transformers.js — Hugging Face's library for running machine learning models in the browser on top of ONNX Runtime Web — instead of the proprietary wrapper I'd dropped. Same principle (an ONNX model downloaded once from a CDN, then cached by the browser), clean license.
Un modello CNN leggero, non un transformer pesante
Nella scelta del modello ho seguito una lezione utile per chiunque lavori con IA nel browser: l'architettura conta più della dimensione del file. Un modello basato su transformer, anche se qualitativamente ottimo e con licenza permissiva, può andare in crash per esaurimento di memoria durante l'inferenza WASM su molte macchine — l'attenzione dei transformer consuma memoria che cresce rapidamente col numero di "patch" dell'immagine, e alla risoluzione tipica di una foto i tensori intermedi diventano enormi. Un modello CNN più piccolo, con un'architettura pensata per la segmentazione (nel mio caso della famiglia IS-Net), gira invece in modo molto più affidabile in WASM.
Il modello scelto come default è quindi un modello CNN leggero con licenza Apache-2.0, che scarica pochi MB e produce in output un'immagine RGBA con un canale alpha di trasparenza.
| Livello | Scelta |
|---|---|
| Runtime | Transformers.js sopra ONNX Runtime Web (WASM) |
| Modello di default | CNN leggero, licenza Apache-2.0, ottimizzato per persone |
| Modello opzionale | Transformer più pesante, licenza MIT, più generico |
| Elaborazione | 100% locale, nessun upload dell'immagine |
| Cache | Browser + service worker, download una tantum |
A light CNN, not a heavy transformer
In choosing the model I followed a lesson useful for anyone working with AI in the browser: architecture matters more than file size. A transformer-based model, even a qualitatively excellent one with a permissive license, can crash from memory exhaustion during WASM inference on many machines — transformer attention consumes memory that grows quickly with the number of image "patches," and at typical photo resolution the intermediate tensors become huge. A smaller CNN model, with an architecture designed for segmentation (in my case from the IS-Net family), runs far more reliably in WASM instead.
The model chosen as the default is therefore a lightweight CNN with an Apache-2.0 license, downloading just a few MB and producing an RGBA image with a transparency alpha channel as output.
| Layer | Choice |
|---|---|
| Runtime | Transformers.js on top of ONNX Runtime Web (WASM) |
| Default model | Light CNN, Apache-2.0 license, optimized for people |
| Optional model | Heavier transformer, MIT license, more general-purpose |
| Processing | 100% local, no image upload |
| Cache | Browser + service worker, one-time download |
Il "matte grezzo": perché resta un alone sui bordi
Il modello lascia spesso un alone di colore residuo nell'area trasparente, perché l'output è una matte alpha morbida invece di una maschera binaria pulita: pixel di sfondo possono avere valori alpha residui del 5-15% invece di 0%. La soluzione non richiede un altro modello, ma un semplice post-processing della curva alpha: valori bassi forzati a trasparenza piena, valori alti forzati a opacità piena, con una transizione smoothstep nel mezzo per preservare i bordi morbidi (capelli, dettagli fini). Su un'immagine di test, questo approccio ha reso oltre il 60% dei pixel completamente trasparenti e oltre il 35% completamente opachi, lasciando solo una piccola frazione in una fascia intermedia — un risultato pulito.
The "raw matte": why a color halo remains at the edges
The model often leaves a residual color halo in the transparent area, because the output is a soft alpha matte rather than a clean binary mask: background pixels can carry residual alpha values of 5-15% instead of 0%. The fix doesn't require another model, just simple post-processing of the alpha curve: low values forced to full transparency, high values forced to full opacity, with a smoothstep transition in between to preserve soft edges (hair, fine detail). On a test image, this approach turned over 60% of pixels fully transparent and over 35% fully opaque, leaving only a small fraction in a mid-range band — a clean result.
Il limite reale: un modello specializzato non è un modello generico
I primi test con immagini reali hanno mostrato risultati deludenti su due casi: un'illustrazione piatta (dove il modello ha rimosso solo il margine attorno alla figura, lasciando tutto il disegno opaco) e una foto scattata in un ambiente poco illuminato e affollato (dove il modello ha praticamente cancellato anche il soggetto principale). Per isolare la causa, ho aggiunto temporaneamente un terzo pannello diagnostico che mostrava la maschera grezza del modello prima di qualsiasi pulizia alpha, confrontandola con il risultato finale.
Il confronto ha chiarito subito la causa: la maschera grezza e il risultato finale erano praticamente identici, quindi il problema non era nel mio post-processing, ma a monte, nel modello stesso. Verificando la scheda tecnica, il modello di default risultava addestrato specificamente su un dataset di segmentazione di persone — non un modello generico per qualsiasi soggetto. Su un ritratto ben illuminato e inquadrato da vicino il risultato è infatti pulito, capelli inclusi; su scene complesse o del tutto diverse dal dataset di addestramento (illustrazioni, foto scure, inquadrature affollate) la qualità scende in modo prevedibile.
Lezione generale: un modello "leggero e affidabile in WASM" spesso lo è proprio perché addestrato su un dominio ristretto. Prima di giudicare un modello "scarso", vale la pena controllare su cosa è stato effettivamente addestrato — il problema può essere di dominio, non di qualità.
The real limit: a specialized model isn't a general-purpose model
The first tests with real images showed disappointing results on two cases: a flat illustration (where the model only removed the margin around the figure, leaving the whole drawing opaque) and a photo taken in a dim, cluttered room (where the model nearly erased the main subject too). To isolate the cause, I temporarily added a third diagnostic panel showing the model's raw mask before any alpha cleanup, comparing it against the final result.
The comparison made the cause immediately clear: the raw mask and the final result were nearly identical, so the problem wasn't in my post-processing — it was upstream, in the model itself. Checking the model card confirmed it: the default model turned out to be trained specifically on a human segmentation dataset — not a general-purpose model for any subject. On a well-lit, close-up portrait the result is indeed clean, hair included; on complex scenes or ones far removed from the training dataset (illustrations, dark photos, cluttered framing), quality drops in a predictable way.
General lesson: a "light and WASM-reliable" model is often exactly that because it's trained on a narrow domain. Before judging a model "poor," it's worth checking what it was actually trained on — the problem may be domain mismatch, not quality.
Un modello superiore, opzionale, non di default
Invece di sostituire il modello leggero con uno più pesante e generico per tutti — pagando in tempo di caricamento anche chi ha un caso d'uso semplice — ho aggiunto un secondo modello, un transformer con licenza MIT più adatto a scene generiche, disponibile solo su richiesta: dopo il primo risultato compare un pulsante "Prova il modello superiore" che rielabora la stessa immagine con il modello più pesante, senza sovrascrivere il risultato precedente finché l'elaborazione non è completata con successo. Se il modello più pesante fallisce per memoria insufficiente, il primo risultato resta intatto e l'utente vede un messaggio d'errore chiaro invece di un tool rotto.
Questo compromesso — leggero di default, pesante su richiesta esplicita — evita di penalizzare la maggioranza degli utenti con un download importante che non useranno mai.
A superior model, optional, not default
Instead of replacing the light model with a heavier, more general one for everyone — making everyone pay the load-time cost even for a simple use case — I added a second model, an MIT-licensed transformer better suited to generic scenes, available only on request: after the first result, a "Try the superior model" button appears that reprocesses the same image with the heavier model, without overwriting the previous result until processing completes successfully. If the heavier model fails from insufficient memory, the first result stays intact and the user sees a clear error message instead of a broken tool.
This trade-off — light by default, heavy on explicit request — avoids penalizing most users with a large download they'll never use.
Il bug serio: il thread principale si blocca durante il calcolo
Passando al modello più pesante è emerso un problema molto più serio di una qualità mediocre: durante l'elaborazione l'intera pagina smetteva di rispondere — non solo il tool, ma anche i link della navigazione in alto. La causa è che, per default, il motore ONNX Runtime Web esegue il calcolo WASM sul thread principale del browser, lo stesso che gestisce clic, scroll e rendering: durante un'inferenza pesante quel thread resta occupato e tutta l'interfaccia si congela.
Il primo tentativo di correzione — attivare un'opzione interna della libreria pensata per delegare il calcolo a un thread separato — non ha risolto il problema, probabilmente per un timing di inizializzazione delicato. La soluzione affidabile è stata scrivere e controllare direttamente un vero Web Worker dedicato: un thread separato che carica la libreria, scarica il modello ed esegue tutta l'inferenza, comunicando con la pagina principale solo tramite messaggi (l'immagine in ingresso, il progresso, il risultato finale). In questo modo il thread principale non fa mai calcolo pesante per costruzione, non per una configurazione sperata.
Se un'elaborazione IA lato client blocca l'intera interfaccia e non solo il componente che la usa, il sospetto numero uno è che il calcolo giri sul thread principale. Un flag di configurazione può non bastare: un Web Worker dedicato, scritto esplicitamente, è la soluzione più affidabile.
The real bug: the main thread freezes during computation
Moving to the heavier model surfaced a much more serious problem than mediocre quality: during processing the entire page stopped responding — not just the tool, but the top navigation links too. The cause is that, by default, the ONNX Runtime Web engine runs WASM computation on the browser's main thread, the same one that handles clicks, scrolling, and rendering: during heavy inference that thread stays busy and the whole interface freezes.
The first fix attempt — enabling an internal library option meant to delegate computation to a separate thread — didn't solve the problem, likely due to delicate initialization timing. The reliable fix was to write and control a real dedicated Web Worker myself: a separate thread that loads the library, downloads the model, and runs all the inference, communicating with the main page only through messages (the input image, progress, the final result). This way the main thread never does heavy computation by construction, not by a configuration I hoped would hold.
If client-side AI processing freezes the whole interface and not just the component using it, the top suspect is computation running on the main thread. A configuration flag may not be enough: a dedicated, explicitly written Web Worker is the more reliable fix.
Gestire un'attesa reale di due minuti
Anche risolto il blocco dell'interfaccia, restava un problema di percezione: il modello più pesante impiega circa due minuti a elaborare un'immagine su hardware comune, e la libreria non espone un vero avanzamento percentuale durante il calcolo (solo durante il download del modello). Una barra "piena e ferma" per due minuti comunica un tool rotto, non un tool lento.
La soluzione finale combina due elementi: una barra che avanza in modo stimato su circa due minuti e mezzo, con il conteggio dei secondi trascorsi sotto, e — se l'elaborazione supera quel tempo stimato — un passaggio automatico a un'animazione indeterminata (una striscia che scorre continuamente), segnale universale di "sto ancora lavorando" senza fingere una percentuale che il modello non fornisce. Un timeout di sicurezza più ampio sblocca comunque l'interfaccia se qualcosa va davvero storto, permettendo di riprovare.
Handling a real two-minute wait
Even once the interface freeze was fixed, a perception problem remained: the heavier model takes about two minutes to process an image on common hardware, and the library doesn't expose real percentage progress during computation (only during the model download). A bar sitting "full and still" for two minutes signals a broken tool, not a slow one.
The final solution combines two elements: a bar that advances on an estimated curve over roughly two and a half minutes, with elapsed seconds shown below, and — if processing runs past that estimate — an automatic switch to an indeterminate animation (a continuously scrolling stripe), the universal signal for "still working" without faking a percentage the model doesn't provide. A wider safety timeout still unlocks the interface if something genuinely goes wrong, letting the person retry.
Cosa mi porto a casa da questo progetto
La lezione più utile non riguarda un singolo bug, ma un ordine di priorità: verificare la licenza prima della qualità, verificare il dominio di addestramento di un modello prima di giudicarne i risultati, e non fidarsi di un flag di libreria per una proprietà critica come "non bloccare il thread principale" — se è critico, va verificato con un test reale, non assunto. Gestire le aspettative in pagina (un avviso su quando il tool funziona meglio) si è rivelato utile quanto il codice stesso: un limite noto e comunicato chiaramente è molto meno frustrante di un risultato inspiegabile.
Se ti interessa il resto del cluster immagini/PDF costruito con lo stesso principio di elaborazione client-side, ne ho parlato anche a proposito della redazione reale dei PDF con PDF.js e pdf-lib. E se il tema è il costo reale di aggiungere l'IA a un tool gratuito, ho fatto i conti in dettaglio nell'articolo sul riassunto PDF con Gemini Flash-Lite.
What I take away from this project
The most useful lesson isn't about a single bug, but about a priority order: check the license before the quality, check a model's training domain before judging its results, and don't trust a library flag for a critical property like "doesn't block the main thread" — if it's critical, verify it with a real test, don't assume it. Managing expectations on the page (a notice about when the tool works best) turned out to be as useful as the code itself: a known, clearly communicated limitation is far less frustrating than an unexplained result.
If you're interested in the rest of the image/PDF cluster built on the same client-side processing principle, I also wrote about real PDF redaction with PDF.js and pdf-lib. And if the topic is the real cost of adding AI to a free tool, I ran the numbers in detail in the article on summarizing PDFs with Gemini Flash-Lite.
Domande frequenti
È sicuro rimuovere lo sfondo di una foto online senza caricarla su un server?
Sì, se lo strumento esegue il modello IA interamente nel browser tramite WebAssembly. Il modello viene scaricato una volta sola e poi cachato, ma l'immagine caricata non lascia mai il dispositivo: nessun upload verso alcun server.
Perché non tutte le librerie open source per l'IA nel browser sono utilizzabili in un progetto commerciale?
Alcune sono distribuite con licenze copyleft forti come la AGPL, che possono obbligare a rilasciare il codice sorgente completo del progetto che le incorpora. Licenze permissive come Apache-2.0 o MIT non impongono questo vincolo.
Perché un modello IA nel browser può bloccare l'intera pagina?
Per default il calcolo WASM avviene sul thread principale, lo stesso che gestisce clic e scroll. La soluzione affidabile è un Web Worker dedicato, un thread separato che lascia libera l'interfaccia per costruzione.
Perché il ritaglio dello sfondo a volte lascia un alone sui bordi?
Il modello produce una matte alpha morbida, non una maschera binaria netta. Un post-processing con soglie e transizione smoothstep elimina l'alone residuo senza bisogno di un modello più pesante.
Perché uno strumento di rimozione sfondo funziona bene su una foto e male su un'altra?
Molti modelli leggeri sono addestrati su un dominio specifico, ad esempio principalmente foto di persone. Su scene molto diverse dal dataset di addestramento la qualità scende in modo prevedibile, non per un bug.
Frequently asked questions
Is it safe to remove a photo's background online without uploading it to a server?
Yes, if the tool runs the AI model entirely in the browser via WebAssembly. The model is downloaded once and then cached, but the uploaded image never leaves the device: no upload to any server.
Why aren't all open source browser-AI libraries usable in a commercial project?
Some are distributed under strong copyleft licenses like AGPL, which can require releasing the full source code of the project embedding them. Permissive licenses like Apache-2.0 or MIT don't impose this constraint.
Why can a browser AI model freeze the entire page?
By default WASM computation runs on the main thread, the same one that handles clicks and scrolling. The reliable fix is a dedicated Web Worker, a separate thread that keeps the interface free by construction.
Why does background removal sometimes leave a color halo at the edges?
The model produces a soft alpha matte, not a clean binary mask. Post-processing with thresholds and a smoothstep transition removes the residual halo without needing a heavier model.
Why does a background removal tool work well on one photo and poorly on another?
Many light models are trained on a specific domain, often mostly photos of people. On scenes far removed from the training dataset, quality drops in a predictable way, not because of a bug.