20 Commits

Author SHA1 Message Date
lhk229 2b3b62ed3f Run the cross-check veto in complex-background mode with a hue-free gate
The veto's suspect zone was defined by background-hue projection, so
--no-chroma silently skipped it. Without a key colour there is no hue
test to lean on, so in complex mode the gate instead requires the second
opinion itself to be confidently near-empty (second_lo -> second_hi ramp,
default 0.15 -> 0.40): regions the HR-matting model decisively rejects
can be cleared, while thin strands it merely blurs to mid-alpha are
untouched -- protecting exactly the crisp-strand advantage the pipeline
has over a raw BiRefNet mask. Flat mode's gate is unchanged.

Validation (BG_IMAGE01-04, cuda fp32): flat default path bit-identical;
complex-mode veto touches 0.007-0.025% of pixels, visibly clearing
blurred residue near strands and milky specks in hair gaps with no
strand erosion. Costs the HR-matting forward in --no-chroma runs
(0.61 -> 1.6 s/image warm GPU); disable with --no-cross-check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:09:57 +08:00
lhk229 e61fbe84f0 Add non-flat background mode (--no-chroma / chroma.enabled: false)
No colour key at all: segmentation alone drives the trimap (mode forced
to 'seg', whose chroma terms degrade to no-ops on a zero background-
confidence map), ViTMatte still refines the unknown band at full
resolution, and pymatting still estimates edge foreground colour. Every
colour-keyed stage is bypassed: background auto-detect, the directional
hue split, chroma alpha suppression, despill, the cross-check veto (its
suspect zone is background-hued by definition; with reuse_as_seg the
second opinion still serves as the segmenter), and the unmix fallback.

Guards: requires segmentation.enabled and matting_method 'vitmatte'
(clear errors otherwise); despill/foreground handle model=None.

Validation: flat-background default path is bit-identical pre/post
(alpha and RGB |D|max = 0 on TestImage3). On a known-alpha subject
composited over a gradient+blotch background, recovered alpha scores
MAE 0.0018 / IoU@0.5 0.993 (0.995 with --cross-check-as-seg); the same
input correctly fails auto-detection in default mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:13:08 +08:00
lhk229 f22af16517 Align defaults with server-edition: reuse-as-seg and despill both OFF
Adopt server-edition's settings.py and default.yaml verbatim (zero diff on
these files between branches now):

- reuse_as_seg: false. The reuse equivalence holds for solid figures but
  breaks on thin pale strokes over a low-contrast background: HR-matting
  (trained on P3M-10k portraits + AM-2k animals) reads them as
  semi-transparent wisps, and as the trimap topology source it destroys
  white calligraphy (TestImage4: stroke alpha 0.92 -> 0.51, 48% of stroke
  pixels below 0.5) that the DIS5K-trained segmenter keeps solid (seg 0.97
  vs 0.40). As the gated cross-check veto the same signal stays harmless
  (hue gate excludes neutral strokes; confidence gate exempts soft edges).
  Opt in with --cross-check-as-seg for character batches.

- despill.enabled: false (mirrors server ba5d667): no positional/semantic
  guard, desaturates and hue-shifts subjects sharing the background's hue.

README updated accordingly; the performance table notes its numbers were
measured with reuse on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:37:02 +08:00
lhk229 72655dea91 Gate seg-reuse to the birefnet backend
reuse_as_seg swaps one BiRefNet-family model for another (the validated
equivalence); it must not silently override an explicitly chosen anime-seg
backend. With --seg-backend anime-seg the segmenter now keeps its own
forward and the cross-check veto runs independently on top -- both
combinable, no flag juggling. Default birefnet path is untouched
(verified byte-identical); anime-seg + cross-check verified to run both
models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:52:21 +08:00
lhk229 10134ad492 Drop dead detect_background_color; sync docs with landed optimizations
- chroma.py: remove detect_background_color — orphaned since auto-detection
  goes through estimate_background_model -> _background_border_cluster;
  nothing in the repo calls it.
- settings.py: fix stale "Segmentation always stays fp32" comment
  (--precision now fans out to the BiRefNet models too).
