murphylmf commited on
Commit
dce63f8
·
1 Parent(s): 6b09604
Files changed (2) hide show
  1. app.py +292 -57
  2. requirements.txt +8 -7
app.py CHANGED
@@ -119,86 +119,321 @@ def download_smpl_assets(body_models_path):
119
  def pack_sequence_to_glb(base_dir, output_path, start_frame, end_frame, scene_rate=1.0):
120
  """
121
  Pack a sequence of meshes/pointclouds into a single GLB file for visualization.
 
 
122
  """
123
  # Create a scene
124
  scene = trimesh.Scene()
125
 
126
- # Iterate over frames
 
 
 
 
 
 
 
 
 
127
  for i in range(start_frame, end_frame):
128
- # Load Human Mesh
129
  human_mesh_path = os.path.join(base_dir, f"smpl_{i:06d}.ply")
130
  if os.path.exists(human_mesh_path):
131
  human_mesh = trimesh.load(human_mesh_path)
132
- # Add to scene with time-based visibility if possible,
133
- # but GLB animation is complex.
134
- # Simplified approach: Merge all into one static scene for now,
135
- # or just one frame.
136
- #
137
- # Better approach for "Video" visualization in web:
138
- # We can't easily make a 4D GLB in pure python without complex animation rigging.
139
- #
140
- # Alternative: Just show the first frame, or a merged static scene.
141
- # The prompt implies a 3D result viewing.
142
- #
143
- # Let's merge all 'scene' points (static) and 'human' meshes (dynamic).
144
- # But showing all human meshes at once looks messy (motion trail).
145
 
146
- # Strategy:
147
- # 1. Add Scene Point Cloud (once, it's static-ish or accumulated)
148
- # 2. Add Human Mesh from the middle frame or first frame?
149
 
150
- pass
151
-
152
- # For the purpose of this demo app, let's load the accumulated scene and one human mesh
153
- # or just the accumulated scene if available.
154
-
155
- # Load Accumulated Scene
156
- scene_ply = os.path.join(os.path.dirname(output_path), f"{os.path.basename(base_dir)}_scene.ply")
157
- if os.path.exists(scene_ply):
158
- scene_pc = trimesh.load(scene_ply)
159
- scene.add_geometry(scene_pc)
160
-
161
- # Load one human mesh (e.g. middle frame)
162
- mid_frame = (start_frame + end_frame) // 2
163
- human_mesh_path = os.path.join(base_dir, f"smpl_{mid_frame:06d}.ply")
164
- if os.path.exists(human_mesh_path):
165
- human_mesh = trimesh.load(human_mesh_path)
166
- human_mesh.visual.vertex_colors = [200, 100, 100, 255] # Reddish
167
- scene.add_geometry(human_mesh)
168
 
169
  scene.export(output_path)
170
 
171
 
172
  def get_player_html(glb_path):
173
  """
174
- Generate HTML to display the GLB file using model-viewer.
 
175
  """
176
- # We need to serve the file. Gradio handles file paths in output components.
177
- # So we return the path to the GLB file, but the output component is HTML.
178
- # To display 3D in HTML, we can use <model-viewer>.
179
- # However, Gradio's Model3D component is easier.
180
- # Let's switch the output to Model3D if possible?
181
- # The user code had `output_html = gr.HTML(...)`.
182
-
183
- # If we stick to HTML:
184
- # We need to base64 encode the GLB or assume it's accessible.
185
- # Gradio files are accessible.
186
-
187
  import base64
188
  with open(glb_path, "rb") as f:
189
  data = f.read()
190
  b64_data = base64.b64encode(data).decode('utf-8')
191
 
192
  html = f"""
193
- <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>
194
- <model-viewer
195
- src="data:model/gltf-binary;base64,{b64_data}"
196
- camera-controls
197
- auto-rotate
198
- shadow-intensity="1"
199
- style="width: 100%; height: 600px;"
200
- >
201
- </model-viewer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  """
203
  return html
204
 
 
119
  def pack_sequence_to_glb(base_dir, output_path, start_frame, end_frame, scene_rate=1.0):
120
  """
121
  Pack a sequence of meshes/pointclouds into a single GLB file for visualization.
122
+ Uses a specific naming convention 'frame_{i}' for each mesh to allow
123
+ 'stop motion' style visualization in the frontend.
124
  """
125
  # Create a scene
126
  scene = trimesh.Scene()
127
 
