Drop the silhouette band overwrite in the directional trimap

The band (dilate-minus-erode ring on the seg silhouette, added in 5f48f7a)
was meant to give ViTMatte room to anti-alias the boundary. Measured on a
warm-background sample (fix_image02, #FFD6C2): ViTMatte outputs near-binary
alpha in the band on crisp-edged anime images (96% hard values, 82% of
silhouette crossings with zero soft pixels), so the band delivered no
anti-aliasing -- it only let ViTMatte relocate the boundary (chewed edges)
and wholly confiscated structures thinner than the ring: 74% of lost hair
strand pixels were rule-FG demoted by the overwrite, then dropped by
ViTMatte. Without the band, 88% of those strands survive, edges follow the
rule-decided contour, and cross-check no longer bites strand edges because
strand cores stay anchored at 255 (re-destroyed: 0 px).

The rules own unknown pixels (background-hued blends, low-seg transitions)
remain ViTMatte territory. fuse_trimap (seg mode) keeps its band untouched --
its rules were designed around it and the removal is only validated for the
directional path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 14:10:08 +00:00
parent 78ef411ad2
commit f1a083349b
+6 -14
View File
@@ -123,16 +123,14 @@ def fuse_trimap_directional(
strongly displaced toward it (e.g. blue between hair strands) is left
unknown for ViTMatte / chroma-suppress. Lower-seg unknowns stay unknown.
A thin unknown band at the segmentation silhouette is preserved so ViTMatte can
anti-alias the boundary (it is deliberately small so interior holes survive).
No silhouette band is forced back to unknown: structures thinner than the band
ring (flying hair strands) got wholly confiscated by dilate-minus-erode and then
dropped by ViTMatte, which on crisp-edged (anime) images outputs near-binary
alpha in the band instead of anti-aliasing (measured: 96% hard values, and 74%
of lost strand pixels were rule-FG demoted by the band). The rules' own unknown
pixels (background-hued blends, low-seg transitions) remain ViTMatte's domain.
"""
cv2 = require_cv2()
shape = seg_mask.shape
fg_safe_radius = radius_from_ratio(
shape, settings.fg_safe_radius_ratio, settings.min_fg_safe_radius
)
band_radius = max(settings.min_fg_safe_radius, fg_safe_radius // 2)
chroma_bg = bg_confidence >= settings.sure_bg_threshold
chroma_fg = bg_confidence <= settings.sure_fg_threshold
chroma_unknown = ~chroma_bg & ~chroma_fg
@@ -161,16 +159,10 @@ def fuse_trimap_directional(
trimap[bg] = 0
trimap[fg | rule3_fg | force_fg] = 255
loose = (seg_mask >= settings.seg_loose_threshold).astype(np.uint8)
kernel = elliptical_kernel(band_radius)
band = cv2.dilate(loose, kernel).astype(bool) & ~cv2.erode(loose, kernel).astype(bool)
trimap[band & ~chroma_bg] = 128
stats = {
"sure_bg_pixels": int((trimap == 0).sum()),
"unknown_pixels": int((trimap == 128).sum()),
"sure_fg_pixels": int((trimap == 255).sum()),
"band_radius": int(band_radius),
"rule3_fg_pixels": int(rule3_fg.sum()),
"force_fg_pixels": int((force_fg & ~fg & ~rule3_fg).sum()),
"bg_hued_pixels": int((bg_hued & chroma_unknown & core).sum()),