Merlimhhs commited on
Commit
ca0222d
·
verified ·
1 Parent(s): a7f4d04

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +257 -30
app.py CHANGED
@@ -1,47 +1,274 @@
1
- import os
 
 
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
- # Depth Anything V2 é uma boa escolha para depth monocular.
11
- # A API do Transformers mostra o uso com pipeline("depth-estimation") e pipe(image)["depth"].
12
- pipe = pipeline(
13
- task="depth-estimation",
14
- model="depth-anything/Depth-Anything-V2-Small-hf"
15
- )
 
16
 
17
- def generate_cinematic_depth(files):
18
- if not files:
19
- return None
20
 
21
- out_dir = Path(tempfile.mkdtemp(prefix="parallax_depth_"))
22
- zip_path = out_dir / "Leva_Parallax_Pro.zip"
23
 
24
- with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
25
- for file_obj in files:
26
- img = Image.open(file_obj.name).convert("RGB")
27
- depth_img = pipe(img)["depth"]
 
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
- depth_img.save(depth_path)
34
- zipf.write(depth_path, arcname=depth_name)
35
 
36
- return str(zip_path)
37
 
38
- with gr.Blocks() as demo:
39
- gr.Markdown("# 🎥 Eternity Engine v3.0 Cinematic Depth")
40
- gr.Markdown("Envie uma leva de imagens e baixe um ZIP com os mapas de profundidade.")
41
- files = gr.File(file_count="multiple", label="1. Envie a leva de imagens")
42
- out = gr.File(label="2. Baixe as máscaras de profundidade (ZIP)")
43
- btn = gr.Button("Gerar Depth Maps")
 
 
 
44
 
45
- btn.click(generate_cinematic_depth, inputs=files, outputs=out)
46
 
47
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()