Justin Means commited on
Commit
0d62c57
Β·
1 Parent(s): cfa6140

Add PLY download and interactive viewer for 3DGS

Browse files

Minimal additions to working Depth Anything 3 space:
- Add PLY file download button for Gaussian splats
- Add viewer section with SuperSplat link for WebXR
- Keep all existing functionality (video rendering, examples, etc.)
- WebXR ready for Quest and Vision Pro

README.md CHANGED
@@ -1,17 +1,25 @@
1
  ---
2
- title: Depth Anything 3
3
- emoji: 🏒
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  python_version: 3.11
10
- pinned: false
11
- license: cc-by-nc-4.0
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
15
 
16
  ## Citation
17
 
@@ -20,7 +28,7 @@ If you find Depth Anything 3 useful in your research or projects, please cite:
20
  ```bibtex
21
  @article{depthanything3,
22
  title={Depth Anything 3: Recovering the visual space from any views},
23
- author={Haotong Lin and Sili Chen and Jun Hao Liew and Donny Y. Chen and Zhenyu Li and Guang Shi and Jiashi Feng and Bingyi Kang},
24
  journal={arXiv preprint arXiv:2511.10647},
25
  year={2025}
26
  ```
 
1
  ---
2
+ title: Depth Anything 3 + Interactive 3DGS Viewer
3
+ emoji: 🌐
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  python_version: 3.11
10
+ pinned: true
11
+ license: apache-2.0
12
+ short_description: Interactive 3D Gaussian Splats with WebXR
13
  ---
14
 
15
+ # Depth Anything 3 + Interactive 3DGS Viewer
16
+
17
+ Generate depth maps and **interactive 3D Gaussian Splats** from images/video. Features PLY download and **WebXR support** for Vision Pro and Quest via SuperSplat.
18
+
19
+ ## Features
20
+ - Full Depth Anything 3 reconstruction
21
+ - Download PLY files for SuperSplat, Blender, etc.
22
+ - WebXR ready for VR/AR viewing
23
 
24
  ## Citation
25
 
 
28
  ```bibtex
29
  @article{depthanything3,
30
  title={Depth Anything 3: Recovering the visual space from any views},
31
+ author={Haotong Lin and Sili Chen and Jun Hao Liew and Donny Y. Chen and Zhenyu Li and Guang Shi and Jiashi Feng and Bingyi Kang},
32
  journal={arXiv preprint arXiv:2511.10647},
33
  year={2025}
34
  ```
depth_anything_3/app/gradio_app.py CHANGED
@@ -223,7 +223,7 @@ class DepthAnything3App:
223
  ) = self.ui_components.create_measure_section()
224
 
225
  with gr.Tab("3DGS Rendered Novel Views"):
226
- gs_video, gs_info = self.ui_components.create_nvs_video()
227
 
228
  # Inference control section (before inference)
229
  (process_res_method_dropdown, infer_gs) = (
@@ -250,9 +250,11 @@ class DepthAnything3App:
250
  gr.update(visible=checked),
251
  gr.update(visible=checked),
252
  gr.update(visible=(not checked)),
 
 
253
  ),
254
  inputs=infer_gs,
255
- outputs=[gs_trj_mode, gs_video_quality, gs_video, gs_info],
256
  )
257
 
258
  # Example scenes section
@@ -296,6 +298,8 @@ class DepthAnything3App:
296
  scene_components,
297
  gs_video,
298
  gs_info,
 
 
299
  gs_trj_mode,
300
  gs_video_quality,
301
  )
@@ -340,6 +344,8 @@ class DepthAnything3App:
340
  scene_components: List[gr.Image],
341
  gs_video: gr.Video,
342
  gs_info: gr.Markdown,
 
 
343
  gs_trj_mode: gr.Dropdown,
344
  gs_video_quality: gr.Dropdown,
345
  ) -> None:
@@ -393,6 +399,8 @@ class DepthAnything3App:
393
  gs_video,
394
  gs_video, # gs_video visibility
395
  gs_info, # gs_info visibility
 
 
