diff --git a/bgfilter/alpha_post.py b/bgfilter/alpha_post.py index 07704bb..206457d 100644 --- a/bgfilter/alpha_post.py +++ b/bgfilter/alpha_post.py @@ -88,7 +88,10 @@ def clean_alpha( def fill_seg_dropouts( - alpha: np.ndarray, seg_mask: np.ndarray, settings: AlphaPostSettings + alpha: np.ndarray, + seg_mask: np.ndarray, + settings: AlphaPostSettings, + gate_mask: np.ndarray | None = None, ) -> np.ndarray: """Rescue alpha where ViTMatte dropped a region the semantic mask asserts. @@ -98,12 +101,21 @@ def fill_seg_dropouts( foreground, restore a faint alpha from the mask. Limited to those dropouts so ViTMatte's sharper edges elsewhere are untouched, and gated by seg_min so the mask's faint halo and the finger gap (low mask) are not filled. + + ``gate_mask`` (when ``seg_fill_intersect`` is on) is a second, reliable mask + -- BiRefNet -- required to also endorse the rescue (>= ``seg_fill_gate_min``). + ``seg_mask`` (anime-seg, high recall) then only proposes wisps; the + intersection with BiRefNet drops its over-inclusive halo, while the rescued + *value* still comes from anime-seg's smooth mask (no grainy BiRefNet value). """ if not settings.seg_fill: return alpha out = alpha.astype(np.float32).copy() rescued = settings.seg_fill_strength * np.clip(seg_mask, 0.0, 1.0) dropout = (out < settings.seg_fill_alpha_max) & (seg_mask >= settings.seg_fill_seg_min) + # Intersection mode: keep only rescues the reliable mask also endorses. + if settings.seg_fill_intersect and gate_mask is not None: + dropout &= gate_mask >= settings.seg_fill_gate_min # The segmentation mask carries scattered low-confidence noise; rescuing it # verbatim sprinkles isolated specks (burrs) over the matte. Keep only # connected structures (genuine wisps), drop the isolated specks. diff --git a/bgfilter/pipeline.py b/bgfilter/pipeline.py index 4f02fee..198a92b 100644 --- a/bgfilter/pipeline.py +++ b/bgfilter/pipeline.py @@ -117,7 +117,11 @@ def _run_image( alpha = clean_alpha(alpha, trimap, settings.alpha_post) if seg_mask is not None: fill_mask = pipeline._segment_fill(rgb, seg_mask) - alpha = fill_seg_dropouts(alpha, fill_mask, settings.alpha_post) + # seg_mask is the reliable trimap backend (BiRefNet); it gates the fill + # when seg_fill_intersect is on (fill_mask ∧ seg_mask). + alpha = fill_seg_dropouts( + alpha, fill_mask, settings.alpha_post, gate_mask=seg_mask + ) foreground = estimate_foreground_rgb( rgb, alpha, bg_confidence, model, settings.foreground ) diff --git a/bgfilter/settings.py b/bgfilter/settings.py index 03988d5..a9927a9 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -49,6 +49,13 @@ class AlphaPostSettings: seg_fill_seg_min: float = 0.10 seg_fill_strength: float = 0.70 seg_fill_min_area_ratio: float = 0.00002 # drop rescued specks below this area + # Intersection fill: gate the (anime-seg) dropout-fill by a second, reliable + # mask (BiRefNet). anime-seg proposes freely at a low seg_fill_seg_min; only + # rescues BiRefNet also endorses (>= seg_fill_gate_min, its threshold left + # unchanged) survive. Trims anime-seg's over-inclusive halo to the structure + # both backends agree on, without reintroducing BiRefNet's grainy value. + seg_fill_intersect: bool = False + seg_fill_gate_min: float = 0.10 @dataclass(frozen=True)