diff --git a/README.md b/README.md index 600f8e6..e6e3279 100644 --- a/README.md +++ b/README.md @@ -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); diff --git a/README_ZH.md b/README_ZH.md index 289c567..1452df0 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -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` 同时 diff --git a/app.py b/app.py index af431c8..5c16494 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/bgfilter/alpha_post.py b/bgfilter/alpha_post.py index 8f40a66..d6f52b5 100644 --- a/bgfilter/alpha_post.py +++ b/bgfilter/alpha_post.py @@ -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) diff --git a/bgfilter/cli.py b/bgfilter/cli.py index 8b27865..f4377f0 100644 --- a/bgfilter/cli.py +++ b/bgfilter/cli.py @@ -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, diff --git a/bgfilter/config.py b/bgfilter/config.py index 688aa1d..d58574f 100644 --- a/bgfilter/config.py +++ b/bgfilter/config.py @@ -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), diff --git a/bgfilter/despill.py b/bgfilter/despill.py index cb86144..6c9c815 100644 --- a/bgfilter/despill.py +++ b/bgfilter/despill.py @@ -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) diff --git a/bgfilter/foreground.py b/bgfilter/foreground.py index 1db738c..d177156 100644 --- a/bgfilter/foreground.py +++ b/bgfilter/foreground.py @@ -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'." diff --git a/bgfilter/pipeline.py b/bgfilter/pipeline.py index f1906aa..b0aeaad 100644 --- a/bgfilter/pipeline.py +++ b/bgfilter/pipeline.py @@ -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), } diff --git a/bgfilter/service.py b/bgfilter/service.py index f7cb2cf..a315ed9 100644 --- a/bgfilter/service.py +++ b/bgfilter/service.py @@ -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), diff --git a/bgfilter/settings.py b/bgfilter/settings.py index ee28fa1..6642f1d 100644 --- a/bgfilter/settings.py +++ b/bgfilter/settings.py @@ -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) diff --git a/configs/default.yaml b/configs/default.yaml index b9b5192..aac544a 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -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 diff --git a/configs/gpu.yaml b/configs/gpu.yaml index 1b17b7c..a01c319 100644 --- a/configs/gpu.yaml +++ b/configs/gpu.yaml @@ -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