Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -15,6 +15,12 @@ from transformers import CLIPTokenizer, CLIPTextModel, CLIPImageProcessor
|
|
| 15 |
import torch.nn as nn
|
| 16 |
import numpy as np
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
class UNetNoCondWrapper(nn.Module):
|
| 19 |
def __init__(self, base_unet: UNet2DModel):
|
| 20 |
super().__init__()
|
|
@@ -71,243 +77,260 @@ class UNetNoCondWrapper(nn.Module):
|
|
| 71 |
# pipe = pipe.to(torch.float32).to(device)
|
| 72 |
# @spaces.GPU
|
| 73 |
|
| 74 |
-
def pil_to_data_uri(img: Image.Image) -> str:
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# # --- new gradio_generate that returns both gallery and 3D html ---
|
| 81 |
-
# def gradio_generate(fibers: Image.Image, rings: Image.Image, num_steps: int):
|
| 82 |
-
# """
|
| 83 |
-
# fibers: PIL image containing 4 tiles in a 2x2 square (order 1,4,2,3)
|
| 84 |
-
# rings: same
|
| 85 |
-
# returns: (list_of_4_PIL_images, html_string_for_3d)
|
| 86 |
-
# """
|
| 87 |
-
# # call your inference function (unchanged)
|
| 88 |
-
# preds = inference(pipe, fibers, rings, int(num_steps)) # preds order: [1,4,2,3] -> TL,TR,BL,BR
|
| 89 |
-
|
| 90 |
-
# # convert preds to RGB for gallery
|
| 91 |
-
# preds_rgb = [p.convert("RGB") if p.mode != "RGB" else p for p in preds]
|
| 92 |
-
|
| 93 |
-
# # build data-urls in same order
|
| 94 |
-
# data_urls = [pil_to_dataurl(im, fmt="PNG") for im in preds_rgb] # [TL,TR,BL,BR]
|
| 95 |
-
|
| 96 |
-
# # safe JSON array literal for injection into HTML template
|
| 97 |
-
# js_img_list = json.dumps(data_urls)
|
| 98 |
-
|
| 99 |
-
# html = HTML_TEMPLATE.replace("___IMGS___", js_img_list)
|
| 100 |
-
|
| 101 |
-
# # gr.Interface expects outputs matching signature; we return (gallery_images, html_string)
|
| 102 |
-
# return preds_rgb, html
|
| 103 |
-
|
| 104 |
-
# # replace the Interface outputs: first a Gallery, second a HTML component
|
| 105 |
-
# iface = gr.Interface(
|
| 106 |
-
# fn=gradio_generate,
|
| 107 |
-
# inputs=[
|
| 108 |
-
# gr.Image(type="pil", label="Fiber"),
|
| 109 |
-
# gr.Image(type="pil", label="Ring"),
|
| 110 |
-
# gr.Number(value=10, label="Number of inference steps"),
|
| 111 |
-
# ],
|
| 112 |
-
# outputs=[
|
| 113 |
-
# gr.HTML(label="3D preview")
|
| 114 |
-
# ],
|
| 115 |
-
# title="Photorealistic wood generator (4 faces)",
|
| 116 |
-
# description="""
|
| 117 |
-
# Upload 2 images (one with four fiber maps and one with four ring maps) arranged as a 2x2 square.
|
| 118 |
-
# The model returns four generated faces (one per tile) and a 3D preview with those faces textured on a box.
|
| 119 |
-
# """
|
| 120 |
-
# )
|
| 121 |
|
| 122 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
try:
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
raise ValueError("La fonction d'inference doit renvoyer une liste/tuple de 4 images.")
|
| 129 |
-
# Prendre les 4 premières images
|
| 130 |
-
imgs = outputs[:4]
|
| 131 |
-
# Convertir en PIL si nécessaire et assurer taille raisonnable
|
| 132 |
pil_imgs = []
|
| 133 |
-
for im in
|
| 134 |
if isinstance(im, np.ndarray):
|
| 135 |
im = Image.fromarray(im)
|
| 136 |
-
# for safety, ensure RGB
|
| 137 |
if im.mode != "RGB":
|
| 138 |
im = im.convert("RGB")
|
| 139 |
pil_imgs.append(im)
|
| 140 |
|
| 141 |
-
#
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
#
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
#
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
Manipulez la souris pour tourner, molette pour zoomer.
|
| 152 |
-
</div>
|
| 153 |
-
</div>
|
| 154 |
-
<script src="https://unpkg.com/three@0.152.2/build/three.min.js"></script>
|
| 155 |
-
<script src="https://unpkg.com/three@0.152.2/examples/js/controls/OrbitControls.js"></script>
|
| 156 |
-
<script>
|
| 157 |
-
(function() {{
|
| 158 |
-
const images = {data_uris!r};
|
| 159 |
-
const container = document.getElementById('viewer');
|
| 160 |
-
|
| 161 |
-
function safeLog(...args) {{ try{{console.log(...args)}}catch(e){{}} }}
|
| 162 |
-
|
| 163 |
-
function initScene() {{
|
| 164 |
-
const w = container.clientWidth || 800;
|
| 165 |
-
const h = container.clientHeight || 480;
|
| 166 |
-
|
| 167 |
-
const scene = new THREE.Scene();
|
| 168 |
-
const camera = new THREE.PerspectiveCamera(45, w/h, 0.1, 1000);
|
| 169 |
-
camera.position.set(2.5, 2.0, 3.5);
|
| 170 |
-
|
| 171 |
-
const renderer = new THREE.WebGLRenderer({{antialias:true}});
|
| 172 |
-
renderer.setSize(w, h);
|
| 173 |
-
renderer.domElement.style.width = '100%';
|
| 174 |
-
renderer.domElement.style.height = '100%';
|
| 175 |
-
container.appendChild(renderer.domElement);
|
| 176 |
-
|
| 177 |
-
// lights
|
| 178 |
-
const ambient = new THREE.AmbientLight(0xffffff, 0.7);
|
| 179 |
-
scene.add(ambient);
|
| 180 |
-
const dir = new THREE.DirectionalLight(0xffffff, 0.6);
|
| 181 |
-
dir.position.set(5,10,7);
|
| 182 |
-
scene.add(dir);
|
| 183 |
-
|
| 184 |
-
// fallback cube (visible immédiatement)
|
| 185 |
-
const geometry = new THREE.BoxGeometry(1.6,1.6,1.6);
|
| 186 |
-
const fallbackMats = [];
|
| 187 |
-
for (let i=0;i<6;i++) fallbackMats.push(new THREE.MeshStandardMaterial({{color:0x999999}}));
|
| 188 |
-
const cube = new THREE.Mesh(geometry, fallbackMats);
|
| 189 |
-
scene.add(cube);
|
| 190 |
-
|
| 191 |
-
// grid for context
|
| 192 |
-
const grid = new THREE.GridHelper(6, 12, 0x888888, 0x444444);
|
| 193 |
-
grid.position.y = -1.2;
|
| 194 |
-
scene.add(grid);
|
| 195 |
-
|
| 196 |
-
// controls
|
| 197 |
-
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 198 |
-
controls.enableDamping = true;
|
| 199 |
-
controls.dampingFactor = 0.08;
|
| 200 |
-
controls.target.set(0,0,0);
|
| 201 |
-
|
| 202 |
-
// attempt to load textures, but keep fallback if it fails
|
| 203 |
-
const loader = new THREE.TextureLoader();
|
| 204 |
-
Promise.all(images.map(uri => new Promise((res, rej) => {{
|
| 205 |
-
try {{
|
| 206 |
-
loader.load(uri, tex => {{ tex.flipY = false; tex.generateMipmaps = true; res(tex); }}, undefined, err => rej(err));
|
| 207 |
-
}} catch(e) {{ rej(e); }}
|
| 208 |
-
}}))).then(textures => {{
|
| 209 |
-
safeLog('Textures loaded', textures);
|
| 210 |
-
const mats = [
|
| 211 |
-
new THREE.MeshStandardMaterial({{map: textures[0]}}), // right
|
| 212 |
-
new THREE.MeshStandardMaterial({{map: textures[1]}}), // left
|
| 213 |
-
new THREE.MeshStandardMaterial({{color:0xcccccc}}), // top
|
| 214 |
-
new THREE.MeshStandardMaterial({{color:0xcccccc}}), // bottom
|
| 215 |
-
new THREE.MeshStandardMaterial({{map: textures[2]}}), // front
|
| 216 |
-
new THREE.MeshStandardMaterial({{map: textures[3]}}) // back
|
| 217 |
-
];
|
| 218 |
-
// set wrapping/filter
|
| 219 |
-
mats.forEach(m => {{
|
| 220 |
-
if (m.map) {{
|
| 221 |
-
m.map.minFilter = THREE.LinearFilter;
|
| 222 |
-
m.map.wrapS = THREE.ClampToEdgeWrapping;
|
| 223 |
-
m.map.wrapT = THREE.ClampToEdgeWrapping;
|
| 224 |
-
}}
|
| 225 |
-
}});
|
| 226 |
-
cube.material = mats;
|
| 227 |
-
}}).catch(err => {{
|
| 228 |
-
safeLog('Erreur chargement textures (on garde fallback):', err);
|
| 229 |
-
// affiche un petit message utilisateur (optionnel)
|
| 230 |
-
const msg = document.getElementById('viewer-msg');
|
| 231 |
-
if (msg) msg.innerText = "Rendu 3D : textures non chargées, affichage fallback (voir console).";
|
| 232 |
-
}});
|
| 233 |
-
|
| 234 |
-
// animation loop
|
| 235 |
-
function animate() {{
|
| 236 |
-
requestAnimationFrame(animate);
|
| 237 |
-
controls.update();
|
| 238 |
-
renderer.render(scene, camera);
|
| 239 |
-
}}
|
| 240 |
-
animate();
|
| 241 |
-
|
| 242 |
-
// responsive: observe container size
|
| 243 |
-
if (typeof ResizeObserver !== 'undefined') {{
|
| 244 |
-
const ro = new ResizeObserver(() => {{
|
| 245 |
-
const ww = container.clientWidth || 800;
|
| 246 |
-
const hh = container.clientHeight || 480;
|
| 247 |
-
renderer.setSize(ww, hh);
|
| 248 |
-
camera.aspect = ww / hh;
|
| 249 |
-
camera.updateProjectionMatrix();
|
| 250 |
-
}});
|
| 251 |
-
ro.observe(container);
|
| 252 |
-
}} else {{
|
| 253 |
-
window.addEventListener('resize', () => {{
|
| 254 |
-
const ww = container.clientWidth || 800;
|
| 255 |
-
const hh = container.clientHeight || 480;
|
| 256 |
-
renderer.setSize(ww, hh);
|
| 257 |
-
camera.aspect = ww / hh;
|
| 258 |
-
camera.updateProjectionMatrix();
|
| 259 |
-
}});
|
| 260 |
-
}}
|
| 261 |
-
}}
|
| 262 |
-
|
| 263 |
-
// wait until container has non-zero size and THREE is available
|
| 264 |
-
let tries = 0;
|
| 265 |
-
function waitForReady() {{
|
| 266 |
-
tries++;
|
| 267 |
-
if (container.clientWidth > 0 && typeof THREE !== 'undefined') {{
|
| 268 |
-
try {{ initScene(); }} catch(e) {{ safeLog('Erreur initScene', e); container.innerHTML = "<div style='padding:10px;color:#900;'>Erreur initialisation rendu 3D (voir console)</div>"; }}
|
| 269 |
-
}} else {{
|
| 270 |
-
if (tries < 60) setTimeout(waitForReady, 100);
|
| 271 |
-
else {{
|
| 272 |
-
// give it one last try
|
| 273 |
-
try {{ initScene(); }} catch(e) {{ safeLog('Derniere tentative échouée', e); container.innerHTML = "<div style='padding:10px;color:#900;'>Impossible d'initialiser le rendu 3D (voir console)</div>"; }}
|
| 274 |
-
}}
|
| 275 |
-
}}
|
| 276 |
-
}}
|
| 277 |
-
waitForReady();
|
| 278 |
-
}})();
|
| 279 |
-
</script>
|
| 280 |
-
"""
|
| 281 |
-
# Retourner : 4 miniatures pour affichage + HTML viewer
|
| 282 |
-
return (*thumb_imgs, html)
|
| 283 |
except Exception as e:
|
| 284 |
traceback.print_exc()
|
| 285 |
-
# en cas d'erreur, renvoyer 4 images
|
| 286 |
blank = Image.new("RGB", (256,256), (220,220,220))
|
| 287 |
-
|
| 288 |
-
return (blank, blank, blank, blank, err_html)
|
| 289 |
|
|
|
|
| 290 |
# Interface Gradio
|
| 291 |
-
|
| 292 |
-
|
|
|
|
| 293 |
with gr.Row():
|
| 294 |
with gr.Column(scale=1):
|
| 295 |
-
inp1 = gr.Image(label="Image 1 (entrée)", type="
|
| 296 |
-
inp2 = gr.Image(label="Image 2 (entrée)", type="
|
| 297 |
-
inp3 = gr.Number(value=10, label="Number of inference steps")
|
| 298 |
run_btn = gr.Button("Lancer l'inference")
|
| 299 |
-
gr.Markdown("Remplace `user_inference` dans `app.py` par
|
| 300 |
with gr.Column(scale=2):
|
| 301 |
-
|
| 302 |
-
# gr.Row().style(mobile_collapse=False)
|
| 303 |
with gr.Row():
|
| 304 |
-
out1 = gr.Image(label="Output 1")
|
| 305 |
-
out2 = gr.Image(label="Output 2")
|
| 306 |
-
out3 = gr.Image(label="Output 3")
|
| 307 |
-
out4 = gr.Image(label="Output 4")
|
| 308 |
|
| 309 |
-
run_btn.click(fn=run, inputs=[inp1, inp2
|
| 310 |
|
| 311 |
if __name__ == "__main__":
|
| 312 |
-
demo.launch(server_name="0.0.0.0", server_port=7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
|
|
|
| 15 |
import torch.nn as nn
|
| 16 |
import numpy as np
|
| 17 |
|
| 18 |
+
import tempfile
|
| 19 |
+
import os
|
| 20 |
+
import shutil
|
| 21 |
+
import uuid
|
| 22 |
+
|
| 23 |
+
|
| 24 |
class UNetNoCondWrapper(nn.Module):
|
| 25 |
def __init__(self, base_unet: UNet2DModel):
|
| 26 |
super().__init__()
|
|
|
|
| 77 |
# pipe = pipe.to(torch.float32).to(device)
|
| 78 |
# @spaces.GPU
|
| 79 |
|
| 80 |
+
# def pil_to_data_uri(img: Image.Image) -> str:
|
| 81 |
+
# buf = io.BytesIO()
|
| 82 |
+
# img.save(buf, format="PNG")
|
| 83 |
+
# b = base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 84 |
+
# return f"data:image/png;base64,{b}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
def user_inference(img1, img2):
|
| 87 |
+
try:
|
| 88 |
+
# si numpy array -> PIL
|
| 89 |
+
if isinstance(img1, np.ndarray):
|
| 90 |
+
img1 = Image.fromarray(img1)
|
| 91 |
+
if isinstance(img2, np.ndarray):
|
| 92 |
+
img2 = Image.fromarray(img2)
|
| 93 |
+
# 4 outputs tests
|
| 94 |
+
o1 = img1.copy().resize((512,512))
|
| 95 |
+
o2 = img2.copy().resize((512,512))
|
| 96 |
+
o3 = img1.transpose(Image.FLIP_LEFT_RIGHT).resize((512,512))
|
| 97 |
+
o4 = img2.rotate(90, expand=True).resize((512,512))
|
| 98 |
+
return [o1, o2, o3, o4]
|
| 99 |
+
except Exception:
|
| 100 |
+
blank = Image.new("RGB", (512,512), (200,200,200))
|
| 101 |
+
return [blank, blank, blank, blank]
|
| 102 |
+
|
| 103 |
+
def build_textured_cube(textures, out_dir=None):
|
| 104 |
+
"""
|
| 105 |
+
textures: list de 4 PIL.Image (RGB)
|
| 106 |
+
out_dir: optionnel dossier de sortie ; si None, un tmpdir est créé
|
| 107 |
+
retourne: chemin absolu vers le fichier .obj
|
| 108 |
+
"""
|
| 109 |
+
if out_dir is None:
|
| 110 |
+
out_dir = tempfile.mkdtemp(prefix="cube_")
|
| 111 |
+
else:
|
| 112 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 113 |
+
|
| 114 |
+
# sauvegarde textures
|
| 115 |
+
tex_filenames = []
|
| 116 |
+
for i, im in enumerate(textures):
|
| 117 |
+
fn = f"tex_{i}.png"
|
| 118 |
+
path = os.path.join(out_dir, fn)
|
| 119 |
+
im.save(path, format="PNG")
|
| 120 |
+
tex_filenames.append(fn)
|
| 121 |
+
|
| 122 |
+
# mtl content (référence les textures sauvegardées)
|
| 123 |
+
mtl_name = "cube.mtl"
|
| 124 |
+
mtl_path = os.path.join(out_dir, mtl_name)
|
| 125 |
+
with open(mtl_path, "w") as f:
|
| 126 |
+
# materials mapping: mat_right, mat_left, mat_top, mat_bottom, mat_front, mat_back
|
| 127 |
+
f.write("# materials for cube\n")
|
| 128 |
+
# right -> textures[0]
|
| 129 |
+
f.write("newmtl mat_right\n")
|
| 130 |
+
f.write(f" Ns 10\n")
|
| 131 |
+
f.write(f" Ka 1.000 1.000 1.000\n")
|
| 132 |
+
f.write(f" Kd 1.000 1.000 1.000\n")
|
| 133 |
+
f.write(f" map_Kd {tex_filenames[0]}\n\n")
|
| 134 |
+
# left -> textures[1]
|
| 135 |
+
f.write("newmtl mat_left\n")
|
| 136 |
+
f.write(" Ns 10\n")
|
| 137 |
+
f.write(" Ka 1.000 1.000 1.000\n")
|
| 138 |
+
f.write(" Kd 1.000 1.000 1.000\n")
|
| 139 |
+
f.write(f" map_Kd {tex_filenames[1]}\n\n")
|
| 140 |
+
# top neutral
|
| 141 |
+
f.write("newmtl mat_top\n")
|
| 142 |
+
f.write(" Ns 10\n")
|
| 143 |
+
f.write(" Ka 1.000 1.000 1.000\n")
|
| 144 |
+
f.write(" Kd 0.8 0.8 0.8\n\n")
|
| 145 |
+
# bottom neutral
|
| 146 |
+
f.write("newmtl mat_bottom\n")
|
| 147 |
+
f.write(" Ns 10\n")
|
| 148 |
+
f.write(" Ka 1.000 1.000 1.000\n")
|
| 149 |
+
f.write(" Kd 0.7 0.7 0.7\n\n")
|
| 150 |
+
# front -> textures[2]
|
| 151 |
+
f.write("newmtl mat_front\n")
|
| 152 |
+
f.write(" Ns 10\n")
|
| 153 |
+
f.write(" Ka 1.000 1.000 1.000\n")
|
| 154 |
+
f.write(" Kd 1.000 1.000 1.000\n")
|
| 155 |
+
f.write(f" map_Kd {tex_filenames[2]}\n\n")
|
| 156 |
+
# back -> textures[3]
|
| 157 |
+
f.write("newmtl mat_back\n")
|
| 158 |
+
f.write(" Ns 10\n")
|
| 159 |
+
f.write(" Ka 1.000 1.000 1.000\n")
|
| 160 |
+
f.write(" Kd 1.000 1.000 1.000\n")
|
| 161 |
+
f.write(f" map_Kd {tex_filenames[3]}\n\n")
|
| 162 |
+
|
| 163 |
+
# OBJ content
|
| 164 |
+
obj_name = "cube.obj"
|
| 165 |
+
obj_path = os.path.join(out_dir, obj_name)
|
| 166 |
+
with open(obj_path, "w") as f:
|
| 167 |
+
f.write(f"mtllib {mtl_name}\n")
|
| 168 |
+
f.write("o CubeTextured\n")
|
| 169 |
+
|
| 170 |
+
# 8 vertices
|
| 171 |
+
verts = [
|
| 172 |
+
(-1.0, -1.0, -1.0), # v1
|
| 173 |
+
( 1.0, -1.0, -1.0), # v2
|
| 174 |
+
( 1.0, 1.0, -1.0), # v3
|
| 175 |
+
(-1.0, 1.0, -1.0), # v4
|
| 176 |
+
(-1.0, -1.0, 1.0), # v5
|
| 177 |
+
( 1.0, -1.0, 1.0), # v6
|
| 178 |
+
( 1.0, 1.0, 1.0), # v7
|
| 179 |
+
(-1.0, 1.0, 1.0), # v8
|
| 180 |
+
]
|
| 181 |
+
for v in verts:
|
| 182 |
+
f.write(f"v {v[0]} {v[1]} {v[2]}\n")
|
| 183 |
+
|
| 184 |
+
# Pour chaque face on va définir 4 UVs (vt) — on crée 4 vt par face (24 au total)
|
| 185 |
+
# uv order: (0,0),(1,0),(1,1),(0,1)
|
| 186 |
+
uv_coords = [(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)]
|
| 187 |
+
for _ in range(6):
|
| 188 |
+
for uv in uv_coords:
|
| 189 |
+
f.write(f"vt {uv[0]} {uv[1]}\n")
|
| 190 |
+
|
| 191 |
+
# Faces:
|
| 192 |
+
# We'll use materials in this order: right, left, top, bottom, front, back
|
| 193 |
+
# Note: vt indices are 1..24
|
| 194 |
+
# indices for convenience
|
| 195 |
+
# vertices indices: v1..v8 (1-based)
|
| 196 |
+
# define faces as quads (OBJ supports quads)
|
| 197 |
+
# right (x = +1): v2, v6, v7, v3
|
| 198 |
+
f.write("usemtl mat_right\n")
|
| 199 |
+
f.write("f 2/1 6/2 7/3 3/4\n")
|
| 200 |
+
# left (x = -1): v5, v1, v4, v8
|
| 201 |
+
f.write("usemtl mat_left\n")
|
| 202 |
+
f.write("f 5/5 1/6 4/7 8/8\n")
|
| 203 |
+
# top (y = +1): v4, v3, v7, v8
|
| 204 |
+
f.write("usemtl mat_top\n")
|
| 205 |
+
f.write("f 4/9 3/10 7/11 8/12\n")
|
| 206 |
+
# bottom (y = -1): v1, v5, v6, v2
|
| 207 |
+
f.write("usemtl mat_bottom\n")
|
| 208 |
+
f.write("f 1/13 5/14 6/15 2/16\n")
|
| 209 |
+
# front (z = +1): v5, v6, v7, v8
|
| 210 |
+
f.write("usemtl mat_front\n")
|
| 211 |
+
f.write("f 5/17 6/18 7/19 8/20\n")
|
| 212 |
+
# back (z = -1): v1, v2, v3, v4
|
| 213 |
+
f.write("usemtl mat_back\n")
|
| 214 |
+
f.write("f 1/21 2/22 3/23 4/24\n")
|
| 215 |
+
|
| 216 |
+
# return absolute path to .obj
|
| 217 |
+
return obj_path, out_dir
|
| 218 |
+
|
| 219 |
+
# -------------------------
|
| 220 |
+
# Fonction appelée par Gradio
|
| 221 |
+
# retourne : 4 miniatures (PIL) + chemin vers le .obj (str)
|
| 222 |
+
# -------------------------
|
| 223 |
+
def run(img1, img2):
|
| 224 |
try:
|
| 225 |
+
outputs = user_inference(img1, img2)
|
| 226 |
+
if not (isinstance(outputs, (list, tuple)) and len(outputs) >= 4):
|
| 227 |
+
raise ValueError("user_inference doit renvoyer une liste/tuple de 4 images.")
|
| 228 |
+
# Prendre les 4 premières images et convertir en PIL RGB
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
pil_imgs = []
|
| 230 |
+
for im in outputs[:4]:
|
| 231 |
if isinstance(im, np.ndarray):
|
| 232 |
im = Image.fromarray(im)
|
|
|
|
| 233 |
if im.mode != "RGB":
|
| 234 |
im = im.convert("RGB")
|
| 235 |
pil_imgs.append(im)
|
| 236 |
|
| 237 |
+
# miniatures affichées dans l'UI
|
| 238 |
+
thumbs = [im.copy().resize((256,256)) for im in pil_imgs]
|
| 239 |
+
|
| 240 |
+
# construire le .obj + textures
|
| 241 |
+
obj_path, tmpdir = build_textured_cube(pil_imgs)
|
| 242 |
+
|
| 243 |
+
# IMPORTANT: on renvoie le chemin absolu vers le fichier .obj
|
| 244 |
+
# Gradio/Spaces chargera le modèle 3D depuis ce fichier et les textures
|
| 245 |
+
# doivent être dans le même dossier que le .obj (ce qui est le cas).
|
| 246 |
+
return (*thumbs, obj_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
except Exception as e:
|
| 248 |
traceback.print_exc()
|
| 249 |
+
# en cas d'erreur, renvoyer 4 images grises et None pour le modèle
|
| 250 |
blank = Image.new("RGB", (256,256), (220,220,220))
|
| 251 |
+
return (blank, blank, blank, blank, None)
|
|
|
|
| 252 |
|
| 253 |
+
# -------------------------
|
| 254 |
# Interface Gradio
|
| 255 |
+
# -------------------------
|
| 256 |
+
with gr.Blocks(title="Cube 3D — coller 4 images sur les faces") as demo:
|
| 257 |
+
gr.Markdown("**Chargez 2 images** → votre code d'inference doit produire 4 images → les 4 images seront collées sur les faces latérales d'un cube (.obj).")
|
| 258 |
with gr.Row():
|
| 259 |
with gr.Column(scale=1):
|
| 260 |
+
inp1 = gr.Image(label="Image 1 (entrée)", type="pil")
|
| 261 |
+
inp2 = gr.Image(label="Image 2 (entrée)", type="pil")
|
|
|
|
| 262 |
run_btn = gr.Button("Lancer l'inference")
|
| 263 |
+
gr.Markdown("Remplace `user_inference` dans `app.py` par ta fonction d'inference réelle.")
|
| 264 |
with gr.Column(scale=2):
|
| 265 |
+
model3d_out = gr.Model3D(label="Cube 3D (obj + mtl + textures)")
|
|
|
|
| 266 |
with gr.Row():
|
| 267 |
+
out1 = gr.Image(label="Output 1 (right)")
|
| 268 |
+
out2 = gr.Image(label="Output 2 (left)")
|
| 269 |
+
out3 = gr.Image(label="Output 3 (front)")
|
| 270 |
+
out4 = gr.Image(label="Output 4 (back)")
|
| 271 |
|
| 272 |
+
run_btn.click(fn=run, inputs=[inp1, inp2], outputs=[out1, out2, out3, out4, model3d_out])
|
| 273 |
|
| 274 |
if __name__ == "__main__":
|
| 275 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
| 276 |
+
|
| 277 |
+
# def run(fibers: Image.Image, rings: Image.Image, num_steps: int):
|
| 278 |
+
# try:
|
| 279 |
+
# # appelle la fonction d'inference de l'utilisateur
|
| 280 |
+
# # outputs = inference(pipe, fibers, rings, int(num_steps))
|
| 281 |
+
# outputs = inference(fibers, rings, int(num_steps))
|
| 282 |
+
# if not (isinstance(outputs, (list,tuple)) and len(outputs) >= 4):
|
| 283 |
+
# raise ValueError("La fonction d'inference doit renvoyer une liste/tuple de 4 images.")
|
| 284 |
+
# # Prendre les 4 premières images
|
| 285 |
+
# imgs = outputs[:4]
|
| 286 |
+
# # Convertir en PIL si nécessaire et assurer taille raisonnable
|
| 287 |
+
# pil_imgs = []
|
| 288 |
+
# for im in imgs:
|
| 289 |
+
# if isinstance(im, np.ndarray):
|
| 290 |
+
# im = Image.fromarray(im)
|
| 291 |
+
# # for safety, ensure RGB
|
| 292 |
+
# if im.mode != "RGB":
|
| 293 |
+
# im = im.convert("RGB")
|
| 294 |
+
# pil_imgs.append(im)
|
| 295 |
+
|
| 296 |
+
# # Préparer miniatures (pour les 4 gr.Image)
|
| 297 |
+
# thumb_imgs = [im.copy().resize((256,256)) for im in pil_imgs]
|
| 298 |
+
|
| 299 |
+
# # Convertir en data-URIs pour Three.js
|
| 300 |
+
# data_uris = [pil_to_data_uri(im) for im in pil_imgs]
|
| 301 |
+
|
| 302 |
+
# # Construire le HTML/JS pour le visualiseur Three.j
|
| 303 |
+
# html = 0
|
| 304 |
+
# # Retourner : 4 miniatures pour affichage + HTML viewer
|
| 305 |
+
# return (*thumb_imgs, html)
|
| 306 |
+
# except Exception as e:
|
| 307 |
+
# traceback.print_exc()
|
| 308 |
+
# # en cas d'erreur, renvoyer 4 images vides et message html d'erreur
|
| 309 |
+
# blank = Image.new("RGB", (256,256), (220,220,220))
|
| 310 |
+
# err_html = f"<div style='color:#900;padding:12px;'>Erreur serveur: {str(e)}</div>"
|
| 311 |
+
# return (blank, blank, blank, blank, err_html)
|
| 312 |
+
|
| 313 |
+
# # Interface Gradio
|
| 314 |
+
# with gr.Blocks(title="3D Cube Viewer - 4 outputs on cube") as demo:
|
| 315 |
+
# gr.Markdown("### App 3D : assemble 4 images produites par votre code d'inference sur les 4 faces d'un cube.")
|
| 316 |
+
# with gr.Row():
|
| 317 |
+
# with gr.Column(scale=1):
|
| 318 |
+
# inp1 = gr.Image(label="Image 1 (entrée)", type="numpy")
|
| 319 |
+
# inp2 = gr.Image(label="Image 2 (entrée)", type="numpy")
|
| 320 |
+
# inp3 = gr.Number(value=10, label="Number of inference steps")
|
| 321 |
+
# run_btn = gr.Button("Lancer l'inference")
|
| 322 |
+
# gr.Markdown("Remplace `user_inference` dans `app.py` par ton code d'inférence existant.")
|
| 323 |
+
# with gr.Column(scale=2):
|
| 324 |
+
# viewer = gr.HTML("<div style='padding:12px;color:#666;'>Le rendu 3D s'affichera ici après inference.</div>", label="Visu 3D")
|
| 325 |
+
# # gr.Row().style(mobile_collapse=False)
|
| 326 |
+
# with gr.Row():
|
| 327 |
+
# out1 = gr.Image(label="Output 1")
|
| 328 |
+
# out2 = gr.Image(label="Output 2")
|
| 329 |
+
# out3 = gr.Image(label="Output 3")
|
| 330 |
+
# out4 = gr.Image(label="Output 4")
|
| 331 |
+
|
| 332 |
+
# run_btn.click(fn=run, inputs=[inp1, inp2, inp3], outputs=[out1, out2, out3, out4, viewer])
|
| 333 |
+
|
| 334 |
+
# if __name__ == "__main__":
|
| 335 |
+
# demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
| 336 |
|