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:
+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)
|
||||
|
||||
Reference in New Issue
Block a user