From 5ddf0cd3926172b5014643391ec05a9ec98df06e Mon Sep 17 00:00:00 2001 From: lingh Date: Tue, 30 Jun 2026 16:58:32 +0800 Subject: [PATCH] Despill across full foreground to cut edge green spill Green spill bleeds into pixels ViTMatte marks as solid foreground (alpha~1), but despill was gated to the soft edge band (0.005 edge_low) instead of the soft band only; neutral pixels stay untouched via the green-excess term. - (2) Weight by alpha at full strength across the opaque range (alpha>=0.5), easing off only where mostly transparent, instead of a tent peaking at 0.5. - (3) Drop bg_confidence as a despill gate; localise purely by green excess. Controlled comparison (same alpha, only despill changed), edge_green_excess: ViTMatte TestImage p95 0.239->0.176, mean 0.131->0.061 ViTMatte TestImage2 p95 0.231->0.165, mean 0.115->0.052 Removes now-unused DespillSettings fields edge_high and bg_confidence_weight. Co-Authored-By: Claude Opus 4.8 --- bgfilter/despill.py | 26 ++++++++++++++++---------- bgfilter/pipeline.py | 2 +- bgfilter/settings.py | 2 -- configs/default.yaml | 2 -- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/bgfilter/despill.py b/bgfilter/despill.py index 4796bbe..35544f1 100644 --- a/bgfilter/despill.py +++ b/bgfilter/despill.py @@ -9,7 +9,6 @@ from .settings import DespillSettings def despill_green( rgb: np.ndarray, alpha: np.ndarray, - bg_confidence: np.ndarray, model: BackgroundModel, settings: DespillSettings, ) -> tuple[np.ndarray, np.ndarray]: @@ -17,7 +16,11 @@ def despill_green( if not settings.enabled: return rgb.copy(), np.zeros(alpha.shape, dtype=np.float32) - edge = (alpha > settings.edge_low) & (alpha < settings.edge_high) + # (1) Despill across the whole foreground, not only the soft edge band. + # Green frequently bleeds into pixels ViTMatte marks as solid foreground + # (alpha ~ 1); the old upper bound left those uncorrected. Neutral pixels + # stay untouched because the green-excess term below is zero for them. + foreground = alpha > settings.edge_low if settings.edge_expand_radius > 0: from .deps import require_cv2 @@ -26,23 +29,26 @@ def despill_green( kernel = cv2.getStructuringElement( cv2.MORPH_ELLIPSE, (radius * 2 + 1, radius * 2 + 1) ) - edge = cv2.dilate(edge.astype(np.uint8), kernel).astype(bool) + foreground = cv2.dilate(foreground.astype(np.uint8), kernel).astype(bool) r = rgb_f[..., 0] g = rgb_f[..., 1] b = rgb_f[..., 2] neutral_green = np.maximum(r, b) + settings.green_excess_margin excess = np.maximum(g - neutral_green, 0.0) + + # (2) Keep full strength across the whole opaque range (alpha >= 0.5) and + # ease off only where the pixel is mostly transparent. The previous tent + # peaked at alpha 0.5 and starved exactly the solid hair that needs despill. alpha_weight = np.maximum( - np.clip((1.0 - np.abs(alpha - 0.5) * 2.0), 0.0, 1.0), - settings.alpha_weight_floor, - ) - bg_weight = ( - settings.bg_confidence_weight * np.clip(bg_confidence, 0.0, 1.0) - + (1.0 - settings.bg_confidence_weight) + np.clip(alpha * 2.0, 0.0, 1.0), settings.alpha_weight_floor ) + + # (3) Localise purely by green excess. bg_confidence is intentionally not a + # gate: it is low on the character edge where the spill lives, so using it + # suppressed the very correction we want. green_weight = np.clip(excess / max(settings.green_excess_margin * 8.0, 1e-6), 0.0, 1.0) - mask = edge.astype(np.float32) * alpha_weight * bg_weight * green_weight + mask = foreground.astype(np.float32) * alpha_weight * green_weight out = rgb_f.copy() target_green = np.minimum(g, neutral_green) diff --git a/bgfilter/pipeline.py b/bgfilter/pipeline.py index b6d6529..bc385be 100644 --- a/bgfilter/pipeline.py +++ b/bgfilter/pipeline.py @@ -76,7 +76,7 @@ def _run_image( rgb, alpha, bg_confidence, model, settings.foreground ) corrected_rgb, despill_mask = despill_green( - foreground.rgb, alpha, bg_confidence, model, settings.despill + foreground.rgb, alpha, model, settings.despill ) save_rgba(output_path, corrected_rgb, alpha) diff --git a/bgfilter/settings.py b/bgfilter/settings.py index 770d39f..84541c0 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -54,11 +54,9 @@ class ForegroundSettings: class DespillSettings: enabled: bool = True edge_low: float = 0.005 - edge_high: float = 0.995 strength: float = 0.92 green_excess_margin: float = 0.015 edge_expand_radius: int = 2 - bg_confidence_weight: float = 0.65 alpha_weight_floor: float = 0.35 diff --git a/configs/default.yaml b/configs/default.yaml index 90cf10f..279036d 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -26,9 +26,7 @@ foreground: despill: enabled: true edge_low: 0.005 - edge_high: 0.995 strength: 0.92 green_excess_margin: 0.015 edge_expand_radius: 2 - bg_confidence_weight: 0.65 alpha_weight_floor: 0.35