diff --git a/README.md b/README.md index c793047..86f99ff 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,14 @@ Pass `--no-chroma` (or `chroma.enabled: false`) to matte images whose background **not** one flat colour — gradients, textures, scenes. There is no colour key in this mode: the segmentation mask alone drives the trimap (mode forced to `seg`), ViTMatte still refines the unknown band at full resolution, and pymatting still -estimates edge foreground colour. Every colour-keyed stage is bypassed: background -auto-detection, the directional hue split, chroma alpha suppression, despill, and -the cross-check veto (with `--cross-check-as-seg` the second model still provides -the segmentation mask). Requires the segmentation pipeline; quality then rests -entirely on the segmenter's mask, so expect flat-background results to stay -stronger on hair-level detail. +estimates edge foreground colour. The colour-keyed stages are bypassed: background +auto-detection, the directional hue split, chroma alpha suppression, and despill. +The cross-model veto still runs (disable with `--no-cross-check`): without a key +colour its suspect zone drops the hue test and instead only vetoes where the +second opinion is itself confidently near-empty (`cross_check.second_lo/hi`), so +thin strands the second model merely blurs are never eroded. Requires the +segmentation pipeline; quality then rests entirely on the segmenter's mask, so +expect flat-background results to stay stronger on hair-level detail. ## Background colour diff --git a/bgfilter/alpha_post.py b/bgfilter/alpha_post.py index 8f40a66..d6f52b5 100644 --- a/bgfilter/alpha_post.py +++ b/bgfilter/alpha_post.py @@ -59,7 +59,7 @@ def suppress_alpha_by_chroma( def cross_check_alpha( alpha: np.ndarray, second_alpha: np.ndarray, - proj: np.ndarray, + proj: np.ndarray | None, lightness: np.ndarray, trimap: np.ndarray, settings: CrossCheckSettings, @@ -76,6 +76,13 @@ def cross_check_alpha( - the primary alpha is high (``gate_lo -> gate_hi`` ramp): pixels the pipeline already renders soft (outer wisps) are exempt by construction. + ``proj`` is None in complex-background mode (chroma disabled): no key colour + means no hue-defined suspect zone, so instead the veto requires the second + opinion itself to be confidently near-empty (full strength at/below + ``second_lo``, none at/above ``second_hi``). A thin strand the downsampled + second model merely blurs to mid-alpha is left untouched; only regions it + decisively rejects can be cleared. + Inside the zone this deliberately overrides the trimap-FG clamp -- the residue it exists to clear is mostly trimap-FG. Sure background cannot be disturbed: min-fusion keeps alpha 0 at 0. @@ -83,16 +90,16 @@ def cross_check_alpha( if not settings.enabled: return alpha cv2 = require_cv2() - zone = ( - (proj >= settings.proj_min) - & (lightness >= settings.l_min) - & (trimap != 0) - ) + zone = (lightness >= settings.l_min) & (trimap != 0) + if proj is not None: + zone &= proj >= settings.proj_min weight = zone.astype(np.float32) if settings.feather_sigma > 0: blur = cv2.GaussianBlur(weight, (0, 0), settings.feather_sigma) weight = np.where(zone, 1.0, np.clip(blur, 0.0, 1.0)).astype(np.float32) gate = weight * _smoothstep(alpha, settings.gate_lo, settings.gate_hi) + if proj is None: + gate = gate * (1.0 - _smoothstep(second_alpha, settings.second_lo, settings.second_hi)) out = alpha * (1.0 - gate) + np.minimum(alpha, second_alpha) * gate return np.clip(out, 0.0, 1.0).astype(np.float32) diff --git a/bgfilter/pipeline.py b/bgfilter/pipeline.py index f6d2880..b0aeaad 100644 --- a/bgfilter/pipeline.py +++ b/bgfilter/pipeline.py @@ -199,16 +199,16 @@ def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult: raw_alpha=raw_alpha if alpha_source == "vitmatte" else None, ) alpha = clean_alpha(alpha, trimap, settings.alpha_post) - # The veto's suspect zone is defined by background hue, so it needs the key - # colour; without chroma the second opinion still serves as seg (reuse above). - if settings.cross_check.enabled and settings.chroma.enabled: + if settings.cross_check.enabled: lab = convert_color_spaces(rgb)[1] if second_alpha is None: second_alpha = pipeline._second_opinion(rgb) alpha = cross_check_alpha( alpha, second_alpha, - bg_hue_projection(lab, model.lab_center), + # No key colour in complex-background mode: proj=None switches the + # veto to its second-opinion-confidence gate (see cross_check_alpha). + bg_hue_projection(lab, model.lab_center) if model is not None else None, lab[..., 0], trimap, settings.cross_check, diff --git a/bgfilter/settings.py b/bgfilter/settings.py index eb516b9..87e0a27 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -6,12 +6,12 @@ from dataclasses import dataclass @dataclass(frozen=True) class ChromaSettings: # False = non-flat-background mode (--no-chroma): no colour key at all. - # Segmentation alone drives the trimap (mode forced to "seg"), and every - # colour-keyed stage is bypassed: background auto-detect, the directional - # hue split, chroma alpha suppression, despill, the cross-check veto (its - # suspect zone is background-hued by definition; with reuse_as_seg the - # second opinion still serves as the segmenter), and the unmix foreground - # fallback. Requires segmentation.enabled and matting_method 'vitmatte'. + # Segmentation alone drives the trimap (mode forced to "seg"), and the + # colour-keyed stages are bypassed: background auto-detect, the directional + # hue split, chroma alpha suppression, despill, and the unmix foreground + # fallback. The cross-check veto still runs (if enabled) with a hue-free + # gate -- see CrossCheckSettings.second_lo/hi. Requires + # segmentation.enabled and matting_method 'vitmatte'. enabled: bool = True border_ratio: float = 0.04 min_samples: int = 2048 @@ -107,6 +107,13 @@ class CrossCheckSettings: feather_sigma: float = 2.0 # Gaussian feather of the zone boundary, in px gate_lo: float = 0.70 # primary alpha below this -> fully exempt gate_hi: float = 0.95 # primary alpha above this -> fully vetoable + # Complex-background mode only (chroma disabled -> no hue-defined suspect + # zone): the veto instead requires the second opinion itself to be + # confidently near-empty -- full strength at/below second_lo, none at/above + # second_hi. Thin structures the downsampled second model merely blurs to + # mid-alpha stay untouched; only decisively-rejected regions can be cleared. + second_lo: float = 0.15 + second_hi: float = 0.40 @dataclass(frozen=True) diff --git a/configs/default.yaml b/configs/default.yaml index 38daf31..76ab6bb 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -5,9 +5,10 @@ screen_color: null chroma: # false = non-flat background mode (--no-chroma): no colour key; segmentation - # alone drives the trimap (mode forced to "seg") and every colour-keyed stage - # is bypassed (auto-detect, hue split, chroma suppression, despill, the - # cross-check veto). Needs segmentation.enabled and matting_method vitmatte. + # alone drives the trimap (mode forced to "seg") and the colour-keyed stages + # are bypassed (auto-detect, hue split, chroma suppression, despill). The + # cross-check veto still runs if enabled, using its hue-free gate (see + # cross_check.second_lo/hi). Needs segmentation.enabled + matting vitmatte. enabled: true model: @@ -98,6 +99,12 @@ cross_check: feather_sigma: 2.0 # zone-boundary feather, px gate_lo: 0.70 # primary alpha below this -> fully exempt gate_hi: 0.95 # primary alpha above this -> fully vetoable + # Complex-background (no-chroma) mode only: no hue-defined suspect zone, so + # the veto requires the second opinion itself to be confidently near-empty -- + # full strength at/below second_lo, fading to none at/above second_hi. Thin + # strands the 2048-input second model merely blurs stay untouched. + second_lo: 0.15 + second_hi: 0.40 foreground: enabled: true