"""RMBG 2.0 model loading and mask generation."""
from pathlib import Path
from typing import Any
from PIL import Image, ImageOps, UnidentifiedImageError
class RmbgError(Exception):
"""Report an expected model, image, device, or output error."""
def validateModelPath(model_path: Path) -> Path:
"""Validate and resolve a local Hugging Face model directory."""
if not model_path.exists():
raise RmbgError(f"model directory does not exist: {model_path}")
if not model_path.is_dir():
raise RmbgError(f"model path is not a directory: {model_path}")
config_path = model_path / "config.json"
if not config_path.is_file():
raise RmbgError(
f"model directory does not contain config.json: {model_path}"
)
return model_path.resolve()
def selectDevice(torch_module: Any, device_name: str) -> str:
"""Select an available PyTorch inference device."""
if device_name == "auto":
if torch_module.cuda.is_available():
return "cuda"
mps = getattr(torch_module.backends, "mps", None)
if mps is not None and mps.is_available():
return "mps"
return "cpu"
if device_name == "cuda" and not torch_module.cuda.is_available():
raise RmbgError("CUDA was requested but is not available")
if device_name == "mps":
mps = getattr(torch_module.backends, "mps", None)
if mps is None or not mps.is_available():
raise RmbgError("MPS was requested but is not available")
return device_name
def loadModel(model_path: Path, device: str) -> Any:
"""Load RMBG from a validated local directory without network access."""
from transformers import AutoModelForImageSegmentation
try:
model = AutoModelForImageSegmentation.from_pretrained(
str(model_path),
local_files_only=True,
trust_remote_code=True,
)
except Exception as error:
raise RmbgError(f"could not load local model: {error}") from error
return model.eval().to(device)
def openImage(input_path: Path) -> Image.Image:
"""Open an image, apply EXIF orientation, and convert it to RGB."""
try:
with Image.open(input_path) as source_image:
return ImageOps.exif_transpose(source_image).convert("RGB")
except FileNotFoundError as error:
raise RmbgError(f"input image does not exist: {input_path}") from error
except (OSError, UnidentifiedImageError) as error:
raise RmbgError(f"could not read input image: {error}") from error
def inferMask(
model: Any,
image: Image.Image,
processing_resolution: int,
device: str,
) -> Image.Image:
"""Infer a soft grayscale mask and restore the source dimensions."""
import torch
from torchvision import transforms
transform_image = transforms.Compose(
[
transforms.Resize(
(processing_resolution, processing_resolution),
antialias=True,
),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225],
),
]
)
input_tensor = transform_image(image).unsqueeze(0).to(device)
try:
with torch.inference_mode():
prediction = model(input_tensor)[-1].sigmoid().cpu()
except RuntimeError as error:
raise RmbgError(f"model inference failed: {error}") from error
mask_tensor = prediction[0].squeeze().clamp(0, 1)
processing_mask = transforms.ToPILImage()(mask_tensor).convert("L")
return processing_mask.resize(image.size, Image.Resampling.BILINEAR)
def saveMask(mask: Image.Image, output_path: Path) -> None:
"""Save an 8-bit grayscale mask to the requested path."""
try:
mask.save(output_path)
except (OSError, ValueError) as error:
raise RmbgError(f"could not save output mask: {error}") from error
def generateMask(
model_path: Path,
input_path: Path,
output_path: Path,
processing_resolution: int = 1024,
device_name: str = "auto",
) -> None:
"""Generate and save a soft foreground mask with a local RMBG model."""
if processing_resolution <= 0:
raise RmbgError("processing resolution must be positive")
local_model_path = validateModelPath(model_path)
import torch
device = selectDevice(torch, device_name)
image = openImage(input_path)
model = loadModel(local_model_path, device)
mask = inferMask(model, image, processing_resolution, device)
saveMask(mask, output_path)