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>
New rule 4 in fuse_trimap_directional: a chroma-unknown pixel whose seg
confidence is >= trimap.seg_force_fg (default 0.98) stays sure-FG even when
background-hued. Bright skin on a warm same-hue-family background (peach,
pale yellow) was being demoted to unknown by the hue split and then lost --
either ViTMatte itself misfires there (raw 0.07-0.13 measured) or the
chroma suppressor does (raw 0.89 halved to 0.45 via the RGB-proximity
confidence path). The segmenter meanwhile rates those pixels a saturated
1.0; that semantic certainty now outranks same-hue colour suspicion.
Safety: chroma_unknown excludes sure background, so a real flat backdrop
can never be forced foreground; the silhouette band still re-opens the
boundary; the cross-check veto still overrides trimap-FG. Set > 1.0 to
disable.
Verified (GPU bf16): yellow-bg frame face 0.84->1.00, peach-bg thigh
0.87->1.00 (residual softness only where chroma is near-sure-bg, by
design). Pastel-blue TestImage3 regression: 0.047% of pixels differ with
cross-check on, 0.058% without it (hair-gap suppression path), both
visually negligible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- chroma: drop BackgroundModel.hsv_center/hue_sigma (unused since the green gating
went away), their estimation, _hue_distance, and the now-unused hsv channel from
convert_color_spaces (one fewer cvtColor per image).
- despill: delete the dead green-channel-algebra path (the pipeline always passes a
colour) and fold the remaining Lab-direction despill into a single despill() with
no screen_color arg (127 -> 60 lines). Renamed despill_green -> despill.
- settings: remove the now-dead ChromaSettings.hue_sigma_min and
DespillSettings.green_excess_margin (plus the matching config lines).
- docs/comments: generalise stale "green" wording to "background colour" (module
docstring, alpha_post/trimap/foreground/settings) and rename the trimap variable
green_bg -> screen_bg.
Kept: the unmix foreground fallback, --matting-method chroma, and green/blue as
illustrative examples. Verified: package compiles, both configs parse, and an
auto-detect end-to-end run is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the two experimental bools (directional, bg_hued_to_bg) with a single
TrimapSettings.mode selector for the segmentation pipeline:
- "directional" (new default): chroma magnitude + seg + a Lab hue-direction
split. In the chroma-unknown zone a confidently-segmented pixel stays
foreground unless it is displaced toward the background hue, so a neutral
background-coloured garment (e.g. a white shirt) is kept while a background-
hued residual (blue between hair strands) is left unknown for ViTMatte /
chroma-suppress to clear.
- "seg": original fuse_trimap, unchanged.
- "directional-hard-bg": aggressive variant that hard-removes background-hued
pixels (can eat cool/shadowed white cloth).
Selectable via configs/default.yaml (trimap.mode) or CLI --trimap-mode; unknown
modes raise. Default CLI output verified byte-identical to the reviewed UNK
result on the pastel sample.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire BiRefNet into the pipeline (lazy-loaded) and build the trimap from the
semantic subject mask refined by the green key, used when segmentation is
enabled. generate_trimap (chroma-only) stays as the no-segmentation fallback.
Authority split: the mask decides subject topology (keeps green-tinted hair as
foreground, drops see-through holes), the bright-green key sharpens the screen
boundary, ViTMatte refines a small protective band. Background follows the mask
directly so interior holes survive; the band is small so it does not swallow
finger gaps.
End-to-end on the samples: green hair preserved, finger-gap green 99%
transparent (BiRefNet marks it background; was 100% foreground before), leg
edges smooth -- topology/colour/edge problems local cues could not solve, with
no hard alpha surgery.
- SegmentationSettings wired into PipelineSettings/config; --device propagates.
- TrimapSettings: seg_core_threshold, seg_loose_threshold.
- New debug output seg_mask.png.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>