update
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""业务服务层(预测器缓存、图像编解码等)。"""
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_background_color(value: str) -> tuple[int, int, int]:
|
||||
text = value.strip()
|
||||
if text.startswith("#"):
|
||||
hex_text = text[1:]
|
||||
if len(hex_text) != 6:
|
||||
raise ValueError("background_color 使用 #RRGGBB 时必须是 6 位十六进制")
|
||||
try:
|
||||
return tuple(int(hex_text[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
|
||||
except ValueError as e:
|
||||
raise ValueError(f"background_color 不是有效十六进制颜色: {value}") from e
|
||||
|
||||
parts = [p.strip() for p in text.split(",")]
|
||||
if len(parts) != 3:
|
||||
raise ValueError("background_color 必须为 #RRGGBB 或 R,G,B")
|
||||
try:
|
||||
rgb = tuple(int(p) for p in parts)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"background_color 不是有效 RGB 颜色: {value}") from e
|
||||
if any(c < 0 or c > 255 for c in rgb):
|
||||
raise ValueError("background_color 的 RGB 分量必须在 0~255")
|
||||
return rgb # type: ignore[return-value]
|
||||
|
||||
|
||||
def align_size_to_input(width: int, height: int, max_side: int) -> tuple[int, int]:
|
||||
run_w, run_h = width, height
|
||||
if max(width, height) > max_side:
|
||||
scale = max_side / float(max(width, height))
|
||||
run_w = int(round(width * scale))
|
||||
run_h = int(round(height * scale))
|
||||
|
||||
run_w = max(8, run_w - (run_w % 8))
|
||||
run_h = max(8, run_h - (run_h % 8))
|
||||
return run_w, run_h
|
||||
|
||||
|
||||
def composite_rgba_on_background(
|
||||
image: Image.Image,
|
||||
background_rgb: tuple[int, int, int],
|
||||
size: tuple[int, int],
|
||||
) -> Image.Image:
|
||||
rgba = image.convert("RGBA")
|
||||
if rgba.size != size:
|
||||
rgba = rgba.resize(size, resample=Image.BICUBIC)
|
||||
|
||||
bg = Image.new("RGB", size, background_rgb)
|
||||
alpha = rgba.getchannel("A")
|
||||
bg.paste(rgba.convert("RGB"), mask=alpha)
|
||||
return bg
|
||||
|
||||
|
||||
def remove_background_by_color(
|
||||
image: Image.Image,
|
||||
background_rgb: tuple[int, int, int],
|
||||
tolerance: int,
|
||||
output_size: tuple[int, int] | None = None,
|
||||
) -> Image.Image:
|
||||
rgba = image.convert("RGBA")
|
||||
if output_size is not None and rgba.size != output_size:
|
||||
rgba = rgba.resize(output_size, resample=Image.BICUBIC)
|
||||
|
||||
arr = np.asarray(rgba, dtype=np.uint8).copy()
|
||||
rgb = arr[:, :, :3].astype(np.int16)
|
||||
bg = np.array(background_rgb, dtype=np.int16).reshape(1, 1, 3)
|
||||
diff = np.abs(rgb - bg)
|
||||
bg_like = np.max(diff, axis=2) <= int(tolerance)
|
||||
bg_mask = _edge_connected_mask(bg_like)
|
||||
arr[bg_mask, 3] = 0
|
||||
return Image.fromarray(arr, mode="RGBA")
|
||||
|
||||
|
||||
def list_png_frames(frame_dir: Path) -> list[Path]:
|
||||
frames = [p for p in frame_dir.iterdir() if p.is_file() and p.suffix.lower() == ".png"]
|
||||
return sorted(frames, key=lambda p: p.name)
|
||||
|
||||
|
||||
def _edge_connected_mask(mask: np.ndarray) -> np.ndarray:
|
||||
h, w = mask.shape
|
||||
visited = np.zeros((h, w), dtype=bool)
|
||||
queue: deque[tuple[int, int]] = deque()
|
||||
|
||||
for x in range(w):
|
||||
if mask[0, x]:
|
||||
queue.append((0, x))
|
||||
if mask[h - 1, x]:
|
||||
queue.append((h - 1, x))
|
||||
for y in range(h):
|
||||
if mask[y, 0]:
|
||||
queue.append((y, 0))
|
||||
if mask[y, w - 1]:
|
||||
queue.append((y, w - 1))
|
||||
|
||||
while queue:
|
||||
y, x = queue.popleft()
|
||||
if visited[y, x] or not mask[y, x]:
|
||||
continue
|
||||
visited[y, x] = True
|
||||
if y > 0:
|
||||
queue.append((y - 1, x))
|
||||
if y + 1 < h:
|
||||
queue.append((y + 1, x))
|
||||
if x > 0:
|
||||
queue.append((y, x - 1))
|
||||
if x + 1 < w:
|
||||
queue.append((y, x + 1))
|
||||
|
||||
return visited
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def depth_to_png16_bytes(depth: np.ndarray) -> bytes:
|
||||
depth = np.asarray(depth, dtype=np.float32)
|
||||
dmin = float(depth.min())
|
||||
dmax = float(depth.max())
|
||||
if dmax > dmin:
|
||||
norm = (depth - dmin) / (dmax - dmin)
|
||||
else:
|
||||
norm = np.zeros_like(depth, dtype=np.float32)
|
||||
# 前后端约定:最远=255,最近=0(8-bit)
|
||||
u8 = ((1.0 - norm) * 255.0).clip(0, 255).astype(np.uint8)
|
||||
img = Image.fromarray(u8, mode="L")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def b64_to_pil_image(b64: str) -> Image.Image:
|
||||
raw = base64.b64decode(b64)
|
||||
return Image.open(io.BytesIO(raw))
|
||||
|
||||
|
||||
def pil_image_to_png_b64(img: Image.Image) -> str:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return base64.b64encode(buf.getvalue()).decode("ascii")
|
||||
|
||||
|
||||
def default_half_mask(img: Image.Image) -> Image.Image:
|
||||
w, h = img.size
|
||||
mask = Image.new("L", (w, h), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rectangle([w // 2, 0, w, h], fill=255)
|
||||
return mask
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from config_loader import load_app_config, get_depth_backend_from_app
|
||||
from model.Animation.animation_loader import (
|
||||
UnifiedAnimationConfig,
|
||||
AnimationBackend,
|
||||
build_animation_predictor,
|
||||
)
|
||||
from model.Depth.depth_loader import UnifiedDepthConfig, DepthBackend, build_depth_predictor
|
||||
from model.Inpaint.inpaint_loader import UnifiedInpaintConfig, InpaintBackend, build_inpaint_predictor
|
||||
from model.Seg.seg_loader import UnifiedSegConfig, SegBackend, build_seg_predictor
|
||||
|
||||
_depth_predictor = None
|
||||
_depth_backend: DepthBackend | None = None
|
||||
|
||||
_seg_cache: dict[str, Any] = {}
|
||||
|
||||
_inpaint_cache: dict[str, Any] = {}
|
||||
|
||||
_animation_cache: dict[str, Any] = {}
|
||||
|
||||
|
||||
def ensure_depth_predictor() -> None:
|
||||
global _depth_predictor, _depth_backend
|
||||
if _depth_predictor is not None and _depth_backend is not None:
|
||||
return
|
||||
|
||||
app_cfg = load_app_config()
|
||||
backend_str = get_depth_backend_from_app(app_cfg)
|
||||
try:
|
||||
backend = DepthBackend(backend_str)
|
||||
except Exception as e:
|
||||
raise ValueError(f"config.py 中 depth.backend 不合法: {backend_str}") from e
|
||||
|
||||
_depth_predictor, _depth_backend = build_depth_predictor(UnifiedDepthConfig(backend=backend))
|
||||
|
||||
|
||||
def get_depth_predictor():
|
||||
ensure_depth_predictor()
|
||||
return _depth_predictor
|
||||
|
||||
|
||||
def get_seg_predictor(model_name: str):
|
||||
if model_name == "mask2former_debug":
|
||||
model_name = "sam"
|
||||
|
||||
if model_name in _seg_cache:
|
||||
return _seg_cache[model_name]
|
||||
|
||||
if model_name == "sam":
|
||||
pred, _ = build_seg_predictor(UnifiedSegConfig(backend=SegBackend.SAM))
|
||||
_seg_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
if model_name == "mask2former":
|
||||
pred, _ = build_seg_predictor(UnifiedSegConfig(backend=SegBackend.MASK2FORMER))
|
||||
_seg_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
raise ValueError(f"未知 segment model_name: {model_name}")
|
||||
|
||||
|
||||
def get_inpaint_predictor(model_name: str):
|
||||
if model_name in _inpaint_cache:
|
||||
return _inpaint_cache[model_name]
|
||||
|
||||
if model_name == "copy":
|
||||
|
||||
def _copy(image: Image.Image, *_args, **_kwargs) -> Image.Image:
|
||||
return image.convert("RGB")
|
||||
|
||||
_inpaint_cache[model_name] = _copy
|
||||
return _copy
|
||||
|
||||
if model_name == "sdxl_inpaint":
|
||||
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.SDXL_INPAINT))
|
||||
_inpaint_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
if model_name == "controlnet":
|
||||
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.CONTROLNET))
|
||||
_inpaint_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
if model_name == "lama":
|
||||
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.LAMA))
|
||||
_inpaint_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
raise ValueError(f"未知 inpaint model_name: {model_name}")
|
||||
|
||||
|
||||
def get_animation_predictor(model_name: str):
|
||||
if model_name in _animation_cache:
|
||||
return _animation_cache[model_name]
|
||||
|
||||
if model_name == "animatediff":
|
||||
pred, _ = build_animation_predictor(
|
||||
UnifiedAnimationConfig(backend=AnimationBackend.ANIMATEDIFF)
|
||||
)
|
||||
_animation_cache[model_name] = pred
|
||||
return pred
|
||||
|
||||
raise ValueError(f"未知 animation model_name: {model_name}")
|
||||
Reference in New Issue
Block a user