Command-line tool that removes image backgrounds with rembg. Runs one image or a whole batch, locally or as a Cloud Run Job reading/writing a GCS bucket. Handles full-body subjects plus fine detail (hair, jewellery, thin props).
input (JPEG/PNG, e.g. 15MP)
│
├─ load + convert to RGB (Pillow)
├─ downscale a COPY to --max-edge (default 2048px) (Pillow LANCZOS)
├─ run the segmentation model on the small copy (rembg → ONNX Runtime)
│ → alpha mask (soft 0–255 matte)
├─ upscale mask back to the ORIGINAL resolution (Pillow LANCZOS)
├─ apply mask as alpha to the full-res RGB original
├─ optionally flatten onto a solid background (--bg)
└─ save (format chosen by output extension)
output (full-res PNG w/ transparency, or flattened JPG/WebP)
Why downscale-then-upscale: the model runs on the small copy (bounded memory / speed), but the mask is scaled back up and applied to the untouched original, so output keeps full resolution. Heavy models (birefnet) resize internally to ~1024px anyway, so their RAM is model-bound; the cap mainly protects lighter models on very large inputs.
| Layer | Choice | Why |
|---|---|---|
| Language | Python 3.11–3.13 | onnxruntime has no 3.14 wheels yet |
| CLI | click |
subcommands, typed options |
| Segmentation | rembg[cpu] → onnxruntime |
pretrained U²-Net / IS-Net / BiRefNet ONNX models, CPU inference |
| Imaging | Pillow |
load, resize, mask apply, encode |
| Batch | concurrent.futures |
in-process (1 worker) or process pool (N workers) |
| Container | python:3.11-slim + Docker |
model pre-baked into the image |
| Cloud | Cloud Run Jobs + GCS via gcsfuse | one-shot batch, no HTTP server needed |
| model | peak RAM | speed¹ | notes |
|---|---|---|---|
birefnet-general (default) |
~4.6 GB | ~45s | best full-body + fine detail; MIT; commercial-safe |
u2net_human_seg |
~0.8 GB | ~6s | light, good body integrity, weaker on thin props |
isnet-general-use |
~1.2 GB | ~10s | light, good detail, tends to crop full-body poses |
u2netp |
small | fastest | low quality; quick previews |
silueta |
small | fast | generic silhouette |
birefnet-portrait |
~4.6 GB | ~45s | head/shoulders framing, not full body |
¹ single 15MP image, 4 CPU threads. Not bundled: bria-rmbg — excellent for commercial
cutouts but CC BY-NC-SA (non-commercial) unless you license it from BRIA.
python3.13 -m venv .venv
.venv/bin/pip install -e ".[dev]"pip install -e (editable install) generates src/removebg.egg-info/ — metadata pip
uses to resolve the package (name, version, entry points, dependency list) without
copying files into site-packages. It's a build artifact, safe to delete/regenerate
(rm -rf src/removebg.egg-info && pip install -e .), and belongs in .gitignore, not
version control.
# single image — output format follows the -o extension
removebg single photo.jpg -o cutout.png # transparent PNG
removebg single photo.jpg -o cutout.jpg --bg white # flattened white-bg JPG
# batch a folder
removebg batch ./in -o ./out --workers 4 --recursive
removebg batch ./in -o ./out --ext .jpg --bg white # all outputs as white-bg JPGKey options (both commands):
--model— see table above (defaultbirefnet-general).--max-edge N— cap long edge fed to the model (default 2048;0= full-res inference).--bg—transparent(default),white,black, or#RRGGBB. JPG/BMP can't hold transparency, so they're auto-flattened onto white if--bgis unset — use--bgto pick a colour. This is the fix if a downstream app "can't open" a transparent PNG.--alpha-matting— extra edge refinement for hair/filaments (slower).
Batch concurrency: --workers 1 runs in-process (one model copy, clean errors). Each
extra worker loads its own copy of the model, so keep workers × model_RAM ≤ RAM. For
birefnet (~4.6 GB/worker) stay at 1 unless you have lots of memory.
The commands below use shell variables so nothing environment-specific is hard-coded. Set them once for your own GCP setup:
export GCP_PROJECT_ID=your-project-id
export GCP_PROJECT_NUMBER=000000000000 # gcloud projects describe $GCP_PROJECT_ID --format='value(projectNumber)'
export REGION=us-central1
export BUCKET=your-bucket-name # inputs in in/, outputs in out/
export IMAGE=${REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/removebg/removebgyou ──gcloud storage cp──▶ gs://${BUCKET}/in/*.jpg
│
gcloud run jobs execute ──▶ removebg-job (Cloud Run Job)
│ gcsfuse mounts gs://${BUCKET} at /data
│ removebg batch /data/in -o /data/out
▼
you ◀─gcloud storage cp── gs://${BUCKET}/out/*.png
# Artifact Registry repo (legacy gcr.io createOnPush is disabled)
gcloud artifacts repositories create removebg \
--repository-format=docker --location=${REGION}
# build + push (Dockerfile pre-bakes the birefnet model → no cold-start download)
gcloud builds submit --tag ${IMAGE}
# bucket
gcloud storage buckets create gs://${BUCKET} --location=${REGION}
# grant the job's service account bucket access (else the gcsfuse mount fails)
gcloud storage buckets add-iam-policy-binding gs://${BUCKET} \
--member="serviceAccount:${GCP_PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
# job — bucket mounted at /data via gcsfuse (no code change needed)
gcloud run jobs create removebg-job \
--image ${IMAGE} \
--region ${REGION} \
--memory 16Gi --cpu 4 \
--execution-environment gen2 \
--task-timeout 3600 --max-retries 1 \
--set-env-vars OMP_NUM_THREADS=4 \
--add-volume name=data,type=cloud-storage,bucket=${BUCKET} \
--add-volume-mount volume=data,mount-path=/data \
--args="batch,/data/in,-o,/data/out,--workers=1"The job runs as the project's default compute service account
(${GCP_PROJECT_NUMBER}-compute@developer.gserviceaccount.com) unless you pass
--service-account. Whichever account it uses needs roles/storage.objectAdmin on the
bucket (granted above).
# 1. upload inputs
gcloud storage cp ./myphotos/* gs://${BUCKET}/in/
# 2. run
gcloud run jobs execute removebg-job --region ${REGION} --wait
# 3. download results
gcloud storage cp -r gs://${BUCKET}/out/* ./results/gcloud builds submit --tag ${IMAGE}
# jobs pick up :latest on the next execution; no re-create needed--execution-environment gen2is required for gcsfuse volume mounts.- The job's service account needs
storage.objectAdminon the bucket, or the mount fails with astorage.objects.listPermissionDenied and the container exits 255 before doing any work. OMP_NUM_THREADS=4(match--cpu): birefnet peaks lower on RAM at 4 threads than at 1, and runs ~40% faster.- Memory is model-bound. birefnet needs ~5 GB resident regardless of input size; 8Gi
is too tight once Linux ONNX Runtime + gcsfuse + the process overhead stack up → OOM,
which surfaces as an opaque
BrokenProcessPool. 16Gi has margin (can trim to ~12Gi).
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="removebg-job"' \
--project=${GCP_PROJECT_ID} --limit=100 --order=asc --format="value(textPayload)" \
| grep -iE "error|traceback|permission denied|killed"removebg/
├── src/removebg/
│ ├── cli.py # click commands: single, batch
│ ├── core.py # per-image pipeline: downscale → infer → mask → flatten → save
│ ├── batch.py # folder walk, in-process or ProcessPool fan-out
│ └── models.py # model registry + cached ONNX sessions
├── tests/ # pytest
├── Dockerfile # slim image, model pre-baked, OMP_NUM_THREADS=4
└── pyproject.toml
.venv/bin/pytest -q