Add cross-model veto of hair-gap background residue (default on)
Colour-drifted background trapped between hair strands defeats every single-signal defence: the chroma key reads it as foreground (bgc ~0.07), the segmenter backs it, ViTMatte rates it opaque, and post-hoc removal is a proven dead end (it shreds the hair volume the same pixels belong to). A second, trimap-free matting model (BiRefNet_HR-matting) is the only tested model that separates this residue from the subject, so its opinion is fused in as a veto: min-fusion that may only LOWER alpha, restricted to the background-hued bright suspect zone (proj >= 3, L >= 45, feathered) and gated by primary-alpha confidence (0.70 -> 0.95 ramp) so soft wisps and dark hair are exempt by construction. - settings/config/CLI: cross_check block, --cross-check/--no-cross-check - alpha_post.cross_check_alpha after clean_alpha; second opinion reuses BiRefNetSegmenter; saved to debug as cross_check_alpha.png - chroma.bg_hue_projection extracted and shared with the trimap - docs: methodology.md (new), hair_gap_artifacts.md (investigation log) Verified: cross-check ON reproduces the visually-reviewed B1gate prototype byte-for-byte on TestImage3; --no-cross-check reproduces the previous baseline byte-for-byte; pink-bg FixImage1 face untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,21 @@ def convert_color_spaces(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
return rgb_f, lab
|
||||
|
||||
|
||||
def bg_hue_projection(
|
||||
lab: np.ndarray, lab_center: tuple[float, float, float]
|
||||
) -> np.ndarray:
|
||||
"""Per-pixel projection of Lab chroma (a*, b*) onto the background hue direction.
|
||||
|
||||
Positive = displaced toward the background hue, ~0 for neutral pixels. Returns
|
||||
zeros when the background is near-neutral (no meaningful hue direction).
|
||||
"""
|
||||
center = np.asarray(lab_center, dtype=np.float32)
|
||||
mag = float(np.hypot(center[1], center[2]))
|
||||
if mag < 1e-3:
|
||||
return np.zeros(lab.shape[:2], dtype=np.float32)
|
||||
return (lab[..., 1] * center[1] + lab[..., 2] * center[2]) / mag
|
||||
|
||||
|
||||
def parse_hex_color(text: str) -> tuple[float, float, float]:
|
||||
"""Parse ``#RRGGBB`` (or ``RRGGBB``) into an RGB triple normalised to 0..1."""
|
||||
s = text.strip().lstrip("#")
|
||||
|
||||
Reference in New Issue
Block a user