87359ca304
Add seg_fill_intersect (default off) and seg_fill_gate_min to AlphaPostSettings, plus a gate_mask arg to fill_seg_dropouts. When enabled, the anime-seg dropout-fill is intersected with the reliable BiRefNet mask (>= gate_min): anime-seg proposes wisps at a low seg_fill_seg_min, but only rescues BiRefNet also endorses survive, trimming anime-seg's over-inclusive fringe while keeping its smooth value. Default off, so the shipped Option B (union) output is byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
6.3 KiB
Python
168 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from .alpha_post import (
|
|
clean_alpha,
|
|
enforce_trimap,
|
|
fill_seg_dropouts,
|
|
suppress_alpha_by_chroma,
|
|
)
|
|
from .chroma import compute_bg_confidence
|
|
from .despill import despill_green
|
|
from .foreground import estimate_foreground_rgb
|
|
from .recolor import recolor_foreground
|
|
from .io import load_rgb, save_gray, save_rgb, save_rgba, write_text
|
|
from .qa import BACKGROUND_COLORS, composite, make_qa_grid, save_previews
|
|
from .settings import PipelineSettings
|
|
from .trimap import fuse_trimap, generate_trimap, trimap_to_alpha_seed
|
|
from .vitmatte_infer import ViTMatteRunner
|
|
|
|
|
|
class MattingPipeline:
|
|
def __init__(self, settings: PipelineSettings):
|
|
self.settings = settings
|
|
self._runner: ViTMatteRunner | None = None
|
|
self._segmenter = None
|
|
self._fill_segmenter = None
|
|
|
|
def _segment(self, rgb: np.ndarray) -> np.ndarray:
|
|
if self._segmenter is None:
|
|
from .segmentation import make_segmenter
|
|
|
|
self._segmenter = make_segmenter(self.settings.segmentation)
|
|
mask = self._segmenter.mask(rgb)
|
|
seg = self.settings.segmentation
|
|
if seg.sharpen > 0:
|
|
from .segmentation import unsharp_mask
|
|
|
|
mask = unsharp_mask(mask, seg.sharpen, seg.sharpen_radius)
|
|
return mask
|
|
|
|
def _segment_fill(self, rgb: np.ndarray, default_mask: np.ndarray) -> np.ndarray:
|
|
"""Mask for the dropout-fill; may use a different (higher-recall) backend
|
|
than the trimap, since the fill only adds and cannot break anything."""
|
|
seg = self.settings.segmentation
|
|
if not seg.fill_backend or seg.fill_backend == seg.backend:
|
|
return default_mask
|
|
if self._fill_segmenter is None:
|
|
from dataclasses import replace
|
|
|
|
from .segmentation import make_segmenter
|
|
|
|
self._fill_segmenter = make_segmenter(
|
|
replace(seg, backend=seg.fill_backend, model_name=seg.fill_model_name)
|
|
)
|
|
return self._fill_segmenter.mask(rgb)
|
|
|
|
def _predict_alpha(self, rgb: np.ndarray, trimap: np.ndarray, bg_confidence: np.ndarray) -> tuple[np.ndarray, str]:
|
|
if self.settings.model.matting_method == "chroma":
|
|
return trimap_to_alpha_seed(trimap, bg_confidence), "chroma"
|
|
if self.settings.model.matting_method != "vitmatte":
|
|
raise RuntimeError(
|
|
f"Unsupported matting method '{self.settings.model.matting_method}'. "
|
|
"Use 'vitmatte' or 'chroma'."
|
|
)
|
|
|
|
try:
|
|
if self._runner is None:
|
|
self._runner = ViTMatteRunner(self.settings.model)
|
|
return self._runner.predict(rgb, trimap), "vitmatte"
|
|
except RuntimeError:
|
|
if not self.settings.model.fallback_to_chroma_alpha:
|
|
raise
|
|
return trimap_to_alpha_seed(trimap, bg_confidence), "chroma_fallback"
|
|
|
|
def run_image(
|
|
self,
|
|
input_path: str | Path,
|
|
output_path: str | Path,
|
|
debug_dir: str | Path | None,
|
|
) -> dict:
|
|
return _run_image(input_path, output_path, debug_dir, self)
|
|
|
|
|
|
def run_image(
|
|
input_path: str | Path,
|
|
output_path: str | Path,
|
|
debug_dir: str | Path | None,
|
|
settings: PipelineSettings,
|
|
) -> dict:
|
|
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:
|
|
settings = pipeline.settings
|
|
rgb = load_rgb(input_path)
|
|
bg_confidence, model = compute_bg_confidence(rgb, settings=settings.chroma)
|
|
if settings.segmentation.enabled:
|
|
seg_mask = pipeline._segment(rgb)
|
|
trimap, trimap_stats = fuse_trimap(seg_mask, bg_confidence, settings.trimap)
|
|
else:
|
|
seg_mask = None
|
|
trimap, trimap_stats = generate_trimap(bg_confidence, settings.trimap)
|
|
alpha, alpha_source = pipeline._predict_alpha(rgb, trimap, bg_confidence)
|
|
|
|
alpha = enforce_trimap(alpha, trimap)
|
|
alpha = suppress_alpha_by_chroma(alpha, bg_confidence, trimap, settings.alpha_post)
|
|
alpha = clean_alpha(alpha, trimap, settings.alpha_post)
|
|
if seg_mask is not None:
|
|
fill_mask = pipeline._segment_fill(rgb, seg_mask)
|
|
# seg_mask is the reliable trimap backend (BiRefNet); it gates the fill
|
|
# when seg_fill_intersect is on (fill_mask ∧ seg_mask).
|
|
alpha = fill_seg_dropouts(
|
|
alpha, fill_mask, settings.alpha_post, gate_mask=seg_mask
|
|
)
|
|
foreground = estimate_foreground_rgb(
|
|
rgb, alpha, bg_confidence, model, settings.foreground
|
|
)
|
|
corrected_rgb, color_mask = despill_green(
|
|
foreground.rgb, alpha, model, settings.despill
|
|
)
|
|
if settings.recolor.enabled:
|
|
recolored = recolor_foreground(corrected_rgb, alpha, seg_mask, settings.recolor)
|
|
corrected_rgb, color_mask = recolored.rgb, recolored.mask
|
|
save_rgba(output_path, corrected_rgb, alpha)
|
|
|
|
result = {
|
|
"input": str(input_path),
|
|
"output": str(output_path),
|
|
"alpha_source": alpha_source,
|
|
"background_model": model.to_dict(),
|
|
"trimap": trimap_stats,
|
|
"settings": asdict(settings),
|
|
}
|
|
|
|
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)
|
|
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)
|
|
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),
|
|
}
|
|
for name, color in BACKGROUND_COLORS.items():
|
|
tiles[f"preview {name}"] = composite(corrected_rgb, alpha, color)
|
|
make_qa_grid(debug, tiles)
|
|
write_text(debug / "metadata.json", json.dumps(result, indent=2, ensure_ascii=False))
|
|
|
|
return result
|