Add complex-background matting mode to the service via background_mode

Backports master's non-flat matting (chroma.enabled: false + the hue-free
cross-check gate) into server-edition, and exposes it over HTTP without
surfacing the internal "chroma" wording: /remove-background gains a
background_mode form field (flat, default | complex). complex maps to
chroma disabled -- no colour key, segmentation alone drives the trimap and
every colour-keyed stage (auto-detect, hue split, chroma suppression,
despill) is bypassed. The cross-check veto still works in complex mode via
its second-opinion-confidence gate (cross_check.second_lo/hi) but defaults
OFF there (it costs the HR-matting forward); an explicit cross_check=on
re-enables it.

No new model weights: complex mode reuses the already-provisioned BiRefNet
seg + ViTMatte (+ optional HR-matting cross-check). Flat mode is unchanged
(bit-identical), and server-edition's own extras (cross_check.lock,
foreground.use_gpu CuPy path) are preserved -- the port is surgical, not a
copy of master's files.

- settings: ChromaSettings.enabled, CrossCheckSettings.second_lo/hi
- config: override_settings chroma passthrough
- despill/foreground: model=None safe guards (foreground keeps GPU path)
- alpha_post: cross_check_alpha hue-free gate when proj is None
- pipeline: _process_rgb complex branch (seg-only trimap, skip colour stages)
- service/app: process(chroma=), background_mode field, complex-defaults-off
  cross-check, X-BGFilter-Background-Mode header
