Quitar el fondo de imágenes por lotes con Python: rembg o una API

Dos scripts completos para quitar el fondo de una carpeta entera de imágenes: uno con la biblioteca de código abierto rembg en tu propio hardware y otro con una API HTTP que gestiona bien los reintentos, los límites de frecuencia y los cobros duplicados.

Actualizado el 23 de octubre de 2026 · 9 min de lectura

Tienes una carpeta con unos cuantos cientos de fotos de producto, retratos o imágenes de anuncios, y a todas hay que quitarles el fondo. Hacerlo a mano en un editor es un día entero de clics. En Python es un bucle, y la decisión de verdad es qué se ejecuta dentro del bucle: un modelo de código abierto en tu propia máquina o una API HTTP.

Esta guía construye las dos opciones, con scripts completos que puedes ejecutar hoy sobre una carpeta, y cubre lo que hace fiable un proceso por lotes: elección del modelo, CPU frente a GPU, reintentos, límites de frecuencia, idempotencia y reanudar tras un fallo.

¿Opción A u opción B?

Modelo local (rembg)API HTTP
CosteSoftware gratuito; pagas en hardware y en tiempoPor imagen
InstalaciónPython 3.11 a 3.13, descarga de modelos, CUDA opcionalUna clave de API y requests
VelocidadDepende de tu CPU o GPU y del modeloDepende del servicio, peticiones en paralelo
CalidadDepende del modelo que elijasDepende del servicio
PrivacidadLas imágenes nunca salen de tu máquinaLas imágenes se suben a un tercero
MantenimientoTú gestionas modelos, drivers y licenciasLo hace el proveedor
Ideal paraTrabajo sin conexión, imágenes sensibles, experimentosPipelines de producción, servidores sin GPU

Muchos equipos usan las dos: un modelo local para experimentos y herramientas internas, y una API en producción, donde importan más una calidad predecible y no tener servidores con GPU.

Opción A: rembg, la vía de código abierto

rembg es una biblioteca y CLI de Python con licencia MIT que integra varios modelos para quitar el fondo y los ejecuta con ONNX Runtime. En el momento de escribir esto, la versión actual es la 2.0.86, y requiere Python 3.11 o posterior y anterior a 3.14.

Instalación

Elige el backend que corresponda a tu hardware. La versión para CPU funciona en todas partes:

python -m venv .venv && source .venv/bin/activate
pip install "rembg[cpu]"        # library only
pip install "rembg[cpu,cli]"    # library plus the rembg command

Para GPU NVIDIA, instala rembg[gpu] en su lugar. El proyecto indica que esto necesita onnxruntime-gpu más una versión compatible de CUDA y cuDNN, y sugiere volver a la versión para CPU si no puedes instalarlos. Las GPU AMD en Linux usan rembg[rocm] con onnxruntime-rocm.

rembg elige automáticamente el proveedor de ejecución CUDA (o ROCm) cuando ONNX Runtime lo indica como disponible, y avisa si recurre a la CPU. Para ver qué detecta ONNX Runtime:

import onnxruntime
print(onnxruntime.get_available_providers())  # look for CUDAExecutionProvider

Elige un modelo

Los modelos se descargan la primera vez que se usan en ~/.rembg/models/ (define REMBG_HOME para cambiarlo). Algunos útiles de la lista del proyecto:

ModeloNotas
bria-rmbgEl predeterminado. RMBG-2.0 de BRIA, unos 1,02 GB, trabaja a 1024 x 1024. Su licencia exige un acuerdo de pago para uso comercial.
birefnet-generalModelo BiRefNet de uso general; buenos bordes suaves
birefnet-general-liteBiRefNet más ligero para uso general
birefnet-portraitAjustado para personas
isnet-general-useModelo IS-Net de uso general
u2netEl modelo general clásico; más rápido, bordes más toscos
u2netpu2net ligero; pruebas rápidas en máquinas lentas
siluetaIgual que u2net, reducido a 43 MB

Hay dos puntos que importan más de lo que parece. Primero, el modelo predeterminado no es gratuito para uso comercial: los pesos de los modelos tienen sus propias licencias, distintas de la licencia MIT de rembg, así que revisa el origen del modelo que vayas a usar en producción. Segundo, los modelos más nuevos (bria-rmbg, birefnet-*, isnet-general-use) dan bordes suaves y bien formados, mientras que la familia u2net, más antigua, tiende a dar bordes más duros y toscos.