- README: add a measured CPU performance section (9700X reference numbers,
  memory ceiling explanation, Zen 2 fallback guidance).
- docs/hair_gap_artifacts.md: record that the cross-check cost note is
  obsolete — reuse_as_seg returns the net model count to 2, bf16 and
  chunked attention absorb the rest.
- docs/green_screen_matting_workflow.md: add the 2026-07 additions to the
  architecture-evolution note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:45:06 +08:00
lhk229 2cc0ce8684 Chunk ViTMatte's global attention to cap its N^2 memory spike
ViTMatte's VitDet backbone runs 4 global attention blocks that materialize
the full [heads x N x N] map: ~19 GB transient at 2048x2048 (16384 tokens),
the pipeline's memory peak. transformers has no SDPA path for this
architecture (the decomposed rel-pos bias is added to raw scores), so
compute the same attention in query-row chunks instead: the bias
factorizes over query rows, making the chunked form mathematically exact
-- output verified BITWISE-identical (unit: fp32/bf16 x 3 sizes; full
pipeline: TestImage3 fp32 and 2048x2048 bf16, all byte-equal).

model.attn_query_chunk (default 2048, 0 = stock one-shot) engages only on
blocks seeing more tokens than the chunk size, so window blocks keep the
original path. Measured @2048x2048 bf16 (9700X): ViTMatte spike
19.8 -> 4.0 GB for ~15% more ViTMatte time; whole pipeline peak
21.4 -> 11.4 GB, warm 51 -> 53 s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:26:59 +08:00
lhk229 9a9084b3d6 Cut pipeline cost: seg-reuse, unified bf16 knob, eager mimalloc purge
Three optimizations from profiling the cross-check-dominated pipeline
(9700X CPU, all pilot-validated on TestImage3/FixImage1):

- Reuse the cross-check HR-matting@2048 forward as the segmentation mask
  (cross_check.reuse_as_seg, default ON; --no-cross-check-as-seg to opt
  out). Skips the BiRefNet@1024 load+forward entirely: ~66s -> ~45s,
  one less 0.9GB model. Trimap 99.8% identical, no structural change.

- --precision bf16 now fans out to all three models: ViTMatte keeps its
  weight cast; both BiRefNets run their forward under autocast with a
  dispatcher-level AutocastCPU fp32 shim for torchvision::deform_conv2d
  (no bf16 CPU kernel, no autocast wrapper upstream). Shared hardware
  gate in bgfilter/precision.py falls back to fp32 off native-bf16
  hardware. TestImage3: 51.9s -> 33.1s; alpha diff max 0.15, none >0.25.

