Run pymatting foreground estimation on GPU via CuPy when available

The multi-level foreground colour estimation (pymatting estimate_foreground_ml)
is the pipeline's CPU bottleneck (~7 s/image). pymatting ships a CuPy backend
(estimate_foreground_ml_cupy) that runs the same multi-level solve on the GPU
~250x faster; verified visually identical to the float64 CPU result (opaque
colours bit-close, the float32 edge delta is alpha-weighted away).

Add ForegroundSettings.use_gpu (default True): with method "ml", try the CuPy
backend when cupy + a CUDA device are present, otherwise transparently fall back
to the CPU solver. pymatting only re-exports the cupy function when cupy is
importable, so CPU-only deployments (no cupy) are unaffected. Requires
cupy-cudaXXx matching the CUDA build on GPU boxes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:32:35 +00:00
parent ece9542978
commit 9094b5d9c0
2 changed files with 45 additions and 0 deletions
+40
View File
@@ -1,11 +1,15 @@
from __future__ import annotations
import logging
import numpy as np
from .chroma import BackgroundModel
from .deps import require_cv2
from .settings import ForegroundSettings
logger = logging.getLogger(__name__)
def estimate_foreground_rgb(
rgb: np.ndarray,
@@ -43,6 +47,10 @@ def _estimate_ml(
high-alpha neighbours instead of being divided out pixel-wise. That removes
background-colour spill on thin hair where a direct unmix (divide by small
alpha) is too noisy to trust.
Runs on the GPU via pymatting's CuPy backend when ``settings.use_gpu`` is set
and cupy + a CUDA device are available (~250x faster; float32, visually
identical to the float64 CPU result); otherwise falls back to the CPU solver.
"""
try:
from pymatting import estimate_foreground_ml
@@ -53,10 +61,42 @@ def _estimate_ml(
image = rgb.astype(np.float64) / 255.0
a = np.clip(alpha.astype(np.float64), 0.0, 1.0)
if settings.use_gpu:
foreground = _estimate_ml_gpu(image, a, settings.ml_regularization)
if foreground is not None:
return np.clip(foreground * 255.0, 0, 255).astype(np.uint8)
foreground = estimate_foreground_ml(image, a, regularization=settings.ml_regularization)
return np.clip(foreground * 255.0, 0, 255).astype(np.uint8)
def _estimate_ml_gpu(
image: np.ndarray, alpha: np.ndarray, regularization: float
) -> np.ndarray | None:
"""Multi-level foreground via pymatting's CuPy backend.
Returns the foreground as float ``[0, 1]``, or ``None`` when the GPU path is
unavailable (cupy not installed, or no CUDA device) so the caller falls back
to the CPU solver. pymatting only re-exports ``estimate_foreground_ml_cupy``
when cupy is importable, so the import itself is the availability probe.
"""
try:
from pymatting import estimate_foreground_ml_cupy
except ImportError:
return None
try:
import cupy
if cupy.cuda.runtime.getDeviceCount() < 1:
return None
foreground = estimate_foreground_ml_cupy(image, alpha, regularization=regularization)
return np.asarray(foreground)
except Exception as exc: # noqa: BLE001 -- any GPU failure falls back to CPU
logger.warning("GPU foreground estimation failed (%s); using CPU", exc)
return None
def _weighted_blur(values: np.ndarray, weights: np.ndarray, radius: int) -> np.ndarray:
if radius <= 0:
return values
+5
View File
@@ -105,6 +105,11 @@ class CrossCheckSettings:
class ForegroundSettings:
enabled: bool = True
method: str = "ml" # "ml" (pymatting) or "unmix" (legacy heuristic)
# With method "ml", use pymatting's CuPy GPU backend when it is available
# (cupy installed + a CUDA device) — ~250x faster, float32 output visually
# identical to the float64 CPU solver. Transparently falls back to CPU when
# cupy/CUDA are absent, so it is safe to leave on for CPU-only deployments.
use_gpu: bool = True
fallback_to_unmix: bool = True
ml_regularization: float = 1e-5
# Legacy "unmix" method parameters (used only when method == "unmix").