From 18c6918e45284bcd5e6dc3716ea3bed399f94cbc Mon Sep 17 00:00:00 2001 From: lingh Date: Wed, 8 Jul 2026 18:15:10 +0800 Subject: [PATCH] Add seg_force_fg: saturated seg confidence overrides the hue-split demotion New rule 4 in fuse_trimap_directional: a chroma-unknown pixel whose seg confidence is >= trimap.seg_force_fg (default 0.98) stays sure-FG even when background-hued. Bright skin on a warm same-hue-family background (peach, pale yellow) was being demoted to unknown by the hue split and then lost -- either ViTMatte itself misfires there (raw 0.07-0.13 measured) or the chroma suppressor does (raw 0.89 halved to 0.45 via the RGB-proximity confidence path). The segmenter meanwhile rates those pixels a saturated 1.0; that semantic certainty now outranks same-hue colour suspicion. Safety: chroma_unknown excludes sure background, so a real flat backdrop can never be forced foreground; the silhouette band still re-opens the boundary; the cross-check veto still overrides trimap-FG. Set > 1.0 to disable. Verified (GPU bf16): yellow-bg frame face 0.84->1.00, peach-bg thigh 0.87->1.00 (residual softness only where chroma is near-sure-bg, by design). Pastel-blue TestImage3 regression: 0.047% of pixels differ with cross-check on, 0.058% without it (hair-gap suppression path), both visually negligible. Co-Authored-By: Claude Fable 5 --- bgfilter/settings.py | 8 ++++++++ bgfilter/trimap.py | 12 ++++++++++-- configs/default.yaml | 6 ++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bgfilter/settings.py b/bgfilter/settings.py index ea72a88..a360a83 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -40,6 +40,14 @@ class TrimapSettings: mode: str = "directional" seg_low: float = 0.15 # seg below this -> background-eligible; at/above -> foreground-eligible bg_hue_proj_min: float = 4.0 # Lab a*/b* projection onto bg direction above which a pixel is background-hued + # Semantic override for the directional hue split: a chroma-unknown pixel whose + # seg confidence is at/above this is kept sure-FG even when background-hued. + # A saturated seg signal outranks colour evidence that only says "same hue + # family as the background" (bright skin on a warm background, measured raw + # ViTMatte failures at proj ~7-10). Chroma sure-background is never overridden, + # and the silhouette band still re-opens the boundary for anti-aliasing. + # Set > 1.0 to disable. (seg masks are uint8-quantised: 0.98 = 250/255.) + seg_force_fg: float = 0.98 @dataclass(frozen=True) diff --git a/bgfilter/trimap.py b/bgfilter/trimap.py index bd03ff0..2e2813f 100644 --- a/bgfilter/trimap.py +++ b/bgfilter/trimap.py @@ -145,14 +145,21 @@ def fuse_trimap_directional( bg = chroma_bg | ((seg_mask < settings.seg_low) & ~chroma_fg) fg = chroma_fg & (seg_mask >= settings.seg_low) rule3_fg = chroma_unknown & core & ~bg_hued + # Rule 4 -- semantic override: a chroma-unknown pixel the segmenter is + # (near-)certain about stays foreground even when background-hued. Colour + # evidence here only says "same hue family as the background" (bright skin + # on a warm background), which is exactly where ViTMatte and the suppressor + # misfire; a saturated seg signal outranks it. chroma_unknown excludes sure + # background, so a real flat-colour backdrop can never be forced foreground. + force_fg = chroma_unknown & (seg_mask >= settings.seg_force_fg) if settings.mode == "directional-hard-bg": # Aggressive variant: hard-remove background-hued pixels instead of leaving # them unknown. Can eat cool/shadowed white cloth, so it is not the default. - bg = bg | (chroma_unknown & core & bg_hued) + bg = bg | (chroma_unknown & core & bg_hued & ~force_fg) trimap = np.full(shape, 128, dtype=np.uint8) trimap[bg] = 0 - trimap[fg | rule3_fg] = 255 + trimap[fg | rule3_fg | force_fg] = 255 loose = (seg_mask >= settings.seg_loose_threshold).astype(np.uint8) kernel = elliptical_kernel(band_radius) @@ -165,6 +172,7 @@ def fuse_trimap_directional( "sure_fg_pixels": int((trimap == 255).sum()), "band_radius": int(band_radius), "rule3_fg_pixels": int(rule3_fg.sum()), + "force_fg_pixels": int((force_fg & ~fg & ~rule3_fg).sum()), "bg_hued_pixels": int((bg_hued & chroma_unknown & core).sum()), } return trimap, stats diff --git a/configs/default.yaml b/configs/default.yaml index 69a71e0..c584ac8 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -46,6 +46,12 @@ trimap: mode: directional seg_low: 0.15 bg_hue_proj_min: 4.0 + # Semantic override (directional mode): chroma-unknown pixels with seg confidence + # at/above this stay sure-FG even when background-hued — rescues bright skin on a + # warm (same-hue-family) background that the hue split would otherwise hand to + # ViTMatte/suppression, which misfire exactly there. Never overrides chroma + # sure-background. Set > 1.0 to disable. + seg_force_fg: 0.98 alpha_post: chroma_suppress: true