- MIMALLOC_PURGE_DELAY=0 (bgfilter/__init__.py, before torch loads):
  Windows torch's bundled mimalloc lazily retains ~10GB of freed
  BiRefNet activations, stacking under ViTMatte's attention peak.
  2048x2048 bf16: peak 25.2 -> 21.4GB and slightly faster (73 -> 63s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:15:12 +08:00
lhk229 dfc85566c7 Add bf16 precision option for ViTMatte (--precision, default fp32)
bf16 halves the matting model's activation memory -- its full-resolution
attention is the pipeline's memory peak -- with visually identical alpha
(measured: 0 px alpha deviation > 0.25 on samples; cross-check veto
behaviour unchanged, region IoU 0.984).

Guarded by a hardware check so it never lands on a slow emulation path:
CUDA requires torch.cuda.is_bf16_supported(); CPU requires the same
oneDNN native-bf16 gate PyTorch uses for matmul routing (AVX512-BF16/
AMX). Without support it warns and falls back to fp32 -- on a Zen2 EPYC
the fallback kernels measured 17-370x slower than fp32, so silent bf16
there would be a performance landmine.

Segmentation stays fp32: torchvision deform_conv2d (used by BiRefNet)
has no bf16 CPU kernel, and the segmenter is not the memory peak.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:32:51 +08:00
lhk229 adbbb9d620 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>
2026-07-04 19:52:24 +08:00
lhk229 cf0ceec496 Remove redundant configs/birefnet.yaml; drop a stray --config example
BiRefNet is the default backend and --seg-backend switches segmenters, so the
birefnet.yaml config (a copy of default.yaml with backend: birefnet) no longer had
a purpose. configs/ now holds just default.yaml, the editable parameter template.
Also dropped the now-redundant --config from the chroma-debug README example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:11:47 +08:00
lhk229 3f66f66ec7 Align default ViTMatte model to base; make --config optional
The ModelSettings.model_name dataclass default was still vitmatte-small (the
phase-1 "start small" choice) while configs/default.yaml used vitmatte-base, so a
bare run (no --config) silently differed from every documented example. Set the
dataclass default to base too: the built-in defaults now match default.yaml
field-for-field, so --config is only needed after editing that file to tune
parameters. README examples drop the now-redundant --config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:09:55 +08:00
lhk229 a6d84845c6 Default to BiRefNet + CPU; add --seg-backend to pick the segmenter
- Segmentation backend now defaults to birefnet (ZhengPeng7/BiRefNet); anime-seg is
  selected at runtime with --seg-backend anime-seg, which also swaps in the matching
  weights (unknown backends raise). Config files stay for detailed tuning, not
  backend selection.
- Default device is now cpu everywhere (dataclass defaults, default.yaml,
  birefnet.yaml); pass --device cuda for GPU. Avoids a hard CUDA-required failure on
  machines without a CUDA-enabled PyTorch build.
- Docs: README + workflow status note updated (backend via --seg-backend, CPU
  default, examples no longer force --device cuda).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:32:13 +08:00
lhk229 62f175948e Auto-detect the background colour (default); drop the green heuristic
Replace the hardcoded green-screen auto-detection with a general flat-colour
detector. When screen_color is null the background colour is now detected from the
image border (dominant colour of the border strip) and used directly as the chroma
model, with a failure gate that raises when the border is not one clean flat colour
(gradient / texture / subject filling the frame).

- chroma: add detect_background_color + _background_border_cluster; the null case of
  estimate_background_model samples the detected cluster directly (no seed search);
  compute_bg_confidence is now always perceptual (Lab/RGB distance). Removed the green
  heuristic (initial_green_candidates), the green multiplicative gating, the now-unused
  _smoothstep, and five green-only ChromaSettings fields.
- pipeline: reuse the detected colour for de-spill in the auto case.
- settings / default.yaml: add detect_* tuning fields; refresh screen_color docs.
- README: document auto-detection + the failure gate, trimap modes, birefnet config.

A supplied --screen-color hex still uses the seed-search refinement. Validated:
detects green / pastel / text samples correctly, raises on gradient / two-colour, CLI
exits 1 on failure, green and pastel mattes unchanged in quality.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:34:31 +08:00
lhk229 22ef1d6336 Sync docs to current architecture
README rewritten for the current design: two pipelines (chroma-only vs single
segmenter, default anime-seg), the screen_color prior (--screen-color / config,
green default), correct debug-file list, and HF/hf-mirror notes for the seg models.
No rescue/recolour layer is documented (it was removed).

green_screen_matting_workflow.md keeps the phase-1 green-screen spec but gains a
status note pointing to the README for the evolved architecture, and its debug-file
list is corrected (foreground_rgb / color_mask / seg_mask, not the old
foreground_background / foreground_correction / despill_mask).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:46:21 +08:00
lhk229 9b7a192599 Use pymatting ML foreground estimation to remove edge spill
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>
2026-06-30 17:33:35 +08:00
Codex d6bb658244 Add foreground color estimation pass 2026-06-30 16:25:56 +08:00
Codex 10705d49df Support YAML pipeline configuration 2026-06-30 15:03:15 +08:00
Codex 923dbe9f09 Add sample smoke check and refine foreground despill 2026-06-30 14:42:08 +08:00
Codex 464a60c733 Improve despill and add quality checks 2026-06-30 14:29:19 +08:00
Codex 8fc53925e0 Implement green screen matting pipeline 2026-06-30 14:14:15 +08:00