Add BEN2 and BiRefNet-HR as selectable Remove Background models

BEN2 becomes the new default local backend (clean cutouts, strong on
hair/fur edges), with BiRefNet-HR available as a high-res/print
alternate and U2Net kept as the lightweight fallback. Both are
MIT-licensed and download weights from HuggingFace on first use
(cached via the existing hf_cache bind mount), unlike U2Net/SAM which
need an explicit download script.

- config: new BG_REMOVAL_MODEL setting (default "ben2")
- tools.py: remove-background-base64 now tries local backends in
  order (request.model override > BG_REMOVAL_MODEL > ben2/u2net),
  falling back to rembg's birefnet-general session as a last resort
- requirements.gpu.txt / Dockerfile.gpu: add ben2 + transformers deps
  needed for the new backends, with a build-time smoke test for ben2
- frontend: model dropdown in the Remove Background dialog, threaded
  through api.js to the new request field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ro4PwQKvSc3CH19LSN21Ht
This commit is contained in:
Claude
2026-06-18 14:20:24 +00:00
parent 781b9e7cdc
commit 38e80f8fb8
9 changed files with 169 additions and 32 deletions
+6
View File
@@ -171,6 +171,12 @@ AUTO_DOWNLOAD_SAM=true
# When false: Skips download, Remove Background falls back to rembg (if installed) # When false: Skips download, Remove Background falls back to rembg (if installed)
AUTO_DOWNLOAD_U2NET=true AUTO_DOWNLOAD_U2NET=true
# Background removal model (Remove Background tool) — used when request.model="auto"
# Options: ben2 (default — best for clean cutouts, hair/edges), birefnet-hr
# (best for high-res/print work, slower), u2net (lightweight, always-on fallback)
# ben2 and birefnet-hr download weights from HuggingFace on first use (GPU image only).
BG_REMOVAL_MODEL=ben2
# Allow users to select model per-edit # Allow users to select model per-edit
ALLOW_MODEL_OVERRIDE=true ALLOW_MODEL_OVERRIDE=true
+4
View File
@@ -54,6 +54,10 @@ RUN echo "BUILDID=$BUILDID" && pip install --no-cache-dir -r requirements.gpu.tx
RUN python -c "from rembg import remove; print('rembg OK')" \ RUN python -c "from rembg import remove; print('rembg OK')" \
|| echo "WARNING: rembg unavailable — Remove Background disabled" || echo "WARNING: rembg unavailable — Remove Background disabled"
# Smoke-test ben2 (weights download from HuggingFace on first use)
RUN python -c "import ben2; print('ben2 OK')" \
|| echo "WARNING: ben2 unavailable — Remove Background falls back to U2Net/rembg"
# Copy backend application # Copy backend application
COPY backend/ . COPY backend/ .
+19 -13
View File
@@ -111,7 +111,7 @@ Override the auto-selected model with `HF_MODEL_TXT2IMG`, `HF_MODEL_INPAINT` in
- **Prepare for Print** — one-click: AI upscale to target DPI + fit to frame - **Prepare for Print** — one-click: AI upscale to target DPI + fit to frame
- **Fit to Frame** — resize/crop/AI-extend to standard print sizes - **Fit to Frame** — resize/crop/AI-extend to standard print sizes
- **Expand Canvas (Outpaint)** — AI extends the image in any direction - **Expand Canvas (Outpaint)** — AI extends the image in any direction
- **Remove Background** — one-click background removal - **Remove Background** — one-click background removal (BEN2 by default, BiRefNet-HR or U2Net selectable)
### Print presets ### Print presets
Frame sizes: 4×6, 5×7, 8×10, 11×14, 16×20, 18×24, 20×24, 24×36 (portrait + landscape) Frame sizes: 4×6, 5×7, 8×10, 11×14, 16×20, 18×24, 20×24, 24×36 (portrait + landscape)
@@ -201,21 +201,27 @@ docker compose -f docker-compose.gpu.yml logs | grep -i sam
If Docker created `./data/` as root and you can't write there without `sudo`, you can also use root's curl as above — the container reads the file regardless of owner. If Docker created `./data/` as root and you can't write there without `sudo`, you can also use root's curl as above — the container reads the file regardless of owner.
**Remove Background fails ("Install u2net or rembg")** **Remove Background fails ("Install ben2, u2net, or rembg")**
The U2Net model auto-downloads (~176MB) from GitHub on first use, same as SAM. If that download fails (DNS/firewall, see above) and `rembg` isn't installed either, you'll see this error. Fix it the same way — download directly on the host: Remove Background tries, in order: the model set by `BG_REMOVAL_MODEL` (default `ben2`), then the other local models, then `rembg` as a last resort. You'll see this error only if all of them fail.
```bash - **ben2 / birefnet-hr** (GPU image only) download their weights from HuggingFace on first use, cached under `./data/hf_cache`. If that download fails (DNS/firewall, see above), check the logs for the specific error:
mkdir -p ./data/models ```bash
sudo curl -L -o ./data/models/u2net.onnx \ docker compose -f docker-compose.gpu.yml logs -f | grep -iE "ben2|birefnet"
https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx ```
``` - **u2net** auto-downloads (~176MB) from GitHub on first use, same as SAM. If that fails too, download it directly on the host:
```bash
mkdir -p ./data/models
sudo curl -L -o ./data/models/u2net.onnx \
https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx
```
The file is ~176 MB. Once it exists at `./data/models/u2net.onnx`, the next "Remove Background" click picks it up — no rebuild or restart needed. Verify with:
```bash
docker compose logs -f | grep -i u2net
# Should show: "U2Net model loaded successfully with OpenCV DNN"
```
The file is ~176 MB. Once it exists at `./data/models/u2net.onnx`, the next "Remove Background" click picks it up — no rebuild or restart needed. Verify with: You can also pick a specific model per-edit from the Remove Background dialog's model dropdown, overriding `BG_REMOVAL_MODEL` for that one call.
```bash
docker compose logs -f | grep -i u2net
# Should show: "U2Net model loaded successfully with OpenCV DNN"
```
**AI models not downloading (container DNS blocked)** **AI models not downloading (container DNS blocked)**
+5
View File
@@ -46,6 +46,11 @@ class Settings(BaseSettings):
# Allow per-edit model override # Allow per-edit model override
allow_model_override: bool = True allow_model_override: bool = True
# Remove Background — preferred local model when request.model="auto"
# Options: ben2 (default, best for clean cutouts/hair), birefnet-hr (best
# for high-res/print work), u2net (lightweight, smallest download)
bg_removal_model: str = "ben2"
# Local GPU diffusion (AI_PROVIDER=local_gpu) # Local GPU diffusion (AI_PROVIDER=local_gpu)
auto_download_models: bool = True # download HF models on first use auto_download_models: bool = True # download HF models on first use
local_gpu_max_pipelines: int = 2 # max diffusion pipelines kept in GPU memory local_gpu_max_pipelines: int = 2 # max diffusion pipelines kept in GPU memory
+112 -15
View File
@@ -35,6 +35,7 @@ class InpaintRequest(BaseModel):
class RemoveBackgroundRequest(BaseModel): class RemoveBackgroundRequest(BaseModel):
image: str # Base64 encoded image image: str # Base64 encoded image
model: Optional[str] = "auto" # "auto", "ben2", "birefnet-hr", "u2net", "rembg"
@router.post("/smart-select-base64") @router.post("/smart-select-base64")
@@ -128,36 +129,54 @@ async def inpaint_base64(request: InpaintRequest):
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
class RemoveBackgroundRequestV2(BaseModel):
image: str # Base64 encoded image
model: Optional[str] = "auto" # "auto", "u2net", "rembg", "birefnet"
@router.post("/remove-background-base64") @router.post("/remove-background-base64")
async def remove_background_base64(request: RemoveBackgroundRequest): async def remove_background_base64(request: RemoveBackgroundRequest):
""" """
Remove background from a base64 encoded image. Remove background from a base64 encoded image.
Tries multiple methods: U2Net (direct), rembg with BiRefNet, rembg default.
request.model selects the backend:
- "auto" (default): BG_REMOVAL_MODEL setting first, then falls back
through the other local models, then rembg as a last resort.
- "ben2" / "birefnet-hr" / "u2net": use only that local model.
- "rembg": skip local models, use rembg directly.
Returns base64 encoded PNG with transparent background. Returns base64 encoded PNG with transparent background.
Used by miniPaint frontend. Used by miniPaint frontend.
""" """
try: try:
from app.config import settings
# Decode base64 image # Decode base64 image
image_bytes = base64.b64decode(request.image) image_bytes = base64.b64decode(request.image)
img = Image.open(BytesIO(image_bytes)).convert('RGB') img = Image.open(BytesIO(image_bytes)).convert('RGB')
local_backends = {
"ben2": _remove_background_ben2,
"birefnet-hr": _remove_background_birefnet_hr,
"u2net": _remove_background_u2net,
}
if request.model in local_backends:
order = [request.model]
elif request.model == "rembg":
order = []
else:
preferred = settings.bg_removal_model if settings.bg_removal_model in local_backends else "ben2"
order = [preferred] + [name for name in ("ben2", "u2net") if name != preferred]
result_bytes = None result_bytes = None
method_used = None method_used = None
# Try U2Net first (direct implementation, no rembg dependency issues) for name in order:
try: try:
result_bytes = await _remove_background_u2net(img) result_bytes = await local_backends[name](img)
method_used = "u2net" method_used = name
except Exception as e: break
print(f"U2Net failed: {e}") except Exception as e:
print(f"{name} failed: {e}")
# Fall back to rembg if U2Net failed # rembg is the universal last resort (also reachable directly via model="rembg")
if result_bytes is None: if result_bytes is None and request.model in ("auto", "rembg"):
try: try:
from rembg import remove, new_session from rembg import remove, new_session
try: try:
@@ -175,7 +194,7 @@ async def remove_background_base64(request: RemoveBackgroundRequest):
if result_bytes is None: if result_bytes is None:
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail="No background removal method available. Install u2net or rembg." detail="No background removal method available. Install ben2, u2net, or rembg."
) )
# Convert result to base64 # Convert result to base64
@@ -328,6 +347,84 @@ async def _remove_background_u2net(img: Image.Image) -> bytes:
return buffer.getvalue() return buffer.getvalue()
# Global BEN2 model cache
_ben2_model = None
async def _remove_background_ben2(img: Image.Image) -> bytes:
"""
Remove background using BEN2 (Confidence Guided Matting) — clean cutouts,
strong on hair/fur edges. MIT licensed. Downloads weights from HF Hub on
first use (cached under the hf_cache bind mount).
"""
global _ben2_model
if _ben2_model is None:
import torch
from ben2 import AutoModel as Ben2AutoModel
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Loading BEN2_Base model on {device} (first run downloads ~170MB from HuggingFace)")
_ben2_model = Ben2AutoModel.from_pretrained("PramaLLC/BEN2")
_ben2_model.to(device).eval()
print("BEN2_Base model loaded")
result = _ben2_model.inference(img.convert('RGB'), refine_foreground=False)
buffer = BytesIO()
result.save(buffer, format='PNG')
return buffer.getvalue()
# Global BiRefNet-HR model cache
_birefnet_hr_model = None
_birefnet_hr_device = None
async def _remove_background_birefnet_hr(img: Image.Image) -> bytes:
"""
Remove background using BiRefNet-HR (2048x2048, MIT licensed) — best for
high-resolution / print work. Downloads weights from HF Hub on first use.
"""
global _birefnet_hr_model, _birefnet_hr_device
import torch
from torchvision import transforms
if _birefnet_hr_model is None:
from transformers import AutoModelForImageSegmentation
_birefnet_hr_device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Loading BiRefNet-HR model on {_birefnet_hr_device} (first run downloads ~900MB from HuggingFace)")
_birefnet_hr_model = AutoModelForImageSegmentation.from_pretrained(
'zhengpeng7/BiRefNet_HR', trust_remote_code=True
)
_birefnet_hr_model.to(_birefnet_hr_device).eval()
print("BiRefNet-HR model loaded")
original_size = img.size
rgb_img = img.convert('RGB')
transform = transforms.Compose([
transforms.Resize((2048, 2048)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
input_tensor = transform(rgb_img).unsqueeze(0).to(_birefnet_hr_device)
with torch.no_grad():
preds = _birefnet_hr_model(input_tensor)[-1].sigmoid().cpu()
mask = transforms.ToPILImage()(preds[0].squeeze()).resize(original_size, Image.Resampling.LANCZOS)
result = rgb_img.convert('RGBA')
result.putalpha(mask)
buffer = BytesIO()
result.save(buffer, format='PNG')
return buffer.getvalue()
@router.post("/remove-background") @router.post("/remove-background")
async def remove_background( async def remove_background(
project_id: Optional[int] = Form(None), project_id: Optional[int] = Form(None),
+11
View File
@@ -31,3 +31,14 @@ sentencepiece>=0.2.0
# Install post-container-start if needed: # Install post-container-start if needed:
# pip install xformers --index-url https://download.pytorch.org/whl/cu121 # pip install xformers --index-url https://download.pytorch.org/whl/cu121
# xformers # xformers
# Background removal — BEN2 (default, clean cutouts/hair) + BiRefNet-HR
# (high-res/print alternate). Both MIT-licensed. Verified against upstream
# source: neither requires torch>=2.5 despite the BiRefNet repo's own
# requirements.txt floor — that pin is for its training/eval scripts, not
# the inference path used here. Weights download from HuggingFace on first
# use (cached via the hf_cache bind mount, same as the diffusion models).
ben2 @ git+https://github.com/PramaLLC/BEN2.git
timm>=1.0.10
einops>=0.6.0
kornia>=0.7.0
+1
View File
@@ -121,6 +121,7 @@ services:
- CORS_ORIGINS=* - CORS_ORIGINS=*
- AUTO_DOWNLOAD_SAM=${AUTO_DOWNLOAD_SAM:-true} - AUTO_DOWNLOAD_SAM=${AUTO_DOWNLOAD_SAM:-true}
- AUTO_DOWNLOAD_U2NET=${AUTO_DOWNLOAD_U2NET:-true} - AUTO_DOWNLOAD_U2NET=${AUTO_DOWNLOAD_U2NET:-true}
- BG_REMOVAL_MODEL=${BG_REMOVAL_MODEL:-ben2}
# ── NVIDIA GPU passthrough ──────────────────────────────────────────────── # ── NVIDIA GPU passthrough ────────────────────────────────────────────────
# Requires nvidia-container-toolkit; see prerequisites at top of this file. # Requires nvidia-container-toolkit; see prerequisites at top of this file.
@@ -49,6 +49,11 @@ class Image_remove_background_class {
title: 'Remove Background', title: 'Remove Background',
params: [ params: [
{ name: "info", title: "AI will detect the main subject and remove the background.", type: "label" }, { name: "info", title: "AI will detect the main subject and remove the background.", type: "label" },
{
name: "model", title: "Model:", value: "auto", type: "select",
values: ["auto", "ben2", "birefnet-hr", "u2net"],
comment: "auto = best available (BEN2 by default). BiRefNet-HR is slower but sharper on high-res/print work.",
},
{ name: "new_layer", title: "Create as new layer:", value: true }, { name: "new_layer", title: "Create as new layer:", value: true },
{ name: "trim_result", title: "Trim transparent edges:", value: false }, { name: "trim_result", title: "Trim transparent edges:", value: false },
], ],
@@ -74,7 +79,7 @@ class Image_remove_background_class {
var imageData = canvas.toDataURL('image/png').split(',')[1]; var imageData = canvas.toDataURL('image/png').split(',')[1];
// Call backend API // Call backend API
var result = await apiService.removeBackground(imageData); var result = await apiService.removeBackground(imageData, params.model);
// Create image from result // Create image from result
var resultImage = new Image(); var resultImage = new Image();
+5 -3
View File
@@ -72,11 +72,12 @@ class ApiService {
} }
/** /**
* Remove background from image using AI (rembg) * Remove background from image using AI (BEN2 / BiRefNet-HR / U2Net / rembg)
* @param {string} imageData - Base64 encoded image data * @param {string} imageData - Base64 encoded image data
* @returns {Promise<{result: string, width: number, height: number}>} - Base64 encoded result with transparency * @param {string} [model='auto'] - "auto", "ben2", "birefnet-hr", "u2net", or "rembg"
* @returns {Promise<{result: string, width: number, height: number, method: string}>} - Base64 encoded result with transparency
*/ */
async removeBackground(imageData) { async removeBackground(imageData, model = 'auto') {
const response = await fetch(`${this.baseUrl}/tools/remove-background-base64`, { const response = await fetch(`${this.baseUrl}/tools/remove-background-base64`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -84,6 +85,7 @@ class ApiService {
}, },
body: JSON.stringify({ body: JSON.stringify({
image: imageData, image: imageData,
model: model,
}), }),
}); });