Add per-request cross_check toggle to the HTTP service (lazy-loaded)
POST /remove-background gains an optional cross_check form field (1/0/true/false/yes/no/on/off; empty = service default). The effective state is reported in the X-BGFilter-Cross-Check response header. The cross-check model is now never preloaded at startup: the first request that enables it pays the one-time load (measured ~8 s on top of the ~50 s cc inference), after which the instance is cached and shared. Verified with a functional test against PipelineManager: cc-off requests never load the model, cc=1 lazy-loads then reuses the cached instance, and omitting the field falls back to the service default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,9 @@ Two endpoints:
|
||||
- `file` (required) — the source image. The field name is fixed as `file`.
|
||||
- `screen_color` (optional) — `#RRGGBB` prior; omit/empty for auto-detect.
|
||||
- `seg_model` (optional) — `birefnet` (default) or `anime-seg`.
|
||||
- `cross_check` (optional) — `1`/`0` (also `true`/`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 in `X-BGFilter-Cross-Check`.
|
||||
|
||||
```bash
|
||||
# default (birefnet + auto background colour)
|
||||
@@ -166,6 +169,10 @@ curl -sS \
|
||||
-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);
|
||||
@@ -174,9 +181,12 @@ caps input size (default ~4MP → `413`); `BGFILTER_PRELOAD=1` loads the default
|
||||
startup. Run a single worker (`--workers 1`) — each worker loads its own copy of the models.
|
||||
|
||||
Unlike the CLI, the service defaults the **cross-model veto to off** — it adds one
|
||||
extra large-model inference (~35 s/image on CPU, 14 s → 49 s total). Opt in with
|
||||
`BGFILTER_CROSS_CHECK=1` (worth it on GPU or when hair-gap residue matters more than
|
||||
latency); `/healthz` reports the effective state as `crossCheck`.
|
||||
extra large-model inference (~35 s/image on CPU, 14 s → 49 s total). Enable it
|
||||
per request with the `cross_check` form field, or flip the service default with
|
||||
`BGFILTER_CROSS_CHECK=1` (worth it on GPU or when hair-gap residue matters more
|
||||
than latency); `/healthz` reports the default as `crossCheck`. The cross-check
|
||||
model is lazy-loaded: it is never preloaded at startup, so the first request that
|
||||
enables it also pays the one-time model load (~1 GB) on top of the inference.
|
||||
|
||||
## Quality Check
|
||||
|
||||
|
||||
@@ -129,6 +129,20 @@ def _normalize_seg_model(value: str | None) -> str | None:
|
||||
return v
|
||||
|
||||
|
||||
def _normalize_cross_check(value: str | None) -> bool | None:
|
||||
"""None/empty -> use the service default; otherwise a boolean string."""
|
||||
if value is None:
|
||||
return None
|
||||
v = value.strip().lower()
|
||||
if v == "":
|
||||
return None
|
||||
if v in ("1", "true", "yes", "on"):
|
||||
return True
|
||||
if v in ("0", "false", "no", "off"):
|
||||
return False
|
||||
raise HTTPException(status_code=400, detail="invalid cross_check")
|
||||
|
||||
|
||||
def _decode_image(data: bytes) -> np.ndarray:
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="empty image")
|
||||
@@ -162,12 +176,15 @@ async def remove_background(
|
||||
file: UploadFile = File(...),
|
||||
screen_color: str | None = Form(None),
|
||||
seg_model: str | None = Form(None),
|
||||
cross_check: str | None = Form(None),
|
||||
):
|
||||
if _manager is None:
|
||||
raise HTTPException(status_code=503, detail="service not ready")
|
||||
|
||||
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)
|
||||
effective_cc = normalized_cc if normalized_cc is not None else _manager.cross_check_enabled
|
||||
|
||||
rgb = _decode_image(await file.read())
|
||||
|
||||
@@ -178,7 +195,10 @@ async def remove_background(
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _manager.process(
|
||||
rgb, screen_color=normalized_color, seg_model=normalized_seg
|
||||
rgb,
|
||||
screen_color=normalized_color,
|
||||
seg_model=normalized_seg,
|
||||
cross_check=normalized_cc,
|
||||
),
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
@@ -200,5 +220,6 @@ async def remove_background(
|
||||
"X-BGFilter-Elapsed-Ms": str(elapsed_ms),
|
||||
"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",
|
||||
}
|
||||
return Response(content=png, media_type="image/png", headers=headers)
|
||||
|
||||
+16
-7
@@ -24,9 +24,9 @@ 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``) never trigger a model reload -- ``screen_color`` only affects
|
||||
this request's chroma/despill, and ``seg_model`` just selects an already-cached
|
||||
(or first-time-loaded) segmenter.
|
||||
``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.
|
||||
"""
|
||||
|
||||
def __init__(self, base_settings: PipelineSettings, preload: bool = True):
|
||||
@@ -40,8 +40,8 @@ class PipelineManager:
|
||||
self._get_runner()
|
||||
if base_settings.segmentation.enabled:
|
||||
self._get_segmenter(base_settings.segmentation.backend)
|
||||
if base_settings.cross_check.enabled:
|
||||
self._get_cross_checker()
|
||||
# The cross-check model is deliberately NOT preloaded: it is opt-in
|
||||
# per request, so the first request that enables it pays the load.
|
||||
|
||||
@property
|
||||
def default_seg_model(self) -> str:
|
||||
@@ -110,12 +110,21 @@ class PipelineManager:
|
||||
return self._cross_checker
|
||||
|
||||
def process(
|
||||
self, rgb: np.ndarray, *, screen_color: str | None, seg_model: str
|
||||
self,
|
||||
rgb: np.ndarray,
|
||||
*,
|
||||
screen_color: str | None,
|
||||
seg_model: str,
|
||||
cross_check: bool | None = None,
|
||||
) -> MattingResult:
|
||||
if seg_model not in _SEG_MODELS:
|
||||
raise ValueError(f"invalid seg_model '{seg_model}'")
|
||||
settings = override_settings(
|
||||
self._base, screen_color=screen_color, seg_backend=seg_model
|
||||
self._base,
|
||||
screen_color=screen_color,
|
||||
seg_backend=seg_model,
|
||||
# None = keep the service default; True/False = per-request override.
|
||||
cross_check=cross_check,
|
||||
)
|
||||
runner = (
|
||||
self._get_runner()
|
||||
|
||||
Reference in New Issue
Block a user