Run the cross-check veto in complex-background mode with a hue-free gate
The veto's suspect zone was defined by background-hue projection, so --no-chroma silently skipped it. Without a key colour there is no hue test to lean on, so in complex mode the gate instead requires the second opinion itself to be confidently near-empty (second_lo -> second_hi ramp, default 0.15 -> 0.40): regions the HR-matting model decisively rejects can be cleared, while thin strands it merely blurs to mid-alpha are untouched -- protecting exactly the crisp-strand advantage the pipeline has over a raw BiRefNet mask. Flat mode's gate is unchanged. Validation (BG_IMAGE01-04, cuda fp32): flat default path bit-identical; complex-mode veto touches 0.007-0.025% of pixels, visibly clearing blurred residue near strands and milky specks in hair gaps with no strand erosion. Costs the HR-matting forward in --no-chroma runs (0.61 -> 1.6 s/image warm GPU); disable with --no-cross-check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+13
-6
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-6
@@ -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)
|
||||
|
||||
+10
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user