상품 사진, 프로필 사진, 매물 사진 수백 장이 든 폴더가 있고, 모든 사진의 배경을 제거해야 한다고 해 봅시다. 편집 프로그램에서 손으로 하면 하루 종일 클릭만 하게 됩니다. Python이라면 반복문 하나면 되고, 진짜 결정할 것은 반복문 안에서 무엇을 돌리느냐입니다. 내 컴퓨터에서 도는 오픈 소스 모델인가, HTTP API인가.
이 가이드는 두 가지를 모두 만들어 봅니다. 오늘 바로 폴더에 실행할 수 있는 완전한 스크립트와 함께, 대량 작업을 믿을 수 있게 만드는 요소인 모델 선택, CPU와 GPU, 재시도, 요청 한도, 멱등성, 중단 후 재개까지 다룹니다.
방법 A냐 방법 B냐
| 로컬 모델(rembg) | HTTP API | |
|---|---|---|
| 비용 | 소프트웨어는 무료, 하드웨어와 시간으로 지불 | 이미지당 |
| 설정 | Python 3.11에서 3.13, 모델 다운로드, 선택적으로 CUDA | API 키와 requests |
| 속도 | CPU나 GPU와 모델에 따라 다름 | 서비스와 병렬 요청에 따라 다름 |
| 품질 | 고른 모델에 따라 다름 | 서비스에 따라 다름 |
| 개인정보 | 이미지가 내 컴퓨터를 떠나지 않음 | 이미지를 제3자에게 업로드 |
| 유지 관리 | 모델, 드라이버, 라이선스를 직접 관리 | 제공업체가 관리 |
| 잘 맞는 곳 | 오프라인 작업, 민감한 이미지, 실험 | 운영 파이프라인, GPU 없는 서버 |
둘 다 쓰는 팀도 많습니다. 실험과 내부 도구에는 로컬 모델을, 예측 가능한 품질과 GPU 서버가 필요 없다는 점이 더 중요한 운영 환경에는 API를 쓰는 식입니다.
방법 A: rembg, 오픈 소스 방식
rembg는 MIT 라이선스의 Python 라이브러리이자 CLI로, 여러 배경 제거 모델을 감싸서 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 commandNVIDIA GPU라면 대신 rembg[gpu]를 설치하세요. 프로젝트에 따르면 이를 위해서는 onnxruntime-gpu와 그에 맞는 CUDA, cuDNN이 필요하며, 설치할 수 없다면 CPU 빌드로 돌아가라고 권합니다. Linux의 AMD GPU는 rembg[rocm]과 onnxruntime-rocm을 씁니다.
rembg는 ONNX Runtime이 사용 가능하다고 보고하면 CUDA(또는 ROCm) 실행 공급자를 자동으로 고르고, 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는 머리카락과 털 같은 부드러운 가장자리 픽셀을 다루는 세 가지 방법을 제공합니다.
- 기본값: 상품, 자동차, 로고처럼 가장자리가 딱딱한 피사체에는 충분합니다.
decontaminate=True(CLI에서는-dc): 기존 배경이 가장자리 픽셀에 남긴 색 테두리를 없앱니다. 바뀌는 것은 색뿐이고 마스크 모양은 절대 바뀌지 않으며, 부담이 아주 적어서 대량 작업 전체에 켜 둬도 안전합니다.alpha_matting=True(-a): 머리카락이 잘리는 등 마스크 모양이 잘못되었을 때 가장자리 자체를 다시 추정합니다. 훨씬 느리고 수렴하지 못할 수도 있으며, 그럴 때 rembg는 decontaminate한 누끼로 돌아갑니다.
무인 대량 작업에는 더 빠르고 실패하지 않는다는 이유로 프로젝트가 -a보다 -dc를 권합니다. 테두리가 생기는 이유는 알파 채널 가이드에서 설명합니다.
빠른 방법: CLI
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입니다.
rembg의 약점
- 모델을 다운로드하는 첫 실행이 느리고, 큰 모델에는 많은 RAM이나 VRAM이 필요합니다.
- 고품질 모델에서는 CPU 처리량이 크지 않습니다. 천 장에 들어가기 전에 대표적인 20장으로 테스트하세요.
- 라이선스와 드라이버는 직접 챙겨야 합니다. 나중의 업그레이드가 결과를 몰래 바꾸지 않도록 rembg와 모델 버전을 고정하세요.
방법 B: HTTP API
API를 쓰면 무거운 작업은 다른 곳의 GPU에서 일어나고, 스크립트는 주로 네트워크를 잘 다루는 일을 합니다. 예제는 removebackground.dev API를 씁니다. 명세가 /openapi.json에 OpenAPI 3.1로 공개되어 있기 때문입니다. 같은 패턴은 비슷한 어떤 서비스에도 적용됩니다.
명세
POST https://removebackground.dev/v1/remove에x-api-key헤더로 키를 넣어 보냅니다.- 이미지는 멀티파트 필드
image_file(JPG, PNG, WebP, 최대 25 MB)로 보내거나,image_url이 있는 JSON으로 보냅니다. - 선택 필드로
format(png,jpg,webp),bg_color,size(preview,medium,hd,full),crop이 있습니다. - 200은 본문이 완성된 이미지라는 뜻입니다. 202는 약 55초가 지나도 아직 처리 중이라는 뜻입니다. 본문은 작업
id가 있는 JSON이고,GET /v1/jobs/{id}/result를 폴링하면 이미지가 준비될 때까지Retry-After와 함께 409not_ready를 돌려줍니다. 202 본문을 이미지로 저장하면 안 됩니다. - 계정당 분당 처리 요청 60건까지이며, 넘으면
Retry-After와 함께 429rate_limited를 받습니다. Idempotency-Key헤더가 재시도를 안전하게 만듭니다. 같은 키로 같은 요청을 보내면 다시 과금하지 않고 원래 작업을 돌려줍니다. 같은 키로 다른 요청을 보내면 409idempotency_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로 실행하세요. 작업자 4개는 무난한 시작점입니다. 응답이 빨라서 분당 한도를 넘으면 429 처리가 기다렸다가 같은 키로 다시 보냅니다. 파일 대신 URL을 보내려면 JSON을 POST하세요.
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 vs JPEG vs 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가 Retry-After 헤더와 함께 429 rate_limited를 돌려줍니다. URL이 많다면 POST /v1/batch가 요청당 최대 100개를 받습니다.
내 이미지로 해보기
새 계정에는 무료 크레딧 50개가 제공됩니다. 이미지당 1크레딧, 구독 없음, 크레딧은 만료되지 않습니다.
배경 지우기출처
- rembg on GitHub (README: installation, models, edge modes)
- rembg on PyPI (version and Python requirement)
- BRIA RMBG-2.0 model card and license
- ONNX Runtime: Get started and installation matrix
- removebackground.dev API reference and guides
- removebackground.dev OpenAPI 3.1 schema
- Requests: Quickstart (multipart file uploads, timeouts)