{"slug": "i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python", "title": "I added AI background removal to my image converter in a week, in Rust, no Python", "summary": "A solo developer added AI background removal to Convertify, a free image converter, in one week using only Rust and no Python. The backend runs ONNX models natively via the ort crate, avoiding the need for a separate Python process. Challenges included handling a 171 MB model file and adapting to ort's non-Send+Sync errors and mutable session requirements.", "body_md": "Part of an ongoing build-in-public series on Convertify, a free image/file converter I build solo. This week: background removal. The honest version, with the walls I hit.\n\nMost \"remove image background\" tutorials end with `pip install rembg`\n\nand a happy screenshot. Mine started with a constraint: **my whole backend is Rust, and I did not want to bolt a Python process onto it just to run one model.**\n\nHere is how the week went. The good parts, and the three or four times I stared at a compiler error wondering if the constraint was worth it.\n\nConvertify is a free image converter. The backend is **Rust + Axum + libvips**, the model has to run **CPU-only on a modest VPS**, and there is no GPU anywhere in the budget. The obvious path for background removal is `rembg`\n\n, which is excellent, but it is Python and ships as a separate server process. Adding it would mean a second runtime, a second thing to deploy, a second thing to crash at 3am.\n\nSo the question for the week was simple: **can I run the same models rembg uses, but natively in Rust?**\n\nShort answer: yes. `rembg`\n\nis, under the hood, just ONNX models plus some image pre and post processing. The models (u2net, isnet, silueta) are all `.onnx`\n\nfiles. If I can run ONNX in Rust and do the image work in libvips (which I already have), there is no Python in the picture at all.\n\nThe pipeline for background removal is not magic, it is five boring steps:\n\nSteps 1, 4, 5 are libvips, which I already use everywhere. Step 3 is ONNX Runtime via the [ ort](https://github.com/pykeio/ort) crate. Step 2 is a tight Rust loop. No Python anywhere.\n\n```\n[dependencies]\nort = { version = \"=2.0.0-rc.12\", features = [\"download-binaries\"] }\n```\n\nThe `download-binaries`\n\nfeature pulls a CPU build of ONNX Runtime at build time, so there is nothing to install on the box. That alone deleted half the \"deploy a Python service\" anxiety.\n\nI grabbed `isnet-general-use.onnx`\n\nfrom the rembg releases, expecting ~44 MB. What landed was **171 MB**. My first thought was a broken download or an HTML error page renamed to `.onnx`\n\n. Quick check:\n\n```\nfile models/isnet-general-use.onnx\nhead -c 200 models/isnet-general-use.onnx | xxd | head\n```\n\nThe header showed a real `pytorch 1.13.1`\n\nsignature and tensor names like `input_image`\n\nand `conv_in.weight`\n\n. So it was a valid ONNX model, just heavier than the name suggested. Lesson: verify the file is actually what you think before you spend an hour debugging \"why is RAM so high.\"\n\n`ort`\n\nerrors are not `Send + Sync`\n\nFirst compile against `anyhow`\n\nand I get hit with this:\n\n```\nthe trait `Sync` is not implemented for `NonNull<OrtSessionOptions>`\nrequired for `anyhow::Error` to implement `From<ort::Error<SessionBuilder>>`\n```\n\n`anyhow::Error`\n\nwants `Send + Sync`\n\n. The `ort`\n\nerror type holds raw pointers into the ONNX Runtime C++ session, which are not `Sync`\n\n. So `?`\n\nstraight into `anyhow`\n\ndoes not compile.\n\nThe fix is to stringify the error at the boundary. `Display`\n\ngives you a `String`\n\n, and `String`\n\nis `Send + Sync`\n\n:\n\n``` js\nlet session = build(model_path, intra_threads)\n    .map_err(|e| anyhow!(\"ort session init: {e}\"))?;\n```\n\nThe pointer never leaves, only the message does. Once I understood *why*, the pattern was mechanical: every `ort`\n\n`?`\n\nthat crosses into `anyhow`\n\ngets a `.map_err(|e| anyhow!(\"...: {e}\"))?`\n\n.\n\n`run`\n\ntakes `&mut self`\n\nThis one actually changed my architecture. In this `ort`\n\nversion, `Session::run`\n\ntakes `&mut self`\n\n. I had the session behind an `Arc`\n\nin my Axum app state so it could be shared. You cannot get `&mut`\n\nthrough an `Arc`\n\n.\n\n```\ncannot borrow `self.session` as mutable, as it is behind a `&` reference\n```\n\nOptions were a session pool, or a `Mutex`\n\n. Since my traffic is low and I gate inference to one at a time anyway, I wrapped the session in a `Mutex`\n\n:\n\n```\npub struct BgRemover {\n    session: Mutex<Session>,\n}\n```\n\n`remove(&self)`\n\nstays `&self`\n\n, so `Arc<BgRemover>`\n\nstill works in app state. The `Mutex`\n\nhands out the `&mut`\n\nfor the single inference call. With a one-permit semaphore in front, the mutex never even contends. When traffic grows, the upgrade path is a pool of sessions, but that is a future-me problem.\n\n`*mut VipsImage`\n\nis not `Send`\n\nlibvips image pointers are not `Send`\n\n, which means they cannot be held across an `.await`\n\n. If I ran inference directly in the async handler, the borrow checker would stop me, and even if it did not, a multi-second CPU inference on an async worker thread would freeze the whole runtime.\n\nThe answer is `spawn_blocking`\n\n. The entire libvips + inference chain runs on a dedicated blocking thread and returns finished PNG bytes (which *are* `Send`\n\n):\n\n``` js\nlet png = tokio::task::spawn_blocking(move || {\n    let _permit = permit;       // hold the semaphore for the whole job\n    remover.remove(&bytes)\n}).await??;\n```\n\nEvery `VipsImage`\n\nis created and dropped inside that closure, never crossing an await point. The async runtime stays free to serve everything else while one image is being cut out.\n\nBecause the handler returns the PNG straight in the HTTP response, **the image is never written to disk**. It comes in as multipart bytes, gets processed in memory, and the result streams back. Nothing is stored, nothing is queued, nothing to clean up.\n\nI did not plan that as a feature, it fell out of the architecture. But \"your photo is processed in memory and never saved\" is a genuinely strong thing to be able to say, and it is true, not marketing.\n\nYes. First real test through Postman with a HEIC photo: **200 OK, transparent PNG out.** The model is ISNet (the IS-Net dichotomous segmentation architecture), and on clean subjects, products, people, logos, the cutout is sharp.\n\n`rembg`\n\nis \"just\" ONNX + image ops. If you already have an image library, you can skip the Python entirely with `ort`\n\n.`ort`\n\n2.0 API churns between rc versions. Pin the exact version and expect to fix one or two method names.`spawn_blocking`\n\nis not optional for CPU-heavy, non-`Send`\n\nwork. It is the whole reason the server stays responsive.If you want to see the result, background removal is live and free (no signup, no watermark) on [Convertify](https://convertifyapp.net/remove-background). Upload a photo, get a transparent PNG. It runs the exact pipeline above.\n\nNext week: turning one tool into a set of use-case pages (passport photos, product shots) without drowning in duplicate content. That one is more SEO than Rust, but the build-in-public log continues.\n\nWhat would you have done differently on the `&mut self`\n\nsession problem? A pool, a mutex, something smarter? Curious how others handle shared ONNX sessions under load.", "url": "https://wpnews.pro/news/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python", "canonical_source": "https://dev.to/serhii_kalyna_730b636889c/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python-2d75", "published_at": "2026-06-29 08:10:04+00:00", "updated_at": "2026-06-29 08:27:30.060586+00:00", "lang": "en", "topics": ["artificial-intelligence", "computer-vision", "developer-tools"], "entities": ["Convertify", "Rust", "Axum", "libvips", "ONNX", "ort", "rembg", "u2net"], "alternates": {"html": "https://wpnews.pro/news/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python", "markdown": "https://wpnews.pro/news/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python.md", "text": "https://wpnews.pro/news/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python.txt", "jsonld": "https://wpnews.pro/news/i-added-ai-background-removal-to-my-image-converter-in-a-week-in-rust-no-python.jsonld"}}