396
  ],
397
  ).then(
398
  fn=lambda: "False",
@@ -476,6 +484,8 @@ class DepthAnything3App:
476
  measure_depth_image,
477
  gs_video,
478
  gs_info,
 
 
479
  )
480
 
481
  def _setup_visualization_handlers(
@@ -573,6 +583,8 @@ class DepthAnything3App:
573
  measure_depth_image: gr.Image,
574
  gs_video: gr.Video,
575
  gs_info: gr.Markdown,
 
 
576
  ) -> None:
577
  """Set up example scene handlers."""
578
 
@@ -624,6 +636,8 @@ class DepthAnything3App:
624
  gs_video,
625
  gs_video, # gs_video_visibility
626
  gs_info, # gs_info_visibility
 
 
627
  is_example,
628
  measure_image,
629
  measure_depth_image,
 
223
  ) = self.ui_components.create_measure_section()
224
 
225
  with gr.Tab("3DGS Rendered Novel Views"):
226
+ gs_video, gs_info, gs_viewer, gs_ply_download = self.ui_components.create_nvs_video()
227
 
228
  # Inference control section (before inference)
229
  (process_res_method_dropdown, infer_gs) = (
 
250
  gr.update(visible=checked),
251
  gr.update(visible=checked),
252
  gr.update(visible=(not checked)),
253
+ gr.update(visible=False), # gs_viewer hidden initially
254
+ gr.update(visible=False), # gs_ply_download hidden initially
255
  ),
256
  inputs=infer_gs,
257
+ outputs=[gs_trj_mode, gs_video_quality, gs_video, gs_info, gs_viewer, gs_ply_download],
258
  )
259
 
260
  # Example scenes section
 
298
  scene_components,
299
  gs_video,
300
  gs_info,
301
+ gs_viewer,
302
+ gs_ply_download,
303
  gs_trj_mode,
304
  gs_video_quality,
305
  )
 
344
  scene_components: List[gr.Image],
345
  gs_video: gr.Video,
346
  gs_info: gr.Markdown,
347
+ gs_viewer: gr.HTML,
348
+ gs_ply_download: gr.File,
349
  gs_trj_mode: gr.Dropdown,
350
  gs_video_quality: gr.Dropdown,
351
  ) -> None:
 
399
  gs_video,
400
  gs_video, # gs_video visibility
401
  gs_info, # gs_info visibility
402
+ gs_viewer, # interactive viewer HTML
403
+ gs_ply_download, # PLY download file
404
  ],
405
  ).then(
406
  fn=lambda: "False",
 
484
  measure_depth_image,
485
  gs_video,
486
  gs_info,
487
+ gs_viewer,
488
+ gs_ply_download,
489
  )
490
 
491
  def _setup_visualization_handlers(
 
583
  measure_depth_image: gr.Image,
584
  gs_video: gr.Video,
585
  gs_info: gr.Markdown,
586
+ gs_viewer: gr.HTML,
587
+ gs_ply_download: gr.File,
588
  ) -> None:
589
  """Set up example scene handlers."""
590
 
 
636
  gs_video,
637
  gs_video, # gs_video_visibility
638
  gs_info, # gs_info_visibility
639
+ gs_viewer, # gs_viewer HTML
640
+ gs_ply_download, # gs_ply file
641
  is_example,
642
  measure_image,
643
  measure_depth_image,
depth_anything_3/app/modules/event_handlers.py CHANGED
@@ -156,6 +156,8 @@ class EventHandlers:
156
  Optional[str], # gs video path
157
  gr.update, # gs video visibility update
158
  gr.update, # gs info visibility update
 
 
159
  ]:
