Update app.py
Browse files
app.py
CHANGED
|
@@ -1,47 +1,274 @@
|
|
| 1 |
-
import
|
|
|
|
|
|
|
| 2 |
import zipfile
|
| 3 |
import tempfile
|
| 4 |
from pathlib import Path
|
| 5 |
|
|
|
|
| 6 |
import gradio as gr
|
|
|
|
|
|
|
| 7 |
from PIL import Image
|
| 8 |
from transformers import pipeline
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
)
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
return None
|
| 20 |
|
| 21 |
-
out_dir = Path(tempfile.mkdtemp(prefix="parallax_depth_"))
|
| 22 |
-
zip_path = out_dir / "Leva_Parallax_Pro.zip"
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
|
| 29 |
-
base_name = Path(file_obj.name).stem
|
| 30 |
-
depth_name = f"depth_{base_name}.png"
|
| 31 |
-
depth_path = out_dir / depth_name
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
-
return str(zip_path)
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
btn.click(generate_cinematic_depth, inputs=files, outputs=out)
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import math
|
| 3 |
+
import shutil
|
| 4 |
import zipfile
|
| 5 |
import tempfile
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
+
import cv2
|
| 9 |
import gradio as gr
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
from PIL import Image
|
| 13 |
from transformers import pipeline
|
| 14 |
|
| 15 |
+
# ----------------------------
|
| 16 |
+
# Config
|
| 17 |
+
# ----------------------------
|
| 18 |
+
DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-base-hf"
|
| 19 |
+
DEFAULT_LAYER_COUNT = 6
|
| 20 |
+
EXPORT_ROOT = Path(tempfile.gettempdir()) / "alvore_studio_exports"
|
| 21 |
+
EXPORT_ROOT.mkdir(parents=True, exist_ok=True)
|
| 22 |
|
| 23 |
+
DEVICE = 0 if torch.cuda.is_available() else -1
|
| 24 |
+
DEPTH_PIPE = pipeline("depth-estimation", model=DEPTH_MODEL_ID, device=DEVICE)
|
|
|
|
| 25 |
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
# ----------------------------
|
| 28 |
+
# Utils
|
| 29 |
+
# ----------------------------
|
| 30 |
+
def pil_rgb(path: str) -> Image.Image:
|
| 31 |
+
return Image.open(path).convert("RGB")
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
def to_np_rgb(img: Image.Image) -> np.ndarray:
|
| 35 |
+
return np.array(img, dtype=np.uint8)
|
| 36 |
|
|
|
|
| 37 |
|
| 38 |
+
def save_png(path: Path, arr: np.ndarray) -> None:
|
| 39 |
+
if arr.ndim == 2:
|
| 40 |
+
Image.fromarray(arr.astype(np.uint8), mode="L").save(path)
|
| 41 |
+
elif arr.shape[2] == 3:
|
| 42 |
+
Image.fromarray(arr.astype(np.uint8), mode="RGB").save(path)
|
| 43 |
+
elif arr.shape[2] == 4:
|
| 44 |
+
Image.fromarray(arr.astype(np.uint8), mode="RGBA").save(path)
|
| 45 |
+
else:
|
| 46 |
+
raise ValueError(f"Formato inesperado: {arr.shape}")
|
| 47 |
|
|
|
|
| 48 |
|
| 49 |
+
def resize_to(arr: np.ndarray, size: tuple[int, int]) -> np.ndarray:
|
| 50 |
+
w, h = size
|
| 51 |
+
return cv2.resize(arr, (w, h), interpolation=cv2.INTER_CUBIC)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def normalize_depth(depth: np.ndarray) -> np.ndarray:
|
| 55 |
+
depth = depth.astype(np.float32)
|
| 56 |
+
mn, mx = float(depth.min()), float(depth.max())
|
| 57 |
+
return (depth - mn) / (mx - mn + 1e-8)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def infer_depth(img_pil: Image.Image) -> np.ndarray:
|
| 61 |
+
with torch.no_grad():
|
| 62 |
+
out = DEPTH_PIPE(img_pil)
|
| 63 |
+
|
| 64 |
+
pred = out["predicted_depth"]
|
| 65 |
+
if hasattr(pred, "detach"):
|
| 66 |
+
pred = pred.detach().cpu().numpy()
|
| 67 |
+
pred = np.squeeze(pred).astype(np.float32)
|
| 68 |
+
|
| 69 |
+
depth = normalize_depth(pred)
|
| 70 |
+
return depth
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def auto_inpaint_from_depth(orig_rgb: np.ndarray, depth: np.ndarray) -> np.ndarray:
|
| 74 |
+
"""
|
| 75 |
+
Se o inpaint não vier pronto, cria um fallback razoável baseado na região mais próxima.
|
| 76 |
+
"""
|
| 77 |
+
mask = (depth > 0.67).astype(np.uint8) * 255
|
| 78 |
+
kernel = np.ones((9, 9), np.uint8)
|
| 79 |
+
mask = cv2.dilate(mask, kernel, iterations=2)
|
| 80 |
+
mask = cv2.GaussianBlur(mask, (0, 0), 3.0)
|
| 81 |
+
return cv2.inpaint(orig_rgb, mask, 3, cv2.INPAINT_TELEA)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def make_layer_stack(orig_rgb: np.ndarray, inpaint_rgb: np.ndarray, depth: np.ndarray, layer_count: int) -> list[np.ndarray]:
|
| 85 |
+
"""
|
| 86 |
+
Cria camadas RGBA.
|
| 87 |
+
1 = perto, 0 = longe.
|
| 88 |
+
"""
|
| 89 |
+
centers = np.linspace(0.0, 1.0, layer_count, dtype=np.float32)
|
| 90 |
+
sigma = max(0.08, 0.42 / max(layer_count, 2))
|
| 91 |
+
|
| 92 |
+
weights = []
|
| 93 |
+
for c in centers:
|
| 94 |
+
w = np.exp(-0.5 * ((depth - c) / sigma) ** 2)
|
| 95 |
+
weights.append(w)
|
| 96 |
+
weights = np.stack(weights, axis=-1)
|
| 97 |
+
weights /= (weights.sum(axis=-1, keepdims=True) + 1e-8)
|
| 98 |
+
|
| 99 |
+
layers = []
|
| 100 |
+
for i, c in enumerate(centers):
|
| 101 |
+
alpha = weights[..., i]
|
| 102 |
+
alpha = cv2.GaussianBlur(alpha, (0, 0), 1.1)
|
| 103 |
+
alpha = np.clip(alpha ** 0.85, 0.0, 1.0)
|
| 104 |
+
|
| 105 |
+
# fundo -> inpaint, frente -> original
|
| 106 |
+
src = (inpaint_rgb.astype(np.float32) * (1.0 - c) + orig_rgb.astype(np.float32) * c)
|
| 107 |
+
rgb = np.clip(src, 0, 255).astype(np.uint8)
|
| 108 |
+
a8 = (alpha * 255.0).astype(np.uint8)
|
| 109 |
+
rgba = np.dstack([rgb, a8])
|
| 110 |
+
layers.append(rgba)
|
| 111 |
+
|
| 112 |
+
return layers
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def create_scene_assets(orig_path: str, inpaint_path: str | None, depth_path: str | None, scene_idx: int, layer_count: int, use_ai_depth: bool):
|
| 116 |
+
scene_name = f"scene_{scene_idx:03d}"
|
| 117 |
+
scene_dir = EXPORT_ROOT / scene_name
|
| 118 |
+
scene_dir.mkdir(parents=True, exist_ok=True)
|
| 119 |
+
|
| 120 |
+
orig_img = pil_rgb(orig_path)
|
| 121 |
+
orig_rgb = to_np_rgb(orig_img)
|
| 122 |
+
size = orig_img.size # (w, h)
|
| 123 |
+
|
| 124 |
+
# Inpaint opcional
|
| 125 |
+
if inpaint_path:
|
| 126 |
+
inpaint_img = pil_rgb(inpaint_path).resize(size, Image.LANCZOS)
|
| 127 |
+
inpaint_rgb = to_np_rgb(inpaint_img)
|
| 128 |
+
else:
|
| 129 |
+
inpaint_rgb = None
|
| 130 |
+
|
| 131 |
+
# Depth
|
| 132 |
+
if depth_path:
|
| 133 |
+
depth_img = Image.open(depth_path).convert("L").resize(size, Image.LANCZOS)
|
| 134 |
+
depth = np.array(depth_img, dtype=np.float32) / 255.0
|
| 135 |
+
elif use_ai_depth:
|
| 136 |
+
depth = infer_depth(orig_img)
|
| 137 |
+
if depth.shape != (size[1], size[0]):
|
| 138 |
+
depth = resize_to(depth, size)
|
| 139 |
+
else:
|
| 140 |
+
depth = np.full((size[1], size[0]), 0.5, dtype=np.float32)
|
| 141 |
+
|
| 142 |
+
depth = np.clip(depth, 0.0, 1.0)
|
| 143 |
+
|
| 144 |
+
# Inpaint fallback
|
| 145 |
+
if inpaint_rgb is None:
|
| 146 |
+
inpaint_rgb = auto_inpaint_from_depth(orig_rgb, depth)
|
| 147 |
+
|
| 148 |
+
# Assets principais
|
| 149 |
+
save_png(scene_dir / f"{scene_name}_original.png", orig_rgb)
|
| 150 |
+
save_png(scene_dir / f"{scene_name}_inpaint.png", inpaint_rgb)
|
| 151 |
+
save_png(scene_dir / f"{scene_name}_depth.png", (depth * 255.0).astype(np.uint8))
|
| 152 |
+
|
| 153 |
+
# Camadas RGBA
|
| 154 |
+
layer_files = []
|
| 155 |
+
layers = make_layer_stack(orig_rgb, inpaint_rgb, depth, layer_count=layer_count)
|
| 156 |
+
for li, layer_rgba in enumerate(layers):
|
| 157 |
+
fname = f"{scene_name}_layer_{li:02d}.png"
|
| 158 |
+
save_png(scene_dir / fname, layer_rgba)
|
| 159 |
+
layer_files.append(fname)
|
| 160 |
+
|
| 161 |
+
# Manifest
|
| 162 |
+
manifest = {
|
| 163 |
+
"scene": scene_name,
|
| 164 |
+
"original": f"{scene_name}_original.png",
|
| 165 |
+
"inpaint": f"{scene_name}_inpaint.png",
|
| 166 |
+
"depth": f"{scene_name}_depth.png",
|
| 167 |
+
"layers": layer_files,
|
| 168 |
+
"layer_count": layer_count,
|
| 169 |
+
"size": {"w": size[0], "h": size[1]},
|
| 170 |
+
}
|
| 171 |
+
(scene_dir / f"{scene_name}_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 172 |
+
|
| 173 |
+
# Thumb
|
| 174 |
+
thumb = orig_rgb.copy()
|
| 175 |
+
thumb = cv2.resize(thumb, (960, int(960 * size[1] / max(size[0], 1))), interpolation=cv2.INTER_AREA)
|
| 176 |
+
save_png(scene_dir / f"{scene_name}_thumb.png", thumb)
|
| 177 |
+
|
| 178 |
+
return scene_dir, manifest
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def zip_folder(folder: Path) -> Path:
|
| 182 |
+
zip_path = folder.with_suffix(".zip")
|
| 183 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 184 |
+
for p in folder.rglob("*"):
|
| 185 |
+
if p.is_file():
|
| 186 |
+
zf.write(p, arcname=p.relative_to(folder))
|
| 187 |
+
return zip_path
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def process_batch(originals, inpaints, depths, layer_count, use_ai_depth):
|
| 191 |
+
originals = list(originals or [])
|
| 192 |
+
inpaints = list(inpaints or [])
|
| 193 |
+
depths = list(depths or [])
|
| 194 |
+
|
| 195 |
+
if not originals:
|
| 196 |
+
raise gr.Error("Envie pelo menos uma imagem original.")
|
| 197 |
+
|
| 198 |
+
session_dir = EXPORT_ROOT / f"session_{tempfile.mkstemp(prefix='alvore_')[1].split('/')[-1]}"
|
| 199 |
+
session_dir.mkdir(parents=True, exist_ok=True)
|
| 200 |
+
|
| 201 |
+
scene_rows = []
|
| 202 |
+
preview_paths = []
|
| 203 |
+
|
| 204 |
+
for i, orig in enumerate(originals, start=1):
|
| 205 |
+
inp = inpaints[i - 1] if i - 1 < len(inpaints) else None
|
| 206 |
+
dep = depths[i - 1] if i - 1 < len(depths) else None
|
| 207 |
+
|
| 208 |
+
scene_dir, manifest = create_scene_assets(
|
| 209 |
+
orig_path=orig,
|
| 210 |
+
inpaint_path=inp,
|
| 211 |
+
depth_path=dep,
|
| 212 |
+
scene_idx=i,
|
| 213 |
+
layer_count=layer_count,
|
| 214 |
+
use_ai_depth=use_ai_depth,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
preview_paths.append(str(scene_dir / f"scene_{i:03d}_thumb.png"))
|
| 218 |
+
scene_rows.append(
|
| 219 |
+
f"Cena {i:03d} | original={Path(orig).name} | "
|
| 220 |
+
f"inpaint={Path(inp).name if inp else 'auto'} | "
|
| 221 |
+
f"depth={Path(dep).name if dep else ('IA' if use_ai_depth else 'flat')} | "
|
| 222 |
+
f"layers={len(manifest['layers'])}"
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
zip_path = zip_folder(session_dir)
|
| 226 |
+
|
| 227 |
+
return (
|
| 228 |
+
preview_paths,
|
| 229 |
+
str(zip_path),
|
| 230 |
+
"\n".join(scene_rows),
|
| 231 |
+
str(session_dir),
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# ----------------------------
|
| 236 |
+
# Gradio UI
|
| 237 |
+
# ----------------------------
|
| 238 |
+
with gr.Blocks(title="ALVORE STUDIO LAYER BUILDER") as demo:
|
| 239 |
+
gr.Markdown(
|
| 240 |
+
"""
|
| 241 |
+
# ALVORE STUDIO LAYER BUILDER
|
| 242 |
+
|
| 243 |
+
Use assim:
|
| 244 |
+
- **Imagem original**: cena completa, com personagem.
|
| 245 |
+
- **Inpaint**: mesma cena sem personagem, se você já tiver.
|
| 246 |
+
- **Depth**: mapa branco/perto, preto/longe. Se não vier, a IA gera.
|
| 247 |
+
- **Layer count**: 6 é um bom ponto de partida de estúdio.
|
| 248 |
+
"""
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
with gr.Row():
|
| 252 |
+
originals = gr.File(label="1. IMAGENS ORIGINAIS", file_count="multiple", file_types=["image"], type="filepath")
|
| 253 |
+
inpaints = gr.File(label="2. INPAINTS", file_count="multiple", file_types=["image"], type="filepath")
|
| 254 |
+
depths = gr.File(label="3. DEPTH MAPS", file_count="multiple", file_types=["image"], type="filepath")
|
| 255 |
+
|
| 256 |
+
with gr.Row():
|
| 257 |
+
layer_count = gr.Slider(4, 8, value=6, step=1, label="Número de camadas")
|
| 258 |
+
use_ai_depth = gr.Checkbox(value=True, label="Gerar depth por IA quando faltar")
|
| 259 |
+
|
| 260 |
+
run_btn = gr.Button("GERAR CAMADAS EM LOTE")
|
| 261 |
+
|
| 262 |
+
gallery = gr.Gallery(label="Prévia", columns=3, height=240)
|
| 263 |
+
zip_out = gr.File(label="ZIP do lote")
|
| 264 |
+
log_out = gr.Textbox(label="Log", lines=8)
|
| 265 |
+
folder_out = gr.Textbox(label="Pasta da sessão", lines=1)
|
| 266 |
+
|
| 267 |
+
run_btn.click(
|
| 268 |
+
fn=process_batch,
|
| 269 |
+
inputs=[originals, inpaints, depths, layer_count, use_ai_depth],
|
| 270 |
+
outputs=[gallery, zip_out, log_out, folder_out],
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
demo.launch()
|