Update app.py
Browse files
app.py
CHANGED
|
@@ -1,223 +1,233 @@
|
|
| 1 |
# app.py
|
| 2 |
-
import os,
|
| 3 |
from typing import Tuple
|
| 4 |
-
import gradio as gr
|
| 5 |
import numpy as np
|
| 6 |
-
from PIL import Image, ImageFilter,
|
| 7 |
-
|
| 8 |
import torch
|
| 9 |
-
from diffusers import
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
#
|
| 13 |
-
#
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
w, h = img.size
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
x0 = (
|
| 49 |
-
y0 = (
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
return mask
|
| 62 |
|
| 63 |
-
def
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
w,h =
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
prompt: str,
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
inner_rect_small = (sx,sy,sx+sw,sy+sh)
|
| 122 |
else:
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
#
|
| 154 |
-
|
| 155 |
-
#
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
"frame, border, mosaic, collage, extra limbs"
|
| 162 |
-
)
|
| 163 |
|
| 164 |
with gr.Blocks(css="""
|
| 165 |
-
#
|
| 166 |
-
.caption {font-size: 0.9em; color: #aaa}
|
| 167 |
""") as demo:
|
| 168 |
-
gr.Markdown("## Qwen
|
| 169 |
-
|
| 170 |
with gr.Row():
|
| 171 |
-
with gr.Column(
|
| 172 |
-
in_img = gr.Image(
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
with gr.Row():
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def _update_preview(img, pad_px):
|
| 192 |
-
if img is None:
|
| 193 |
-
return None, ""
|
| 194 |
-
p, t = preview_frame(img, int(pad_px))
|
| 195 |
-
return p, t
|
| 196 |
-
|
| 197 |
-
in_img.change(_update_preview, [in_img, pad], [prev, info])
|
| 198 |
-
pad.release(_update_preview, [in_img, pad], [prev, info])
|
| 199 |
-
|
| 200 |
-
def go(image, pad_px, feather_px, prompt, negative_prompt, steps, cfg, seed):
|
| 201 |
-
if image is None:
|
| 202 |
-
raise gr.Error("Загрузите изображение.")
|
| 203 |
-
res, path, meta_text = outpaint_generate(
|
| 204 |
-
image, int(pad_px),
|
| 205 |
-
prompt, negative_prompt,
|
| 206 |
-
int(steps), float(cfg), int(feather_px), int(seed)
|
| 207 |
-
)
|
| 208 |
-
return res, meta_text, path
|
| 209 |
-
|
| 210 |
-
go_btn.click(
|
| 211 |
-
go,
|
| 212 |
-
[in_img, pad, feather, prmpt, nprmpt, steps, cfg, seed],
|
| 213 |
-
[out_img, meta, file_out]
|
| 214 |
)
|
| 215 |
|
| 216 |
if __name__ == "__main__":
|
| 217 |
-
|
| 218 |
-
demo.launch(
|
| 219 |
-
server_name="0.0.0.0",
|
| 220 |
-
server_port=7860,
|
| 221 |
-
inbrowser=False,
|
| 222 |
-
allowed_paths=["/mnt/data"]
|
| 223 |
-
)
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
import os, io, math, random, time
|
| 3 |
from typing import Tuple
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
+
from PIL import Image, ImageFilter, ImageOps
|
|
|
|
| 6 |
import torch
|
| 7 |
+
from diffusers import StableDiffusionXLInpaintPipeline
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
# -----------------------------
|
| 11 |
+
# Settings
|
| 12 |
+
# -----------------------------
|
| 13 |
+
DEFAULT_PROMPT = "extend the image naturally"
|
| 14 |
+
DEFAULT_NEG = (
|
| 15 |
+
"text, letters, words, numbers, caption, watermark, logo, signature, frame, border, "
|
| 16 |
+
"collage tiles, UI elements, artifacts, deformed, duplicate, blurry"
|
| 17 |
+
)
|
| 18 |
+
SAFE_MAX = int(os.getenv("SAFE_MAX", 1792)) # макс. сторона генерации
|
| 19 |
+
HF_TOKEN = os.getenv("HF_TOKEN", None)
|
| 20 |
+
|
| 21 |
+
# -----------------------------
|
| 22 |
+
# Load pipeline (no xformers)
|
| 23 |
+
# -----------------------------
|
| 24 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 25 |
+
pipe = StableDiffusionXLInpaintPipeline.from_pretrained(
|
| 26 |
+
"diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
| 27 |
+
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
|
| 28 |
+
use_safetensors=True,
|
| 29 |
+
variant="fp16" if device == "cuda" else None,
|
| 30 |
+
token=HF_TOKEN,
|
| 31 |
+
)
|
| 32 |
+
pipe = pipe.to(device)
|
| 33 |
+
|
| 34 |
+
# включаем SDPA/attention slicing вместо xformers
|
| 35 |
+
try:
|
| 36 |
+
if device == "cuda":
|
| 37 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 38 |
+
else:
|
| 39 |
+
pipe.enable_attention_slicing("max")
|
| 40 |
+
except Exception:
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
|
| 44 |
+
# -----------------------------
|
| 45 |
+
# Helpers
|
| 46 |
+
# -----------------------------
|
| 47 |
+
def _to_pil(img) -> Image.Image:
|
| 48 |
+
return img if isinstance(img, Image.Image) else Image.fromarray(img)
|
| 49 |
+
|
| 50 |
+
def _round64(x: int) -> int:
|
| 51 |
+
return int(math.ceil(x / 64) * 64)
|
| 52 |
+
|
| 53 |
+
def fit_to_safe_max(w: int, h: int, safe_max: int) -> Tuple[int, int, float]:
|
| 54 |
+
"""масштабирует (w,h) до safe_max, сохраняя пропорции; возвращает новые w,h и scale"""
|
| 55 |
+
s = 1.0
|
| 56 |
+
max_side = max(w, h)
|
| 57 |
+
if max_side > safe_max:
|
| 58 |
+
s = safe_max / max_side
|
| 59 |
+
w = int(round(w * s))
|
| 60 |
+
h = int(round(h * s))
|
| 61 |
+
# SDXL требует кратность 64
|
| 62 |
+
w = max(512, _round64(w))
|
| 63 |
+
h = max(512, _round64(h))
|
| 64 |
+
return w, h, s
|
| 65 |
+
|
| 66 |
+
def make_square_canvas(img: Image.Image, L: int, R: int, T: int, B: int) -> Tuple[int, int, Tuple[int,int,int,int]]:
|
| 67 |
+
"""Считает квадратную сторону и box для вставки оригинала"""
|
| 68 |
w, h = img.size
|
| 69 |
+
tgt_w = w + L + R
|
| 70 |
+
tgt_h = h + T + B
|
| 71 |
+
side = max(tgt_w, tgt_h)
|
| 72 |
+
# центровка оригинала
|
| 73 |
+
x0 = (side - w) // 2
|
| 74 |
+
y0 = (side - h) // 2
|
| 75 |
+
return side, side, (x0, y0, x0 + w, y0 + h)
|
| 76 |
+
|
| 77 |
+
def build_bg_from_edges(img: Image.Image, side: int) -> Image.Image:
|
| 78 |
+
"""Создаёт «умный» фон из повторённых краёв, слегка блюрит."""
|
| 79 |
+
arr = np.asarray(img.convert("RGB"))
|
| 80 |
+
pad_top = (side - img.height) // 2
|
| 81 |
+
pad_bottom = side - img.height - pad_top
|
| 82 |
+
pad_left = (side - img.width) // 2
|
| 83 |
+
pad_right = side - img.width - pad_left
|
| 84 |
+
padded = np.pad(arr, ((pad_top, pad_bottom), (pad_left, pad_right), (0, 0)), mode="edge")
|
| 85 |
+
bg = Image.fromarray(padded, mode="RGB").filter(ImageFilter.GaussianBlur(radius=8))
|
| 86 |
+
return bg
|
| 87 |
+
|
| 88 |
+
def build_mask(side: int, orig_box: Tuple[int,int,int,int], feather: int) -> Image.Image:
|
| 89 |
+
"""Белое = генерировать, чёрное = сохраняем оригинал"""
|
| 90 |
+
mask = Image.new("L", (side, side), 255)
|
| 91 |
+
x0, y0, x1, y1 = orig_box
|
| 92 |
+
# чёрный прямоугольник по оригиналу
|
| 93 |
+
ImageDraw = ImageDraw_ensure()
|
| 94 |
+
d = ImageDraw.Draw(mask)
|
| 95 |
+
d.rectangle([x0, y0, x1, y1], fill=0)
|
| 96 |
+
if feather > 0:
|
| 97 |
+
mask = mask.filter(ImageFilter.GaussianBlur(radius=feather))
|
| 98 |
return mask
|
| 99 |
|
| 100 |
+
def ImageDraw_ensure():
|
| 101 |
+
from PIL import ImageDraw
|
| 102 |
+
return ImageDraw
|
| 103 |
+
|
| 104 |
+
def feather_alpha_mask(size: Tuple[int,int], feather: int) -> Image.Image:
|
| 105 |
+
"""Белый центр, мягкий край (для вставки оригинала поверх результата)"""
|
| 106 |
+
w, h = size
|
| 107 |
+
m = Image.new("L", (w, h), 255)
|
| 108 |
+
if feather <= 0:
|
| 109 |
+
return m
|
| 110 |
+
# делаем тёмную рамку и размазываем
|
| 111 |
+
d = ImageDraw_ensure().Draw(m)
|
| 112 |
+
d.rectangle([0,0,w-1,h-1], outline=0, width=feather*2)
|
| 113 |
+
m = m.filter(ImageFilter.GaussianBlur(radius=feather))
|
| 114 |
+
# нормализация – центр белее
|
| 115 |
+
return m
|
| 116 |
+
|
| 117 |
+
def paste_center_preserving(base: Image.Image, original: Image.Image, box: Tuple[int,int,int,int], feather: int) -> Image.Image:
|
| 118 |
+
"""Вставляет оригинал по box с мягким пером по краям."""
|
| 119 |
+
x0, y0, x1, y1 = box
|
| 120 |
+
orig_rgba = original.convert("RGBA")
|
| 121 |
+
alpha = feather_alpha_mask(original.size, feather)
|
| 122 |
+
orig_rgba.putalpha(alpha)
|
| 123 |
+
base = base.convert("RGBA")
|
| 124 |
+
base.alpha_composite(orig_rgba, dest=(x0, y0))
|
| 125 |
+
return base.convert("RGB")
|
| 126 |
+
|
| 127 |
+
def outpaint_once(
|
| 128 |
+
img: Image.Image,
|
| 129 |
+
L: int, R: int, T: int, B: int,
|
| 130 |
+
prompt: str, neg: str,
|
| 131 |
+
steps: int, cfg: float, seed: int,
|
| 132 |
+
feather: int, safe_max: int
|
| 133 |
+
) -> Tuple[Image.Image, Image.Image, Tuple[int,int,int,int], int]:
|
| 134 |
+
"""Генерит квадратное полотно, возвращает (outpaint, original, orig_box, used_seed)"""
|
| 135 |
+
img = _to_pil(img).convert("RGB")
|
| 136 |
+
# квадрат и позиция оригинала
|
| 137 |
+
W, H, orig_box = make_square_canvas(img, L, R, T, B)
|
| 138 |
+
# размер генерации с ограничением
|
| 139 |
+
gen_w, gen_h, _ = fit_to_safe_max(W, H, safe_max)
|
| 140 |
+
side = max(gen_w, gen_h)
|
| 141 |
+
# фон и маска
|
| 142 |
+
bg = build_bg_from_edges(img, side)
|
| 143 |
+
# пересчитываем orig_box под side (если side!=W/H, центруем)
|
| 144 |
+
cx, cy = side//2, side//2
|
| 145 |
+
x0 = cx - img.width//2
|
| 146 |
+
y0 = cy - img.height//2
|
| 147 |
+
orig_box_side = (x0, y0, x0 + img.width, y0 + img.height)
|
| 148 |
+
# кладём оригинал на фон (как исходник для inpaint)
|
| 149 |
+
cond = bg.copy()
|
| 150 |
+
cond.paste(img, (x0, y0))
|
| 151 |
+
mask = build_mask(side, orig_box_side, feather=max(1, feather//2))
|
| 152 |
+
# генерация
|
| 153 |
+
if seed is None or seed < 0:
|
| 154 |
+
seed = random.randint(0, 2**31-1)
|
| 155 |
+
g = torch.Generator(device=device)
|
| 156 |
+
if device == "cuda":
|
| 157 |
+
g = g.manual_seed(seed)
|
|
|
|
| 158 |
else:
|
| 159 |
+
random.seed(seed)
|
| 160 |
+
out = pipe(
|
| 161 |
+
prompt=prompt if prompt.strip() else DEFAULT_PROMPT,
|
| 162 |
+
negative_prompt=(neg.strip() or DEFAULT_NEG),
|
| 163 |
+
image=cond,
|
| 164 |
+
mask_image=mask,
|
| 165 |
+
num_inference_steps=int(steps),
|
| 166 |
+
guidance_scale=float(cfg),
|
| 167 |
+
generator=g,
|
| 168 |
+
width=side,
|
| 169 |
+
height=side,
|
| 170 |
+
).images[0]
|
| 171 |
+
return out, img, orig_box_side, seed
|
| 172 |
+
|
| 173 |
+
# -----------------------------
|
| 174 |
+
# Gradio core
|
| 175 |
+
# -----------------------------
|
| 176 |
+
def run(
|
| 177 |
+
img, prompt, neg, L, R, T, B,
|
| 178 |
+
steps, cfg, seed, feather, safe_max
|
| 179 |
+
):
|
| 180 |
+
if img is None:
|
| 181 |
+
raise gr.Error("Загрузи изображение.")
|
| 182 |
+
out, orig, box, used_seed = outpaint_once(
|
| 183 |
+
img=img, L=L, R=R, T=T, B=B,
|
| 184 |
+
prompt=prompt or DEFAULT_PROMPT,
|
| 185 |
+
neg=neg or DEFAULT_NEG,
|
| 186 |
+
steps=steps, cfg=cfg, seed=seed,
|
| 187 |
+
feather=feather, safe_max=safe_max
|
| 188 |
+
)
|
| 189 |
+
# поверх возвращаем оригинал (чтобы его качество не менялось)
|
| 190 |
+
final = paste_center_preserving(out, orig, box, feather=max(8, feather))
|
| 191 |
+
# сохраняем полный PNG
|
| 192 |
+
side = final.size[0]
|
| 193 |
+
ts = int(time.time())
|
| 194 |
+
fname = f"/tmp/outpaint_{side}x{side}_{used_seed}.png"
|
| 195 |
+
final.save(fname, "PNG")
|
| 196 |
+
return final, fname, used_seed
|
|
|
|
|
|
|
| 197 |
|
| 198 |
with gr.Blocks(css="""
|
| 199 |
+
#note {opacity:.8}
|
|
|
|
| 200 |
""") as demo:
|
| 201 |
+
gr.Markdown("## Qwen Image — Seamless Background Expand")
|
|
|
|
| 202 |
with gr.Row():
|
| 203 |
+
with gr.Column():
|
| 204 |
+
in_img = gr.Image(label="Картинка", type="pil")
|
| 205 |
+
prompt = gr.Textbox(label="Prompt", value=DEFAULT_PROMPT, lines=2)
|
| 206 |
+
neg = gr.Textbox(label="Negative prompt", value=DEFAULT_NEG, lines=2)
|
| 207 |
+
with gr.Row():
|
| 208 |
+
L = gr.Number(label="Left (px)", value=256, precision=0)
|
| 209 |
+
R = gr.Number(label="Right (px)", value=256, precision=0)
|
| 210 |
+
with gr.Row():
|
| 211 |
+
T = gr.Number(label="Top (px)", value=256, precision=0)
|
| 212 |
+
B = gr.Number(label="Bottom (px)", value=256, precision=0)
|
| 213 |
+
with gr.Row():
|
| 214 |
+
steps = gr.Slider(10, 60, value=28, step=1, label="Steps")
|
| 215 |
+
cfg = gr.Slider(1.0, 12.0, value=5.5, step=0.1, label="Guidance scale")
|
| 216 |
with gr.Row():
|
| 217 |
+
seed = gr.Number(label="Seed (-1=random)", value=-1, precision=0)
|
| 218 |
+
feather = gr.Slider(0, 64, value=24, step=1, label="Feather (px)")
|
| 219 |
+
safe_max = gr.Slider(1024, 2048, value=1792, step=64, label="Max side for generation (px)")
|
| 220 |
+
run_btn = gr.Button("Expand", variant="primary")
|
| 221 |
+
with gr.Column():
|
| 222 |
+
out_img = gr.Image(label="Результат", interactive=False)
|
| 223 |
+
file_out = gr.File(label="Скачать PNG (полный размер)")
|
| 224 |
+
used_seed = gr.Number(label="Used seed", interactive=False)
|
| 225 |
+
|
| 226 |
+
run_btn.click(
|
| 227 |
+
fn=run,
|
| 228 |
+
inputs=[in_img, prompt, neg, L, R, T, B, steps, cfg, seed, feather, safe_max],
|
| 229 |
+
outputs=[out_img, file_out, used_seed],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
)
|
| 231 |
|
| 232 |
if __name__ == "__main__":
|
| 233 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|