Backport server-edition's pipeline structure to master

Adopt the service branch's refactors verbatim so the two branches stop
drifting on these files:

- pipeline.py: split _run_image into _process_rgb + MattingResult (an
  in-memory API, exposed as MattingPipeline.run_rgb) and allow injecting
  pre-loaded models into MattingPipeline. Disk I/O and debug dumps stay
  in _run_image; the algorithm is line-for-line the same.
- foreground.py: estimate_foreground_rgb returns the foreground array
  directly; the ForegroundEstimate dataclass carried background/
  correction fields nothing consumed.

Verified: default TestImage3 run byte-identical to pre-backport master,
all debug artifacts written, run_rgb output matches run_image bytes
under save_rgba's quantization; both files now diff-clean against
server-edition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 19:49:53 +08:00
parent 72655dea91
commit e234a1a13d
2 changed files with 87 additions and 70 deletions
+79 -34
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import json
from dataclasses import asdict
from dataclasses import asdict, dataclass
from pathlib import Path
import numpy as np
@@ -32,12 +32,41 @@ from .trimap import (
from .vitmatte_infer import ViTMatteRunner
@dataclass
class MattingResult:
"""In-memory result of one matting run.
``rgb`` is the despilled foreground (uint8 HxWx3) and ``alpha`` is float32 HxW in
0..1; together they compose the output RGBA. The remaining arrays are the
intermediate maps used for debug previews.
"""
rgb: np.ndarray
alpha: np.ndarray
metadata: dict
bg_confidence: np.ndarray
trimap: np.ndarray
seg_mask: np.ndarray | None
cross_check_alpha: np.ndarray | None
color_mask: np.ndarray
input_rgb: np.ndarray
class MattingPipeline:
def __init__(self, settings: PipelineSettings):
def __init__(
self,
settings: PipelineSettings,
*,
runner: ViTMatteRunner | None = None,
segmenter=None,
cross_checker=None,
):
self.settings = settings
self._runner: ViTMatteRunner | None = None
self._segmenter = None
self._cross_checker = None
# Pre-loaded models can be injected so a long-lived service shares them
# across requests instead of reloading per call; None = lazy-load on first use.
self._runner: ViTMatteRunner | None = runner
self._segmenter = segmenter
self._cross_checker = cross_checker
def _segment(self, rgb: np.ndarray) -> np.ndarray:
if self._segmenter is None:
@@ -79,6 +108,10 @@ class MattingPipeline:
raise
return trimap_to_alpha_seed(trimap, bg_confidence), "chroma_fallback"
def run_rgb(self, rgb: np.ndarray) -> MattingResult:
"""Run the full pipeline on an in-memory RGB array (no disk I/O)."""
return _process_rgb(rgb, self)
def run_image(
self,
input_path: str | Path,
@@ -97,14 +130,8 @@ def run_image(
return MattingPipeline(settings).run_image(input_path, output_path, debug_dir)
def _run_image(
input_path: str | Path,
output_path: str | Path,
debug_dir: str | Path | None,
pipeline: MattingPipeline,
) -> dict:
def _process_rgb(rgb: np.ndarray, pipeline: MattingPipeline) -> MattingResult:
settings = pipeline.settings
rgb = load_rgb(input_path)
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
@@ -168,41 +195,59 @@ def _run_image(
foreground = estimate_foreground_rgb(
rgb, alpha, bg_confidence, model, settings.foreground
)
corrected_rgb, color_mask = despill(
foreground.rgb, alpha, model, settings.despill
)
save_rgba(output_path, corrected_rgb, alpha)
corrected_rgb, color_mask = despill(foreground, alpha, model, settings.despill)
result = {
"input": str(input_path),
"output": str(output_path),
metadata = {
"alpha_source": alpha_source,
"background_model": model.to_dict(),
"trimap": trimap_stats,
"settings": asdict(settings),
}
return MattingResult(
rgb=corrected_rgb,
alpha=alpha,
metadata=metadata,
bg_confidence=bg_confidence,
trimap=trimap,
seg_mask=seg_mask,
cross_check_alpha=second_alpha,
color_mask=color_mask,
input_rgb=rgb,
)
def _run_image(
input_path: str | Path,
output_path: str | Path,
debug_dir: str | Path | None,
pipeline: MattingPipeline,
) -> dict:
res = _process_rgb(load_rgb(input_path), pipeline)
save_rgba(output_path, res.rgb, res.alpha)
result = {"input": str(input_path), "output": str(output_path), **res.metadata}
if debug_dir is not None:
debug = Path(debug_dir)
debug.mkdir(parents=True, exist_ok=True)
save_gray(debug / "bg_confidence.png", bg_confidence)
save_gray(debug / "trimap.png", trimap)
if seg_mask is not None:
save_gray(debug / "seg_mask.png", seg_mask)
if second_alpha is not None:
save_gray(debug / "cross_check_alpha.png", second_alpha)
save_gray(debug / "alpha.png", alpha)
save_rgb(debug / "foreground_rgb.png", corrected_rgb)
save_gray(debug / "color_mask.png", color_mask)
save_previews(debug, corrected_rgb, alpha)
save_gray(debug / "bg_confidence.png", res.bg_confidence)
save_gray(debug / "trimap.png", res.trimap)
if res.seg_mask is not None:
save_gray(debug / "seg_mask.png", res.seg_mask)
if res.cross_check_alpha is not None:
save_gray(debug / "cross_check_alpha.png", res.cross_check_alpha)
save_gray(debug / "alpha.png", res.alpha)
save_rgb(debug / "foreground_rgb.png", res.rgb)
save_gray(debug / "color_mask.png", res.color_mask)
save_previews(debug, res.rgb, res.alpha)
tiles = {
"input": rgb,
"bg confidence": np.repeat((bg_confidence[..., None] * 255).astype(np.uint8), 3, axis=2),
"trimap": np.repeat(trimap[..., None], 3, axis=2),
"alpha": np.repeat((alpha[..., None] * 255).astype(np.uint8), 3, axis=2),
"input": res.input_rgb,
"bg confidence": np.repeat((res.bg_confidence[..., None] * 255).astype(np.uint8), 3, axis=2),
"trimap": np.repeat(res.trimap[..., None], 3, axis=2),
"alpha": np.repeat((res.alpha[..., None] * 255).astype(np.uint8), 3, axis=2),
}
for name, color in BACKGROUND_COLORS.items():
tiles[f"preview {name}"] = composite(corrected_rgb, alpha, color)
tiles[f"preview {name}"] = composite(res.rgb, res.alpha, color)
make_qa_grid(debug, tiles)
write_text(debug / "metadata.json", json.dumps(result, indent=2, ensure_ascii=False))