Opciones de calidad de bordes

rembg ofrece tres formas de tratar los píxeles de borde suaves, como el pelo humano o el de los animales:

  • Predeterminado: suficiente para sujetos de bordes nítidos, como productos, coches y logotipos.
  • decontaminate=True (-dc en la CLI): elimina el halo de color que el fondo antiguo deja en los píxeles del borde. Solo cambia el color, nunca la forma de la máscara, apenas cuesta nada y se puede dejar activado sin riesgo en todo un lote.
  • alpha_matting=True (-a): vuelve a calcular el propio borde cuando la forma de la máscara es incorrecta, por ejemplo con mechones cortados. Es mucho más lento y puede no converger, en cuyo caso rembg recurre a un recorte descontaminado.

Para lotes desatendidos, el proyecto recomienda -dc en lugar de -a porque es más rápido y no puede fallar. La guía sobre el canal alfa explica por qué aparecen los halos.

La vía rápida: la CLI

rembg p -m birefnet-general -dc photos/ cutouts/

rembg p procesa una carpeta entera; añade -w para vigilar la carpeta y procesar los archivos nuevos según aparecen.

El script de Python

Esta versión carga el modelo una sola vez, se salta los archivos ya hechos para que puedas pararlo y reiniciarlo, y sigue adelante cuando falla un archivo:

import sys
from pathlib import Path

from rembg import new_session, remove

EXTS = {".jpg", ".jpeg", ".png", ".webp"}


def main(in_dir: str, out_dir: str, model: str = "birefnet-general") -> None:
    src_dir, dst_dir = Path(in_dir), Path(out_dir)
    dst_dir.mkdir(parents=True, exist_ok=True)
    session = new_session(model)  # load the model once, reuse it for every image

    files = sorted(p for p in src_dir.iterdir() if p.suffix.lower() in EXTS)
    for i, src in enumerate(files, 1):
        dst = dst_dir / f"{src.stem}.png"
        if dst.exists():
            continue  # resume where a previous run stopped
        try:
            out = remove(src.read_bytes(), session=session, decontaminate=True)
            dst.write_bytes(out)
            print(f"[{i}/{len(files)}] ok      {src.name}")
        except Exception as e:  # one bad file shouldn't stop the batch
            print(f"[{i}/{len(files)}] failed  {src.name}: {e}")


if __name__ == "__main__":
    main(*sys.argv[1:])

Ejecútalo con python batch_rembg.py photos cutouts birefnet-general. Crear la sesión una sola vez es importante: cargar un modelo tarda mucho más que ejecutarlo, y el README de rembg recomienda reutilizar la sesión para el rendimiento en lotes. Las salidas siempre son PNG porque el resultado tiene canal alfa.

Dónde se queda corto rembg

  • La primera ejecución es lenta mientras se descarga el modelo, y los modelos grandes necesitan mucha RAM o VRAM.
  • El rendimiento en CPU es modesto con los modelos de alta calidad. Prueba con 20 imágenes representativas antes de lanzarte con mil.
  • Las licencias y los drivers son cosa tuya. Fija las versiones de rembg y del modelo para que una actualización posterior no cambie los resultados sin que te des cuenta.

Opción B: una API HTTP

Con una API, el trabajo pesado se hace en las GPU de otro, y tu script se dedica sobre todo a gestionar bien la red. El ejemplo usa la API de removebackground.dev porque su contrato está publicado como OpenAPI 3.1 en /openapi.json; los mismos patrones sirven para cualquier servicio parecido.

El contrato

  • POST https://removebackground.dev/v1/remove con tu clave en la cabecera x-api-key.
  • Envía la imagen como campo multipart image_file (JPG, PNG o WebP, hasta 25 MB), o como JSON con image_url.
  • Los campos opcionales incluyen format (png, jpg o webp), bg_color, size (preview, medium, hd, full) y crop.
  • 200 significa que el cuerpo es la imagen terminada. 202 significa que sigue procesándose tras unos 55 segundos: el cuerpo es un JSON con un id de trabajo, y consultas GET /v1/jobs/{id}/result, que responde 409 not_ready con Retry-After hasta que la imagen está lista. Nunca guardes el cuerpo de un 202 como imagen.
  • Hasta 60 peticiones de procesado por minuto por cuenta; por encima recibes 429 rate_limited con Retry-After.
  • Una cabecera Idempotency-Key hace seguros los reintentos: la misma clave con la misma petición devuelve el trabajo original en vez de cobrar otra vez. La misma clave con una petición distinta responde 409 idempotency_key_reused.
  • Los resultados se borran a las 48 horas, así que guárdalos cuando los recibas.

