Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -1,75 +1,62 @@
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import shlex
|
3 |
import spaces
|
4 |
import subprocess
|
5 |
-
import
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
import argparse
|
10 |
-
import numpy as np
|
11 |
-
import gradio as gr
|
12 |
-
from PIL import Image, ImageOps
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
CUDA_TOOLKIT_URL = "https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run"
|
20 |
CUDA_TOOLKIT_FILE = f"/tmp/{os.path.basename(CUDA_TOOLKIT_URL)}"
|
21 |
-
if not os.path.exists("/usr/local/cuda
|
22 |
-
|
23 |
-
subprocess.
|
24 |
-
subprocess.
|
|
|
25 |
else:
|
26 |
-
|
27 |
|
28 |
os.environ["CUDA_HOME"] = "/usr/local/cuda"
|
29 |
os.environ["PATH"] = f"{os.environ['CUDA_HOME']}/bin:{os.environ['PATH']}"
|
30 |
os.environ["LD_LIBRARY_PATH"] = f"{os.environ['CUDA_HOME']}/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
|
31 |
os.environ["TORCH_CUDA_ARCH_LIST"] = "8.0;8.6"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
-
|
34 |
-
print("--- Versiones de librerías ---")
|
35 |
-
os.system("pip list | grep torch")
|
36 |
-
os.system('nvcc -V')
|
37 |
-
print("-----------------------------")
|
38 |
-
|
39 |
-
# --- RUTAS CORREGIDAS ---
|
40 |
-
# Construir rutas absolutas basadas en la ubicación del script app.py
|
41 |
-
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
42 |
-
PROJECT_ROOT = os.path.join(APP_DIR, "Step1X-3D")
|
43 |
-
DIFFERENTIABLE_RENDERER_PATH = os.path.join(PROJECT_ROOT, "step1x3d_texture/differentiable_renderer/")
|
44 |
-
CUSTOM_RASTERIZER_WHL_PATH = os.path.join(PROJECT_ROOT, "custom_rasterizer-0.1-cp310-cp310-linux_x86_64.whl")
|
45 |
-
|
46 |
-
print(f"Ruta del renderizador diferenciable: {DIFFERENTIABLE_RENDERER_PATH}")
|
47 |
-
print(f"Ruta del rasterizador personalizado (.whl): {CUSTOM_RASTERIZER_WHL_PATH}")
|
48 |
-
|
49 |
-
|
50 |
-
print("Compilando extensión C++/CUDA para el renderizador diferenciable...")
|
51 |
-
# Usamos cwd (current working directory) para ejecutar el comando en la carpeta correcta
|
52 |
-
subprocess.run(
|
53 |
-
"python setup.py install",
|
54 |
-
shell=True, check=True, cwd=DIFFERENTIABLE_RENDERER_PATH
|
55 |
-
)
|
56 |
-
|
57 |
-
print("Instalando la extensión C++/CUDA para el rasterizador personalizado...")
|
58 |
-
subprocess.run(
|
59 |
-
shlex.split(f"pip install {CUSTOM_RASTERIZER_WHL_PATH}"),
|
60 |
-
check=True
|
61 |
-
)
|
62 |
-
print("Compilaciones finalizadas.")
|
63 |
-
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
68 |
from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline, AutoencoderKL, EulerAncestralDiscreteScheduler
|
69 |
from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
|
70 |
from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import Step1X3DTexturePipeline
|
71 |
from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
|
72 |
|
|
|
|
|
|
|
|
|
73 |
parser = argparse.ArgumentParser()
|
74 |
parser.add_argument("--geometry_model", type=str, default="Step1X-3D-Geometry-Label-1300m")
|
75 |
parser.add_argument("--texture_model", type=str, default="Step1X-3D-Texture")
|
@@ -78,40 +65,43 @@ args = parser.parse_args()
|
|
78 |
|
79 |
os.makedirs(args.cache_dir, exist_ok=True)
|
80 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
81 |
-
|
|
|
|
|
82 |
|
83 |
-
#
|
84 |
-
|
85 |
-
geometry_model_path = os.path.join(PROJECT_ROOT) # La ruta base para from_pretrained
|
86 |
geometry_model = Step1X3DGeometryPipeline.from_pretrained(
|
87 |
"stepfun-ai/Step1X-3D", subfolder=args.geometry_model
|
88 |
).to(device)
|
89 |
-
print("Pipeline de geometría cargado.")
|
90 |
|
91 |
-
|
92 |
-
texture_model_path = os.path.join(PROJECT_ROOT) # La ruta base para from_pretrained
|
93 |
texture_model = Step1X3DTexturePipeline.from_pretrained("stepfun-ai/Step1X-3D", subfolder=args.texture_model)
|
94 |
-
print("Pipeline de texturizado cargado.")
|
95 |
-
|
96 |
|
97 |
-
#
|
98 |
-
|
99 |
-
controlnet = ControlNetModel.from_pretrained("xinsir/controlnet-scribble-sdxl-1.0", torch_dtype=
|
100 |
-
vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=
|
101 |
pipe_control = StableDiffusionXLControlNetPipeline.from_pretrained(
|
102 |
-
"sd-community/sdxl-flash", controlnet=controlnet, vae=vae, torch_dtype=
|
103 |
)
|
104 |
pipe_control.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe_control.scheduler.config)
|
105 |
pipe_control.to(device)
|
106 |
-
|
107 |
|
108 |
|
109 |
-
#
|
110 |
-
# 3. FUNCIONES DE
|
111 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
@spaces.GPU(duration=60)
|
114 |
-
def
|
115 |
"""
|
116 |
Paso 0: Convierte un boceto en una imagen de alta calidad usando ControlNet.
|
117 |
"""
|
@@ -120,43 +110,48 @@ def enhance_sketch(image, prompt):
|
|
120 |
|
121 |
input_image = image.convert("RGB")
|
122 |
|
|
|
123 |
width, height = input_image.size
|
124 |
ratio = np.sqrt(1024.0 * 1024.0 / (width * height))
|
125 |
new_width, new_height = int(width * ratio), int(height * ratio)
|
126 |
input_image = input_image.resize((new_width, new_height))
|
127 |
input_image = ImageOps.invert(input_image)
|
128 |
|
129 |
-
final_prompt =
|
130 |
-
negative_prompt
|
|
|
|
|
|
|
131 |
|
132 |
-
|
|
|
133 |
output_image = pipe_control(
|
134 |
prompt=final_prompt,
|
135 |
-
negative_prompt=
|
136 |
image=input_image,
|
137 |
-
num_inference_steps=
|
138 |
-
controlnet_conditioning_scale=
|
139 |
-
guidance_scale=
|
140 |
width=new_width,
|
141 |
height=new_height,
|
|
|
142 |
).images[0]
|
143 |
|
144 |
save_name = str(uuid.uuid4())
|
145 |
processed_image_path = f"{args.cache_dir}/{save_name}_processed.png"
|
146 |
output_image.save(processed_image_path)
|
147 |
|
148 |
-
|
149 |
-
return processed_image_path
|
150 |
|
151 |
@spaces.GPU(duration=180)
|
152 |
def generate_geometry(input_image_path, guidance_scale, inference_steps, max_facenum, symmetry, edge_type):
|
153 |
-
"""
|
154 |
-
Paso 1: Genera la geometría a partir de una imagen.
|
155 |
-
"""
|
156 |
if not input_image_path or not os.path.exists(input_image_path):
|
157 |
-
raise gr.Error("Primero debes proporcionar una imagen
|
158 |
|
159 |
-
|
|
|
160 |
if "Label" in args.geometry_model:
|
161 |
symmetry_values = ["x", "asymmetry"]
|
162 |
out = geometry_model(
|
@@ -175,108 +170,143 @@ def generate_geometry(input_image_path, guidance_scale, inference_steps, max_fac
|
|
175 |
max_facenum=int(max_facenum),
|
176 |
)
|
177 |
|
178 |
-
save_name =
|
179 |
-
geometry_save_path = f"{args.cache_dir}/{save_name}.glb"
|
180 |
geometry_mesh = out.mesh[0]
|
181 |
geometry_mesh.export(geometry_save_path)
|
182 |
|
183 |
torch.cuda.empty_cache()
|
184 |
-
|
185 |
-
return geometry_save_path
|
186 |
|
187 |
@spaces.GPU(duration=120)
|
188 |
def generate_texture(input_image_path, geometry_path):
|
189 |
-
"""
|
190 |
-
Paso 2: Aplica la textura a una geometría existente.
|
191 |
-
"""
|
192 |
if not geometry_path or not os.path.exists(geometry_path):
|
193 |
raise gr.Error("Por favor, primero genera la geometría antes de texturizar.")
|
194 |
if not input_image_path or not os.path.exists(input_image_path):
|
195 |
-
raise gr.Error("Se necesita
|
196 |
|
197 |
-
|
198 |
geometry_mesh = trimesh.load(geometry_path)
|
199 |
|
|
|
200 |
geometry_mesh = remove_degenerate_face(geometry_mesh)
|
201 |
geometry_mesh = reduce_face(geometry_mesh)
|
202 |
|
203 |
textured_mesh = texture_model(input_image_path, geometry_mesh)
|
204 |
|
205 |
-
save_name = os.path.basename(geometry_path).replace(".glb", "")
|
206 |
-
textured_save_path = f"{args.cache_dir}/{save_name}
|
207 |
textured_mesh.export(textured_save_path)
|
208 |
|
209 |
torch.cuda.empty_cache()
|
210 |
-
|
211 |
return textured_save_path
|
212 |
|
213 |
-
#
|
214 |
-
# 4. INTERFAZ DE
|
215 |
-
#
|
216 |
|
217 |
-
with gr.Blocks(title="Step1X-3D:
|
218 |
-
gr.Markdown("# Step1X-3D:
|
219 |
-
gr.Markdown("Flujo de trabajo
|
220 |
|
|
|
221 |
processed_image_path_state = gr.State()
|
222 |
geometry_path_state = gr.State()
|
223 |
|
224 |
with gr.Row():
|
225 |
with gr.Column(scale=2):
|
226 |
-
|
227 |
-
input_image = gr.Image(label="
|
228 |
-
prompt = gr.Textbox(label="
|
229 |
-
|
230 |
-
with gr.Accordion(label="Opciones
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
235 |
edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Edge Type", value="sharp", type="value")
|
236 |
-
|
237 |
with gr.Row():
|
238 |
-
|
239 |
with gr.Row():
|
240 |
-
btn_geo = gr.Button("
|
241 |
-
btn_tex = gr.Button("
|
242 |
-
|
243 |
-
with gr.Column(scale=
|
244 |
-
|
245 |
-
processed_image_preview = gr.Image(label="
|
246 |
-
|
247 |
-
|
248 |
|
249 |
with gr.Column(scale=1):
|
250 |
gr.Examples(
|
251 |
examples=[
|
252 |
-
[
|
253 |
-
[
|
254 |
-
[
|
|
|
|
|
|
|
|
|
|
|
255 |
],
|
256 |
-
inputs=[input_image, prompt],
|
257 |
-
cache_examples=False
|
258 |
)
|
259 |
|
260 |
# --- Lógica de la Interfaz ---
|
261 |
|
262 |
-
def
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
)
|
275 |
|
276 |
-
btn_geo.click(
|
277 |
-
fn=
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
)
|
279 |
|
280 |
-
btn_tex.click(
|
|
|
|
|
|
|
|
|
281 |
|
282 |
-
demo.launch(ssr_mode=False
|
|
|
1 |
+
# ==============================================================================
|
2 |
+
# 1. INSTALACIÓN DEL ENTORNO Y DEPENDENCIAS
|
3 |
+
# ==============================================================================
|
4 |
import os
|
5 |
import shlex
|
6 |
import spaces
|
7 |
import subprocess
|
8 |
+
import logging
|
9 |
+
|
10 |
+
# Configuración del logging para depuración
|
11 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - Step1X-3D - %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
|
12 |
|
13 |
+
def install_dependencies():
|
14 |
+
"""Instala el toolkit de CUDA y compila las extensiones C++/CUDA necesarias."""
|
15 |
+
logging.info("Iniciando la instalación de dependencias...")
|
16 |
+
|
17 |
+
# Instalar CUDA Toolkit
|
18 |
CUDA_TOOLKIT_URL = "https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run"
|
19 |
CUDA_TOOLKIT_FILE = f"/tmp/{os.path.basename(CUDA_TOOLKIT_URL)}"
|
20 |
+
if not os.path.exists("/usr/local/cuda"):
|
21 |
+
logging.info("Descargando e instalando CUDA Toolkit...")
|
22 |
+
subprocess.call(["wget", "-q", CUDA_TOOLKIT_URL, "-O", CUDA_TOOLKIT_FILE])
|
23 |
+
subprocess.call(["chmod", "+x", CUDA_TOOLKIT_FILE])
|
24 |
+
subprocess.call([CUDA_TOOLKIT_FILE, "--silent", "--toolkit"])
|
25 |
else:
|
26 |
+
logging.info("CUDA Toolkit ya está instalado.")
|
27 |
|
28 |
os.environ["CUDA_HOME"] = "/usr/local/cuda"
|
29 |
os.environ["PATH"] = f"{os.environ['CUDA_HOME']}/bin:{os.environ['PATH']}"
|
30 |
os.environ["LD_LIBRARY_PATH"] = f"{os.environ['CUDA_HOME']}/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
|
31 |
os.environ["TORCH_CUDA_ARCH_LIST"] = "8.0;8.6"
|
32 |
+
|
33 |
+
# Compilar extensiones personalizadas
|
34 |
+
logging.info("Compilando extensiones de renderizado...")
|
35 |
+
renderer_path = "/home/user/app/step1x3d_texture/differentiable_renderer/"
|
36 |
+
subprocess.run(f"cd {renderer_path} && python setup.py install", shell=True, check=True)
|
37 |
+
subprocess.run(shlex.split("pip install custom_rasterizer-0.1-cp310-cp310-linux_x86_64.whl"), check=True)
|
38 |
+
|
39 |
+
logging.info("Instalación completada.")
|
40 |
+
os.system('nvcc -V')
|
41 |
|
42 |
+
install_dependencies()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
+
import uuid
|
45 |
+
import torch
|
46 |
+
import trimesh
|
47 |
+
import argparse
|
48 |
+
import numpy as np
|
49 |
+
import gradio as gr
|
50 |
+
from PIL import Image, ImageOps
|
51 |
from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline, AutoencoderKL, EulerAncestralDiscreteScheduler
|
52 |
from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
|
53 |
from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import Step1X3DTexturePipeline
|
54 |
from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
|
55 |
|
56 |
+
# ==============================================================================
|
57 |
+
# 2. CONFIGURACIÓN Y CARGA DE MODELOS
|
58 |
+
# ==============================================================================
|
59 |
+
|
60 |
parser = argparse.ArgumentParser()
|
61 |
parser.add_argument("--geometry_model", type=str, default="Step1X-3D-Geometry-Label-1300m")
|
62 |
parser.add_argument("--texture_model", type=str, default="Step1X-3D-Texture")
|
|
|
65 |
|
66 |
os.makedirs(args.cache_dir, exist_ok=True)
|
67 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
68 |
+
torch_dtype = torch.float16
|
69 |
+
|
70 |
+
logging.info("Cargando modelos... Este proceso puede tardar varios minutos.")
|
71 |
|
72 |
+
# Carga de modelos de Step1X-3D
|
73 |
+
logging.info(f"Cargando modelo de geometría: {args.geometry_model}")
|
|
|
74 |
geometry_model = Step1X3DGeometryPipeline.from_pretrained(
|
75 |
"stepfun-ai/Step1X-3D", subfolder=args.geometry_model
|
76 |
).to(device)
|
|
|
77 |
|
78 |
+
logging.info(f"Cargando modelo de textura: {args.texture_model}")
|
|
|
79 |
texture_model = Step1X3DTexturePipeline.from_pretrained("stepfun-ai/Step1X-3D", subfolder=args.texture_model)
|
|
|
|
|
80 |
|
81 |
+
# Carga de modelos de ControlNet para el pre-procesamiento de bocetos
|
82 |
+
logging.info("Cargando modelos para el pre-procesamiento de bocetos (SDXL + ControlNet)...")
|
83 |
+
controlnet = ControlNetModel.from_pretrained("xinsir/controlnet-scribble-sdxl-1.0", torch_dtype=torch_dtype)
|
84 |
+
vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch_dtype)
|
85 |
pipe_control = StableDiffusionXLControlNetPipeline.from_pretrained(
|
86 |
+
"sd-community/sdxl-flash", controlnet=controlnet, vae=vae, torch_dtype=torch_dtype
|
87 |
)
|
88 |
pipe_control.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe_control.scheduler.config)
|
89 |
pipe_control.to(device)
|
90 |
+
logging.info("Todos los modelos han sido cargados correctamente.")
|
91 |
|
92 |
|
93 |
+
# ==============================================================================
|
94 |
+
# 3. FUNCIONES DE GENERACIÓN POR PASOS
|
95 |
+
# ==============================================================================
|
96 |
+
|
97 |
+
def apply_3d_style(prompt: str) -> tuple[str, str]:
|
98 |
+
"""Aplica el estilo '3D Model' por defecto al prompt."""
|
99 |
+
style_prompt = "professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting"
|
100 |
+
negative_prompt = "ugly, deformed, noisy, low poly, blurry, painting"
|
101 |
+
return style_prompt.replace("{prompt}", prompt), negative_prompt
|
102 |
|
103 |
@spaces.GPU(duration=60)
|
104 |
+
def process_sketch(image, prompt, negative_prompt, guidance_scale, num_steps, controlnet_scale):
|
105 |
"""
|
106 |
Paso 0: Convierte un boceto en una imagen de alta calidad usando ControlNet.
|
107 |
"""
|
|
|
110 |
|
111 |
input_image = image.convert("RGB")
|
112 |
|
113 |
+
# Pre-procesamiento de la imagen de entrada (invertir y redimensionar)
|
114 |
width, height = input_image.size
|
115 |
ratio = np.sqrt(1024.0 * 1024.0 / (width * height))
|
116 |
new_width, new_height = int(width * ratio), int(height * ratio)
|
117 |
input_image = input_image.resize((new_width, new_height))
|
118 |
input_image = ImageOps.invert(input_image)
|
119 |
|
120 |
+
final_prompt, final_negative_prompt = apply_3d_style(prompt)
|
121 |
+
if negative_prompt: # Añadir negativo del usuario si existe
|
122 |
+
final_negative_prompt = f"{final_negative_prompt}, {negative_prompt}"
|
123 |
+
|
124 |
+
logging.info(f"Mejorando boceto con prompt: '{final_prompt}'")
|
125 |
|
126 |
+
generator = torch.Generator(device=device).manual_seed(np.random.randint(0, 2**32 - 1))
|
127 |
+
|
128 |
output_image = pipe_control(
|
129 |
prompt=final_prompt,
|
130 |
+
negative_prompt=final_negative_prompt,
|
131 |
image=input_image,
|
132 |
+
num_inference_steps=int(num_steps),
|
133 |
+
controlnet_conditioning_scale=float(controlnet_scale),
|
134 |
+
guidance_scale=float(guidance_scale),
|
135 |
width=new_width,
|
136 |
height=new_height,
|
137 |
+
generator=generator,
|
138 |
).images[0]
|
139 |
|
140 |
save_name = str(uuid.uuid4())
|
141 |
processed_image_path = f"{args.cache_dir}/{save_name}_processed.png"
|
142 |
output_image.save(processed_image_path)
|
143 |
|
144 |
+
logging.info(f"Boceto mejorado y guardado en: {processed_image_path}")
|
145 |
+
return processed_image_path
|
146 |
|
147 |
@spaces.GPU(duration=180)
|
148 |
def generate_geometry(input_image_path, guidance_scale, inference_steps, max_facenum, symmetry, edge_type):
|
149 |
+
"""Paso 1: Genera la geometría a partir de la imagen procesada."""
|
|
|
|
|
150 |
if not input_image_path or not os.path.exists(input_image_path):
|
151 |
+
raise gr.Error("Primero debes procesar un boceto o proporcionar una imagen de entrada válida.")
|
152 |
|
153 |
+
logging.info(f"Iniciando generación de geometría desde: {os.path.basename(input_image_path)}")
|
154 |
+
|
155 |
if "Label" in args.geometry_model:
|
156 |
symmetry_values = ["x", "asymmetry"]
|
157 |
out = geometry_model(
|
|
|
170 |
max_facenum=int(max_facenum),
|
171 |
)
|
172 |
|
173 |
+
save_name = os.path.basename(input_image_path).replace("_processed.png", "")
|
174 |
+
geometry_save_path = f"{args.cache_dir}/{save_name}_geometry.glb"
|
175 |
geometry_mesh = out.mesh[0]
|
176 |
geometry_mesh.export(geometry_save_path)
|
177 |
|
178 |
torch.cuda.empty_cache()
|
179 |
+
logging.info(f"Geometría guardada en: {geometry_save_path}")
|
180 |
+
return geometry_save_path
|
181 |
|
182 |
@spaces.GPU(duration=120)
|
183 |
def generate_texture(input_image_path, geometry_path):
|
184 |
+
"""Paso 2: Aplica la textura a la geometría generada."""
|
|
|
|
|
185 |
if not geometry_path or not os.path.exists(geometry_path):
|
186 |
raise gr.Error("Por favor, primero genera la geometría antes de texturizar.")
|
187 |
if not input_image_path or not os.path.exists(input_image_path):
|
188 |
+
raise gr.Error("Se necesita la imagen procesada para el texturizado.")
|
189 |
|
190 |
+
logging.info(f"Iniciando texturizado para la malla: {os.path.basename(geometry_path)}")
|
191 |
geometry_mesh = trimesh.load(geometry_path)
|
192 |
|
193 |
+
# Post-procesamiento
|
194 |
geometry_mesh = remove_degenerate_face(geometry_mesh)
|
195 |
geometry_mesh = reduce_face(geometry_mesh)
|
196 |
|
197 |
textured_mesh = texture_model(input_image_path, geometry_mesh)
|
198 |
|
199 |
+
save_name = os.path.basename(geometry_path).replace("_geometry.glb", "")
|
200 |
+
textured_save_path = f"{args.cache_dir}/{save_name}_textured.glb"
|
201 |
textured_mesh.export(textured_save_path)
|
202 |
|
203 |
torch.cuda.empty_cache()
|
204 |
+
logging.info(f"Malla texturizada guardada en: {textured_save_path}")
|
205 |
return textured_save_path
|
206 |
|
207 |
+
# ==============================================================================
|
208 |
+
# 4. INTERFAZ DE GRADIO
|
209 |
+
# ==============================================================================
|
210 |
|
211 |
+
with gr.Blocks(title="Step1X-3D", css="footer {display: none !important;} a {text-decoration: none !important;}") as demo:
|
212 |
+
gr.Markdown("# Step1X-3D: De Boceto a Malla 3D Texturizada")
|
213 |
+
gr.Markdown("Flujo de trabajo en 3 pasos: **0. Procesar Boceto → 1. Generar Geometría → 2. Generar Textura**")
|
214 |
|
215 |
+
# Estados para mantener las rutas de los archivos entre pasos
|
216 |
processed_image_path_state = gr.State()
|
217 |
geometry_path_state = gr.State()
|
218 |
|
219 |
with gr.Row():
|
220 |
with gr.Column(scale=2):
|
221 |
+
# --- Panel de Entradas ---
|
222 |
+
input_image = gr.Image(label="Paso 0: Carga tu boceto o imagen", type="pil", image_mode="RGB")
|
223 |
+
prompt = gr.Textbox(label="Describe tu objeto", value="a comfortable armchair")
|
224 |
+
|
225 |
+
with gr.Accordion(label="Opciones Avanzadas", open=False):
|
226 |
+
gr.Markdown("### Opciones de Procesado de Boceto (Paso 0)")
|
227 |
+
neg_prompt_sketch = gr.Textbox(label="Negative Prompt (Boceto)", value="text, signature, watermark")
|
228 |
+
guidance_sketch = gr.Slider(0.1, 10.0, label="Guidance Scale (Boceto)", value=5.0, step=0.1)
|
229 |
+
steps_sketch = gr.Slider(1, 50, label="Steps (Boceto)", value=25, step=1)
|
230 |
+
controlnet_scale = gr.Slider(0.1, 2.0, label="ControlNet Scale", value=0.85, step=0.05)
|
231 |
+
|
232 |
+
gr.Markdown("---")
|
233 |
+
gr.Markdown("### Opciones de Generación 3D (Paso 1)")
|
234 |
+
guidance_3d = gr.Number(label="Guidance Scale (3D)", value="7.5")
|
235 |
+
steps_3d = gr.Slider(label="Inference Steps (3D)", minimum=1, maximum=100, value=50)
|
236 |
+
max_facenum = gr.Number(label="Max Face Num", value="200000")
|
237 |
+
symmetry = gr.Radio(choices=["symmetry", "asymmetry"], label="Symmetry", value="symmetry", type="index")
|
238 |
edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Edge Type", value="sharp", type="value")
|
239 |
+
|
240 |
with gr.Row():
|
241 |
+
btn_process_sketch = gr.Button("0. Procesar Boceto", variant="secondary")
|
242 |
with gr.Row():
|
243 |
+
btn_geo = gr.Button("1. Generar Geometría", interactive=False)
|
244 |
+
btn_tex = gr.Button("2. Generar Textura", interactive=False)
|
245 |
+
|
246 |
+
with gr.Column(scale=3):
|
247 |
+
# --- Panel de Salidas ---
|
248 |
+
processed_image_preview = gr.Image(label="Resultado del Boceto Procesado", type="filepath", interactive=False, height=400)
|
249 |
+
geometry_preview = gr.Model3D(label="Vista Previa de la Geometría", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
|
250 |
+
textured_preview = gr.Model3D(label="Vista Previa del Modelo Texturizado", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
|
251 |
|
252 |
with gr.Column(scale=1):
|
253 |
gr.Examples(
|
254 |
examples=[
|
255 |
+
["examples/images/000.png", "a futuristic spaceship"],
|
256 |
+
["examples/images/001.png", "a cartoon style monster"],
|
257 |
+
["examples/images/004.png", "a red sports car"],
|
258 |
+
["examples/images/008.png", "a medieval sword"],
|
259 |
+
["examples/images/028.png", "a vintage camera"],
|
260 |
+
["examples/images/032.png", "a cute robot"],
|
261 |
+
["examples/images/061.png", "a delicious hamburger"],
|
262 |
+
["examples/images/107.png", "a golden trophy"],
|
263 |
],
|
264 |
+
inputs=[input_image, prompt], cache_examples=False
|
|
|
265 |
)
|
266 |
|
267 |
# --- Lógica de la Interfaz ---
|
268 |
|
269 |
+
def on_sketch_processed(path):
|
270 |
+
"""Función a ejecutar cuando el boceto se ha procesado."""
|
271 |
+
return {
|
272 |
+
processed_image_path_state: path,
|
273 |
+
btn_geo: gr.update(interactive=True, variant="primary"),
|
274 |
+
btn_tex: gr.update(interactive=False),
|
275 |
+
geometry_preview: gr.update(value=None),
|
276 |
+
textured_preview: gr.update(value=None),
|
277 |
+
}
|
278 |
+
|
279 |
+
def on_geometry_generated(path):
|
280 |
+
"""Función a ejecutar cuando la geometría se ha generado."""
|
281 |
+
return {
|
282 |
+
geometry_path_state: path,
|
283 |
+
btn_tex: gr.update(interactive=True, variant="primary"),
|
284 |
+
}
|
285 |
+
|
286 |
+
btn_process_sketch.click(
|
287 |
+
fn=process_sketch,
|
288 |
+
inputs=[input_image, prompt, neg_prompt_sketch, guidance_sketch, steps_sketch, controlnet_scale],
|
289 |
+
outputs=[processed_image_preview]
|
290 |
+
).then(
|
291 |
+
fn=on_sketch_processed,
|
292 |
+
inputs=[processed_image_preview],
|
293 |
+
outputs=[processed_image_path_state, btn_geo, btn_tex, geometry_preview, textured_preview]
|
294 |
)
|
295 |
|
296 |
+
btn_geo.click(
|
297 |
+
fn=generate_geometry,
|
298 |
+
inputs=[processed_image_path_state, guidance_3d, steps_3d, max_facenum, symmetry, edge_type],
|
299 |
+
outputs=[geometry_preview]
|
300 |
+
).then(
|
301 |
+
fn=on_geometry_generated,
|
302 |
+
inputs=[geometry_preview],
|
303 |
+
outputs=[geometry_path_state, btn_tex]
|
304 |
)
|
305 |
|
306 |
+
btn_tex.click(
|
307 |
+
fn=generate_texture,
|
308 |
+
inputs=[processed_image_path_state, geometry_path_state],
|
309 |
+
outputs=[textured_preview],
|
310 |
+
)
|
311 |
|
312 |
+
demo.launch(ssr_mode=False)
|