用 Python 批量去除图片背景:rembg 与 API 对比

两个完整脚本,帮你去除整个文件夹图片的背景:一个在你自己的硬件上使用开源 rembg 库,另一个使用 HTTP API,并正确处理重试、速率限制和重复扣费。

更新于 2026年10月23日 · 阅读约 4 分钟

你有一个文件夹,里面是几百张产品照片、证件照或房源图片,每一张都要去除背景。在编辑器里手动处理,要点上一整天。用 Python 写,只是一个循环,真正要决定的是循环里跑什么:自己机器上的开源模型,还是 HTTP API。

本指南两种方案都会搭建,给出今天就能在文件夹上运行的完整脚本,并讲清楚让批处理任务可靠的关键环节:模型选择、CPU 与 GPU、重试、速率限制、幂等性,以及崩溃后如何续跑。

方案 A 还是方案 B?

本地模型(rembg)HTTP API
成本软件免费;代价是硬件和时间按张计费
配置Python 3.11 到 3.13,下载模型,可选 CUDA一个 API 密钥和 requests
速度取决于你的 CPU 或 GPU 以及模型取决于服务,可并行请求
质量取决于你选的模型取决于服务
隐私图片永远不离开你的机器图片会上传给第三方
维护模型、驱动和许可证都由你管理由服务商负责
适合离线工作、敏感图片、实验生产流水线、没有 GPU 的服务器

很多团队两种都用:本地模型用于实验和内部工具,API 用于生产环境,因为在生产中,稳定的质量和不用维护 GPU 服务器更重要。

方案 A:rembg,开源路线

rembg 是一个采用 MIT 许可证的 Python 库和命令行工具,封装了多个去背景模型,并用 ONNX Runtime 运行。撰写本文时,最新版本是 2.0.86,要求 Python 3.11 或更高、低于 3.14。

安装

选择与你的硬件匹配的后端。CPU 版本在任何地方都能用:

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

如果是 NVIDIA GPU,改为安装 rembg[gpu]。项目说明指出,这需要 onnxruntime-gpu 以及匹配的 CUDA 和 cuDNN,如果装不上,建议退回 CPU 版本。Linux 上的 AMD GPU 使用 rembg[rocm] 和 onnxruntime-rocm。

当 ONNX Runtime 报告 CUDA(或 ROCm)执行提供程序可用时,rembg 会自动选用它,回退到 CPU 时会给出警告。要查看 ONNX Runtime 能检测到什么:

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

选择模型

模型在首次使用时下载到 ~/.rembg/models/(设置 REMBG_HOME 可以修改位置)。以下是项目列表中比较实用的几个:

模型说明
bria-rmbg默认模型。BRIA 的 RMBG-2.0,约 1.02 GB,以 1024 x 1024 运行。其许可证要求商业使用须签订付费协议。
birefnet-general通用 BiRefNet 模型;柔和边缘效果好
birefnet-general-lite更轻量的通用 BiRefNet
birefnet-portrait针对人像优化
isnet-general-use通用 IS-Net 模型
u2net经典通用模型;更快,边缘更生硬
u2netp轻量版 u2net;适合在慢机器上快速测试
silueta与 u2net 相同,缩小到 43 MB

有两点比看上去更重要。第一,默认模型不能免费用于商业用途:模型权重有自己的许可证,与 rembg 的 MIT 许可证是分开的,所以无论你最终用哪个模型,都要查清它的来源。第二,较新的模型(bria-rmbg、birefnet-*、isnet-general-use)边缘柔和、形状准确,而较老的 u2net 系列边缘往往更硬、更生硬。

边缘质量选项

对于头发、毛发这类柔和的边缘像素,rembg 提供三种处理方式:

  • 默认: 适合产品、汽车和 logo 这类硬边主体。
  • decontaminate=True(命令行用 -dc):去除旧背景在边缘像素中留下的彩色光晕。它只改变颜色,从不改变蒙版形状,开销很小,整批开着也没问题。
  • alpha_matting=True(-a):在蒙版形状不对时(例如发丝被切掉)重新估算边缘本身。它慢得多,而且可能无法收敛,这时 rembg 会退回去色边后的抠图结果。

对于无人值守的批处理,项目推荐用 -dc 而不是 -a,因为它更快,也不会失败。alpha 通道指南 解释了光晕产生的原因。

快捷方式:命令行

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

rembg p 处理整个文件夹;加上 -w 可以监视文件夹,在新文件出现时自动处理。

Python 脚本

这个版本只加载一次模型,跳过已经处理过的文件(所以可以随时停下再重启),并且在某个文件失败时继续运行:

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:])

用 python batch_rembg.py photos cutouts birefnet-general 运行。只创建一次会话很关键:加载模型的时间远远超过运行模型的时间,rembg 的 README 也建议批处理时复用会话以提升性能。输出始终是 PNG,因为结果带有 alpha 通道。

rembg 的不足

  • 首次运行很慢,因为要下载模型,大模型还需要大量内存或显存。
  • 用高质量模型时 CPU 吞吐量一般。 在投入处理一千张之前,先用 20 张有代表性的图片测试。
  • 许可证和驱动都得你自己负责。 固定 rembg 和模型的版本,以免日后升级时悄悄改变结果。

