From f7e12d3498168daf25c3d66a48e569e823c852c2 Mon Sep 17 00:00:00 2001 From: lingh Date: Tue, 30 Jun 2026 18:36:43 +0800 Subject: [PATCH] Carve background green embedded in the foreground (hole handling) The green in the finger gap survives keying because it is dark and desaturated (brightness ~0.58, hue-shifted), so chroma confidence reads ~0 and the trimap leaves it in the unknown band, where ViTMatte fills the pocket as foreground. It is also connected to the exterior screen (an open notch), so neither confidence suppression nor classic hole-filling removes it. Add carve_background_holes (doc section 10): a pixel that is clearly green-dominant in the input *and* mostly surrounded by genuine (non-green) foreground is set to alpha 0. The surround cue separates such an intrusion (finger gaps, inter-strand pockets) from the outer hair edge, which has background on one side and is preserved. Runs after alpha cleanup. On TestImage the finger-gap survivors (green & alpha>0.5) drop 170 -> 28 with no visible loss of hair wisps; inter-strand green pockets clear too. Knobs in AlphaPostSettings (hole_*, default on, surround_min 0.4). Co-Authored-By: Claude Opus 4.8 --- bgfilter/alpha_post.py | 30 ++++++++++++++++++++++++++++++ bgfilter/pipeline.py | 8 +++++++- bgfilter/settings.py | 8 ++++++++ configs/default.yaml | 5 +++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/bgfilter/alpha_post.py b/bgfilter/alpha_post.py index 5151c32..468929d 100644 --- a/bgfilter/alpha_post.py +++ b/bgfilter/alpha_post.py @@ -45,6 +45,36 @@ def suppress_alpha_by_chroma( return np.clip(out, 0.0, 1.0) +def carve_background_holes( + alpha: np.ndarray, rgb: np.ndarray, settings: AlphaPostSettings +) -> np.ndarray: + """Remove green-dominant pixels that are embedded inside the foreground. + + Targets background green that pokes into the silhouette (gaps between fingers, + pockets between hair strands) where the green is too dark/desaturated for the + chroma confidence to flag, so the matte wrongly fills it as foreground. A pixel + is carved when it is clearly green-dominant *and* mostly surrounded by genuine + (non-green) foreground, which separates such an intrusion from the outer hair + edge (background on one side) that must be preserved. + """ + if not settings.hole_carve: + return alpha + cv2 = require_cv2() + rgb_f = rgb.astype(np.float32) / 255.0 + dom = rgb_f[..., 1] - np.maximum(rgb_f[..., 0], rgb_f[..., 2]) + green = dom > settings.hole_green_dominance + # Foreground that is genuinely opaque and not itself green, so the surround + # measure reflects real body/hair rather than the green we want to remove. + solid_fg = ((alpha > settings.hole_fg_alpha) & ~green).astype(np.float32) + radius = max(1, int(round(max(alpha.shape) * settings.hole_surround_radius_ratio))) + ksize = radius * 2 + 1 + surround = cv2.GaussianBlur(solid_fg, (ksize, ksize), 0) + carve = green & (surround > settings.hole_surround_min) + out = alpha.astype(np.float32).copy() + out[carve] = 0.0 + return np.clip(out, 0.0, 1.0) + + def _remove_small_components(mask: np.ndarray, min_area: int) -> np.ndarray: cv2 = require_cv2() count, labels, stats, _ = cv2.connectedComponentsWithStats(mask.astype(np.uint8), 8) diff --git a/bgfilter/pipeline.py b/bgfilter/pipeline.py index 298d50f..3612b2f 100644 --- a/bgfilter/pipeline.py +++ b/bgfilter/pipeline.py @@ -6,7 +6,12 @@ from pathlib import Path import numpy as np -from .alpha_post import clean_alpha, enforce_trimap, suppress_alpha_by_chroma +from .alpha_post import ( + carve_background_holes, + clean_alpha, + enforce_trimap, + suppress_alpha_by_chroma, +) from .chroma import compute_bg_confidence from .despill import despill_green from .foreground import estimate_foreground_rgb @@ -73,6 +78,7 @@ def _run_image( alpha = enforce_trimap(alpha, trimap) alpha = suppress_alpha_by_chroma(alpha, bg_confidence, trimap, settings.alpha_post) alpha = clean_alpha(alpha, trimap, settings.alpha_post) + alpha = carve_background_holes(alpha, rgb, settings.alpha_post) foreground = estimate_foreground_rgb( rgb, alpha, bg_confidence, model, settings.foreground ) diff --git a/bgfilter/settings.py b/bgfilter/settings.py index 9fb2c30..f68716d 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -39,6 +39,14 @@ class AlphaPostSettings: chroma_suppress_bg_low: float = 0.35 chroma_suppress_bg_high: float = 0.80 chroma_suppress_strength: float = 1.0 + # Background-hole carving (doc section 10): remove green-dominant pixels that + # are embedded in the foreground (finger gaps, inter-strand pockets) where the + # green is too dark for the chroma confidence to flag. + hole_carve: bool = True + hole_green_dominance: float = 0.10 + hole_fg_alpha: float = 0.5 + hole_surround_radius_ratio: float = 0.012 + hole_surround_min: float = 0.4 @dataclass(frozen=True) diff --git a/configs/default.yaml b/configs/default.yaml index 2ec8849..f0e8abc 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -15,6 +15,11 @@ alpha_post: chroma_suppress_bg_low: 0.35 chroma_suppress_bg_high: 0.80 chroma_suppress_strength: 1.0 + hole_carve: true + hole_green_dominance: 0.10 + hole_fg_alpha: 0.5 + hole_surround_radius_ratio: 0.012 + hole_surround_min: 0.4 foreground: enabled: true