128
+ # 1. Add Accumulated Scene Point Cloud (Static Background)
129
+ # The frontend looks for 'scene' in the name to adjust point size.
130
+ scene_ply = os.path.join(os.path.dirname(output_path), f"{os.path.basename(base_dir)}_scene.ply")
131
+ if os.path.exists(scene_ply):
132
+ scene_pc = trimesh.load(scene_ply)
133
+ # Add to scene
134
+ scene.add_geometry(scene_pc, node_name='scene_cloud', geom_name='scene_cloud')
135
+
136
+ # 2. Iterate over frames and add Human Meshes
137
+ # The frontend looks for 'frame_{i}' to toggle visibility.
138
  for i in range(start_frame, end_frame):
 
139
  human_mesh_path = os.path.join(base_dir, f"smpl_{i:06d}.ply")
140
  if os.path.exists(human_mesh_path):
141
  human_mesh = trimesh.load(human_mesh_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ # Color it if needed (default logic seemed to want colors)
144
+ # But usually ply has colors. If not, maybe set a default.
145
+ # user previous logic: human_mesh.visual.vertex_colors = [200, 100, 100, 255]
146
 
147
+ # Naming is CRITICAL for the frontend logic
148
+ node_name = f"frame_{i}"
149
+ scene.add_geometry(human_mesh, node_name=node_name, geom_name=node_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  scene.export(output_path)
152
 
153
 
154
  def get_player_html(glb_path):
155
  """
156
+ Generate HTML to display the GLB file using Three.js with 'Stop Motion' logic.
157
+ Extracts logic from index.html provided by user.
158
  """
 
 
 
 
 
 
 
 
 
 
 
159
  import base64
160
  with open(glb_path, "rb") as f:
161
  data = f.read()
162
  b64_data = base64.b64encode(data).decode('utf-8')
163
 
164
  html = f"""
165
+ <!DOCTYPE html>
166
+ <html>
167
+ <head>
168
+ <meta charset="utf-8">
169
+ <style>
170
+ #canvas-container {{
171
+ width: 100%;
172
+ height: 600px; /* Increased height */
173
+ background: #f5f5f5;
174
+ border-radius: 8px;
175
+ position: relative;
176
+ overflow: hidden;
177
+ box-shadow: inset 0 0 20px rgba(0,0,0,0.05);
178
+ }}
179
+
180
+ #loading-overlay {{
181
+ position: absolute; top:0; left:0; width:100%; height:100%;
182
+ background: rgba(0,0,0,0.7); color: white;
183
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
184
+ z-index: 10;
185
+ }}
186
+
187
+ .player-controls {{
188
+ margin-top: 10px;
189
+ padding: 10px;
190
+ background: #fff;
191
+ border-radius: 8px;
192
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 15px;
196
+ }}
197
+
198
+ .button {{
199
+ padding: 8px 16px;
200
+ border: none;
201
+ border-radius: 4px;
202
+ background: #363636;
203
+ color: white;
204
+ cursor: pointer;
205
+ font-weight: bold;
206
+ }}
207
+ .button:hover {{ background: #4a4a4a; }}
208
+
209
+ input[type=range] {{
210
+ flex-grow: 1;
211
+ cursor: pointer;
212
+ }}
213
+
214
+ .tag {{
215
+ background: #f5f5f5;
216
+ padding: 5px 10px;
217
+ border-radius: 4px;
218
+ font-family: monospace;
219
+ white-space: nowrap;
220
+ }}
221
+ </style>
222
+
223
+ <!-- Import Map for Three.js -->
224
+ <script type="importmap">
225
+ {{
226
+ "imports": {{
227
+ "three": "https://unpkg.com/three@0.158.0/build/three.module.js",
228
+ "three/addons/": "https://unpkg.com/three@0.158.0/examples/jsm/"
229
+ }}
230
+ }}
231
+ </script>
232
+ </head>
233
+ <body>
234
+ <div id="player-wrapper">
235
+ <div id="canvas-container">
236
+ <div id="loading-overlay">
237
+ <p>Loading 3D Sequence...</p>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="player-controls">
242
+ <button id="play-btn" class="button">Play</button>
243
+ <input id="frame-slider" type="range" min="0" max="0" value="0" step="1">
244
+ <span id="frame-count" class="tag">Frame: 0</span>
245
+ </div>
246
+ </div>
247
+
248
+ <script type="module">
249
+ import * as THREE from 'three';
250
+ import {{ OrbitControls }} from 'three/addons/controls/OrbitControls.js';
251
+ import {{ GLTFLoader }} from 'three/addons/loaders/GLTFLoader.js';
252
+
253
+ // Config
254
+ const MODEL_DATA = "data:model/gltf-binary;base64,{b64_data}";
255
+ const FPS = 10;
256
+
257
+ let scene, camera, renderer, controls;
258
+ let frames = [];
259
+ let currentFrame = 0;
260
+ let isPlaying = false;
261
+ let intervalId = null;
262
+
263
+ const container = document.getElementById('canvas-container');
264
+ const slider = document.getElementById('frame-slider');
265
+ const playBtn = document.getElementById('play-btn');
266
+ const frameLabel = document.getElementById('frame-count');
267
+ const loadingOverlay = document.getElementById('loading-overlay');
268
+
269
+ init();
270
+
271
+ function init() {{
272
+ scene = new THREE.Scene();
273
+ scene.background = new THREE.Color(0xf5f5f5);
274
+
275
+ // Setup Camera
276
+ camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 1000);
277
+ // Initial position - will be roughly looking at origin
278
+ camera.position.set(0, 0, 5);
279
+
280
+ renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: true }});
281
+ renderer.setSize(container.clientWidth, container.clientHeight);
282
+ renderer.setPixelRatio(window.devicePixelRatio);
283
+ renderer.useLegacyLights = false;
284
+
285
+ container.appendChild(renderer.domElement);
286
+
287
+ // Lights
288
+ const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 3.0);
289
+ scene.add(hemiLight);
290
+
291
+ const dirLight = new THREE.DirectionalLight(0xffffff, 3.0);
292
+ dirLight.position.set(5, 10, 7);
293
+ scene.add(dirLight);
294
+
295
+ const frontLight = new THREE.DirectionalLight(0xffffff, 2.0);
296
+ frontLight.position.set(0, 0, 5);
297
+ scene.add(frontLight);
298
+
299
+ // Controls
300
+ controls = new OrbitControls(camera, renderer.domElement);
301
+ controls.enableDamping = true;
302
+ controls.dampingFactor = 0.05;
303
+ controls.target.set(0, 0, 0);
304
+
305
+ // Load GLB
306
+ const loader = new GLTFLoader();
307
+
308
+ loader.load(MODEL_DATA, function (gltf) {{
309
+ const root = gltf.scene;
310
+ scene.add(root);
311
+
312
+ frames = [];
313
+
314
+ // Traverse to find frames and fix materials
315
+ root.traverse((node) => {{
316
+
317
+ // Material adjustments for visual appeal
318
+ if (node.isMesh) {{
319
+ node.geometry.computeVertexNormals();
320
+ // Reset colors if needed to avoid black meshes
321
+ if (node.geometry.attributes.color) {{
322
+ // Optionally keep vertex colors if they are good
323
+ // node.geometry.deleteAttribute('color');
324
+ }}
325
+
326
+ // Simple material override for consistency
327
+ node.material = new THREE.MeshStandardMaterial({{
328
+ color: 0xff9966,
329
+ roughness: 0.4,
330
+ metalness: 0.0,
331
+ side: THREE.DoubleSide
332
+ }});
333
+ }}
334
+
335
+ // Point cloud adjustments
336
+ if (node.isPoints) {{
337
+ if (node.name && node.name.toLowerCase().includes('scene')) {{
338
+ node.material.size = 0.05;
339
+ node.material.sizeAttenuation = true;
340
+ }}
341
+ if (node.name && node.name.toLowerCase().includes('human')) {{
342
+ node.material.size = 0.005;
343
+ }}
344
+ }}
345
+
346
+ // Collect Frames based on naming convention
347
+ if (node.name && node.name.startsWith('frame_')) {{
348
+ const parts = node.name.split('_');
349
+ // Expect frame_0, frame_1, etc.
350
+ if (parts.length >= 2 && !isNaN(parseInt(parts[1]))) {{
351
+ const idx = parseInt(parts[1]);
352
+ frames[idx] = node;
353
+ node.visible = false;
354
+ }}
355
+ }}
356
+ }});
357
+
358
+ // Filter out undefined slots in case of non-sequential or gaps (though logic should be sequential)
359
+ // Note: If using frames[idx], gaps might leave undefined.
360
+ // Better to compact if we just want a sequence, but index matching is good.
361
+ // Let's rely on filter to clean up.
362
+ frames = frames.filter(n => n !== undefined);
363
+
364
+ console.log(`Loaded ${{frames.length}} frames.`);
365
+
366
+ if (frames.length > 0) {{
367
+ slider.max = frames.length - 1;
368
+ loadingOverlay.style.display = 'none';
369
+ showFrame(0);
370
+
371
+ // Auto-center camera on the first frame if possible
372
+ // const box = new THREE.Box3().setFromObject(frames[0]);
373
+ // const center = box.getCenter(new THREE.Vector3());
374
+ // controls.target.copy(center);
375
+ // camera.position.add(center); // Adjust relative to new center
376
+
377
+ }} else {{
378
+ loadingOverlay.innerHTML = "<p>No frames found (looking for nodes named 'frame_N').</p>";
379
+ }}
380
+
381
+ }}, undefined, function (error) {{
382
+ console.error(error);
383
+ loadingOverlay.innerHTML = "<p>Error loading model.</p>";
384
+ }});
385
+
386
+ window.addEventListener('resize', onWindowResize);
387
+ animate();
388
+ }}
389
+
390
+ function showFrame(idx) {{
391
+ if (!frames[idx]) return;
392
+ if (frames[currentFrame]) frames[currentFrame].visible = false;
393
+ frames[idx].visible = true;
394
+ currentFrame = idx;
395
+ slider.value = idx;
396
+ frameLabel.innerText = `Frame: ${{idx}}`;
397
+ }}
398
+
399
+ function togglePlay() {{
400
+ if (frames.length === 0) return;
401
+ isPlaying = !isPlaying;
402
+
403
+ playBtn.innerText = isPlaying ? "Pause" : "Play";
404
+
405
+ if (isPlaying) {{
406
+ intervalId = setInterval(() => {{
407
+ let next = currentFrame + 1;
408
+ if (next >= frames.length) next = 0;
409
+ showFrame(next);
410
+ }}, 1000 / FPS);
411
+ }} else {{
412
+ clearInterval(intervalId);
413
+ }}
414
+ }}
415
+
416
+ slider.addEventListener('input', (e) => {{
417
+ if (isPlaying) togglePlay();
418
+ showFrame(parseInt(e.target.value));
419
+ }});
420
+
421
+ playBtn.addEventListener('click', togglePlay);
422
+
423
+ function onWindowResize() {{
424
+ camera.aspect = container.clientWidth / container.clientHeight;
425
+ camera.updateProjectionMatrix();
426
+ renderer.setSize(container.clientWidth, container.clientHeight);
427
+ }}
428
+
429
+ function animate() {{
430
+ requestAnimationFrame(animate);
431
+ controls.update();
432
+ renderer.render(scene, camera);
433
+ }}
434
+ </script>
435
+ </body>
436
+ </html>
437
  """
438
  return html
439
 
requirements.txt CHANGED
@@ -3,19 +3,20 @@ torchvision==0.19.1
3
  numpy
4
  scipy
5
  trimesh
6
- tqdm
 
7
  opencv-python-headless
8
  pillow
9
- gradio
10
- spaces
11
- ninja
12
  einops
13
  safetensors
14
- huggingface_hub<0.25.0
15
- open3d==0.19.0
 
 
 
16
  ultralytics==8.3.227
17
  timm==1.0.24
18
  git+https://github.com/EasternJournalist/utils3d.git@3fab839f0be9931dac7c8488eb0e1600c236e183
19
  mmcv==2.2.0 --find-links https://download.openmmlab.com/mmcv/dist/cu121/torch2.4/index.html
20
  pytorch3d @ https://dl.fbaipublicfiles.com/pytorch3d/packaging/wheels/py310_cu121_pyt241/pytorch3d-0.7.8-cp310-cp310-linux_x86_64.whl
21
- smplx
 
3
  numpy
4
  scipy
5
  trimesh
6
+ plyfile
7
+ open3d==0.19.0
8
  opencv-python-headless
9
  pillow
 
 
 
10
  einops
11
  safetensors
12
+ gradio>=5.0.0
13
+ spaces
14
+ huggingface_hub>=0.25.0
15
+ tqdm
16
+ ninja
17
  ultralytics==8.3.227
18
  timm==1.0.24
19
  git+https://github.com/EasternJournalist/utils3d.git@3fab839f0be9931dac7c8488eb0e1600c236e183
20
  mmcv==2.2.0 --find-links https://download.openmmlab.com/mmcv/dist/cu121/torch2.4/index.html
21
  pytorch3d @ https://dl.fbaipublicfiles.com/pytorch3d/packaging/wheels/py310_cu121_pyt241/pytorch3d-0.7.8-cp310-cp310-linux_x86_64.whl
22
+ smplx