Problem
Design teams manually trace and remove backgrounds from product images using Photoshop or Figma tools. For catalogs with hundreds of images, this takes days of tedious work. Cloud APIs (remove.bg, Canva) charge per image and require uploading potentially sensitive content. You need a fast, free, local solution that runs on CPU with no API keys.
Solution
Single-file Python script using PEP 723 inline dependencies:
# /// script
# requires-python = ">=3.10"
# dependencies = ["rembg[cpu]", "Pillow"]
# ///
"""Batch background removal using rembg (HuggingFace U2-Net model)."""
import argparse
import sys
from multiprocessing import Pool, cpu_count
from pathlib import Path
from PIL import Image
from rembg import remove, new_session
SUPPORTED = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
def process_image(args: tuple[Path, Path, str]) -> str:
input_path, output_dir, model = args
try:
session = new_session(model)
img = Image.open(input_path)
result = remove(img, session=session)
out_path = output_dir / f"{input_path.stem}.png"
result.save(out_path)
return f"OK: {input_path.name}"
except Exception as e:
return f"FAIL: {input_path.name} ({e})"
def main():
parser = argparse.ArgumentParser(description="Batch background removal")
parser.add_argument("input_dir", type=Path, help="Directory of input images")
parser.add_argument("output_dir", type=Path, help="Directory for output PNGs")
parser.add_argument("--model", default="u2net", choices=["u2net", "u2netp", "isnet-general-use"],
help="Model to use (u2netp is faster, isnet-general-use is higher quality)")
parser.add_argument("--workers", type=int, default=max(1, cpu_count() - 1),
help="Number of parallel workers")
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
files = [f for f in args.input_dir.iterdir() if f.suffix.lower() in SUPPORTED]
if not files:
print(f"No supported images found in {args.input_dir}")
sys.exit(1)
print(f"Processing {len(files)} images with {args.workers} workers using {args.model}...")
tasks = [(f, args.output_dir, args.model) for f in files]
with Pool(args.workers) as pool:
for result in pool.imap_unordered(process_image, tasks):
print(result)
print("Done!")
if __name__ == "__main__":
main()
Run it:
# With uv (reads inline dependencies automatically)
uv run rembg_batch.py ./product-photos ./transparent-output
# Faster model for quick previews
uv run rembg_batch.py ./photos ./output --model u2netp --workers 8
# Higher quality model for final output
uv run rembg_batch.py ./photos ./output --model isnet-general-use
The first run downloads a 170MB model from HuggingFace. Subsequent runs use the cached model.
Why It Works
rembg wraps HuggingFace's U2-Net segmentation model, which runs inference entirely on the local CPU with no API calls or GPU required. The multiprocessing pool saturates all available CPU cores, processing hundreds of images in minutes rather than days. PEP 723 inline script metadata means no virtual environment setup -- uv run handles dependency installation automatically.
Context
- Model options:
u2net(balanced),u2netp(2x faster, slightly lower quality),isnet-general-use(best quality for complex edges) - For images with faces, chain this with the macOS Vision framework face detection for crop-then-remove workflows
- Output is always PNG with alpha transparency -- convert to WebP with Pillow if file size matters
- On Apple Silicon Macs, expect roughly 2-4 seconds per image on CPU with
u2net - Add
--alpha-mattingflag in rembg for higher quality edges on hair and fur