This commit is contained in:
2026-05-14 18:37:18 +08:00
parent 5e6d8046e1
commit 7a2ffc91a2
15 changed files with 200 additions and 56 deletions
+1 -10
View File
@@ -25,14 +25,6 @@ QPushButton* makeAlgoButton(const QString& title, QWidget* parent) {
} // namespace } // namespace
QString BlackholeResolveDialog::defaultModelInpaintPrompt() {
return QStringLiteral(
"中国画局部,需去除人物并只补背景:请严格依据掩膜外已见的笔墨皴擦、设色层次、留白与纸绢肌理,"
"在掩膜内自然延展,补全为山石、坡岸、云水、苔点或空灵留白等,与外缘笔法、墨色与气韵连贯衔接;"
"禁止出现人物轮廓、肢体、衣褶、五官、手足、发丝、肤色、投影及去人后的模糊晕带、鬼影边缘与色块台阶,"
"整体须像原画从未画入人物一样自然,不留修补感。");
}
BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent) BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent)
: QDialog(parent), : QDialog(parent),
m_blackholeName(blackholeName) { m_blackholeName(blackholeName) {
@@ -142,8 +134,7 @@ void BlackholeResolveDialog::buildDetailPage() {
pLay->setSpacing(8); pLay->setSpacing(8);
m_promptEdit = new QPlainTextEdit(panel); m_promptEdit = new QPlainTextEdit(panel);
m_promptEdit->setPlainText(BlackholeResolveDialog::defaultModelInpaintPrompt()); m_promptEdit->setPlaceholderText(QStringLiteral("可选"));
m_promptEdit->setPlaceholderText(QStringLiteral("可按画面增删;若全部删空,请求时仍使用内置默认描述"));
m_promptEdit->setMinimumHeight(72); m_promptEdit->setMinimumHeight(72);
pLay->addWidget(m_promptEdit); pLay->addWidget(m_promptEdit);
@@ -19,9 +19,6 @@ public:
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; } Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
QString promptText() const; QString promptText() const;
/// 模型补全(LaMa 等)内置正向提示:中国画去人物、补背景纹理,无人物残留。
[[nodiscard]] static QString defaultModelInpaintPrompt();
private: private:
void buildSelectPage(); void buildSelectPage();
void buildDetailPage(); void buildDetailPage();
+4 -12
View File
@@ -5050,21 +5050,13 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
client->setBaseUrl(QUrl(base)); client->setBaseUrl(QUrl(base));
QString immediateErr; QString immediateErr;
QString prompt = dlg.promptText(); const QString prompt = dlg.promptText();
if (prompt.isEmpty()) {
prompt = BlackholeResolveDialog::defaultModelInpaintPrompt();
}
QNetworkReply* reply = client->inpaintAsync( QNetworkReply* reply = client->inpaintAsync(
cropPng, cropPng,
maskPng, maskPng,
// LaMa(物体移除 / 擦除) QStringLiteral("flux_fill"),
QStringLiteral("lama"),
prompt, prompt,
// 抑制人物与去人常见伪影(中英混合便于通用后端) QString(),
QStringLiteral(
"person, human, face, body, hands, feet, portrait, silhouette, skin, clothing folds, hair, "
"shadow, ghosting, blur halo, seam, banding, watermark, text, logo, signature, jpeg artifacts"),
// 去人物场景:strength 太低容易“保留人物残影”;适当提高更干净
0.72, 0.72,
1024, 1024,
&immediateErr); &immediateErr);
@@ -5077,7 +5069,7 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
} }
auto* task = new CancelableTaskDialog(QStringLiteral("黑洞修复"), auto* task = new CancelableTaskDialog(QStringLiteral("黑洞修复"),
QStringLiteral("正在请求后端模型补全,请稍候…"), QStringLiteral("补全中"),
this); this);
task->setAttribute(Qt::WA_DeleteOnClose, true); task->setAttribute(Qt::WA_DeleteOnClose, true);
connect(task, &CancelableTaskDialog::canceled, this, [reply, task]() { connect(task, &CancelableTaskDialog::canceled, this, [reply, task]() {
+1
View File
@@ -0,0 +1 @@
HF_TOKEN=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
+7 -3
View File
@@ -15,7 +15,7 @@ router = APIRouter(tags=["inpaint"])
@router.post("/inpaint") @router.post("/inpaint")
def inpaint(req: InpaintRequest) -> Dict[str, Any]: def inpaint(req: InpaintRequest) -> Dict[str, Any]:
try: try:
model_name = req.model_name or "sdxl_inpaint" model_name = req.model_name or "flux_fill"
pil = b64_to_pil_image(req.image_b64).convert("RGB") pil = b64_to_pil_image(req.image_b64).convert("RGB")
if req.mask_b64: if req.mask_b64:
@@ -24,13 +24,17 @@ def inpaint(req: InpaintRequest) -> Dict[str, Any]:
mask = default_half_mask(pil) mask = default_half_mask(pil)
predictor = get_inpaint_predictor(model_name) predictor = get_inpaint_predictor(model_name)
call_kw: Dict[str, Any] = {"strength": req.strength, "max_side": req.max_side}
if req.guidance_scale is not None:
call_kw["guidance_scale"] = req.guidance_scale
if req.num_inference_steps is not None:
call_kw["num_inference_steps"] = req.num_inference_steps
out = predictor( out = predictor(
pil, pil,
mask, mask,
req.prompt or "", req.prompt or "",
req.negative_prompt or "", req.negative_prompt or "",
strength=req.strength, **call_kw,
max_side=req.max_side,
) )
out_dir = OUTPUT_DIR / "inpaint" out_dir = OUTPUT_DIR / "inpaint"
+11 -11
View File
@@ -43,19 +43,19 @@ def get_models() -> Dict[str, Any]:
"mask2former": {"name": "Mask2Former (not implemented)"}, "mask2former": {"name": "Mask2Former (not implemented)"},
}, },
"inpaint": { "inpaint": {
"copy": {"name": "Copy (no-op)", "params": []}, "flux_fill": {
"sdxl_inpaint": { "name": "FLUX.1 Fill (default)",
"name": "SDXL Inpaint", "params": [{"id": "prompt", "label": "prompt", "optional": True}],
"params": [
{"id": "prompt", "label": "提示词", "optional": True},
],
}, },
"lama": {"name": "LaMa (Erase / Remove Object)", "params": []}, "sdxl_inpaint": {
"name": "sdxl_inpaint",
"params": [{"id": "prompt", "label": "prompt", "optional": True}],
},
"lama": {"name": "lama", "params": []},
"copy": {"name": "copy (debug)", "params": []},
"controlnet": { "controlnet": {
"name": "ControlNet Inpaint (canny)", "name": "controlnet",
"params": [ "params": [{"id": "prompt", "label": "prompt", "optional": True}],
{"id": "prompt", "label": "提示词", "optional": True},
],
}, },
}, },
"animation": { "animation": {
+2
View File
@@ -52,6 +52,8 @@ class InpaintRequest(ImageInput):
negative_prompt: Optional[str] = Field("", description="负向 prompt") negative_prompt: Optional[str] = Field("", description="负向 prompt")
mask_b64: Optional[str] = Field(None, description="mask PNG base64(可选)") mask_b64: Optional[str] = Field(None, description="mask PNG base64(可选)")
max_side: int = Field(1024, ge=128, le=2048) max_side: int = Field(1024, ge=128, le=2048)
guidance_scale: Optional[float] = Field(None, ge=0.0, le=200.0, description="扩散 CFG;不传则各后端默认")
num_inference_steps: Optional[int] = Field(None, ge=1, le=150, description="采样步数;不传则各后端默认")
class AnimateRequest(BaseModel): class AnimateRequest(BaseModel):
+5
View File
@@ -76,6 +76,11 @@ def get_inpaint_predictor(model_name: str):
_inpaint_cache[model_name] = _copy _inpaint_cache[model_name] = _copy
return _copy return _copy
if model_name == "flux_fill":
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.FLUX_FILL))
_inpaint_cache[model_name] = pred
return pred
if model_name == "sdxl_inpaint": if model_name == "sdxl_inpaint":
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.SDXL_INPAINT)) pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.SDXL_INPAINT))
_inpaint_cache[model_name] = pred _inpaint_cache[model_name] = pred
+16 -7
View File
@@ -86,10 +86,17 @@ class InpaintModelInfo(TypedDict):
family: str family: str
name: str name: str
description: str description: str
backend: str # "sdxl_inpaint" | "controlnet" backend: str # "flux_fill" | "sdxl_inpaint" | "controlnet"
INPAINT_MODELS: List[InpaintModelInfo] = [ INPAINT_MODELS: List[InpaintModelInfo] = [
{
"id": "flux_fill",
"family": "FLUX",
"name": "FLUX.1 Fill",
"description": "FLUX Fill 专用补全",
"backend": "flux_fill",
},
{ {
"id": "sdxl_inpaint", "id": "sdxl_inpaint",
"family": "SDXL", "family": "SDXL",
@@ -100,8 +107,8 @@ INPAINT_MODELS: List[InpaintModelInfo] = [
{ {
"id": "controlnet", "id": "controlnet",
"family": "ControlNet", "family": "ControlNet",
"name": "ControlNet (placeholder)", "name": "SD1.5 + ControlNet Inpaint",
"description": "ControlNet 补全/控制生成(当前统一封装暂未实现)", "description": "runway inpaint + v11p inpaint ControlNet",
"backend": "controlnet", "backend": "controlnet",
}, },
] ]
@@ -159,10 +166,12 @@ class DepthConfig:
@dataclass @dataclass
class InpaintConfig: class InpaintConfig:
# 统一补全默认后端 # 统一补全默认后端(与 model key 对齐;实际路由以请求的 model_name 为准)
backend: Literal["sdxl_inpaint", "controlnet"] = "sdxl_inpaint" backend: Literal["flux_fill", "sdxl_inpaint", "controlnet"] = "flux_fill"
# SDXL Inpaint 的基础模型(可写 HuggingFace model id 或本地目录 # FLUX Fillgated:须在 Hugging Face 同意条款,并建议设置环境变量 HF_TOKEN
sdxl_base_model: str = "stabilityai/stable-diffusion-xl-base-1.0" flux_fill_model: str = "black-forest-labs/FLUX.1-Fill-dev"
# SDXL:须为专用 inpaint 权重(勿用 xl-base 当补全底模)
sdxl_base_model: str = "diffusers/stable-diffusion-xl-1.0-inpainting-0.1"
# ControlNet Inpaint 基础模型与 controlnet 权重 # ControlNet Inpaint 基础模型与 controlnet 权重
controlnet_base_model: str = "runwayml/stable-diffusion-inpainting" controlnet_base_model: str = "runwayml/stable-diffusion-inpainting"
controlnet_model: str = "lllyasviel/control_v11p_sd15_inpaint" controlnet_model: str = "lllyasviel/control_v11p_sd15_inpaint"
+6
View File
@@ -85,6 +85,12 @@ def get_sdxl_base_model_from_app(app_cfg: AppConfig | None = None) -> str:
return app_cfg.inpaint.sdxl_base_model return app_cfg.inpaint.sdxl_base_model
def get_flux_fill_model_from_app(app_cfg: AppConfig | None = None) -> str:
if app_cfg is None:
app_cfg = load_app_config()
return app_cfg.inpaint.flux_fill_model
def get_controlnet_base_model_from_app(app_cfg: AppConfig | None = None) -> str: def get_controlnet_base_model_from_app(app_cfg: AppConfig | None = None) -> str:
if app_cfg is None: if app_cfg is None:
app_cfg = load_app_config() app_cfg = load_app_config()
+2
View File
@@ -1 +1,3 @@
from .flux_fill.loader import make_flux_fill_predictor
__all__ = ["make_flux_fill_predictor"]
@@ -0,0 +1,3 @@
from .loader import make_flux_fill_predictor
__all__ = ["make_flux_fill_predictor"]
@@ -0,0 +1,109 @@
from __future__ import annotations
import os
from typing import Any, Callable
import numpy as np
from PIL import Image
def make_flux_fill_predictor(
model_id: str,
device: str | None = None,
) -> Callable[[Image.Image, Image.Image, str, str], Image.Image]:
import torch
from diffusers import FluxFillPipeline
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda" and torch.cuda.is_bf16_supported():
torch_dtype = torch.bfloat16
elif device == "cuda":
torch_dtype = torch.float16
else:
torch_dtype = torch.float32
hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
pipe = FluxFillPipeline.from_pretrained(
model_id,
torch_dtype=torch_dtype,
token=hf_token,
)
if device == "cuda":
try:
pipe.enable_model_cpu_offload()
except Exception:
try:
pipe.enable_vae_tiling()
except Exception:
pass
pipe.to("cuda")
else:
pipe.to(device)
def _predict(
image: Image.Image,
mask: Image.Image,
prompt: str,
negative_prompt: str = "",
strength: float = 1.0,
guidance_scale: float = 30.0,
num_inference_steps: int = 28,
max_side: int = 1024,
max_sequence_length: int = 512,
**_: Any,
) -> Image.Image:
_ = negative_prompt
image = image.convert("RGB")
mask = mask.convert("L")
orig_w, orig_h = image.size
run_w, run_h = orig_w, orig_h
if max(orig_w, orig_h) > max_side:
scale = max_side / float(max(orig_w, orig_h))
run_w = int(round(orig_w * scale))
run_h = int(round(orig_h * scale))
run_w = max(8, run_w - (run_w % 8))
run_h = max(8, run_h - (run_h % 8))
if (run_w, run_h) != (orig_w, orig_h):
image_run = image.resize((run_w, run_h), resample=Image.BICUBIC)
mask_run = mask.resize((run_w, run_h), resample=Image.NEAREST)
else:
image_run = image
mask_run = mask
p = (prompt or "").strip()
if not p:
p = "Chinese ink painting background, natural texture, seamless fill, no figures"
st = float(strength)
st = min(1.0, max(0.05, st))
if device == "cuda":
try:
torch.cuda.empty_cache()
except Exception:
pass
out = pipe(
prompt=p,
image=image_run,
mask_image=mask_run,
height=run_h,
width=run_w,
strength=st,
guidance_scale=float(guidance_scale),
num_inference_steps=int(num_inference_steps),
max_sequence_length=int(max_sequence_length),
).images[0]
out = out.convert("RGB")
if out.size != (orig_w, orig_h):
out = out.resize((orig_w, orig_h), resample=Image.BICUBIC)
return out
return _predict
+29 -5
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
统一的补全Inpaint模型加载入口 统一的补全Inpaint模型加载入口
当前支持 当前支持
- FLUX Filldiffusers FluxFillPipeline FLUX.1-Fill-dev
- SDXL Inpaintdiffusers AutoPipelineForInpainting - SDXL Inpaintdiffusers AutoPipelineForInpainting
- ControlNetStable Diffusion + ControlNet Inpaint - ControlNetStable Diffusion + ControlNet Inpaint
- LaMatorchscript big-lama本地封装 - LaMatorchscript big-lama本地封装
@@ -11,7 +12,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Callable from typing import Any, Callable
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@@ -21,10 +22,12 @@ from config_loader import (
get_sdxl_base_model_from_app, get_sdxl_base_model_from_app,
get_controlnet_base_model_from_app, get_controlnet_base_model_from_app,
get_controlnet_model_from_app, get_controlnet_model_from_app,
get_flux_fill_model_from_app,
) )
class InpaintBackend(str, Enum): class InpaintBackend(str, Enum):
FLUX_FILL = "flux_fill"
SDXL_INPAINT = "sdxl_inpaint" SDXL_INPAINT = "sdxl_inpaint"
CONTROLNET = "controlnet" CONTROLNET = "controlnet"
LAMA = "lama" LAMA = "lama"
@@ -36,6 +39,7 @@ class UnifiedInpaintConfig:
device: str | None = None device: str | None = None
# SDXL base model (HF id 或本地目录),不填则用 config.py 的默认值 # SDXL base model (HF id 或本地目录),不填则用 config.py 的默认值
sdxl_base_model: str | None = None sdxl_base_model: str | None = None
flux_fill_model: str | None = None
@dataclass @dataclass
@@ -100,7 +104,7 @@ def _make_sdxl_inpaint_predictor(
- 输出PIL RGB 结果图 - 输出PIL RGB 结果图
""" """
import torch import torch
from diffusers import AutoPipelineForText2Image, AutoPipelineForInpainting from diffusers import AutoPipelineForInpainting
app_cfg = load_app_config() app_cfg = load_app_config()
base_model = cfg.sdxl_base_model or get_sdxl_base_model_from_app(app_cfg) base_model = cfg.sdxl_base_model or get_sdxl_base_model_from_app(app_cfg)
@@ -112,13 +116,12 @@ def _make_sdxl_inpaint_predictor(
torch_dtype = torch.float16 if device == "cuda" else torch.float32 torch_dtype = torch.float16 if device == "cuda" else torch.float32
pipe_t2i = AutoPipelineForText2Image.from_pretrained( pipe = AutoPipelineForInpainting.from_pretrained(
base_model, base_model,
torch_dtype=torch_dtype, torch_dtype=torch_dtype,
variant="fp16" if device == "cuda" else None, variant="fp16" if device == "cuda" else None,
use_safetensors=True, use_safetensors=True,
).to(device) ).to(device)
pipe = AutoPipelineForInpainting.from_pipe(pipe_t2i).to(device)
# 省显存设置(尽量不改变输出语义) # 省显存设置(尽量不改变输出语义)
# 注意:CPU offload 会明显变慢,但能显著降低显存占用。 # 注意:CPU offload 会明显变慢,但能显著降低显存占用。
@@ -149,6 +152,7 @@ def _make_sdxl_inpaint_predictor(
guidance_scale: float = 7.5, guidance_scale: float = 7.5,
num_inference_steps: int = 30, num_inference_steps: int = 30,
max_side: int = 1024, max_side: int = 1024,
**_: Any,
) -> Image.Image: ) -> Image.Image:
image = image.convert("RGB") image = image.convert("RGB")
# diffusers 要求 mask 为单通道,白色区域为需要重绘 # diffusers 要求 mask 为单通道,白色区域为需要重绘
@@ -257,6 +261,7 @@ def _make_controlnet_predictor(_: UnifiedInpaintConfig):
num_inference_steps: int = 30, num_inference_steps: int = 30,
controlnet_conditioning_scale: float = 1.0, controlnet_conditioning_scale: float = 1.0,
max_side: int = 768, max_side: int = 768,
**_: Any,
) -> Image.Image: ) -> Image.Image:
import cv2 import cv2
import numpy as np import numpy as np
@@ -316,6 +321,22 @@ def _make_controlnet_predictor(_: UnifiedInpaintConfig):
return _predict return _predict
def _make_flux_fill_predictor(cfg: UnifiedInpaintConfig):
import torch
from .flux_fill.loader import make_flux_fill_predictor as _flux_fill_factory
app_cfg = load_app_config()
model_id = cfg.flux_fill_model or get_flux_fill_model_from_app(app_cfg)
device = cfg.device
if device is None:
device = app_cfg.inpaint.device
device = "cuda" if device.startswith("cuda") and torch.cuda.is_available() else "cpu"
return _flux_fill_factory(model_id=model_id, device=device)
def _make_lama_predictor(cfg: UnifiedInpaintConfig): def _make_lama_predictor(cfg: UnifiedInpaintConfig):
import torch import torch
@@ -340,7 +361,7 @@ def _make_lama_predictor(cfg: UnifiedInpaintConfig):
max_side: int = 1024, max_side: int = 1024,
**_kwargs, **_kwargs,
) -> Image.Image: ) -> Image.Image:
# LaMa 不使用 prompt/negative_prompt,也不使用 strength/steps 等扩散参数 # big-lama:仅 (RGB, mask);无文本条件。
_ = (strength, guidance_scale, num_inference_steps, max_side) _ = (strength, guidance_scale, num_inference_steps, max_side)
return lama(image, mask) return lama(image, mask)
@@ -355,6 +376,9 @@ def build_inpaint_predictor(
""" """
cfg = cfg or UnifiedInpaintConfig() cfg = cfg or UnifiedInpaintConfig()
if cfg.backend == InpaintBackend.FLUX_FILL:
return _make_flux_fill_predictor(cfg), InpaintBackend.FLUX_FILL
if cfg.backend == InpaintBackend.SDXL_INPAINT: if cfg.backend == InpaintBackend.SDXL_INPAINT:
return _make_sdxl_inpaint_predictor(cfg), InpaintBackend.SDXL_INPAINT return _make_sdxl_inpaint_predictor(cfg), InpaintBackend.SDXL_INPAINT
+4 -5
View File
@@ -1,12 +1,11 @@
"""
source .venv/bin/activate # 仓库根 hfut-bishe/.venv
cd python_server && uvicorn app.main:app --host 0.0.0.0 --port 8000
"""
from __future__ import annotations from __future__ import annotations
import os import os
from dotenv import load_dotenv
load_dotenv()
from app.main import app from app.main import app
__all__ = ["app"] __all__ = ["app"]