Add bgfilter/recolor.py: after despill, propagate clean neighbour chroma into
pixels whose colour is still off (residual green, or the magenta a green unmix
overshoots into), keeping luminance partway so hair texture survives. It installs
a real observed colour rather than subtracting, so it cannot overshoot. Despill
already neutralises the matte edge (off ~ 0 there), so recolor only touches the
contaminated interior and the wisps the dropout-fill rescued.
Runs as a final colour pass on the despilled foreground; pymatting + despill keep
their stronger edge handling. On the samples the magenta fringe drops sharply
(TestImage vis-magenta 0.065->0.014, TestImage2 0.085->0.031) with edge green
unchanged (vis-green ~0.02). Debug output color_mask.png replaces the despill /
foreground-background / foreground-correction maps; smoke updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add fill_seg_dropouts: where ViTMatte's alpha is ~0 but BiRefNet confidently
asserts foreground, restore a faint alpha from the mask (strength * seg_mask).
This recovers green-contaminated wisps that ViTMatte cuts because they are
nearly the background colour, while leaving ViTMatte's sharper edges elsewhere
untouched (gated to true dropouts) and not refilling the finger gap (low mask).
On the TestImage2 shoulder, faint wisps ViTMatte cut drop from 72% to 33%; the
finger gap stays transparent. The rescued wisps are still green-tinted -- the
recolor pass (next) restores their colour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route faint hair (BiRefNet M 0.08-0.15) into the unknown band instead of sure
background, giving ViTMatte a chance at it. On its own this does not recover the
green-contaminated shoulder wisps: those are ~pure green (dominance +0.88, like
the screen) so ViTMatte cuts them regardless of trimap -- that is handled by the
upcoming segmentation dropout-fill. The finger gap (M~0.02) stays sure background.
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>
Adds bgfilter/segmentation.py: BiRefNetSegmenter produces a soft subject mask
that defines topology -- it includes colour-contaminated foreground (green-tinted
hair) and excludes see-through holes (finger gaps), the distinction chroma/colour
cues cannot make. Validated on both samples: green hair -> foreground, finger gap
-> background, both correct.
Foundation of the semantic-guided redesign; the mask will drive trimap
construction next, with ViTMatte still refining soft edges.
- SegmentationSettings (ZhengPeng7/BiRefNet, 1024 input, fp32).
- Deps: torchvision, timm, einops, kornia. Weights load from HuggingFace via
trust_remote_code; behind a firewall set HF_ENDPOINT=https://hf-mirror.com and
bypass the proxy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This reverts commit f7e12d3.
The carve set alpha to 0 on a colour-thresholded mask across the whole
foreground, which cannot distinguish background green from green the AI
generator bled onto the subject. It therefore (1) deleted green-tinted hair
strands and (2) hard-cut the green-spill silhouette edge, replacing ViTMatte's
anti-aliased edge with a jagged one. Local cues (surround, opacity, thinness,
connectivity, neighbour colour) could not reliably separate green hair from
green holes, so the mask is not fixable by tuning. Reverting restores intact
hair and smooth edges; the finger-gap green stays a minor residual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The green in the finger gap survives keying because it is dark and desaturated
(brightness ~0.58, hue-shifted), so chroma confidence reads ~0 and the trimap
leaves it in the unknown band, where ViTMatte fills the pocket as foreground.
It is also connected to the exterior screen (an open notch), so neither
confidence suppression nor classic hole-filling removes it.
Add carve_background_holes (doc section 10): a pixel that is clearly
green-dominant in the input *and* mostly surrounded by genuine (non-green)
foreground is set to alpha 0. The surround cue separates such an intrusion
(finger gaps, inter-strand pockets) from the outer hair edge, which has
background on one side and is preserved. Runs after alpha cleanup.
On TestImage the finger-gap survivors (green & alpha>0.5) drop 170 -> 28 with
no visible loss of hair wisps; inter-strand green pockets clear too. Knobs in
AlphaPostSettings (hole_*, default on, surround_min 0.4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull alpha toward 0 in the unknown band where bg_confidence is high, so green
that survives in hair gaps and hole pockets goes transparent (doc section 10).
Runs between trimap enforcement and alpha cleanup; sure-foreground pixels are
never touched, keyed by the bg_confidence we already compute.
On the two samples this clears the faint outer green halo (edge pixels
19.3k -> 16.0k) but has little *visible* effect, because their residual is no
longer green: it is magenta from the pymatting unmix on genuine semi-transparent
hair (vis-magenta p95 ~0.07, identical pre/post despill), which this pass
deliberately leaves alone. The magenta needs a separate colour fix.
Knobs live in AlphaPostSettings (chroma_suppress*, default on).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the full-foreground despill (5ddf0cd) the residual green was confined to
semi-transparent hair (alpha < 0.5): 85% of the worst pixels had alpha < 0.3,
where a pixel-wise unmix (divide by small alpha) is too noisy to trust.
Replace the hand-rolled unmix/local-blur estimator with pymatting's
estimate_foreground_ml (Germer et al. multi-level closed form), which propagates
reliable foreground colour from high-alpha neighbours into the fringe and
estimates the background, so green spill is unmixed rather than clamped. The
despill pass stays as a light cleanup on top.
- foreground.method selects "ml" (default) or "unmix" (legacy heuristic kept as
a fallback when pymatting is unavailable, matching the chroma fallback idiom).
- ForegroundEstimate now exposes rgb (F), background (B) and a correction map;
debug outputs become foreground_rgb / foreground_background / foreground_correction.
Controlled comparison (same ViTMatte alpha, only foreground method changed),
edge_green_excess:
TestImage p95 0.176->0.031, mean 0.061->0.005
TestImage2 p95 0.165->0.004, mean 0.052->0.002
Adds pymatting (pulls in numba) to requirements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Green spill bleeds into pixels ViTMatte marks as solid foreground (alpha~1),
but despill was gated to the soft edge band (0.005<alpha<0.995) and weakened
exactly there: the alpha tent peaked at 0.5 and bg_confidence (low on the
character edge) further suppressed the correction.
- (1) Apply despill to the whole foreground (alpha > edge_low) instead of the
soft band only; neutral pixels stay untouched via the green-excess term.
- (2) Weight by alpha at full strength across the opaque range (alpha>=0.5),
easing off only where mostly transparent, instead of a tent peaking at 0.5.
- (3) Drop bg_confidence as a despill gate; localise purely by green excess.
Controlled comparison (same alpha, only despill changed), edge_green_excess:
ViTMatte TestImage p95 0.239->0.176, mean 0.131->0.061
ViTMatte TestImage2 p95 0.231->0.165, mean 0.115->0.052
Removes now-unused DespillSettings fields edge_high and bg_confidence_weight.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>