160
  """
161
  Perform reconstruction using the already-created target_dir/images.
@@ -184,6 +186,8 @@ class EventHandlers:
184
  None,
185
  gr.update(visible=False), # gs_video
186
  gr.update(visible=True), # gs_info
 
 
187
  )
188
 
189
  start_time = time.time()
@@ -226,10 +230,14 @@ class EventHandlers:
226
  # The GLB file is already generated by the API
227
  glbfile = os.path.join(target_dir, "scene.glb")
228
 
229
- # Handle 3DGS video based on infer_gs flag
230
  gsvideo_path = None
231
  gs_video_visible = False
232
  gs_info_visible = True
 
 
 
 
233
 
234
  if infer_gs:
235
  try:
@@ -240,6 +248,26 @@ class EventHandlers:
240
  gsvideo_path = None
241
  print("3DGS video not found, but infer_gs was enabled")
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # Cleanup
244
  cleanup_memory()
245
 
@@ -268,6 +296,8 @@ class EventHandlers:
268
  gsvideo_path,
269
  gr.update(visible=gs_video_visible), # gs_video visibility
270
  gr.update(visible=gs_info_visible), # gs_info visibility
 
 
271
  )
272
 
273
  def update_visualization(
@@ -367,6 +397,8 @@ class EventHandlers:
367
  Optional[str],
368
  gr.update,
369
  gr.update,
 
 
370
  ]:
371
  """
372
  Load a scene from examples directory.
@@ -376,7 +408,7 @@ class EventHandlers:
376
  examples_dir: Path to examples directory (if None, uses workspace_dir/examples)
377
 
378
  Returns:
379
- Tuple of (reconstruction_output, target_dir, image_paths, log_message, processed_data, measure_view_selector, gs_video, gs_video_vis, gs_info_vis) # noqa: E501
380
  """
381
  if examples_dir is None:
382
  # Get workspace directory from environment variable
@@ -393,6 +425,10 @@ class EventHandlers:
393
  gs_video_path = None
394
  gs_video_visible = False
395
  gs_info_visible = True
 
 
 
 
396
 
397
  if target_dir and target_dir != "None":
398
  predictions_path = os.path.join(target_dir, "predictions.npz")
@@ -444,6 +480,32 @@ class EventHandlers:
444
  except Exception as e:
445
  print(f"Error loading cached 3DGS video: {e}")
446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  return (
448
  reconstruction_output,
449
  target_dir,
@@ -454,6 +516,8 @@ class EventHandlers:
454
  gs_video_path,
455
  gr.update(visible=gs_video_visible),
456
  gr.update(visible=gs_info_visible),
 
 
457
  )
458
 
459
  def navigate_depth_view(
 
156
  Optional[str], # gs video path
157
  gr.update, # gs video visibility update
158
  gr.update, # gs info visibility update
159
+ gr.update, # gs_viewer HTML update
160
+ gr.update, # gs_ply_download file update
161
  ]:
162
  """
163
  Perform reconstruction using the already-created target_dir/images.
 
186
  None,
187
  gr.update(visible=False), # gs_video
188
  gr.update(visible=True), # gs_info
189
+ gr.update(visible=False), # gs_viewer
190
+ gr.update(visible=False), # gs_ply_download
191
  )
192
 
193
  start_time = time.time()
 
230
  # The GLB file is already generated by the API
231
  glbfile = os.path.join(target_dir, "scene.glb")
232
 
233
+ # Handle 3DGS video and PLY based on infer_gs flag
234
  gsvideo_path = None
235
  gs_video_visible = False
236
  gs_info_visible = True
237
+ gs_ply_path = None
238
+ gs_ply_visible = False
239
+ gs_viewer_visible = False
240
+ gs_viewer_html = ""
241
 
242
  if infer_gs:
243
  try:
 
248
  gsvideo_path = None
249
  print("3DGS video not found, but infer_gs was enabled")
250
 