- cli: --chroma/--no-chroma
- configs/docs: gpu.yaml + default.yaml + README/README_ZH

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 14:04:58 +08:00
parent 28d3fe8842
commit c56e58affe
13 changed files with 166 additions and 33 deletions
+12 -3
View File
@@ -208,9 +208,10 @@ is expected to authenticate). `/healthz` is always open. See [DEPLOY.md](DEPLOY.
| Field | Required | Values | Meaning |
| --- | --- | --- | --- |
| `file` | yes | image file | Source image (field name fixed as `file`). |
| `screen_color` | no | `#RRGGBB`, or omit/empty/`auto` | Background-colour prior; omit to auto-detect from the border. |
| `background_mode` | no | `flat` (default) / `complex` | `flat` = solid-colour background (colour-keyed pipeline). `complex` = non-flat/scene background: no colour key, the segmenter alone drives the matte. `screen_color` is ignored in `complex`; expect flat backgrounds to stay stronger on hair detail. |
| `screen_color` | no | `#RRGGBB`, or omit/empty/`auto` | Background-colour prior; omit to auto-detect from the border. Ignored when `background_mode=complex`. |
| `seg_model` | no | `birefnet` (default) / `anime-seg` | Segmentation backend. |
| `cross_check` | no | `1`/`0` (also `true`/`false`/`yes`/`no`/`on`/`off`) | Per-request override of the cross-model veto; omit/empty = service default. |
| `cross_check` | no | `1`/`0` (also `true`/`false`/`yes`/`no`/`on`/`off`) | Per-request override of the cross-model veto; omit/empty = service default in `flat` mode, but **defaults off** in `complex` mode (set `on` to enable its hue-free gate). |
### Response headers (on `200`)
@@ -220,13 +221,14 @@ is expected to authenticate). `/healthz` is always open. See [DEPLOY.md](DEPLOY.
| `X-BGFilter-Seg-Model` | `birefnet` | Segmenter actually used. |
| `X-BGFilter-Screen-Color` | `#CFEFFF` / `auto` | Background colour used. |
| `X-BGFilter-Cross-Check` | `on` / `off` | Effective cross-check state. |
| `X-BGFilter-Background-Mode` | `flat` / `complex` | Background mode actually used. |
### Status codes
| Code | When |
| --- | --- |
| `200` | Success — body is the RGBA PNG. |
| `400` | Bad input: unreadable image, invalid `screen_color`/`seg_model`/`cross_check`, or auto-detect failed (pass `screen_color`). |
| `400` | Bad input: unreadable image, invalid `screen_color`/`seg_model`/`cross_check`/`background_mode`, or auto-detect failed (pass `screen_color`). |
| `401` | Auth enabled and the token is missing or wrong. |
| `413` | Image exceeds `BGFILTER_MAX_IMAGE_PIXELS` (~4 MP by default). |
| `500` | Inference failed. |
@@ -247,6 +249,13 @@ curl -sS \
-F "seg_model=anime-seg" \
-F "cross_check=off" \
http://127.0.0.1:18083/remove-background -o output.png
# non-flat / scene background (no colour key); cross-check defaults off here,
# add cross_check=on to enable the hue-free residue veto
curl -sS \
-F "file=@scene.jpg" \
-F "background_mode=complex" \
http://127.0.0.1:18083/remove-background -o output.png
```
Config comes from `BGFILTER_CONFIG` (defaults to `configs/default.yaml` when present);
+12 -3
View File
@@ -187,9 +187,10 @@ python -m uvicorn app:app --host 127.0.0.1 --port 18083 --workers 1
| 字段 | 必填 | 取值 | 含义 |
| --- | --- | --- | --- |
| `file` | 是 | 图片文件 | 源图(字段名固定为 `file`)。 |
| `screen_color` | 否 | `#RRGGBB`,或留空/`auto` | 背景色先验;留空则从边框自动探测。 |
| `background_mode` | 否 | `flat`(默认)/ `complex` | `flat` = 纯色背景(走色键管线)。`complex` = 非纯色/实景背景:无色键,完全由分割器驱动抠图;此模式下 `screen_color` 被忽略,发丝细节通常弱于纯色模式。 |
| `screen_color` | 否 | `#RRGGBB`,或留空/`auto` | 背景色先验;留空则从边框自动探测。`background_mode=complex` 时忽略。 |
| `seg_model` | 否 | `birefnet`(默认)/ `anime-seg` | 分割后端。 |
| `cross_check` | 否 | `1`/`0`(也接受 `true`/`false`/`yes`/`no`/`on`/`off`) | 按请求覆盖交叉否决;留空 = 服务端默认。 |
| `cross_check` | 否 | `1`/`0`(也接受 `true`/`false`/`yes`/`no`/`on`/`off`) | 按请求覆盖交叉否决;留空`flat` 模式 = 服务端默认,但在 `complex` 模式**默认关闭**(传 `on` 启用其无色相门控)。 |
### 响应头(`200` 时)
@@ -199,13 +200,14 @@ python -m uvicorn app:app --host 127.0.0.1 --port 18083 --workers 1
| `X-BGFilter-Seg-Model` | `birefnet` | 实际使用的分割器。 |
| `X-BGFilter-Screen-Color` | `#CFEFFF` / `auto` | 实际使用的背景色。 |
| `X-BGFilter-Cross-Check` | `on` / `off` | 本次实际的 cross-check 状态。 |
| `X-BGFilter-Background-Mode` | `flat` / `complex` | 本次实际的背景模式。 |
### 状态码
| 码 | 何时 |
| --- | --- |
| `200` | 成功——响应体是 RGBA PNG。 |
| `400` | 输入有误:图片无法解码,`screen_color`/`seg_model`/`cross_check` 非法,或自动探测失败(请传 `screen_color`)。 |
| `400` | 输入有误:图片无法解码,`screen_color`/`seg_model`/`cross_check`/`background_mode` 非法,或自动探测失败(请传 `screen_color`)。 |
| `401` | 鉴权已开且 token 缺失或错误。 |
| `413` | 图片超过 `BGFILTER_MAX_IMAGE_PIXELS`(默认约 4 MP)。 |
| `500` | 推理失败。 |
@@ -226,6 +228,13 @@ curl -sS \
-F "seg_model=anime-seg" \
-F "cross_check=off" \
http://127.0.0.1:18083/remove-background -o output.png
# 非纯色/实景背景(无色键);此模式 cross-check 默认关闭,
# 需要时加 cross_check=on 启用无色相残留否决
curl -sS \
-F "file=@scene.jpg" \
-F "background_mode=complex" \
http://127.0.0.1:18083/remove-background -o output.png
```
配置来自 `BGFILTER_CONFIG`(存在时默认 `configs/default.yaml`);`BGFILTER_DEVICE` 同时
+29
View File
@@ -168,6 +168,23 @@ def _normalize_cross_check(value: str | None) -> bool | None:
raise HTTPException(status_code=400, detail="invalid cross_check")
def _normalize_background_mode(value: str | None) -> str | None:
"""None/empty -> service default (flat); else "flat" or "complex".
"flat" (default) is the colour-keyed pipeline; "complex" is non-flat/scene
matting with no colour key (segmentation drives the trimap). We deliberately
do not surface the internal "chroma" wording here.
"""
if value is None:
return None
v = value.strip().lower()
if v == "":
return None
if v in ("flat", "complex"):
return v
raise HTTPException(status_code=400, detail="invalid background_mode")
def _decode_image(data: bytes) -> np.ndarray:
if not data:
raise HTTPException(status_code=400, detail="empty image")
@@ -202,6 +219,7 @@ async def remove_background(
screen_color: str | None = Form(None),
seg_model: str | None = Form(None),
cross_check: str | None = Form(None),
background_mode: str | None = Form(None),
_auth: None = Depends(require_auth),
):
if _manager is None:
@@ -210,6 +228,15 @@ async def remove_background(
normalized_color = _normalize_screen_color(screen_color)
normalized_seg = _normalize_seg_model(seg_model) or _manager.default_seg_model
normalized_cc = _normalize_cross_check(cross_check)
# None/"flat" -> flat (chroma on); "complex" -> non-flat (chroma off).
normalized_bg = _normalize_background_mode(background_mode)
chroma_enabled = normalized_bg != "complex"
effective_bg = "complex" if not chroma_enabled else "flat"
# Complex mode defaults the cross-check veto OFF (it costs the HR-matting
# forward and only clears residue near strands); an explicit cross_check=on
# still enables its hue-free gate.
if not chroma_enabled and normalized_cc is None:
normalized_cc = False
# Honors the lane lock (a locked cpu-fast box reports/runs "off" even on cross_check=on).
effective_cc = _manager.effective_cross_check(normalized_cc)
@@ -226,6 +253,7 @@ async def remove_background(
screen_color=normalized_color,
seg_model=normalized_seg,
cross_check=normalized_cc,
chroma=chroma_enabled,
),
)
except RuntimeError as exc:
@@ -248,5 +276,6 @@ async def remove_background(
"X-BGFilter-Seg-Model": normalized_seg,
"X-BGFilter-Screen-Color": normalized_color if normalized_color else "auto",
"X-BGFilter-Cross-Check": "on" if effective_cc else "off",
"X-BGFilter-Background-Mode": effective_bg,
}
return Response(content=png, media_type="image/png", headers=headers)
+13 -6
View File
@@ -59,7 +59,7 @@ def suppress_alpha_by_chroma(
def cross_check_alpha(
alpha: np.ndarray,
second_alpha: np.ndarray,
proj: np.ndarray,
proj: np.ndarray | None,
lightness: np.ndarray,
trimap: np.ndarray,
settings: CrossCheckSettings,
@@ -76,6 +76,13 @@ def cross_check_alpha(
- the primary alpha is high (``gate_lo -> gate_hi`` ramp): pixels the
pipeline already renders soft (outer wisps) are exempt by construction.
``proj`` is None in complex-background mode (chroma disabled): no key colour
means no hue-defined suspect zone, so instead the veto requires the second
opinion itself to be confidently near-empty (full strength at/below
``second_lo``, none at/above ``second_hi``). A thin strand the downsampled
second model merely blurs to mid-alpha is left untouched; only regions it
decisively rejects can be cleared.
Inside the zone this deliberately overrides the trimap-FG clamp -- the
residue it exists to clear is mostly trimap-FG. Sure background cannot be
disturbed: min-fusion keeps alpha 0 at 0.
@@ -83,16 +90,16 @@ def cross_check_alpha(
if not settings.enabled:
return alpha
cv2 = require_cv2()
zone = (
(proj >= settings.proj_min)
& (lightness >= settings.l_min)
& (trimap != 0)
)
zone = (lightness >= settings.l_min) & (trimap != 0)
if proj is not None:
zone &= proj >= settings.proj_min
weight = zone.astype(np.float32)
if settings.feather_sigma > 0:
blur = cv2.GaussianBlur(weight, (0, 0), settings.feather_sigma)
weight = np.where(zone, 1.0, np.clip(blur, 0.0, 1.0)).astype(np.float32)
gate = weight * _smoothstep(alpha, settings.gate_lo, settings.gate_hi)
if proj is None:
gate = gate * (1.0 - _smoothstep(second_alpha, settings.second_lo, settings.second_hi))
out = alpha * (1.0 - gate) + np.minimum(alpha, second_alpha) * gate
return np.clip(out, 0.0, 1.0).astype(np.float32)
+2
View File
@@ -24,6 +24,7 @@ def main(
device: str | None = typer.Option(None, "--device"),
precision: str | None = typer.Option(None, "--precision", help="Compute precision for all models (ViTMatte cast + BiRefNet autocast): fp32 (default) | bf16 (faster + halves matting activation memory; needs bf16-capable hardware, else falls back to fp32)"),
screen_color: str | None = typer.Option(None, "--screen-color", help="Background colour prior as #RRGGBB (default: auto-detect the flat background colour)"),
chroma: bool | None = typer.Option(None, "--chroma/--no-chroma", help="Flat-colour background mode (default: on). --no-chroma is non-flat/complex-background mode: no colour key, segmentation drives the trimap; needs a segmentation backend + matting_method vitmatte"),
matting_method: str | None = typer.Option(None, "--matting-method"),
fallback_to_chroma_alpha: bool | None = typer.Option(None, "--fallback-to-chroma-alpha/--no-fallback-to-chroma-alpha"),
sure_bg_threshold: float | None = typer.Option(None, "--sure-bg-threshold", min=0.0, max=1.0),
@@ -43,6 +44,7 @@ def main(
device=device,
precision=precision,
screen_color=screen_color,
chroma=chroma,
matting_method=matting_method,
fallback_to_chroma_alpha=fallback_to_chroma_alpha,
sure_bg_threshold=sure_bg_threshold,
+4 -1
View File
@@ -99,6 +99,9 @@ def override_settings(settings: PipelineSettings, **overrides: Any) -> PipelineS
}
if overrides.get("trimap_mode") is not None:
trimap_updates["mode"] = overrides["trimap_mode"]
chroma_updates: dict[str, Any] = {}
if overrides.get("chroma") is not None:
chroma_updates["enabled"] = overrides["chroma"]
despill_updates: dict[str, Any] = {}
if overrides.get("despill") is not None:
despill_updates["enabled"] = overrides["despill"]
@@ -130,7 +133,7 @@ def override_settings(settings: PipelineSettings, **overrides: Any) -> PipelineS
else settings.screen_color
)
return PipelineSettings(
chroma=chroma,
chroma=_update_dataclass(chroma, chroma_updates),
trimap=_update_dataclass(trimap, trimap_updates),
alpha_post=alpha_post,
cross_check=_update_dataclass(settings.cross_check, cross_check_updates),
+3 -2
View File
@@ -10,7 +10,7 @@ from .settings import DespillSettings
def despill(
rgb: np.ndarray,
alpha: np.ndarray,
model: BackgroundModel,
model: BackgroundModel | None,
settings: DespillSettings,
) -> tuple[np.ndarray, np.ndarray]:
"""Remove background-colour spill from the foreground.
@@ -24,7 +24,8 @@ def despill(
Returns the corrected RGB (uint8) and the per-pixel despill weight (float32).
"""
if not settings.enabled:
# model is None with chroma disabled: no key colour means no spill direction.
if not settings.enabled or model is None:
return rgb.copy(), np.zeros(alpha.shape, dtype=np.float32)
bg_lab = np.asarray(model.lab_center, dtype=np.float32)
+7 -2
View File
@@ -15,7 +15,7 @@ def estimate_foreground_rgb(
rgb: np.ndarray,
alpha: np.ndarray,
bg_confidence: np.ndarray,
model: BackgroundModel,
model: BackgroundModel | None,
settings: ForegroundSettings,
) -> np.ndarray:
"""Estimated foreground colour F (uint8 HxWx3) for compositing over alpha."""
@@ -27,10 +27,15 @@ def estimate_foreground_rgb(
try:
return _estimate_ml(rgb, alpha, settings)
except RuntimeError:
if not settings.fallback_to_unmix:
if not settings.fallback_to_unmix or model is None:
raise
method = "unmix"
if method == "unmix":
if model is None:
raise RuntimeError(
"Foreground method 'unmix' needs the chroma background model, which "
"does not exist with chroma disabled; use method 'ml'."
)
return _estimate_unmix(rgb, alpha, bg_confidence, model, settings)
raise RuntimeError(
f"Unsupported foreground method '{settings.method}'. Use 'ml' or 'unmix'."
+34 -13
View File
@@ -132,10 +132,26 @@ def run_image(
def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
settings = pipeline.settings
screen_color = parse_hex_color(settings.screen_color) if settings.screen_color else None
bg_confidence, model = compute_bg_confidence(
rgb, settings=settings.chroma, screen_color=screen_color
)
if settings.chroma.enabled:
screen_color = parse_hex_color(settings.screen_color) if settings.screen_color else None
bg_confidence, model = compute_bg_confidence(
rgb, settings=settings.chroma, screen_color=screen_color
)
else:
if not settings.segmentation.enabled:
raise RuntimeError(
"chroma.enabled: false (non-flat background mode) needs the segmentation "
"pipeline; enable segmentation or re-enable chroma."
)
if settings.model.matting_method == "chroma":
raise RuntimeError(
"matting_method 'chroma' needs the chroma key; use 'vitmatte' or re-enable chroma."
)
# Non-flat background: no colour key exists. A zero background-confidence
# map makes every chroma-fused formula degrade to its seg-only form; the
# colour-keyed stages (hue split, suppression, veto, despill) are skipped.
bg_confidence = np.zeros(rgb.shape[:2], dtype=np.float32)
model = None
second_alpha = None
if settings.segmentation.enabled:
reuse = (
@@ -154,7 +170,9 @@ def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
seg_mask = second_alpha
else:
seg_mask = pipeline._segment(rgb)
mode = settings.trimap.mode
# The directional modes are colour tests against the key colour; without
# one, the seg-topology trimap is the only meaningful choice.
mode = settings.trimap.mode if settings.chroma.enabled else "seg"
if mode == "seg":
trimap, trimap_stats = fuse_trimap(seg_mask, bg_confidence, settings.trimap)
elif mode in ("directional", "directional-hard-bg"):
@@ -173,12 +191,13 @@ def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
raw_alpha, alpha_source = pipeline._predict_alpha(rgb, trimap, bg_confidence)
alpha = enforce_trimap(raw_alpha, trimap)
alpha = suppress_alpha_by_chroma(
alpha, bg_confidence, trimap, settings.alpha_post,
# The matte-confidence gate only makes sense for a real matting prediction;
# a chroma-seeded alpha is itself colour evidence, so no gate there.
raw_alpha=raw_alpha if alpha_source == "vitmatte" else None,
)
if settings.chroma.enabled:
alpha = suppress_alpha_by_chroma(
alpha, bg_confidence, trimap, settings.alpha_post,
# The matte-confidence gate only makes sense for a real matting prediction;
# a chroma-seeded alpha is itself colour evidence, so no gate there.
raw_alpha=raw_alpha if alpha_source == "vitmatte" else None,
)
alpha = clean_alpha(alpha, trimap, settings.alpha_post)
if settings.cross_check.enabled:
lab = convert_color_spaces(rgb)[1]
@@ -187,7 +206,9 @@ def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
alpha = cross_check_alpha(
alpha,
second_alpha,
bg_hue_projection(lab, model.lab_center),
# No key colour in complex-background mode: proj=None switches the
# veto to its second-opinion-confidence gate (see cross_check_alpha).
bg_hue_projection(lab, model.lab_center) if model is not None else None,
lab[..., 0],
trimap,
settings.cross_check,
@@ -199,7 +220,7 @@ def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
metadata = {
"alpha_source": alpha_source,
"background_model": model.to_dict(),
"background_model": model.to_dict() if model is not None else None,
"trimap": trimap_stats,
"settings": asdict(settings),
}
+7 -3
View File
@@ -25,9 +25,10 @@ class PipelineManager:
ViTMatte is the matting model shared by both segmentation backends, so it is
loaded once and reused. Each segmentation backend is lazily loaded once on
first use and cached by name. Per-request parameters (``screen_color``,
``seg_model``, ``cross_check``) never trigger a model reload -- ``screen_color``
only affects this request's chroma/despill, and ``seg_model``/``cross_check``
select an already-cached (or first-time-loaded, then cached) model.
``seg_model``, ``cross_check``, ``chroma``) never trigger a model reload --
they only re-parametrise the pipeline over already-cached (or first-time-
loaded, then cached) models. ``chroma=False`` selects non-flat/complex
background mode (no colour key); ``screen_color`` is then unused.
"""
def __init__(self, base_settings: PipelineSettings, preload: bool = True):
@@ -142,6 +143,7 @@ class PipelineManager:
screen_color: str | None,
seg_model: str,
cross_check: bool | None = None,
chroma: bool | None = None,
) -> MattingResult:
if seg_model not in _SEG_MODELS:
raise ValueError(f"invalid seg_model '{seg_model}'")
@@ -149,6 +151,8 @@ class PipelineManager:
self._base,
screen_color=screen_color,
seg_backend=seg_model,
# None leaves the base default; False selects non-flat/complex mode.
chroma=chroma,
# Honor the lane lock: a locked lane ignores the per-request override
# (a cpu-fast box never runs cross-check, even on cross_check=on).
cross_check=self.effective_cross_check(cross_check),
+15
View File
@@ -5,6 +5,14 @@ from dataclasses import dataclass
@dataclass(frozen=True)
class ChromaSettings:
# False = non-flat-background mode (--no-chroma): no colour key at all.
# Segmentation alone drives the trimap (mode forced to "seg"), and the
# colour-keyed stages are bypassed: background auto-detect, the directional
# hue split, chroma alpha suppression, despill, and the unmix foreground
# fallback. The cross-check veto still runs (if enabled) with a hue-free
# gate -- see CrossCheckSettings.second_lo/hi. Requires
# segmentation.enabled and matting_method 'vitmatte'.
enabled: bool = True
border_ratio: float = 0.04
min_samples: int = 2048
lab_sigma_min: float = 10.0
@@ -104,6 +112,13 @@ class CrossCheckSettings:
feather_sigma: float = 2.0 # Gaussian feather of the zone boundary, in px
gate_lo: float = 0.70 # primary alpha below this -> fully exempt
gate_hi: float = 0.95 # primary alpha above this -> fully vetoable
# Complex-background mode only (chroma disabled -> no hue-defined suspect
# zone): the veto instead requires the second opinion itself to be
# confidently near-empty -- full strength at/below second_lo, none at/above
# second_hi. Thin structures the downsampled second model merely blurs to
# mid-alpha stay untouched; only decisively-rejected regions can be cleared.
second_lo: float = 0.15
second_hi: float = 0.40
@dataclass(frozen=True)
+14
View File
@@ -3,6 +3,15 @@
# like "#CFEFFF" sets it explicitly. Also overridable via --screen-color.
screen_color: null
chroma:
# true = flat-colour background (default). false = non-flat/complex background
# mode (--no-chroma): no colour key; segmentation alone drives the trimap (mode
# forced to "seg") and the colour-keyed stages are bypassed (auto-detect, hue
# split, chroma suppression, despill). The cross-check veto still runs if
# enabled, using its hue-free gate (cross_check.second_lo/hi). Needs
# segmentation.enabled + matting_method vitmatte.
enabled: true
model:
model_name: hustvl/vitmatte-base-composition-1k
device: cpu
@@ -91,6 +100,11 @@ cross_check:
feather_sigma: 2.0 # zone-boundary feather, px
gate_lo: 0.70 # primary alpha below this -> fully exempt
gate_hi: 0.95 # primary alpha above this -> fully vetoable
# Complex-background (no-chroma) mode only: no hue-defined suspect zone, so the
# veto requires the second opinion itself to be confidently near-empty -- full
# strength at/below second_lo, fading to none at/above second_hi.
second_lo: 0.15
second_hi: 0.40
foreground:
enabled: true
+14
View File
@@ -4,6 +4,15 @@
# off (it false-positives on subjects sharing the background hue).
screen_color: null
chroma:
# true = flat-colour background (default). false = non-flat/complex background
# mode: no colour key; segmentation alone drives the trimap (mode forced to
# "seg") and the colour-keyed stages are bypassed (auto-detect, hue split,
# chroma suppression, despill). The cross-check veto still runs if enabled,
# using its hue-free gate (cross_check.second_lo/hi). The service selects this
# per request via background_mode=flat|complex; this is only the base default.
enabled: true
model:
model_name: hustvl/vitmatte-base-composition-1k
device: cuda
@@ -51,6 +60,11 @@ cross_check:
feather_sigma: 2.0
gate_lo: 0.70
gate_hi: 0.95
# Complex-background (non-flat) mode only: no hue-defined suspect zone, so the
# veto requires the second opinion itself to be confidently near-empty -- full
# strength at/below second_lo, fading to none at/above second_hi.
second_lo: 0.15
second_hi: 0.40
foreground:
enabled: true