方案 B:HTTP API

使用 API 时,重活在别人的 GPU 上完成,你的脚本主要负责把网络处理好。示例使用 removebackground.dev API,因为它的接口规范以 OpenAPI 3.1 发布在 /openapi.json;同样的模式也适用于任何类似的服务。

接口规范

  • POST https://removebackground.dev/v1/remove,把密钥放在 x-api-key 请求头中。
  • 以 multipart 字段 image_file 发送图片(JPG、PNG 或 WebP,最大 25 MB),或以 JSON 形式发送 image_url。
  • 可选字段包括 format(png、jpg 或 webp)、bg_color、size(preview、medium、hd、full)和 crop。
  • 200 表示响应体就是处理好的图片。202 表示约 55 秒后仍在处理:响应体是带有任务 id 的 JSON,你需要轮询 GET /v1/jobs/{id}/result,在图片就绪之前它会返回 409 not_ready 和 Retry-After。千万不要把 202 的响应体当作图片保存。
  • 每个账户每分钟最多 60 个处理请求;超过后会收到 429 rate_limited 和 Retry-After。
  • Idempotency-Key 请求头让重试变得安全:相同的键加相同的请求会返回原来的任务,而不会再次扣费。相同的键加不同的请求会返回 409 idempotency_key_reused。
  • 结果会在 48 小时后删除,所以拿到后就要保存。

避免重复付费的重试规则

响应怎么做
200保存图片
202轮询任务结果;不要重新上传
400, 401, 402, 413, 422修正请求、密钥、积分或图片;不要循环重试
429等待 Retry-After 秒,用相同的键重发
500, 503, 超时, 连接中断退避后用相同的键重发
502这张图片处理失败且未扣费;记录下来

诀窍是让幂等键确定:由图片字节加选项算出哈希。如果脚本崩溃后重新运行,每次重发都带着相同的键,所以不会有任何图片被处理两次或扣费两次。

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])

用 REMOVEBG_API_KEY=... python batch_api.py photos cutouts 运行。四个工作线程是比较保守的起点;如果响应很快、超出了每分钟限制,429 处理逻辑会等待后用相同的键重发。如果要发送 URL 而不是文件,就以 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),
)

对于很长的 URL 列表,同一个 API 还提供 POST /v1/batch,可以把最多 100 个图片 URL 作为一个任务排队,然后轮询结果。

让任何批处理任务更可靠

  • 跳过已完成的输出,这样重新运行时会接着处理,而不是从头开始。两个脚本都这样做了。
  • 注意文件名冲突。 同一文件夹里的 shoe.jpg 和 shoe.png 都会对应到 shoe.png;请重命名输入文件,或在输出文件名中保留原扩展名。
  • 把失败记录到文件中,之后只重跑失败的那些。
  • 抽查样本:在处理全部图片之前,以 100% 缩放在深色和浅色背景上检查。
  • 不要动原图。 写入单独的输出文件夹。
  • 按目的地选择输出格式。 需要透明用 PNG 或 WebP,电商平台用白底 JPG。参见 PNG、JPEG 与 WebP 对比。

常见问题

rembg 可以免费商用吗?

rembg 本身采用 MIT 许可证,但每个模型都有自己的许可证。默认模型 BRIA 的 RMBG-2.0(bria-rmbg)商业使用需要签订付费协议,所以请查看你所用模型的许可证,或换一个模型。

rembg 会自动使用我的 GPU 吗?

会,前提是你安装了 rembg[gpu],并且 ONNX Runtime 能加载 CUDA 提供程序(这需要匹配的 CUDA 和 cuDNN)。rembg 在可用时使用 CUDA 或 ROCm,回退到 CPU 时会发出警告。可以用 onnxruntime.get_available_providers() 检查。

哪个 rembg 模型质量最好?

较新的模型,即 bria-rmbg(默认)、birefnet 系列和 isnet-general-use,比较老的 u2net 系列边缘更柔和、形状更准确。人像可以先试 birefnet-portrait。用你自己的一批图片测试,因为效果因主体而异。

重试 API 调用时怎样避免被重复扣费?

发送 Idempotency-Key 请求头,并在同一张图片的每次重试中复用它。用图片字节和选项的哈希来生成这个键,即使重新运行整个脚本,也会复用相同的键。

脚本收到 202 响应时该怎么处理?

把它当作仍在处理中,而不是图片。从 JSON 响应体中读取任务 id,轮询 GET /v1/jobs/{id}/result,每当它返回 409 not_ready 时按 Retry-After 等待。不要重新上传图片。

向 removebackground.dev API 发请求最快能多快?

每个账户每分钟最多 60 个处理请求。超过后,API 会返回 429 rate_limited 和 Retry-After 请求头。如果有很多 URL,POST /v1/batch 每次请求最多接受 100 个。

用你自己的图片试试

新账户可获得 50 个免费额度。每张图片 1 个额度,无需订阅,额度永不过期。

去除背景

资料来源