251
+ # Check for PLY file
252
+ try:
253
+ gs_ply_path = sorted(glob(os.path.join(target_dir, "gs_ply", "*.ply")))[-1]
254
+ gs_ply_visible = True
255
+ gs_viewer_visible = True
256
+ gs_viewer_html = """
257
+ <div style="width:100%; padding:20px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
258
+ border-radius:12px; margin-top:10px; text-align:center;">
259
+ <h3 style="color:#fff; margin-bottom:15px;">πŸ₯½ Interactive 3D Gaussian Splat Viewer</h3>
260
+ <p style="color:#aaa; margin-bottom:15px;">
261
+ Download the PLY file below and open it in
262
+ <a href="https://playcanvas.com/supersplat/editor" target="_blank" style="color:#4da6ff;">SuperSplat Editor</a>
263
+ for an interactive WebXR experience on Quest or Vision Pro!
264
+ </p>
265
+ </div>
266
+ """
267
+ except IndexError:
268
+ gs_ply_path = None
269
+ print("3DGS PLY file not found")
270
+
271
  # Cleanup
272
  cleanup_memory()
273
 
 
296
  gsvideo_path,
297
  gr.update(visible=gs_video_visible), # gs_video visibility
298
  gr.update(visible=gs_info_visible), # gs_info visibility
299
+ gr.update(value=gs_viewer_html, visible=gs_viewer_visible), # gs_viewer
300
+ gr.update(value=gs_ply_path, visible=gs_ply_visible), # gs_ply_download
301
  )
302
 
303
  def update_visualization(
 
397
  Optional[str],
398
  gr.update,
399
  gr.update,
400
+ gr.update, # gs_viewer HTML
401
+ gr.update, # gs_ply_download file
402
  ]:
403
  """
404
  Load a scene from examples directory.
 
408
  examples_dir: Path to examples directory (if None, uses workspace_dir/examples)
409
 
410
  Returns:
411
+ Tuple of (reconstruction_output, target_dir, image_paths, log_message, processed_data, measure_view_selector, gs_video, gs_video_vis, gs_info_vis, gs_viewer, gs_ply) # noqa: E501
412
  """
413
  if examples_dir is None:
414
  # Get workspace directory from environment variable
 
425
  gs_video_path = None
426
  gs_video_visible = False
427
  gs_info_visible = True
428
+ gs_ply_path = None
429
+ gs_ply_visible = False
430
+ gs_viewer_visible = False
431
+ gs_viewer_html = ""
432
 
433
  if target_dir and target_dir != "None":
434
  predictions_path = os.path.join(target_dir, "predictions.npz")
 
480
  except Exception as e:
481
  print(f"Error loading cached 3DGS video: {e}")
482
 
483
+ # Check for cached 3DGS PLY file
484
+ gs_ply_dir = os.path.join(target_dir, "gs_ply")
485
+ if os.path.exists(gs_ply_dir):
486
+ try:
487
+ from glob import glob
488
+
489
+ gs_plys = sorted(glob(os.path.join(gs_ply_dir, "*.ply")))
490
+ if gs_plys:
491
+ gs_ply_path = gs_plys[-1]
492
+ gs_ply_visible = True
493
+ gs_viewer_visible = True
494
+ gs_viewer_html = """
495
+ <div style="width:100%; padding:20px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
496
+ border-radius:12px; margin-top:10px; text-align:center;">
497
+ <h3 style="color:#fff; margin-bottom:15px;">πŸ₯½ Interactive 3D Gaussian Splat Viewer</h3>
498
+ <p style="color:#aaa; margin-bottom:15px;">
499
+ Download the PLY file below and open it in
500
+ <a href="https://playcanvas.com/supersplat/editor" target="_blank" style="color:#4da6ff;">SuperSplat Editor</a>
501
+ for an interactive WebXR experience on Quest or Vision Pro!
502
+ </p>
503
+ </div>
504
+ """
505
+ print(f"Loaded cached 3DGS PLY: {gs_ply_path}")
506
+ except Exception as e:
507
+ print(f"Error loading cached 3DGS PLY: {e}")
508
+
509
  return (
510
  reconstruction_output,
511
  target_dir,
 
516
  gs_video_path,
517
  gr.update(visible=gs_video_visible),
518
  gr.update(visible=gs_info_visible),
519
+ gr.update(value=gs_viewer_html, visible=gs_viewer_visible),
520
+ gr.update(value=gs_ply_path, visible=gs_ply_visible),
521
  )
522
 
