Raju Shingadiya Raju Shingadiya
All posts

DropBG: cutting out backgrounds without sending anyone your photos

Every background remover wants your image on their server and your card on file. This one is 42 MB, runs on the phone in about a second, and costs nothing.

A subject cleanly cut out from its background by DropBG
RMBG-1.4 output, generated on-device in roughly a second

Background removal is one of those tasks that got quietly good a couple of years ago and then got quietly monetised. The tools that work well are web services: upload, wait, get a watermarked preview, pay to download it at full resolution. The free ones cap you at a few images a day.

None of that is a technical necessity. The models that do this are small. RMBG-1.4 quantised to ONNX is 42 MB — smaller than most games' splash screen assets. It runs on a phone CPU in about a second. The upload step exists because it's where the business model lives, not because the phone can't do it.

So DropBG does it on the phone.

Original photograph with its background intact
Before
The same photograph with the background removed to transparency
After

The numbers

MetricValue
Model (quantised ONNX)42 MB
Model load, cold~1–3 s
Inference at 1024×1024~1–2 s
APK (debug)88 MB
MinimumAndroid 8.0, API 26

That's on a mid-range arm64 device. Compared to Footprints — my 507 MB language model experiment — this one is almost polite.

How it works

The model is a segmentation network. It takes an image and returns a single-channel mask: for every pixel, how confident it is that the pixel belongs to the foreground. Everything else is plumbing.

BackgroundRemover.kt does this:

  1. Decode the picked image, then fix its EXIF orientation (more on that below).
  2. Resize to 1024×1024 and normalise each channel to roughly -0.5..0.5, laid out as a planar 1×3×1024×1024 tensor.
  3. Run it through ONNX Runtime with 4 intra-op threads.
  4. Normalise the raw output to 0..255 and build a greyscale mask bitmap.
  5. Scale the mask back up to the photo's real dimensions.
  6. Draw the original, then composite the mask with PorterDuff.Mode.DST_IN.

That last step is the neat one and it's pure Android, no ML involved. DST_IN keeps the destination pixels only where the source has alpha — so drawing a greyscale mask over the photo in that mode punches the background out to transparency in a single hardware-accelerated canvas operation. No per-pixel alpha loop. The graphics stack has done this since forever; it just isn't the first tool people reach for.

Why the mask is normalised rather than thresholded. The model's raw output isn't bounded, so the engine finds the min and max and rescales into 0..255. That keeps the intermediate values instead of forcing every pixel to fully-in or fully-out — which is what gives you soft edges on hair and fur rather than a cardboard cutout.

EXIF orientation, the bug that gets everyone

Phone cameras frequently store the sensor image unrotated and record the intended rotation in an EXIF tag. BitmapFactory.decodeStream ignores that tag entirely.

So without correction, a portrait photo arrives sideways. The model then does its job perfectly on a sideways image, and you get a beautifully segmented result that's rotated 90 degrees. It looks like a model failure and it isn't one.

correctOrientation() reads the tag and applies a Matrix before anything else touches the bitmap — all eight cases, including the flips and transposes nobody remembers exist. Boring code, and the difference between a working app and a confusing one.

The app

DropBG idle screen prompting for an image
Pick an image
DropBG result screen showing the cutout
Result, with before/after toggle
DropBG history of previously processed images
History

A sealed class UiState drives everything — Idle, ImageSelected, Processing, Done, Error — which means the UI can't get into a state the ViewModel didn't intend. For an app whose core action is a slow async operation that can fail, making the states explicit and exhaustive was worth more than any other structural decision.

The before/after toggle is instant because both bitmaps are already in memory. Save writes to the gallery, share opens the system sheet. That's it.

What it's bad at

Fine hair against a busy background still produces halos. Semi-transparent objects — glass, smoke, veils — confuse it, because the model outputs one confidence value per pixel and genuine transparency isn't really representable that way. Very small subjects lose detail to the 1024×1024 downscale.

The cloud tools are better at these edge cases, and they're better because they run much larger models on much larger hardware. I'm not going to pretend otherwise. What I'll claim is that for the ordinary case — a person, a product, a pet, reasonably lit — the difference is small enough that the privacy and the zero cost win comfortably.

The wider point

DropBG took a weekend, and most of that was UI. The model already existed, ONNX Runtime already runs on Android, and the compositing is a stock canvas call.

That's the part worth noticing. A whole category of paid web service is, technically, a 42 MB file and a few hundred lines of Kotlin. The infrastructure isn't the hard part any more — it hasn't been for a while.


← All posts