Reglas de reintento para no pagar dos veces

RespuestaQué hacer
200Guarda la imagen
202Consulta el resultado del trabajo; no vuelvas a subirla
400, 401, 402, 413, 422Corrige la petición, la clave, los créditos o la imagen; no repitas en bucle
429Espera Retry-After segundos y reenvía con la misma clave
500, 503, timeout, conexión cortadaEspera un poco más cada vez y reenvía con la misma clave
502Esa imagen falló y no se cobró; regístralo

El truco es que la clave de idempotencia sea determinista: un hash de los bytes de la imagen más las opciones. Si el script se cae y lo vuelves a ejecutar, cada reenvío lleva la misma clave, así que nada se procesa ni se cobra dos veces.

El script de Python

import hashlib
import mimetypes
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

import requests

API = os.environ.get("REMOVEBG_API", "https://removebackground.dev")
KEY = os.environ["REMOVEBG_API_KEY"]  # keep the key out of your code
OPTIONS = {"format": "png", "size": "full"}  # or {"format": "jpg", "bg_color": "ffffff"}
WORKERS = 4  # the API allows 60 processing requests a minute
EXTS = {".jpg", ".jpeg", ".png", ".webp"}
RETRYABLE = {429, 500, 503}


def idempotency_key(data: bytes) -> str:
    # Same image + same options = same key, so a rerun never pays twice.
    h = hashlib.sha256(data)
    h.update(repr(sorted(OPTIONS.items())).encode())
    return h.hexdigest()


def wait(res, default=2.0):
    try:
        return float(res.headers.get("Retry-After", default))
    except ValueError:
        return default


def save(res, dst: Path) -> Path:
    if res.status_code != 200 or not res.headers.get("Content-Type", "").startswith("image/"):
        raise RuntimeError(f"HTTP {res.status_code}: {res.text[:300]}")
    dst.write_bytes(res.content)
    return dst


def process(src: Path, dst: Path) -> Path:
    data = src.read_bytes()
    mime = mimetypes.guess_type(src.name)[0] or "application/octet-stream"
    headers = {"x-api-key": KEY, "Idempotency-Key": idempotency_key(data)}

    for attempt in range(6):
        try:
            res = requests.post(
                f"{API}/v1/remove",
                files={"image_file": (src.name, data, mime)},
                data=OPTIONS,
                headers=headers,
                timeout=(10, 120),
            )
        except (requests.ConnectionError, requests.Timeout):
            time.sleep(2 ** attempt)  # same key: safe to resend
            continue
        if res.status_code in RETRYABLE:
            time.sleep(wait(res, 2 ** attempt))
            continue
        break
    else:
        raise RuntimeError("gave up after retries")

    if res.status_code != 202:
        return save(res, dst)  # 200 is the image; 4xx and 502 raise

    # 202: still processing. Poll the same job; never upload the image again.
    job_id = res.json()["id"]
    deadline = time.monotonic() + 600
    while time.monotonic() < deadline:
        time.sleep(wait(res))
        try:
            res = requests.get(f"{API}/v1/jobs/{job_id}/result", headers=headers, timeout=30)
        except (requests.ConnectionError, requests.Timeout):
            continue  # reading a job is free and safe to repeat
        if res.status_code == 409 and res.json()["error"]["code"] == "not_ready":
            continue
        if res.status_code >= 500:
            continue
        return save(res, dst)
    raise TimeoutError(f"job {job_id} still running; fetch it later")