523
  def navigate_depth_view(
depth_anything_3/app/modules/ui_components.py CHANGED
@@ -83,12 +83,12 @@ class UIComponents:
83
  elem_id="reconstruction_3d_viewer",
84
  )
85
 
86
- def create_nvs_video(self) -> Tuple[gr.Video, gr.Markdown]:
87
  """
88
- Create the 3DGS rendered video display component and info message.
89
 
90
  Returns:
91
- Tuple of (video component, info message component)
92
  """
93
  with gr.Column():
94
  gs_info = gr.Markdown(
@@ -99,7 +99,8 @@ class UIComponents:
99
  "Next, in **Visualization Options**, "
100
  "*optionally* configure the **rendering trajectory** (default: smooth) "
101
  "and **video quality** (default: low), "
102
- "then click **Reconstruct**."
 
103
  ),
104
  visible=True,
105
  height=520,
@@ -110,7 +111,40 @@ class UIComponents:
110
  interactive=False,
111
  visible=False,
112
  )
113
- return gs_video, gs_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  def create_depth_section(self) -> Tuple[gr.Button, gr.Dropdown, gr.Button, gr.Image]:
116
  """
 
83
  elem_id="reconstruction_3d_viewer",
84
  )
85
 
86
+ def create_nvs_video(self) -> Tuple[gr.Video, gr.Markdown, gr.HTML, gr.File]:
87
  """
88
+ Create the 3DGS rendered video display component, info message, interactive viewer, and PLY download.
89
 
90
  Returns:
91
+ Tuple of (video component, info message, interactive viewer HTML, PLY download)
92
  """
93
  with gr.Column():
94
  gs_info = gr.Markdown(
 
99
  "Next, in **Visualization Options**, "
100
  "*optionally* configure the **rendering trajectory** (default: smooth) "
101
  "and **video quality** (default: low), "
102
+ "then click **Reconstruct**.<br><br>"
103
+ "πŸ₯½ **WebXR Ready** - After reconstruction, use the interactive viewer below for VR on Quest or Vision Pro!"
104
  ),
105
  visible=True,
106
  height=520,
 
111
  interactive=False,
112
  visible=False,
113
  )
114
+ gs_viewer = gr.HTML(
115
+ value=self._get_empty_viewer_html(),
116
+ visible=False,
117
+ label="Interactive 3DGS Viewer",
118
+ )
119
+ gs_ply_download = gr.File(
120
+ label="Download Gaussian Splat (.ply) - Use in SuperSplat, Blender, etc.",
121
+ visible=False,
122
+ interactive=False,
123
+ )
124
+ return gs_video, gs_info, gs_viewer, gs_ply_download
125
+
126
+ def _get_empty_viewer_html(self) -> str:
127
+ """Return placeholder HTML for empty viewer state."""
128
+ return """
129
+ <div style="width:100%; height:400px; display:flex; align-items:center; justify-content:center;
130
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius:12px; margin-top:10px;">
131
+ <p style="color:#888; font-size:1.1em;">Interactive 3DGS Viewer will appear here after reconstruction</p>
132
+ </div>
133
+ """
134
+
135
+ def get_viewer_html_with_instructions(self) -> str:
136
+ """Return HTML with instructions for using SuperSplat viewer."""
137
+ return """
138
+ <div style="width:100%; padding:20px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
139
+ border-radius:12px; margin-top:10px; text-align:center;">
140
+ <h3 style="color:#fff; margin-bottom:15px;">πŸ₯½ Interactive 3D Gaussian Splat Viewer</h3>
141
+ <p style="color:#aaa; margin-bottom:15px;">
142
+ Download the PLY file below and open it in
143
+ <a href="https://playcanvas.com/supersplat/editor" target="_blank" style="color:#4da6ff;">SuperSplat Editor</a>
144
+ for an interactive WebXR experience on Quest or Vision Pro!
145
+ </p>
146
+ </div>
147
+ """
148
 
149
  def create_depth_section(self) -> Tuple[gr.Button, gr.Dropdown, gr.Button, gr.Image]:
150
  """