Merge master: matte-confidence gate + cross-model hair-gap veto

Brings in the two algorithm optimisations from master:
- Gate chroma suppression by the matte's own confidence (639fdf0)
- Add cross-model veto of hair-gap background residue (adbbb9d)

Server-edition integration on top of the plain merge:
- PipelineManager caches the cross-check model (BiRefNet_HR-matting) and
  injects it into MattingPipeline, so HTTP requests never reload it.
- MattingResult carries cross_check_alpha; debug output matches master.
- /healthz reports crossCheck; README lists models/BiRefNet_HR-matting
  in the local-weights bundle.

Verified: CLI --no-cross-check baseline and full cross-check run both
pass on samples/TestImage3.png; cross_check_alpha.png written to debug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 20:37:07 +08:00
13 changed files with 547 additions and 22 deletions
+36 -3
View File
@@ -8,8 +8,8 @@ import numpy as np
from .config import _SEG_MODELS, override_settings
from .pipeline import MattingPipeline, MattingResult
from .segmentation import make_segmenter
from .settings import PipelineSettings
from .segmentation import BiRefNetSegmenter, make_segmenter
from .settings import PipelineSettings, SegmentationSettings
from .vitmatte_infer import ViTMatteRunner
logger = logging.getLogger("bgfilter.service")
@@ -33,12 +33,15 @@ class PipelineManager:
self._base = base_settings
self._runner: ViTMatteRunner | None = None
self._segmenters: dict[str, object] = {}
self._cross_checker: BiRefNetSegmenter | None = None
self._load_lock = threading.Lock()
if preload:
if base_settings.model.matting_method == "vitmatte":
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()
@property
def default_seg_model(self) -> str:
@@ -48,6 +51,10 @@ class PipelineManager:
def device(self) -> str:
return self._base.model.device
@property
def cross_check_enabled(self) -> bool:
return self._base.cross_check.enabled
def _get_runner(self) -> ViTMatteRunner:
if self._runner is None:
with self._load_lock:
@@ -81,6 +88,27 @@ class PipelineManager:
self._segmenters[backend] = seg
return seg
def _get_cross_checker(self) -> BiRefNetSegmenter:
# The cross-check model depends only on base settings (never on per-request
# params), so one shared instance serves all requests.
if self._cross_checker is None:
with self._load_lock:
if self._cross_checker is None:
check = self._base.cross_check
logger.info(
"loading cross-check model: model=%s device=%s",
check.model_name,
self._base.segmentation.device,
)
self._cross_checker = BiRefNetSegmenter(
SegmentationSettings(
model_name=check.model_name,
device=self._base.segmentation.device,
input_size=check.input_size,
)
)
return self._cross_checker
def process(
self, rgb: np.ndarray, *, screen_color: str | None, seg_model: str
) -> MattingResult:
@@ -97,5 +125,10 @@ class PipelineManager:
segmenter = (
self._get_segmenter(seg_model) if settings.segmentation.enabled else None
)
pipeline = MattingPipeline(settings, runner=runner, segmenter=segmenter)
cross_checker = (
self._get_cross_checker() if settings.cross_check.enabled else None
)
pipeline = MattingPipeline(
settings, runner=runner, segmenter=segmenter, cross_checker=cross_checker
)
return pipeline.run_rgb(rgb)