update
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""HTTP 路由(按领域拆分)。"""
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import shutil
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter
|
||||
from PIL import Image
|
||||
|
||||
from ..deps import OUTPUT_DIR
|
||||
from ..schemas import AnimateRequest, CharacterAnimateRequest
|
||||
from ..services.animation_frames import (
|
||||
align_size_to_input,
|
||||
composite_rgba_on_background,
|
||||
list_png_frames,
|
||||
parse_background_color,
|
||||
remove_background_by_color,
|
||||
)
|
||||
from ..services.image_io import b64_to_pil_image, pil_image_to_png_b64
|
||||
from ..services.predictors import get_animation_predictor
|
||||
|
||||
router = APIRouter(tags=["animation"])
|
||||
|
||||
|
||||
@router.post("/animate")
|
||||
def animate(req: AnimateRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
model_name = req.model_name or "animatediff"
|
||||
predictor = get_animation_predictor(model_name)
|
||||
|
||||
result_path = predictor(
|
||||
prompt=req.prompt,
|
||||
negative_prompt=req.negative_prompt or "",
|
||||
num_inference_steps=req.num_inference_steps,
|
||||
guidance_scale=req.guidance_scale,
|
||||
width=req.width,
|
||||
height=req.height,
|
||||
video_length=req.video_length,
|
||||
seed=req.seed,
|
||||
)
|
||||
|
||||
out_dir = OUTPUT_DIR / "animation"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_path = out_dir / f"{model_name}_{ts}.gif"
|
||||
shutil.copy2(result_path, out_path)
|
||||
|
||||
return {"success": True, "output_path": str(out_path)}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@router.post("/animate/character_sequence")
|
||||
def animate_character_sequence(req: CharacterAnimateRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
输入透明背景角色 PNG,生成 30fps 角色动画 PNG 序列。
|
||||
|
||||
流程:
|
||||
- 将角色合成到指定纯色背景,作为 AnimateDiff 控制图
|
||||
- 以与输入相近、且对齐到 8 的分辨率生成 PNG 序列
|
||||
- 对每帧按背景色做色键剔除,返回透明背景 PNG 序列给前端
|
||||
"""
|
||||
try:
|
||||
model_name = req.model_name or "animatediff"
|
||||
background_rgb = parse_background_color(req.background_color)
|
||||
|
||||
character = b64_to_pil_image(req.image_b64).convert("RGBA")
|
||||
orig_w, orig_h = character.size
|
||||
run_w, run_h = align_size_to_input(orig_w, orig_h, req.max_side)
|
||||
|
||||
predictor = get_animation_predictor(model_name)
|
||||
|
||||
out_dir = OUTPUT_DIR / "animation" / "character_sequence"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
seq_dir = out_dir / f"{model_name}_{ts}"
|
||||
seq_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
control_path = seq_dir / "control_image.png"
|
||||
control_image = composite_rgba_on_background(
|
||||
character,
|
||||
background_rgb,
|
||||
size=(run_w, run_h),
|
||||
)
|
||||
control_image.save(control_path)
|
||||
|
||||
frame_dir = predictor(
|
||||
prompt=req.prompt,
|
||||
negative_prompt=req.negative_prompt or "",
|
||||
num_inference_steps=req.num_inference_steps,
|
||||
guidance_scale=req.guidance_scale,
|
||||
width=run_w,
|
||||
height=run_h,
|
||||
video_length=req.video_length,
|
||||
seed=req.seed,
|
||||
control_image_path=str(control_path),
|
||||
output_format="png_sequence",
|
||||
)
|
||||
|
||||
frame_paths = list_png_frames(frame_dir)
|
||||
if not frame_paths:
|
||||
raise FileNotFoundError(f"未找到 AnimateDiff 输出 PNG 序列: {frame_dir}")
|
||||
|
||||
frames: list[dict[str, Any]] = []
|
||||
processed_dir = seq_dir / "frames"
|
||||
processed_dir.mkdir(parents=True, exist_ok=True)
|
||||
for idx, frame_path in enumerate(frame_paths):
|
||||
frame = remove_background_by_color(
|
||||
Image.open(frame_path),
|
||||
background_rgb,
|
||||
tolerance=req.background_tolerance,
|
||||
output_size=(orig_w, orig_h),
|
||||
)
|
||||
out_path = processed_dir / f"{idx:04d}.png"
|
||||
frame.save(out_path)
|
||||
frames.append(
|
||||
{
|
||||
"index": idx,
|
||||
"image_b64": pil_image_to_png_b64(frame),
|
||||
"path": str(out_path),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"fps": 30,
|
||||
"frame_count": len(frames),
|
||||
"width": orig_w,
|
||||
"height": orig_h,
|
||||
"background_color": req.background_color,
|
||||
"output_dir": str(processed_dir),
|
||||
"frames": frames,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e), "frames": []}
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import Response
|
||||
|
||||
from ..schemas import DepthRequest
|
||||
from ..services.depth_io import depth_to_png16_bytes
|
||||
from ..services.image_io import b64_to_pil_image
|
||||
from ..services.predictors import get_depth_predictor
|
||||
|
||||
router = APIRouter(tags=["depth"])
|
||||
|
||||
|
||||
@router.post("/depth")
|
||||
def depth(req: DepthRequest):
|
||||
"""
|
||||
计算深度并直接返回二进制 PNG(16-bit 灰度)。
|
||||
|
||||
约束:
|
||||
- 前端不传/不选模型;模型选择写死在后端 config.py
|
||||
- 成功:HTTP 200 + Content-Type: image/png
|
||||
- 失败:HTTP 500,detail 为错误信息
|
||||
"""
|
||||
try:
|
||||
pil = b64_to_pil_image(req.image_b64).convert("RGB")
|
||||
predictor = get_depth_predictor()
|
||||
depth_arr = predictor(pil) # type: ignore[misc]
|
||||
png_bytes = depth_to_png16_bytes(np.asarray(depth_arr))
|
||||
return Response(content=png_bytes, media_type="image/png")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from ..deps import OUTPUT_DIR
|
||||
from ..schemas import InpaintRequest
|
||||
from ..services.image_io import b64_to_pil_image, default_half_mask, pil_image_to_png_b64
|
||||
from ..services.predictors import get_inpaint_predictor
|
||||
|
||||
router = APIRouter(tags=["inpaint"])
|
||||
|
||||
|
||||
@router.post("/inpaint")
|
||||
def inpaint(req: InpaintRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
model_name = req.model_name or "sdxl_inpaint"
|
||||
pil = b64_to_pil_image(req.image_b64).convert("RGB")
|
||||
|
||||
if req.mask_b64:
|
||||
mask = b64_to_pil_image(req.mask_b64).convert("L")
|
||||
else:
|
||||
mask = default_half_mask(pil)
|
||||
|
||||
predictor = get_inpaint_predictor(model_name)
|
||||
out = predictor(
|
||||
pil,
|
||||
mask,
|
||||
req.prompt or "",
|
||||
req.negative_prompt or "",
|
||||
strength=req.strength,
|
||||
max_side=req.max_side,
|
||||
)
|
||||
|
||||
out_dir = OUTPUT_DIR / "inpaint"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"{model_name}_inpaint.png"
|
||||
out.save(out_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"output_path": str(out_path),
|
||||
"output_image_b64": pil_image_to_png_b64(out),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(tags=["meta"])
|
||||
|
||||
|
||||
@router.get("/models")
|
||||
def get_models() -> Dict[str, Any]:
|
||||
"""
|
||||
返回一个兼容 Qt 前端的 schema:
|
||||
{
|
||||
"models": {
|
||||
"depth": { "key": { "name": "..."} ... },
|
||||
"segment": { ... },
|
||||
"inpaint": { "key": { "name": "...", "params": [...] } ... }
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {
|
||||
"models": {
|
||||
"depth": {
|
||||
"midas": {"name": "MiDaS (default)"},
|
||||
"zoedepth_n": {"name": "ZoeDepth (ZoeD_N)"},
|
||||
"zoedepth_k": {"name": "ZoeDepth (ZoeD_K)"},
|
||||
"zoedepth_nk": {"name": "ZoeDepth (ZoeD_NK)"},
|
||||
"depth_anything_v2_vits": {"name": "Depth Anything V2 (vits)"},
|
||||
"depth_anything_v2_vitb": {"name": "Depth Anything V2 (vitb)"},
|
||||
"depth_anything_v2_vitl": {"name": "Depth Anything V2 (vitl)"},
|
||||
"depth_anything_v2_vitg": {"name": "Depth Anything V2 (vitg)"},
|
||||
"dpt_large": {"name": "DPT (large)"},
|
||||
"dpt_hybrid": {"name": "DPT (hybrid)"},
|
||||
"midas_dpt_beit_large_512": {"name": "MiDaS (dpt_beit_large_512)"},
|
||||
"midas_dpt_swin2_large_384": {"name": "MiDaS (dpt_swin2_large_384)"},
|
||||
"midas_dpt_swin2_tiny_256": {"name": "MiDaS (dpt_swin2_tiny_256)"},
|
||||
"midas_dpt_levit_224": {"name": "MiDaS (dpt_levit_224)"},
|
||||
},
|
||||
"segment": {
|
||||
"sam": {"name": "SAM (vit_h)"},
|
||||
"mask2former_debug": {"name": "SAM (compat mask2former_debug)"},
|
||||
"mask2former": {"name": "Mask2Former (not implemented)"},
|
||||
},
|
||||
"inpaint": {
|
||||
"copy": {"name": "Copy (no-op)", "params": []},
|
||||
"sdxl_inpaint": {
|
||||
"name": "SDXL Inpaint",
|
||||
"params": [
|
||||
{"id": "prompt", "label": "提示词", "optional": True},
|
||||
],
|
||||
},
|
||||
"lama": {"name": "LaMa (Erase / Remove Object)", "params": []},
|
||||
"controlnet": {
|
||||
"name": "ControlNet Inpaint (canny)",
|
||||
"params": [
|
||||
{"id": "prompt", "label": "提示词", "optional": True},
|
||||
],
|
||||
},
|
||||
},
|
||||
"animation": {
|
||||
"animatediff": {
|
||||
"name": "AnimateDiff (Text-to-Video)",
|
||||
"params": [
|
||||
{"id": "prompt", "label": "提示词", "optional": False},
|
||||
{"id": "negative_prompt", "label": "负向提示词", "optional": True},
|
||||
{"id": "num_inference_steps", "label": "采样步数", "optional": True},
|
||||
{"id": "guidance_scale", "label": "CFG Scale", "optional": True},
|
||||
{"id": "width", "label": "宽度", "optional": True},
|
||||
{"id": "height", "label": "高度", "optional": True},
|
||||
{"id": "video_length", "label": "帧数", "optional": True},
|
||||
{"id": "seed", "label": "随机种子", "optional": True},
|
||||
],
|
||||
},
|
||||
"character_sequence": {
|
||||
"name": "Character PNG Sequence (30 FPS)",
|
||||
"endpoint": "/animate/character_sequence",
|
||||
"params": [
|
||||
{"id": "image_b64", "label": "透明背景角色 PNG", "optional": False},
|
||||
{"id": "prompt", "label": "动画提示词", "optional": False},
|
||||
{"id": "background_color", "label": "纯色背景", "optional": True},
|
||||
{"id": "video_length", "label": "帧数", "optional": True},
|
||||
{"id": "background_tolerance", "label": "背景剔除阈值", "optional": True},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health() -> Dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter
|
||||
from PIL import Image
|
||||
|
||||
from ..deps import OUTPUT_DIR
|
||||
from ..schemas import SamPromptSegmentRequest, SegmentRequest
|
||||
from ..services.image_io import b64_to_pil_image
|
||||
from ..services.predictors import get_seg_predictor
|
||||
from model.Seg.seg_loader import expand_mask, mask_to_contour_xy, run_sam_prompt
|
||||
|
||||
router = APIRouter(tags=["segment"])
|
||||
|
||||
|
||||
@router.post("/segment")
|
||||
def segment(req: SegmentRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
model_name = req.model_name or "sam"
|
||||
pil = b64_to_pil_image(req.image_b64).convert("RGB")
|
||||
rgb = np.array(pil, dtype=np.uint8)
|
||||
|
||||
predictor = get_seg_predictor(model_name)
|
||||
label_map = predictor(rgb).astype(np.int32)
|
||||
|
||||
out_dir = OUTPUT_DIR / "segment"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"{model_name}_label.png"
|
||||
Image.fromarray(np.clip(label_map, 0, 255).astype(np.uint8), mode="L").save(out_path)
|
||||
|
||||
return {"success": True, "label_path": str(out_path)}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@router.post("/segment/sam_prompt")
|
||||
def segment_sam_prompt(req: SamPromptSegmentRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
交互式 SAM:裁剪图 + 点/框提示,返回掩膜外轮廓点列(裁剪像素坐标)。
|
||||
"""
|
||||
try:
|
||||
pil = b64_to_pil_image(req.image_b64).convert("RGB")
|
||||
rgb = np.array(pil, dtype=np.uint8)
|
||||
h, w = rgb.shape[0], rgb.shape[1]
|
||||
|
||||
if req.overlay_b64:
|
||||
ov = b64_to_pil_image(req.overlay_b64)
|
||||
if ov.size != (w, h):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"overlay 尺寸 {ov.size} 与 image {w}x{h} 不一致",
|
||||
"contour": [],
|
||||
}
|
||||
|
||||
if len(req.point_coords) != len(req.point_labels):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "point_coords 与 point_labels 长度不一致",
|
||||
"contour": [],
|
||||
}
|
||||
if len(req.point_coords) < 1:
|
||||
return {"success": False, "error": "至少需要一个提示点", "contour": []}
|
||||
|
||||
pc = np.array(req.point_coords, dtype=np.float32)
|
||||
if pc.ndim != 2 or pc.shape[1] != 2:
|
||||
return {"success": False, "error": "point_coords 每项须为 [x,y]", "contour": []}
|
||||
|
||||
pl = np.array(req.point_labels, dtype=np.int64)
|
||||
box = np.array(req.box_xyxy, dtype=np.float32)
|
||||
|
||||
mask = run_sam_prompt(rgb, pc, pl, box_xyxy=box)
|
||||
if not np.any(mask):
|
||||
return {"success": False, "error": "SAM 未产生有效掩膜", "contour": []}
|
||||
|
||||
mask = expand_mask(mask, int(req.expand_px))
|
||||
contour = mask_to_contour_xy(mask, epsilon_px=2.0)
|
||||
if len(contour) < 3:
|
||||
return {"success": False, "error": "轮廓点数不足", "contour": []}
|
||||
|
||||
return {"success": True, "contour": contour, "error": None}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e), "contour": []}
|
||||
Reference in New Issue
Block a user