Add optional BiRefNet-gated intersection for the dropout-fill

Add seg_fill_intersect (default off) and seg_fill_gate_min to AlphaPostSettings, plus a gate_mask arg to fill_seg_dropouts. When enabled, the anime-seg dropout-fill is intersected with the reliable BiRefNet mask (>= gate_min): anime-seg proposes wisps at a low seg_fill_seg_min, but only rescues BiRefNet also endorses survive, trimming anime-seg's over-inclusive fringe while keeping its smooth value. Default off, so the shipped Option B (union) output is byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 14:47:09 +08:00
parent 9805b51b99
commit 87359ca304
3 changed files with 25 additions and 2 deletions
+13 -1
View File
@@ -88,7 +88,10 @@ def clean_alpha(
def fill_seg_dropouts(
alpha: np.ndarray, seg_mask: np.ndarray, settings: AlphaPostSettings
alpha: np.ndarray,
seg_mask: np.ndarray,
settings: AlphaPostSettings,
gate_mask: np.ndarray | None = None,
) -> np.ndarray:
"""Rescue alpha where ViTMatte dropped a region the semantic mask asserts.
@@ -98,12 +101,21 @@ def fill_seg_dropouts(
foreground, restore a faint alpha from the mask. Limited to those dropouts so
ViTMatte's sharper edges elsewhere are untouched, and gated by seg_min so the
mask's faint halo and the finger gap (low mask) are not filled.
``gate_mask`` (when ``seg_fill_intersect`` is on) is a second, reliable mask
-- BiRefNet -- required to also endorse the rescue (>= ``seg_fill_gate_min``).
``seg_mask`` (anime-seg, high recall) then only proposes wisps; the
intersection with BiRefNet drops its over-inclusive halo, while the rescued
*value* still comes from anime-seg's smooth mask (no grainy BiRefNet value).
"""
if not settings.seg_fill:
return alpha
out = alpha.astype(np.float32).copy()
rescued = settings.seg_fill_strength * np.clip(seg_mask, 0.0, 1.0)
dropout = (out < settings.seg_fill_alpha_max) & (seg_mask >= settings.seg_fill_seg_min)
# Intersection mode: keep only rescues the reliable mask also endorses.
if settings.seg_fill_intersect and gate_mask is not None:
dropout &= gate_mask >= settings.seg_fill_gate_min
# The segmentation mask carries scattered low-confidence noise; rescuing it
# verbatim sprinkles isolated specks (burrs) over the matte. Keep only
# connected structures (genuine wisps), drop the isolated specks.
+5 -1
View File
@@ -117,7 +117,11 @@ def _run_image(
alpha = clean_alpha(alpha, trimap, settings.alpha_post)
if seg_mask is not None:
fill_mask = pipeline._segment_fill(rgb, seg_mask)
alpha = fill_seg_dropouts(alpha, fill_mask, settings.alpha_post)
# seg_mask is the reliable trimap backend (BiRefNet); it gates the fill
# when seg_fill_intersect is on (fill_mask ∧ seg_mask).
alpha = fill_seg_dropouts(
alpha, fill_mask, settings.alpha_post, gate_mask=seg_mask
)
foreground = estimate_foreground_rgb(
rgb, alpha, bg_confidence, model, settings.foreground
)
+7
View File
@@ -49,6 +49,13 @@ class AlphaPostSettings:
seg_fill_seg_min: float = 0.10
seg_fill_strength: float = 0.70
seg_fill_min_area_ratio: float = 0.00002 # drop rescued specks below this area
# Intersection fill: gate the (anime-seg) dropout-fill by a second, reliable
# mask (BiRefNet). anime-seg proposes freely at a low seg_fill_seg_min; only
# rescues BiRefNet also endorses (>= seg_fill_gate_min, its threshold left
# unchanged) survive. Trims anime-seg's over-inclusive halo to the structure
# both backends agree on, without reintroducing BiRefNet's grainy value.
seg_fill_intersect: bool = False
seg_fill_gate_min: float = 0.10
@dataclass(frozen=True)