despill pulls foreground chroma along the background-hue axis with no positional or semantic guard, so a subject sharing the background's hue (e.g. a blue suit on a blue backdrop) is desaturated and hue-shifted (measured ~ΔE 35, blue -> purple) — a false positive on correct colours. Flip the DespillSettings.enabled dataclass default to False so every lane that omits a despill section — notably cpu-fast — inherits it off, and mirror it in default.yaml. Re-enable per-config or with --despill when edge spill genuinely matters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BgFilter
Offline character matting for AI-generated images on a flat-colour background.
The background colour is auto-detected from the image border (green, pastel, any
flat colour); pass --screen-color to set it explicitly.
RGB input
-> chroma bg-confidence (keyed to the auto-detected or given colour)
-> [optional] semantic segmentation mask
-> trimap -> ViTMatte -> alpha cleanup
-> cross-model veto (second matting opinion on bg-hued residue)
-> pymatting foreground -> despill -> RGBA PNG -> QA previews
Pipelines
Two pipelines, selected by segmentation.enabled in the config:
- Chroma-only (
enabled: false) — Chroma + ViTMatte, no segmentation model. Lightest / fastest; leans entirely on the colour key for topology. - Single segmenter (
enabled: true, default) — one segmentation model drives the trimap topology (holes, hair), ViTMatte then refines the soft edges. Backend is switchable:birefnet(default, general salient objects — text, logos, photos; needstrust_remote_code) oranime-seg(ONNX, tuned for anime characters). Switch at runtime with--seg-backend anime-seg(it also selects the matching weights).
Both share pymatting foreground estimation and a colour de-spill. There is no green-contamination rescue / recolour layer — with clean source images it is unnecessary, so it was removed.
Both also run a cross-model veto by default: a second, trimap-free matting
model (ZhengPeng7/BiRefNet_HR-matting) may only lower alpha, only on
background-hued bright pixels the primary result is confident about — this clears
colour-drifted background residue trapped between hair strands that the chroma
key, the segmenter and ViTMatte all read as foreground. Costs one extra model
download (~0.9 GB) and one inference pass per image; disable with
--no-cross-check (see the cross_check config section, and
docs/hair_gap_artifacts.md for the analysis behind it).
When the cross-check is on, that same HR-matting forward can optionally be
reused as the segmentation mask (--cross-check-as-seg), skipping the
primary seg model entirely (one less model to load, ~20 s faster per image on
CPU, ~1 GB less VRAM on GPU). Pilot-validated (TestImage3 / FixImage1): trimap
99.8% identical, no structural change to fingers, hair wisps or thin lines.
Off by default: the dedicated segmenter keeps its own forward and the veto
stays an independent second signal. The reuse is a same-family swap, so it
applies to the birefnet backend only: with --seg-backend anime-seg the
anime segmenter always keeps its own forward.
The segmentation trimap defaults to directional mode (chroma + seg + a hue-direction
split: it keeps a background-coloured garment such as a white shirt while dropping a
background-hued residual such as blue trapped between hair strands). Switch with
--trimap-mode seg (topology only, no hue split) or directional-hard-bg (aggressive
— hard-removes background-hued pixels; can eat cool/shadowed white cloth).
Background colour
By default (screen_color: null) the background colour is auto-detected from the
image border: the dominant flat colour of the border strip becomes the key colour. If
the border is not one clean flat colour — a gradient, texture, or a subject filling the
frame — detection fails with an error; pass --screen-color explicitly in that case.
To set it yourself, give a hex prior:
... --screen-color "#CFEFFF"
or screen_color: "#CFEFFF" in the config. Either way the chroma key scores pixels by
perceptual (Lab/RGB) distance to the colour, and de-spill removes chroma along that
colour's direction. (A supplied hex is refined against nearby border pixels; an
auto-detected colour is used directly.)
Environment
Use the conda environment lightML.
conda activate lightML
pip install -r requirements.txt
If the shell is not activated, call the environment Python directly:
D:\MiniConda\envs\lightML\python.exe -m bgfilter.cli --help
Single Image
D:\MiniConda\envs\lightML\python.exe -m bgfilter.cli `
--input Samples\TestImage.png `
--output Outputs\TestImage_rgba.png `
--debug-dir Outputs\TestImage_debug
No --config is needed — the built-in defaults are identical to
configs/default.yaml. Pass --config configs\default.yaml only after you edit that
file to tune the detailed parameters. Runtime choices stay on the command line:
--device (default CPU; use --device cuda for GPU), --precision (default
fp32; bf16 speeds up all three models and halves the matting model's activation
memory with visually identical alpha — needs bf16-capable hardware, falls back to
fp32 elsewhere; large inputs additionally get ViTMatte's global attention computed
in query chunks by default — exact, bitwise-identical, caps the memory spike at
~4 GB instead of ~19 GB at 2048x2048, see model.attn_query_chunk), --seg-backend
(default birefnet; anime-seg for anime characters), --screen-color (default:
auto-detect the flat background), and --trimap-mode.
Batch
D:\MiniConda\envs\lightML\python.exe -m bgfilter.cli `
--input-dir Samples `
--output-dir Outputs `
--debug-dir Outputs\debug
Performance (CPU reference numbers)
Measured on a Ryzen 9700X (Zen 5, native bf16), 32 GB RAM, with cross-check on,
--cross-check-as-seg enabled and chunked global attention on. (Reuse is
off by default on this branch; without it, add the primary BiRefNet@1024
forward — roughly +20 s/image at these sizes.)
| input | precision | warm / image | peak memory |
|---|---|---|---|
| 1024x1536 | fp32 | ~45 s | — |
| 1024x1536 | bf16 | ~33 s | — |
| 2048x2048 | bf16 | ~53 s | 11.4 GB (batch), 8.1 GB (single image) |
The memory ceiling is the cross-check BiRefNet_HR forward (fixed
input_size 2048 regardless of the image size) — genuine live activations
of a full-resolution dense prediction net. Before chunked attention and the
mimalloc fix, the same 2048x2048 bf16 run peaked at 25.2 GB; a fully
un-optimized fp32 run would need an estimated 55-60 GB (ViTMatte's un-chunked
N^2 attention alone ~39 GB). CPUs without native bf16 (e.g. Zen 2) auto-fall
back to fp32 — on such machines prefer --no-cross-check if memory or
latency is tight.
Chroma-alpha debug mode
--matting-method chroma skips ViTMatte and uses chroma confidence directly as the
alpha seed. Useful for fast inspection of chroma confidence, trimap, and despill.
(Distinct from the chroma-only pipeline above, which still runs ViTMatte.)
D:\MiniConda\envs\lightML\python.exe -m bgfilter.cli `
--input-dir Samples `
--output-dir Outputs\chroma `
--debug-dir Outputs\chroma_debug `
--matting-method chroma `
--device cpu
Outputs
For each processed image, the CLI writes an RGBA PNG and optional debug files:
bg_confidence.png
trimap.png
seg_mask.png # segmentation pipeline only
alpha.png
foreground_rgb.png # despilled foreground colour
color_mask.png # per-pixel despill weight
preview_black.png
preview_white.png
preview_gray.png
preview_red.png
preview_blue.png
qa_grid.png
metadata.json
HTTP service
An HTTP wrapper (app.py + bgfilter/service.py) exposes the pipeline as a
long-running FastAPI service. Models load once and are reused across requests.
For deploying to a Linux server (dependencies, weights, systemd, offline mode), see
DEPLOY.md.
D:\MiniConda\envs\lightML\python.exe -m uvicorn app:app `
--host 127.0.0.1 --port 18083 --workers 1
Two endpoints:
GET /healthz— liveness/config JSON.POST /remove-background—multipart/form-data, returnsimage/png.file(required) — the source image. The field name is fixed asfile.screen_color(optional) —#RRGGBBprior; omit/empty for auto-detect.seg_model(optional) —birefnet(default) oranime-seg.cross_check(optional) —1/0(alsotrue/false/yes/no/on/off); per-request override of the cross-model veto. Omit/empty to use the service default. The response reports the effective state inX-BGFilter-Cross-Check.
# default (birefnet + auto background colour)
curl -sS -F "file=@input.png" \
http://127.0.0.1:18083/remove-background -o output.png
# explicit background colour + anime segmenter
curl -sS \
-F "file=@input.png" \
-F "screen_color=#CFEFFF" \
-F "seg_model=anime-seg" \
http://127.0.0.1:18083/remove-background -o output.png
# enable the cross-model veto for this request only (see below; slow on CPU)
curl -sS -F "file=@input.png" -F "cross_check=1" \
http://127.0.0.1:18083/remove-background -o output.png
Config comes from BGFILTER_CONFIG (defaults to configs/default.yaml when present);
BGFILTER_DEVICE overrides both model and segmentation device; BGFILTER_MAX_IMAGE_PIXELS
caps input size (default ~4MP → 413); BGFILTER_PRELOAD=1 loads the default models at
startup. Run a single worker (--workers 1) — each worker loads its own copy of the models.
Like the CLI, the service runs the cross-model veto by default (config
default: on); its HR-matting model is preloaded with the rest of the warm set.
Set BGFILTER_CROSS_CHECK=0 to turn it off service-wide, or use the per-request
cross_check form field to override either way; /healthz reports the default
as crossCheck. With cross_check.reuse_as_seg: true in the config (off by
default) the HR-matting forward replaces the primary seg forward instead of
adding to it, and the then-redundant primary segmenter is skipped at startup.
Quality Check
The quality checker measures alpha validity and edge spill on semi-transparent edge pixels.
D:\MiniConda\envs\lightML\python.exe -m bgfilter.quality_cli `
Outputs\TestImage_rgba.png `
--max-edge-green-excess-p95 0.30
Run the bundled sample smoke check:
D:\MiniConda\envs\lightML\python.exe scripts\smoke_samples.py `
--samples-dir Samples `
--output-dir Outputs\smoke_samples `
--config configs\default.yaml `
--device cpu `
--fallback-to-chroma-alpha `
--max-edge-green-excess-p95 0.30
Notes
Samples/andOutputs/are ignored by Git.- ViTMatte and segmentation weights load from Hugging Face on first use.
anime-seg(skytnt/anime-seg) is a plain ONNX download;birefnet(ZhengPeng7/BiRefNet) ships custom modelling code so it needstrust_remote_code=Trueplustimm/einops/kornia. - Downloading the weights behind a firewall — the reliable combination is the
hf-mirror.commirror with the local proxy bypassed and Xet disabled:Two gotchas this avoids: (1) mirror + an overseas proxy makes the mirror's$env:HF_ENDPOINT = "https://hf-mirror.com" # domestic mirror $env:NO_PROXY = "*" # bypass the proxy; the mirror is direct $env:HF_HUB_DISABLE_XET = "1" # these repos are Xet-backed; force classic HTTPresolvebounce back tohuggingface.co, which recenthuggingface_hubrejects withFileMetadataError; (2) withhf-xetinstalled the Xet download path fails instantly. Once the weights are cached, run offline withHF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1(as the service does in production). - Bundling weights in the project — instead of the HF cache, drop each model into a
plain folder named after the repo basename under
models/:models/vitmatte-base-composition-1k,models/BiRefNet,models/anime-seg,models/BiRefNet_HR-matting(cross-check second opinion, default on). The loader prefers a matching local folder and falls back to the HF repo id / cache when absent, so it is opt-in. Populate them with e.g.hf download ZhengPeng7/BiRefNet --local-dir models/BiRefNet. Override the base directory withBGFILTER_WEIGHTS_DIR.models/is gitignored. - Foreground colour estimation uses pymatting's
estimate_foreground_mlto propagate clean foreground colour into semi-transparent edges before de-spill. Setforeground.method: unmixto fall back to the legacy heuristic. docs/green_screen_matting_workflow.mdis the original phase-1 green-screen spec; this README reflects the current, generalised architecture.