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:
+42
-5
@@ -6,13 +6,23 @@ from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .alpha_post import clean_alpha, enforce_trimap, suppress_alpha_by_chroma
|
||||
from .chroma import compute_bg_confidence, parse_hex_color
|
||||
from .alpha_post import (
|
||||
clean_alpha,
|
||||
cross_check_alpha,
|
||||
enforce_trimap,
|
||||
suppress_alpha_by_chroma,
|
||||
)
|
||||
from .chroma import (
|
||||
bg_hue_projection,
|
||||
compute_bg_confidence,
|
||||
convert_color_spaces,
|
||||
parse_hex_color,
|
||||
)
|
||||
from .despill import despill
|
||||
from .foreground import estimate_foreground_rgb
|
||||
from .io import load_rgb, save_gray, save_rgb, save_rgba, write_text
|
||||
from .qa import BACKGROUND_COLORS, composite, make_qa_grid, save_previews
|
||||
from .settings import PipelineSettings
|
||||
from .settings import PipelineSettings, SegmentationSettings
|
||||
from .trimap import (
|
||||
fuse_trimap,
|
||||
fuse_trimap_directional,
|
||||
@@ -27,6 +37,7 @@ class MattingPipeline:
|
||||
self.settings = settings
|
||||
self._runner: ViTMatteRunner | None = None
|
||||
self._segmenter = None
|
||||
self._cross_checker = None
|
||||
|
||||
def _segment(self, rgb: np.ndarray) -> np.ndarray:
|
||||
if self._segmenter is None:
|
||||
@@ -35,6 +46,20 @@ class MattingPipeline:
|
||||
self._segmenter = make_segmenter(self.settings.segmentation)
|
||||
return self._segmenter.mask(rgb)
|
||||
|
||||
def _second_opinion(self, rgb: np.ndarray) -> np.ndarray:
|
||||
if self._cross_checker is None:
|
||||
from .segmentation import BiRefNetSegmenter
|
||||
|
||||
check = self.settings.cross_check
|
||||
self._cross_checker = BiRefNetSegmenter(
|
||||
SegmentationSettings(
|
||||
model_name=check.model_name,
|
||||
device=self.settings.segmentation.device,
|
||||
input_size=check.input_size,
|
||||
)
|
||||
)
|
||||
return self._cross_checker.mask(rgb)
|
||||
|
||||
def _predict_alpha(self, rgb: np.ndarray, trimap: np.ndarray, bg_confidence: np.ndarray) -> tuple[np.ndarray, str]:
|
||||
if self.settings.model.matting_method == "chroma":
|
||||
return trimap_to_alpha_seed(trimap, bg_confidence), "chroma"
|
||||
@@ -89,8 +114,6 @@ def _run_image(
|
||||
if mode == "seg":
|
||||
trimap, trimap_stats = fuse_trimap(seg_mask, bg_confidence, settings.trimap)
|
||||
elif mode in ("directional", "directional-hard-bg"):
|
||||
from .chroma import convert_color_spaces
|
||||
|
||||
lab = convert_color_spaces(rgb)[1]
|
||||
trimap, trimap_stats = fuse_trimap_directional(
|
||||
seg_mask, bg_confidence, lab, model.lab_center, settings.trimap
|
||||
@@ -113,6 +136,18 @@ def _run_image(
|
||||
raw_alpha=raw_alpha if alpha_source == "vitmatte" else None,
|
||||
)
|
||||
alpha = clean_alpha(alpha, trimap, settings.alpha_post)
|
||||
second_alpha = None
|
||||
if settings.cross_check.enabled:
|
||||
lab = convert_color_spaces(rgb)[1]
|
||||
second_alpha = pipeline._second_opinion(rgb)
|
||||
alpha = cross_check_alpha(
|
||||
alpha,
|
||||
second_alpha,
|
||||
bg_hue_projection(lab, model.lab_center),
|
||||
lab[..., 0],
|
||||
trimap,
|
||||
settings.cross_check,
|
||||
)
|
||||
foreground = estimate_foreground_rgb(
|
||||
rgb, alpha, bg_confidence, model, settings.foreground
|
||||
)
|
||||
@@ -137,6 +172,8 @@ def _run_image(
|
||||
save_gray(debug / "trimap.png", trimap)
|
||||
if seg_mask is not None:
|
||||
save_gray(debug / "seg_mask.png", seg_mask)
|
||||
if second_alpha is not None:
|
||||
save_gray(debug / "cross_check_alpha.png", second_alpha)
|
||||
save_gray(debug / "alpha.png", alpha)
|
||||
save_rgb(debug / "foreground_rgb.png", corrected_rgb)
|
||||
save_gray(debug / "color_mask.png", color_mask)
|
||||
|
||||
Reference in New Issue
Block a user