def main(in_dir: str, out_dir: str) -> None:
    src_dir, dst_dir = Path(in_dir), Path(out_dir)
    dst_dir.mkdir(parents=True, exist_ok=True)
    ext = ".jpg" if OPTIONS.get("format") == "jpg" else "." + OPTIONS.get("format", "png")
    todo = [
        (p, dst_dir / (p.stem + ext))
        for p in sorted(src_dir.iterdir())
        if p.suffix.lower() in EXTS and not (dst_dir / (p.stem + ext)).exists()
    ]
    print(f"{len(todo)} images to process")

    failed = []
    with ThreadPoolExecutor(max_workers=WORKERS) as pool:
        jobs = {pool.submit(process, s, d): s for s, d in todo}
        for fut in as_completed(jobs):
            src = jobs[fut]
            try:
                print("ok    ", src.name, "->", fut.result().name)
            except Exception as e:
                failed.append(src.name)
                print("failed", src.name, e)
    print(f"done: {len(todo) - len(failed)} ok, {len(failed)} failed")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Ejecútalo con REMOVEBG_API_KEY=... python batch_api.py photos cutouts. Cuatro workers es un comienzo prudente; si las respuestas rápidas te hacen superar el límite por minuto, la gestión del 429 espera y reenvía con la misma clave. Para enviar URL en lugar de archivos, envía JSON:

res = requests.post(
    f"{API}/v1/remove",
    json={"image_url": "https://example.com/shoe.jpg", "format": "png"},
    headers={"x-api-key": KEY, "Idempotency-Key": "shoe-front-v1"},
    timeout=(10, 120),
)

Para listas largas de URL, la misma API tiene POST /v1/batch, que pone en cola hasta 100 URL de imágenes como un solo trabajo que luego consultas.

Cómo hacer robusto cualquier proceso por lotes

  • Sáltate las salidas ya terminadas para que una nueva ejecución continúe en lugar de empezar de cero. Los dos scripts lo hacen.
  • Cuidado con los nombres repetidos. shoe.jpg y shoe.png en la misma carpeta acaban los dos en shoe.png; renombra las entradas o incluye la extensión original en el nombre de salida.
  • Registra los fallos en un archivo y vuelve a ejecutar solo esos.
  • Revisa una muestra al 100% de zoom sobre fondos oscuros y claros antes de procesarlo todo.
  • No toques los originales. Escribe en una carpeta de salida aparte.
  • Elige el formato de salida según el destino. PNG o WebP para transparencia, JPG sobre blanco para marketplaces. Consulta PNG vs JPEG vs WebP.

Preguntas

¿rembg es gratuito para uso comercial?

rembg en sí tiene licencia MIT, pero cada modelo tiene su propia licencia. El modelo predeterminado, RMBG-2.0 de BRIA (bria-rmbg), exige un acuerdo de pago para uso comercial, así que revisa la licencia del modelo que uses o elige otro.

¿rembg usa mi GPU automáticamente?

Sí, si instalas rembg[gpu] y ONNX Runtime puede cargar el proveedor CUDA (lo que requiere versiones compatibles de CUDA y cuDNN). rembg usa CUDA o ROCm cuando están disponibles y avisa cuando recurre a la CPU. Compruébalo con onnxruntime.get_available_providers().

¿Qué modelo de rembg da la mejor calidad?

Los modelos más nuevos, bria-rmbg (el predeterminado), la familia birefnet e isnet-general-use, dan bordes más suaves y mejor formados que la familia u2net, más antigua. birefnet-portrait es un buen punto de partida para personas. Prueba con una muestra de tus propias imágenes, porque los resultados cambian según el sujeto.

¿Cómo evito que me cobren dos veces al reintentar llamadas a la API?

Envía una cabecera Idempotency-Key y reutilízala en cada reintento de la misma imagen. Si la generas a partir de un hash de los bytes de la imagen y de las opciones, incluso volver a ejecutar todo el script reutiliza las mismas claves.

¿Qué debe hacer mi script con una respuesta 202?

Tratarla como un trabajo que sigue en proceso, no como una imagen. Lee el id del trabajo en el cuerpo JSON y consulta GET /v1/jobs/{id}/result, esperando lo que indique Retry-After cada vez que responda 409 not_ready. No vuelvas a subir la imagen.

¿A qué ritmo puedo enviar peticiones a la API de removebackground.dev?

Hasta 60 peticiones de procesado por minuto por cuenta. Por encima de eso, la API responde 429 rate_limited con una cabecera Retry-After. Para muchas URL, POST /v1/batch acepta hasta 100 por petición.

Pruébalo con tu propia imagen

Las cuentas nuevas reciben 50 créditos gratis. Un crédito por imagen, sin suscripción, y los créditos no caducan.

Quitar un fondo

Fuentes