stable-makeup / app.py
guobc2026
Stable-Makeup: HF Spaces deployment
21db46b
Raw
History Blame Contribute Delete
7.7 kB
"""Stable-Makeup: 妆容迁移 · ModelScope 创空间 xGPU
基于 sky24h/Stable-Makeup-unofficial 成功部署方案。
模型从 ModelScope 加载,预训练权重从 HuggingFace Hub 拉取。
"""
import os
import numpy as np
from PIL import Image
import torch
import gradio as gr
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
os.chdir(PROJECT_ROOT)
# ═══ 设备 · 精度自适应 ═══
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
print(f"[Stable-Makeup] 设备: {DEVICE}, 精度: {DTYPE}")
if DEVICE == "cuda":
print(f"[Stable-Makeup] GPU: {torch.cuda.get_device_name(0)}, 显存: {torch.cuda.get_device_properties(0).total_memory/1024**3:.0f}GB")
# ═══ 固定随机种子(与 sky24h 一致) ═══
torch.manual_seed(1024)
if torch.cuda.is_available():
torch.cuda.manual_seed(1024)
torch.cuda.manual_seed_all(1024)
# ═══════════════════════════════════════════════════════════
# 模型初始化(模块级加载)
# ═══════════════════════════════════════════════════════════
def _ms_download(repo_id, cache_dir="./models"):
"""从 ModelScope 下载模型"""
from modelscope import snapshot_download
return snapshot_download(repo_id, cache_dir=cache_dir)
def init_pipeline():
"""初始化推理管线——float16 GPU + DDIM"""
from diffusers import UNet2DConditionModel as OriginalUNet2DConditionModel
from diffusers import DDIMScheduler, ControlNetModel
from pipeline_sd15 import StableDiffusionControlNetPipeline
from detail_encoder.encoder_plus import detail_encoder
# ── SD1.5 基础模型 ──
sd15_dir = _ms_download("AI-ModelScope/stable-diffusion-v1-5")
print(f"[Stable-Makeup] SD1.5: {sd15_dir}")
Unet = OriginalUNet2DConditionModel.from_pretrained(
sd15_dir, subfolder="unet", torch_dtype=DTYPE, local_files_only=True
).to(DEVICE)
# ── 双路 ControlNet ──
id_encoder = ControlNetModel.from_unet(Unet)
pose_encoder = ControlNetModel.from_unet(Unet)
# ── 妆容编码器(CLIP + SSR attention) ──
clip_dir = _ms_download("AI-ModelScope/clip-vit-large-patch14")
print(f"[Stable-Makeup] CLIP: {clip_dir}")
makeup_encoder = detail_encoder(Unet, clip_dir, DEVICE, dtype=DTYPE)
# ── 加载 Stable-Makeup 预训练权重(本地优先 → Google Drive 兜底) ──
print("[Stable-Makeup] 加载预训练权重...")
import gc
REQUIRED = ["pytorch_model.bin", "pytorch_model_1.bin", "pytorch_model_2.bin"]
WEIGHTS_DIR = os.path.join(PROJECT_ROOT, "weights")
GDRIVE_FOLDER = "1397t27GrUyLPnj17qVpKWGwg93EcaFfg"
# 检查缺失文件 → 自动从 Google Drive 下载(HF Spaces 海外服务器可直连)
os.makedirs(WEIGHTS_DIR, exist_ok=True)
missing = [f for f in REQUIRED if not os.path.exists(os.path.join(WEIGHTS_DIR, f))]
if missing:
print(f"[Stable-Makeup] 缺失: {missing},从 Google Drive 下载...")
try:
import gdown
gdown.download_folder(id=GDRIVE_FOLDER, output=WEIGHTS_DIR, quiet=False)
except Exception as e:
print(f"[Stable-Makeup] ⚠️ 自动下载失败: {e}")
print(f" 请手动下载: https://drive.google.com/drive/folders/{GDRIVE_FOLDER}")
print(f" 放到: {WEIGHTS_DIR}/")
raise
def _load_weight(filename):
local_path = os.path.join(WEIGHTS_DIR, filename)
print(f" ✅ 加载: {local_path} ({os.path.getsize(local_path)/1024**3:.1f}GB)")
return torch.load(local_path, map_location="cpu")
id_encoder.load_state_dict(_load_weight("pytorch_model_1.bin"), strict=False)
gc.collect()
pose_encoder.load_state_dict(_load_weight("pytorch_model_2.bin"), strict=False)
gc.collect()
makeup_encoder.load_state_dict(_load_weight("pytorch_model.bin"), strict=False)
gc.collect()
id_encoder.to(device=DEVICE, dtype=DTYPE)
pose_encoder.to(device=DEVICE, dtype=DTYPE)
makeup_encoder.to(device=DEVICE, dtype=DTYPE)
# ── 推理管线 ──
pipe = StableDiffusionControlNetPipeline.from_pretrained(
sd15_dir,
safety_checker=None,
unet=Unet,
controlnet=[id_encoder, pose_encoder],
torch_dtype=DTYPE,
local_files_only=True,
).to(DEVICE)
# DDIMScheduler(论文原版,与 sky24h 一致)
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
print("[Stable-Makeup] ✅ 模型加载完成 (float16 GPU + DDIM)")
return pipe, makeup_encoder
# ── 启动时加载模型 ──
pipeline, makeup_encoder = init_pipeline()
# ═══════════════════════════════════════════════════════════
# 推理函数
# ═══════════════════════════════════════════════════════════
def get_draw(pil_img, size):
"""生成人脸结构控制图(PIL → cv2 BGR → SPIGA → 骨架图)"""
import cv2
from spiga_draw import spiga_process, spiga_segmentation
cv2_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
spigas = spiga_process(cv2_img)
if spigas is False:
width, height = pil_img.size
return Image.new("RGB", (width, height), color=(0, 0, 0))
return spiga_segmentation(spigas, size=size)
def makeup_transfer(id_image, makeup_image, guidance_scale=1.6):
"""妆容迁移——与 sky24h 推理逻辑完全一致"""
size = 512
id_image_resized = id_image.resize((size, size))
makeup_image_resized = makeup_image.resize((size, size))
pose_image = get_draw(id_image_resized, size=size)
result = makeup_encoder.generate(
id_image=[id_image_resized, pose_image],
makeup_image=makeup_image_resized,
guidance_scale=guidance_scale,
pipe=pipeline,
)
return result
# ═══════════════════════════════════════════════════════════
# Gradio UI
# ═══════════════════════════════════════════════════════════
with gr.Blocks(title="Stable-Makeup 妆容迁移") as demo:
gr.Markdown("""
# 💄 Stable-Makeup · 妆容迁移
上传素颜照 + 参考妆容图,AI 将妆容迁移到你的照片上。
基于 [Stable-Makeup](https://arxiv.org/abs/2403.07764) (arXiv 2403.07764)
""")
with gr.Row():
with gr.Column():
id_img = gr.Image(label="素颜照", type="pil", height=400)
with gr.Column():
makeup_img = gr.Image(label="参考妆容", type="pil", height=400)
guidance = gr.Slider(
minimum=1.01, maximum=3.0, value=1.6, step=0.05,
label="妆容浓度 (guidance_scale)",
info="淡妆建议 1.05-1.15,浓妆建议 2.0"
)
btn = gr.Button("开始试妆", variant="primary")
output = gr.Image(label="试妆结果", type="pil")
btn.click(
fn=makeup_transfer,
inputs=[id_img, makeup_img, guidance],
outputs=output,
)
if __name__ == "__main__":
demo.queue(max_size=10).launch()