PLUS-WAVE commited on
Commit
41ff959
·
verified ·
1 Parent(s): dc22079

Deploy InfiniSplat ZeroGPU demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +9 -0
  2. .gitignore +42 -0
  3. INSTALL.md +72 -0
  4. README.md +137 -6
  5. app.py +28 -0
  6. assets/github_demo.gif +3 -0
  7. config/experiment/infinisplat_hypersim_lidar.yaml +5 -0
  8. config/experiment/infinisplat_hypersim_rgb.yaml +5 -0
  9. config/inference.yaml +4 -0
  10. config/model/decoder/gsplat.yaml +10 -0
  11. config/model/encoder/infinisplat.yaml +28 -0
  12. config/model/encoder/infinisplat_infinidepth.yaml +28 -0
  13. config/viewer_settings.json +49 -0
  14. docs/inference.md +154 -0
  15. examples/data/lidar_demo/eth3d_kicker.npz +3 -0
  16. examples/data/lidar_demo/eth3d_kicker.png +3 -0
  17. examples/data/lidar_demo/eth3d_pipes.npz +3 -0
  18. examples/data/lidar_demo/eth3d_pipes.png +3 -0
  19. examples/data/lidar_demo/waymo_147.npz +3 -0
  20. examples/data/lidar_demo/waymo_147.png +3 -0
  21. examples/data/lidar_demo/waymo_9.npz +3 -0
  22. examples/data/lidar_demo/waymo_9.png +3 -0
  23. examples/data/rgb_demo/eth3d_courtyard.png +3 -0
  24. examples/data/rgb_demo/maksim-shutov-unsplash.jpg +3 -0
  25. examples/data/rgb_demo/pexels-masi.jpg +3 -0
  26. examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG +3 -0
  27. packages.txt +6 -0
  28. requirements.txt +30 -0
  29. scripts/download_checkpoints.sh +14 -0
  30. src/__init__.py +1 -0
  31. src/demo/__init__.py +2 -0
  32. src/demo/config.py +32 -0
  33. src/demo/hf_runtime.py +320 -0
  34. src/demo/hf_ui.py +658 -0
  35. src/demo/infer_batch_images.py +486 -0
  36. src/demo/infer_single_image.py +889 -0
  37. src/model/__init__.py +1 -0
  38. src/model/decoder/__init__.py +23 -0
  39. src/model/decoder/decoder.py +28 -0
  40. src/model/decoder/decoder_gsplat.py +98 -0
  41. src/model/encoder/__init__.py +19 -0
  42. src/model/encoder/blocks/torchhub/dinov3/.docstr.yaml +6 -0
  43. src/model/encoder/blocks/torchhub/dinov3/.gitignore +18 -0
  44. src/model/encoder/blocks/torchhub/dinov3/CODE_OF_CONDUCT.md +80 -0
  45. src/model/encoder/blocks/torchhub/dinov3/CONTRIBUTING.md +31 -0
  46. src/model/encoder/blocks/torchhub/dinov3/DATASETS.md +43 -0
  47. src/model/encoder/blocks/torchhub/dinov3/LICENSE.md +66 -0
  48. src/model/encoder/blocks/torchhub/dinov3/MODEL_CARD.md +432 -0
  49. src/model/encoder/blocks/torchhub/dinov3/README.md +882 -0
  50. src/model/encoder/blocks/torchhub/dinov3/VENDORED_FROM.md +9 -0
.gitattributes CHANGED
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/github_demo.gif filter=lfs diff=lfs merge=lfs -text
37
+ examples/data/lidar_demo/eth3d_kicker.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/data/lidar_demo/eth3d_pipes.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/data/lidar_demo/waymo_147.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/data/lidar_demo/waymo_9.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/data/rgb_demo/eth3d_courtyard.png filter=lfs diff=lfs merge=lfs -text
42
+ examples/data/rgb_demo/maksim-shutov-unsplash.jpg filter=lfs diff=lfs merge=lfs -text
43
+ examples/data/rgb_demo/pexels-masi.jpg filter=lfs diff=lfs merge=lfs -text
44
+ examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .cache/
6
+ *.egg-info/
7
+
8
+ # Local environments and editors
9
+ .venv/
10
+ .env
11
+ .vscode/
12
+ .idea/
13
+ .DS_Store
14
+
15
+ # Runtime outputs
16
+ /outputs/
17
+ /wandb/
18
+ /logs/
19
+ /cache/
20
+ /trash/
21
+ /.trash/
22
+
23
+ # Large model/data artifacts
24
+ checkpoints/*.ckpt
25
+ checkpoints/*.pth
26
+ checkpoints/*.pt
27
+ checkpoints/**/*.ckpt
28
+ checkpoints/**/*.pth
29
+ checkpoints/**/*.pt
30
+ /data/
31
+ /archives/
32
+
33
+ # Demo outputs, but keep demo inputs under examples/data tracked when added.
34
+ examples/data/**/outputs/
35
+ *.ply
36
+ *.mp4
37
+ *.avi
38
+ *.mov
39
+ *.webm
40
+ *.mkv
41
+ *.compressed.ply
42
+ *.sog
INSTALL.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 📦 Environment & Checkpoints
2
+
3
+ ### 1) Create environment ([miniforge](https://github.com/conda-forge/miniforge) is recommended)
4
+
5
+ If using `conda`, replace `mamba` with `conda` in the following commands and use the `conda-forge` channel when installing `gxx`:
6
+
7
+ ```bash
8
+ mamba create -n infinisplat python=3.10
9
+ mamba activate infinisplat
10
+
11
+ # Optional: When gsplat compilation fails due to g++ version or CUDA toolkit issues.
12
+ # mamba install gxx=10
13
+ # mamba install nvidia/label/cuda-12.8.0::cuda-toolkit -c nvidia/label/cuda-12.8.0
14
+ # export CUDA_HOME=$CONDA_PREFIX
15
+ ```
16
+
17
+ ### 2) Install dependencies
18
+
19
+ ```bash
20
+ # Install uv
21
+ pip install uv
22
+
23
+ # Install PyTorch with CUDA 12.8
24
+ uv pip install torch==2.9.0 torchvision==0.24.0 xformers==0.0.33.post1 --index-url https://download.pytorch.org/whl/cu128
25
+
26
+ # Install package dependencies
27
+ uv pip install -r requirements.txt
28
+ ```
29
+
30
+ ### 3) Optional output dependencies
31
+
32
+ PLY export works without the dependencies in this section.
33
+
34
+ Install `gsplat` only when novel-view video rendering is needed:
35
+
36
+ ```bash
37
+ uv pip install git+https://github.com/nerfstudio-project/gsplat.git
38
+ ```
39
+
40
+ Interactive HTML export requires Node.js and the PlayCanvas `splat-transform` CLI, but does not require `gsplat`:
41
+
42
+ ```bash
43
+ npm install -g @playcanvas/splat-transform
44
+ splat-transform -v
45
+ ```
46
+
47
+ Missing optional dependencies are handled independently: without `gsplat`, MP4 rendering is skipped; without `splat-transform`, HTML conversion is skipped. In both cases, inference still exports the Gaussian PLY.
48
+
49
+ ### 4) Download checkpoints
50
+
51
+ Download both released checkpoints with:
52
+
53
+ ```bash
54
+ bash scripts/download_checkpoints.sh
55
+ ```
56
+
57
+ This downloads the model files from [`PLUS-WAVE/InfiniSplat`](https://huggingface.co/PLUS-WAVE/InfiniSplat) into the local `checkpoints/` directory.
58
+
59
+ #### Model Zoo
60
+
61
+ | Category | Model | Use Case | Download |
62
+ |---|---|---|---|
63
+ | 3DGS | `InfiniSplat RGB` | RGB-Only Gaussian Inference | [infinisplat_rgb.ckpt](https://huggingface.co/PLUS-WAVE/InfiniSplat/blob/main/checkpoints/infinisplat_rgb.ckpt) |
64
+ | 3DGS | `InfiniSplat Depth Sensor` | Gaussian Inference with RGB + Depth | [infinisplat_lidar.ckpt](https://huggingface.co/PLUS-WAVE/InfiniSplat/blob/main/checkpoints/infinisplat_lidar.ckpt) |
65
+
66
+ The expected layout is:
67
+
68
+ ```text
69
+ checkpoints/
70
+ ├── infinisplat_rgb.ckpt
71
+ └── infinisplat_lidar.ckpt
72
+ ```
README.md CHANGED
@@ -1,13 +1,144 @@
1
  ---
2
  title: InfiniSplat
3
- emoji: 💻
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: InfiniSplat
3
+ emoji: 🌌
4
+ colorFrom: gray
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
+ python_version: "3.10.13"
9
  app_file: app.py
10
+ short_description: Single-image Gaussian reconstruction
11
+ startup_duration_timeout: 30m
12
+ models:
13
+ - PLUS-WAVE/InfiniSplat
14
+ preload_from_hub:
15
+ - PLUS-WAVE/InfiniSplat checkpoints/infinisplat_rgb.ckpt
16
  ---
17
 
18
+ <div align="center">
19
+
20
+ <h1>🌌 InfiniSplat: Implicit Gaussian Decoding for Large-Baseline Monocular View Synthesis</h1>
21
+
22
+ <div align="center">
23
+ <a href="https://pluswave.top/InfiniSplat-page/">
24
+ <img src="https://img.shields.io/badge/Project-Page-red?logo=googlechrome&logoColor=red">
25
+ </a>
26
+ <a href="#">
27
+ <img src="https://img.shields.io/badge/arXiv-Paper-blue?logo=arxiv&logoColor=blue">
28
+ </a>
29
+ <a href="https://pluswave.top/InfiniSplat-page/#visualization">
30
+ <img src="https://img.shields.io/badge/Gallery-Visualization-green?logo=googlephotos&logoColor=white">
31
+ </a>
32
+ <a href="https://huggingface.co/PLUS-WAVE/InfiniSplat">
33
+ <img src="https://img.shields.io/badge/HuggingFace-Model-yellow?logo=huggingface&logoColor=yellow">
34
+ </a>
35
+ </div>
36
+
37
+ <p align="center">
38
+ <a href="https://plus-wave.github.io/">Jiawei Wang*</a> •
39
+ <a href="https://ritianyu.github.io/">Hao Yu*</a> •
40
+ <a href="https://github.com/Edisoneh">Yongzhen Hu</a> •
41
+ <a href="https://github.com/shmily768">Xinyi Yang</a> •
42
+ <a href="#">Tao Ni</a> •
43
+ <a href="#">Xin Zhan</a> •
44
+ <a href="#">Junbo Chen</a> <br>
45
+ <a href="https://xzhou.me/">Xiaowei Zhou</a> •
46
+ <a href="https://csse.szu.edu.cn/staff/ruizhenhu/">Ruizhen Hu</a> •
47
+ <a href="https://pengsida.net/">Sida Peng</a>
48
+ </p>
49
+
50
+ <!-- <p align="center"><sup>* Equal contribution.</sup></p> -->
51
+
52
+ </div>
53
+
54
+ <div align="center">
55
+
56
+ <img src="assets/github_demo.gif" alt="InfiniSplat Demo" width="90%" />
57
+
58
+ </div>
59
+
60
+ ## 📣 News
61
+
62
+ > **[2026-07]** 🎉 Inference code for RGB-only and depth-sensor-guided 3D Gaussian reconstruction is available now!
63
+
64
+ ## 🧩 What can InfiniSplat do?
65
+
66
+ InfiniSplat supports two practical modes for single-image 3D Gaussian reconstruction:
67
+
68
+ | Capability | Input | Output |
69
+ | --- | --- | --- |
70
+ | Monocular 3D Gaussian Reconstruction | RGB Image | 3DGS |
71
+ | Depth-Sensor-Guided 3D Gaussian Reconstruction | RGB Image + Depth | 3DGS |
72
+
73
+ ## ⚙️ Installation
74
+
75
+ Please see [INSTALL.md](INSTALL.md) for environment setup and checkpoint download.
76
+
77
+ ## 🚀 Inference
78
+
79
+ ### RGB Only
80
+
81
+ Run a single image:
82
+
83
+ ```bash
84
+ python -m src.demo.infer_batch_images --input examples/data/rgb_demo/pexels-masi.jpg
85
+ ```
86
+
87
+ Run a directory using the bundled examples:
88
+
89
+ ```bash
90
+ python -m src.demo.infer_batch_images --input examples/data/rgb_demo
91
+ ```
92
+
93
+ ### Depth-Sensor-Guided Reconstruction
94
+
95
+ Run a single RGB and depth pair with matching filename stems in the same directory:
96
+
97
+ ```bash
98
+ python -m src.demo.infer_batch_images \
99
+ --mode lidar \
100
+ --input examples/data/lidar_demo/eth3d_kicker.png
101
+ ```
102
+
103
+ Run the bundled RGB and depth pairs:
104
+
105
+ ```bash
106
+ python -m src.demo.infer_batch_images \
107
+ --mode lidar \
108
+ --input examples/data/lidar_demo
109
+ ```
110
+
111
+ See [docs/inference.md](docs/inference.md) for camera parameters, recursive directory scanning, output control, and other optional arguments.
112
+
113
+ ## Hugging Face Space
114
+
115
+ The root `app.py` exposes RGB reconstruction as a Gradio ZeroGPU Space. The encoder
116
+ is loaded once at startup, GPU inference writes a temporary CPU artifact, and CPU
117
+ post-processing exports a Gaussian PLY plus a standalone SuperSplat HTML viewer.
118
+ The released RGB checkpoint is downloaded from `PLUS-WAVE/InfiniSplat`. This RGB
119
+ path does not require xFormers or any custom CUDA extension.
120
+
121
+ Run the Space app locally from the prepared inference environment:
122
+
123
+ ```bash
124
+ pip install spaces
125
+ INFINISPLAT_CHECKPOINT=checkpoints/infinisplat_rgb.ckpt python app.py
126
+ ```
127
+
128
+ Select ZeroGPU in the Space hardware settings after creating the Gradio Space.
129
+
130
+ ## 🙏 Acknowledgments
131
+
132
+ We sincerely thank the authors of [DINOv3](https://github.com/facebookresearch/dinov3), [Depth Pro](https://github.com/apple/ml-depth-pro), [InfiniDepth](https://github.com/zju3dv/InfiniDepth), and [gsplat](https://github.com/nerfstudio-project/gsplat) for their excellent work. InfiniSplat is built on top of these projects.
133
+
134
+ ---
135
+
136
+ <div align="center">
137
+
138
+ <img src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Hand%20gestures/Folded%20Hands%20Light%20Skin%20Tone.png" alt="Thanks" width="25" height="25" />
139
+
140
+ **Thank you for your interest in InfiniSplat!**
141
+
142
+ <sub>⭐ Star this repo if you find it interesting!</sub>
143
+
144
+ </div>
app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ os.environ.setdefault("GRADIO_SSR_MODE", "false")
6
+
7
+ import spaces # noqa: F401 - must patch torch before model modules are imported
8
+
9
+ from src.demo.hf_runtime import InfiniSplatRuntime
10
+ from src.demo.hf_ui import APP_CSS, APP_THEME, OUTPUT_ROOT, create_demo
11
+
12
+
13
+ runtime = InfiniSplatRuntime.load()
14
+ demo = create_demo(runtime)
15
+
16
+
17
+ if __name__ == "__main__":
18
+ demo.launch(
19
+ server_name="0.0.0.0",
20
+ server_port=int(os.environ.get("PORT", "7860")),
21
+ allowed_paths=[str(OUTPUT_ROOT)],
22
+ max_file_size="20mb",
23
+ show_error=True,
24
+ ssr_mode=False,
25
+ footer_links=[],
26
+ theme=APP_THEME,
27
+ css=APP_CSS,
28
+ )
assets/github_demo.gif ADDED

Git LFS Details

  • SHA256: 5404cba207de26991e016da78dcdf48b27157d3967bc53a895d0990d7dfb4dc1
  • Pointer size: 132 Bytes
  • Size of remote file: 9.59 MB
config/experiment/infinisplat_hypersim_lidar.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # @package _global_
2
+ defaults:
3
+ - override /model/encoder: infinisplat_infinidepth
4
+ - override /model/decoder: gsplat
5
+ - _self_
config/experiment/infinisplat_hypersim_rgb.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # @package _global_
2
+ defaults:
3
+ - override /model/encoder: infinisplat
4
+ - override /model/decoder: gsplat
5
+ - _self_
config/inference.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ defaults:
2
+ - model/encoder: infinisplat
3
+ - model/decoder: gsplat
4
+ - _self_
config/model/decoder/gsplat.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # GSplat Decoder Configuration
3
+ # ============================================================================
4
+ # This is the SINGLE SOURCE OF TRUTH for all default configuration values.
5
+ # The corresponding Python dataclass (DecoderGsplatCfg) only provides
6
+ # type annotations without default values.
7
+ # ============================================================================
8
+
9
+ name: gsplat
10
+ background_color: [0.0, 0.0, 0.0]
config/model/encoder/infinisplat.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: infinisplat
2
+
3
+ sample_point_num: 1500000
4
+
5
+ image_basic_dim: 128
6
+
7
+ image_backbone_type: vitl16
8
+
9
+ implicit_gs_query_batch_size: 80000
10
+ implicit_gs_hidden_list: [1024, 256, 64]
11
+
12
+ gaussian_decoder:
13
+ delta_factor_xy: 0.001
14
+ delta_factor_z: 0.001
15
+ delta_factor_scale: 1.0
16
+ delta_factor_rotation: 1.0
17
+ delta_factor_color: 0.1
18
+ delta_factor_opacity: 1.0
19
+ scale_min: 0.0
20
+ scale_max: 4.0
21
+ init_opacity: 0.5
22
+ opacity_min: 0.01
23
+ opacity_max: 0.99
24
+ rgb_min: 0.01
25
+ rgb_max: 0.99
26
+ depth_normalization_min: 1.0
27
+ depth_normalization_max: 100.0
28
+ base_scale_multiplier: 1.0
config/model/encoder/infinisplat_infinidepth.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: infinisplat_infinidepth
2
+
3
+ sample_point_num: 1500000
4
+
5
+ image_basic_dim: 128
6
+
7
+ image_backbone_type: vitl16
8
+
9
+ implicit_gs_query_batch_size: 80000
10
+ implicit_gs_hidden_list: [1024, 256, 64]
11
+
12
+ gaussian_decoder:
13
+ delta_factor_xy: 0.001
14
+ delta_factor_z: 0.001
15
+ delta_factor_scale: 1.0
16
+ delta_factor_rotation: 1.0
17
+ delta_factor_color: 0.1
18
+ delta_factor_opacity: 1.0
19
+ scale_min: 0.0
20
+ scale_max: 4.0
21
+ init_opacity: 0.5
22
+ opacity_min: 0.01
23
+ opacity_max: 0.99
24
+ rgb_min: 0.01
25
+ rgb_max: 0.99
26
+ depth_normalization_min: 1.0
27
+ depth_normalization_max: 100.0
28
+ base_scale_multiplier: 1.0
config/viewer_settings.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 2,
3
+ "tonemapping": "none",
4
+ "highPrecisionRendering": false,
5
+ "background": {
6
+ "color": [0.0, 0.0, 0.0]
7
+ },
8
+ "postEffectSettings": {
9
+ "sharpness": {
10
+ "enabled": false,
11
+ "amount": 0
12
+ },
13
+ "bloom": {
14
+ "enabled": false,
15
+ "intensity": 1,
16
+ "blurLevel": 2
17
+ },
18
+ "grading": {
19
+ "enabled": false,
20
+ "brightness": 0,
21
+ "contrast": 1,
22
+ "saturation": 1,
23
+ "tint": [1, 1, 1]
24
+ },
25
+ "vignette": {
26
+ "enabled": false,
27
+ "intensity": 0.5,
28
+ "inner": 0.3,
29
+ "outer": 0.75,
30
+ "curvature": 1
31
+ },
32
+ "fringing": {
33
+ "enabled": false,
34
+ "intensity": 0.5
35
+ }
36
+ },
37
+ "animTracks": [],
38
+ "cameras": [
39
+ {
40
+ "initial": {
41
+ "position": [0.0, 0.0, 0.0],
42
+ "target": [0.0, 0.0, 1.0],
43
+ "fov": 60.0
44
+ }
45
+ }
46
+ ],
47
+ "annotations": [],
48
+ "startMode": "default"
49
+ }
docs/inference.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inference Guide
2
+
3
+ Run all commands from the repository root through the Python entry point:
4
+
5
+ ```bash
6
+ python -m src.demo.infer_batch_images [OPTIONS]
7
+ ```
8
+
9
+ ## Modes
10
+
11
+ | Mode | Encoder configuration | Default checkpoint | Prompt depth |
12
+ | --- | --- | --- | --- |
13
+ | `rgb` | RGB-only | `checkpoints/infinisplat_rgb.ckpt` | Not used |
14
+ | `lidar` | RGB + prompt-conditioned InfiniDepth | `checkpoints/infinisplat_lidar.ckpt` | Required |
15
+
16
+ The selected checkpoint contains the full inference model. Encoder and decoder weights are not downloaded separately.
17
+
18
+ ## Input discovery
19
+
20
+ `--input` accepts one image or a directory. Supported image extensions are `.jpg`, `.jpeg`, `.png`, `.bmp`, and `.webp`. Directory entries are sorted to make batch selection deterministic.
21
+
22
+ By default, only the selected directory is scanned. Add `--recursive` to scan its subdirectories and `--limit N` to process only the first `N` selected images. A limit of `0` means no cap.
23
+
24
+ When `--input` is omitted, each mode uses its bundled example directory:
25
+
26
+ ```text
27
+ rgb -> examples/data/rgb_demo
28
+ lidar -> examples/data/lidar_demo
29
+ ```
30
+
31
+ ## Depth pairing
32
+
33
+ By default, depth files are searched in the input directory and paired by filename stem. For a single-image input, this is the directory containing the image. For example:
34
+
35
+ ```text
36
+ data/frame_001.jpg
37
+ data/frame_001.npy
38
+ ```
39
+
40
+ Run this pair with:
41
+
42
+ ```bash
43
+ python -m src.demo.infer_batch_images \
44
+ --mode lidar \
45
+ --input data/frame_001.jpg
46
+ ```
47
+
48
+ or to process all images and depths in a directory:
49
+
50
+ ```bash
51
+ python -m src.demo.infer_batch_images \
52
+ --mode lidar \
53
+ --input data
54
+ ```
55
+
56
+ ---
57
+
58
+
59
+ To explicitly select the depth file for a single image, use `--prompt-depth`:
60
+
61
+ ```bash
62
+ python -m src.demo.infer_batch_images \
63
+ --mode lidar \
64
+ --input /path/to/frame_001.jpg \
65
+ --prompt-depth /path/to/frame_001.npy
66
+ ```
67
+
68
+ To keep depth files in a separate directory, set `--prompt-depth-dir`; each image is still paired with a depth file that has the same stem:
69
+
70
+ ```bash
71
+ python -m src.demo.infer_batch_images \
72
+ --mode lidar \
73
+ --input /path/to/images \
74
+ --prompt-depth-dir /path/to/depths
75
+ ```
76
+
77
+ `--prompt-depth` applies the same file to every selected image, so it should normally be used only with a single-image input. Candidate extensions are tried in this order when pairing automatically: `.npz`, `.npy`, `.png`, `.h5`, `.hdf5`, `.exr`.
78
+
79
+ ## Depth input format
80
+
81
+ Prompt depth must be spatially aligned with the RGB image and use larger values for farther points. Metric scale is optional: scale-ambiguous depth maps are also supported, while metric input preserves the scene scale in the exported 3DGS. Disparity or inverse depth must be converted to depth first.
82
+
83
+ Depth arrays should use shape `[H, W]`. Plain arrays and dense maps are accepted; `.npz` files may instead store a sparse `mask` and `value` pair. Valid values must be finite and strictly between 1 and 100 after decoding, so relative depth in `[0, 1]` must be rescaled first. Integer PNG values above 255 are treated as millimeters and divided by 1000. At most 1500 valid samples are used as prompts.
84
+
85
+ ## Camera intrinsics
86
+
87
+ Camera parameters are resolved in the following order:
88
+
89
+ 1. `--intrinsics-file`: a YAML or JSON file containing a 3 x 3 pixel-space matrix.
90
+ 2. `--focal-px`: focal length in pixels; `fx = fy`, with a centered principal point.
91
+ 3. `--focal-mm`: 35 mm full-frame-equivalent focal length.
92
+ 4. Image EXIF focal length.
93
+ 5. A fixed 30 mm full-frame-equivalent fallback.
94
+
95
+ The three command-line overrides are mutually exclusive. One override is reused for every image in a batch.
96
+
97
+ An intrinsics file may place the matrix at either `intrinsics_px` or `camera.intrinsics_px`:
98
+
99
+ ```yaml
100
+ intrinsics_px:
101
+ - [1200.0, 0.0, 768.0]
102
+ - [0.0, 1200.0, 576.0]
103
+ - [0.0, 0.0, 1.0]
104
+ ```
105
+
106
+ Values must describe the original input image in pixels. The loader scales the matrix automatically during preprocessing.
107
+
108
+ ## Optional video render resolution
109
+
110
+ When the optional `gsplat` package is installed, videos render at the original image resolution when possible. Very large frames are scaled down to a maximum long edge of 3840 pixels and a maximum area of `3840 x 2160` pixels. Each video contains 60 frames at 10 FPS.
111
+
112
+ ## Outputs and resume behavior
113
+
114
+ The default output root is `outputs/demo/<mode>`. Every image receives its own directory:
115
+
116
+ ```text
117
+ outputs/demo/rgb/example/
118
+ ├── example.ply
119
+ ├── example.mp4 # optional: requires gsplat
120
+ └── example.html # optional: requires splat-transform
121
+ ```
122
+
123
+ PLY export is always enabled and does not require `gsplat`. Video export is enabled by default when `gsplat` is installed; otherwise it is skipped with a warning. Pass `--no-video` to disable it explicitly. HTML export is independent of `gsplat` and is skipped with a warning when the optional `splat-transform` executable is unavailable.
124
+
125
+ If every requested artifact already exists, the case is skipped. If the PLY and requested video exist but HTML is missing, only HTML conversion runs. Pass `--overwrite` to recompute requested outputs.
126
+
127
+ The HTML converter reads `config/viewer_settings.json` directly. It does not generate a hidden viewer-settings file.
128
+
129
+ ## Common options
130
+
131
+ | Option | Description |
132
+ | --- | --- |
133
+ | `--mode {rgb,lidar}` | Select the model mode. |
134
+ | `--checkpoint PATH` | Override the checkpoint selected by the mode. |
135
+ | `--input PATH` | Process one image or a directory. |
136
+ | `--output-dir PATH` | Override `outputs/demo/<mode>`. |
137
+ | `--recursive` | Search input subdirectories. |
138
+ | `--limit N` | Process at most `N` selected images; `0` means all. |
139
+ | `--overwrite` | Recompute outputs that already exist. |
140
+ | `--device DEVICE` | Override automatic device selection, for example `cuda:0`. |
141
+ | `--intrinsics-file PATH` | Use a 3 x 3 pixel-space intrinsics matrix. |
142
+ | `--focal-px VALUE` | Use a focal length in pixels. |
143
+ | `--focal-mm VALUE` | Use a full-frame-equivalent focal length in millimeters. |
144
+ | `--prompt-depth PATH` | Use one prompt-depth file. |
145
+ | `--prompt-depth-dir PATH` | Pair depth files by image stem. |
146
+ | `--disable-floater-filter` | Keep Gaussians removed by the final floater filter. |
147
+ | `--no-video` | Skip MP4 rendering. |
148
+ | `--no-export-html` | Skip HTML viewer export. |
149
+
150
+ Print the authoritative option list with:
151
+
152
+ ```bash
153
+ python -m src.demo.infer_batch_images --help
154
+ ```
examples/data/lidar_demo/eth3d_kicker.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d65d75094be01eb8bab4f99ca859c94b3d04f2c0a68884c470385feb06fbb165
3
+ size 632654
examples/data/lidar_demo/eth3d_kicker.png ADDED

Git LFS Details

  • SHA256: 8199d4de68cd7ff463b1a65aae4e57b005423ed6a08cb4a2f34cd8752ba91d59
  • Pointer size: 132 Bytes
  • Size of remote file: 2.44 MB
examples/data/lidar_demo/eth3d_pipes.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e22a52079f426bb0373e5902a5f77da50fd0426d0ee9f134a64635bd79278597
3
+ size 677470
examples/data/lidar_demo/eth3d_pipes.png ADDED

Git LFS Details

  • SHA256: 3f05d6dd593f3b957926f863bee1e9e763fc6da1126c94faf1aab234cd50e0e9
  • Pointer size: 132 Bytes
  • Size of remote file: 1.79 MB
examples/data/lidar_demo/waymo_147.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:befe9ad8a84cbd082927486d276b4a25db94247a67e8e0ea11def64b9a6c627b
3
+ size 84094
examples/data/lidar_demo/waymo_147.png ADDED

Git LFS Details

  • SHA256: e19430e2d388012f4d40ab58fd6119a5d120f3e0c89b2114a33840b106b46e0e
  • Pointer size: 132 Bytes
  • Size of remote file: 3.01 MB
examples/data/lidar_demo/waymo_9.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b750d2180e31896b1363fa6edbff735af9d053fcceb5c31e5d8c55211aa5401c
3
+ size 91954
examples/data/lidar_demo/waymo_9.png ADDED

Git LFS Details

  • SHA256: 2e0c2d393e9aea3e548b5a8c4c49119fce462e892d78e13c4b1e9770d7f3c0be
  • Pointer size: 132 Bytes
  • Size of remote file: 2.63 MB
examples/data/rgb_demo/eth3d_courtyard.png ADDED

Git LFS Details

  • SHA256: 90c018f87fbb253c200f74ec283a31fbc2f7c7c0a60e873e1868070211b6cf3d
  • Pointer size: 132 Bytes
  • Size of remote file: 2.19 MB
examples/data/rgb_demo/maksim-shutov-unsplash.jpg ADDED

Git LFS Details

  • SHA256: ee3ddeb74016d0ff3ccefcbdee48fff082d9de609f8b3c4b578a368b4bd5820f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.51 MB
examples/data/rgb_demo/pexels-masi.jpg ADDED

Git LFS Details

  • SHA256: afb47ee1d909e7cd12decef181150a18352028481e12bcf5b7332b2dc02e5702
  • Pointer size: 132 Bytes
  • Size of remote file: 2.1 MB
examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG ADDED

Git LFS Details

  • SHA256: 77d8ab2af4b4b89f048a1c5074ed944f53137f71a8e69f6abbfe6b33f5bb8ee5
  • Pointer size: 131 Bytes
  • Size of remote file: 302 kB
packages.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ libgl1
2
+ libglib2.0-0
3
+ libvulkan1
4
+ mesa-vulkan-drivers
5
+ nodejs
6
+ npm
requirements.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Keep the torch family aligned with the supported ZeroGPU runtime.
2
+ torch==2.9.1
3
+ torchvision==0.24.1
4
+
5
+ # Inference configuration and CLI
6
+ hydra-core
7
+ omegaconf
8
+ dacite
9
+ termcolor
10
+ rich
11
+ tqdm
12
+
13
+ # Tensor, vision, and geometry
14
+ numpy<2.0
15
+ scipy
16
+ pandas
17
+ h5py
18
+ einops>=0.4.1
19
+ jaxtyping
20
+ opencv-python-headless
21
+ scikit-learn
22
+ Pillow
23
+ imageio[ffmpeg]
24
+ matplotlib
25
+ plyfile
26
+ torchmetrics
27
+
28
+ # Model backbones and checkpoint download
29
+ timm
30
+ regex
scripts/download_checkpoints.sh ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "${BASH_SOURCE[0]}")/.."
5
+
6
+ python - <<'PY'
7
+ from huggingface_hub import snapshot_download
8
+
9
+ snapshot_download(
10
+ repo_id="PLUS-WAVE/InfiniSplat",
11
+ allow_patterns="checkpoints/*.ckpt",
12
+ local_dir=".",
13
+ )
14
+ PY
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """InfiniSplat source package."""
src/demo/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Single-image demo inference utilities."""
2
+
src/demo/config.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Type, TypeVar
3
+
4
+ from dacite import from_dict
5
+ from omegaconf import DictConfig, OmegaConf
6
+
7
+ from src.model.decoder import DecoderCfg
8
+ from src.model.encoder import EncoderCfg
9
+
10
+
11
+ @dataclass
12
+ class ModelCfg:
13
+ decoder: DecoderCfg
14
+ encoder: EncoderCfg
15
+
16
+
17
+ @dataclass
18
+ class RootCfg:
19
+ model: ModelCfg
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ def load_typed_config(cfg: DictConfig, data_class: Type[T]) -> T:
26
+ """Convert one resolved Hydra config into its inference dataclass."""
27
+ return from_dict(data_class, OmegaConf.to_container(cfg, resolve=True))
28
+
29
+
30
+ def load_typed_root_config(cfg: DictConfig) -> RootCfg:
31
+ """Load the typed root configuration used by demo inference."""
32
+ return load_typed_config(cfg, RootCfg)
src/demo/hf_runtime.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import shlex
8
+ import shutil
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Sequence
13
+
14
+ import torch
15
+ from huggingface_hub import hf_hub_download
16
+
17
+ from src.demo.infer_single_image import (
18
+ load_demo_config,
19
+ load_demo_encoder,
20
+ load_demo_image_bundle,
21
+ patch_supersplat_html_auto_rotate,
22
+ patch_supersplat_html_viewer_bridge,
23
+ run_single_image_inference,
24
+ )
25
+ from src.model.encoder import Encoder
26
+ from src.utils.gaussians import Gaussians3D, save_ply
27
+
28
+ MODEL_REPO_ID = "PLUS-WAVE/InfiniSplat"
29
+ RGB_CHECKPOINT_FILE = "checkpoints/infinisplat_rgb.ckpt"
30
+ RGB_EXPERIMENT = "infinisplat_hypersim_rgb"
31
+ SPLAT_TRANSFORM_PACKAGE = "@playcanvas/splat-transform@2.1.0"
32
+ VIEWER_SETTINGS = Path(__file__).resolve().parents[2] / "config" / "viewer_settings.json"
33
+ ARTIFACT_VERSION = 1
34
+ VIEWER_ASSET_FILENAMES = ("index.js", "index.css", "settings.json")
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class GaussianArtifact:
39
+ """CPU-resident Gaussian tensors and camera metadata for one request."""
40
+
41
+ gaussians: Gaussians3D
42
+ focal_length_px: float
43
+ image_shape: tuple[int, int]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ExportedArtifacts:
48
+ """Public output files generated for one request."""
49
+
50
+ scene_ply: Path
51
+ viewer_html: Path
52
+ standalone_html: Path
53
+
54
+
55
+ def _gaussians_to_dict(gaussians: Gaussians3D) -> dict[str, torch.Tensor | None]:
56
+ return {
57
+ "mean_vectors": gaussians.mean_vectors.detach().cpu(),
58
+ "singular_values": gaussians.singular_values.detach().cpu(),
59
+ "quaternions": gaussians.quaternions.detach().cpu(),
60
+ "colors": gaussians.colors.detach().cpu(),
61
+ "opacities": gaussians.opacities.detach().cpu(),
62
+ "covariances": None if gaussians.covariances is None else gaussians.covariances.detach().cpu(),
63
+ }
64
+
65
+
66
+ def save_gaussian_artifact(artifact: GaussianArtifact, path: Path) -> Path:
67
+ """Serialize one request artifact using the safe torch data subset."""
68
+ path.parent.mkdir(parents=True, exist_ok=True)
69
+ torch.save(
70
+ {
71
+ "version": ARTIFACT_VERSION,
72
+ "gaussians": _gaussians_to_dict(artifact.gaussians),
73
+ "focal_length_px": artifact.focal_length_px,
74
+ "image_shape": list(artifact.image_shape),
75
+ },
76
+ path,
77
+ )
78
+ return path
79
+
80
+
81
+ def load_gaussian_artifact(path: Path) -> GaussianArtifact:
82
+ """Load an internal request artifact without permitting arbitrary objects."""
83
+ payload = torch.load(path, map_location="cpu", weights_only=True)
84
+ if payload["version"] != ARTIFACT_VERSION:
85
+ raise ValueError(f"Unsupported Gaussian artifact version: {payload['version']}")
86
+ tensors = payload["gaussians"]
87
+ gaussians = Gaussians3D(
88
+ mean_vectors=tensors["mean_vectors"],
89
+ singular_values=tensors["singular_values"],
90
+ quaternions=tensors["quaternions"],
91
+ colors=tensors["colors"],
92
+ opacities=tensors["opacities"],
93
+ covariances=tensors["covariances"],
94
+ )
95
+ return GaussianArtifact(
96
+ gaussians=gaussians,
97
+ focal_length_px=float(payload["focal_length_px"]),
98
+ image_shape=tuple(int(value) for value in payload["image_shape"]),
99
+ )
100
+
101
+
102
+ def _default_splat_transform_prefix() -> list[str]:
103
+ override = os.environ.get("SPLAT_TRANSFORM")
104
+ if override:
105
+ return shlex.split(override)
106
+ return ["npx", "--yes", SPLAT_TRANSFORM_PACKAGE]
107
+
108
+
109
+ def build_splat_transform_command(
110
+ scene_ply: Path,
111
+ output_html: Path,
112
+ viewer_settings: Path = VIEWER_SETTINGS,
113
+ command_prefix: Sequence[str] | None = None,
114
+ ) -> list[str]:
115
+ """Build the fixed, non-interactive SuperSplat HTML conversion command."""
116
+ prefix = list(command_prefix) if command_prefix is not None else _default_splat_transform_prefix()
117
+ return [
118
+ *prefix,
119
+ "--quiet",
120
+ "--overwrite",
121
+ "--unbundled",
122
+ "--viewer-settings",
123
+ str(viewer_settings),
124
+ str(scene_ply),
125
+ "--filter-harmonics",
126
+ "0",
127
+ str(output_html),
128
+ ]
129
+
130
+
131
+ def _replace_once(source: str, old: str, new: str, description: str) -> str:
132
+ if source.count(old) != 1:
133
+ raise RuntimeError(
134
+ f"Could not bundle SuperSplat {description}; the viewer template changed."
135
+ )
136
+ return source.replace(old, new, 1)
137
+
138
+
139
+ def build_standalone_viewer(viewer_html: Path, output_html: Path) -> Path:
140
+ """Bundle an unbundled SuperSplat viewer for single-file download."""
141
+ output_dir = viewer_html.parent
142
+ scene_sog = viewer_html.with_suffix(".sog")
143
+ source = viewer_html.read_text(encoding="utf-8")
144
+ css = (output_dir / "index.css").read_text(encoding="utf-8")
145
+ javascript = (output_dir / "index.js").read_text(encoding="utf-8")
146
+ settings = json.loads((output_dir / "settings.json").read_text(encoding="utf-8"))
147
+ encoded_scene = base64.b64encode(scene_sog.read_bytes()).decode("ascii")
148
+
149
+ source = _replace_once(
150
+ source,
151
+ '<link rel="stylesheet" href="./index.css">',
152
+ f"<style>\n{css}\n </style>",
153
+ "stylesheet",
154
+ )
155
+ source = _replace_once(
156
+ source,
157
+ "import { main } from './index.js';",
158
+ javascript,
159
+ "script",
160
+ )
161
+ source = _replace_once(
162
+ source,
163
+ "settings: fetch(settingsUrl).then(response => response.json())",
164
+ f"settings: {json.dumps(settings, separators=(',', ':'), ensure_ascii=False)}",
165
+ "settings",
166
+ )
167
+ source = _replace_once(
168
+ source,
169
+ f'fetch("{scene_sog.name}")',
170
+ f'fetch("data:application/octet-stream;base64,{encoded_scene}")',
171
+ "scene data",
172
+ )
173
+ output_html.write_text(source, encoding="utf-8")
174
+ return output_html
175
+
176
+
177
+ def install_shared_viewer_assets(viewer_html: Path) -> Path:
178
+ """Point an unbundled viewer at content-addressed shared static assets."""
179
+ asset_paths = [viewer_html.parent / name for name in VIEWER_ASSET_FILENAMES]
180
+ hasher = hashlib.sha256()
181
+ for asset_path in asset_paths:
182
+ hasher.update(asset_path.name.encode("utf-8"))
183
+ hasher.update(asset_path.read_bytes())
184
+ digest = hasher.hexdigest()[:16]
185
+ shared_dir = viewer_html.parent.parent / "_viewer_assets" / digest
186
+ shared_dir.mkdir(parents=True, exist_ok=True)
187
+ for asset_path in asset_paths:
188
+ destination = shared_dir / asset_path.name
189
+ if not destination.exists():
190
+ shutil.copyfile(asset_path, destination)
191
+
192
+ shared_prefix = f"../_viewer_assets/{digest}"
193
+ source = viewer_html.read_text(encoding="utf-8")
194
+ source = _replace_once(
195
+ source,
196
+ "./index.css",
197
+ f"{shared_prefix}/index.css",
198
+ "shared stylesheet path",
199
+ )
200
+ source = _replace_once(
201
+ source,
202
+ "./index.js",
203
+ f"{shared_prefix}/index.js",
204
+ "shared script path",
205
+ )
206
+ source = _replace_once(
207
+ source,
208
+ "./settings.json",
209
+ f"{shared_prefix}/settings.json",
210
+ "shared settings path",
211
+ )
212
+ viewer_html.write_text(source, encoding="utf-8")
213
+ return shared_dir
214
+
215
+
216
+ def export_gaussian_artifact(
217
+ artifact_path: Path,
218
+ output_dir: Path,
219
+ command_prefix: Sequence[str] | None = None,
220
+ ) -> ExportedArtifacts:
221
+ """Export unchanged Gaussians for a cached viewer and standalone download."""
222
+ artifact = load_gaussian_artifact(artifact_path)
223
+ gaussians = artifact.gaussians
224
+
225
+ output_dir.mkdir(parents=True, exist_ok=True)
226
+ scene_ply = output_dir / "scene.ply"
227
+ viewer_html = output_dir / "viewer.html"
228
+ standalone_html = output_dir / "scene.html"
229
+ save_ply(
230
+ gaussians=gaussians,
231
+ f_px=artifact.focal_length_px,
232
+ image_shape=artifact.image_shape,
233
+ path=scene_ply,
234
+ )
235
+ converter_environment = os.environ.copy()
236
+ xdg_runtime_dir = output_dir / ".xdg-runtime"
237
+ xdg_runtime_dir.mkdir(mode=0o700, exist_ok=True)
238
+ converter_environment["XDG_RUNTIME_DIR"] = str(xdg_runtime_dir)
239
+ subprocess.run(
240
+ build_splat_transform_command(
241
+ scene_ply=scene_ply,
242
+ output_html=viewer_html,
243
+ command_prefix=command_prefix,
244
+ ),
245
+ check=True,
246
+ env=converter_environment,
247
+ )
248
+ patch_supersplat_html_auto_rotate(viewer_html)
249
+ patch_supersplat_html_viewer_bridge(
250
+ viewer_html,
251
+ viewer_script_path=output_dir / "index.js",
252
+ )
253
+ build_standalone_viewer(viewer_html, standalone_html)
254
+ install_shared_viewer_assets(viewer_html)
255
+ return ExportedArtifacts(
256
+ scene_ply=scene_ply,
257
+ viewer_html=viewer_html,
258
+ standalone_html=standalone_html,
259
+ )
260
+
261
+
262
+ def resolve_rgb_checkpoint() -> Path:
263
+ """Resolve the local override or download the released RGB checkpoint."""
264
+ local_checkpoint = os.environ.get("INFINISPLAT_CHECKPOINT")
265
+ if local_checkpoint:
266
+ checkpoint_path = Path(local_checkpoint)
267
+ if not checkpoint_path.is_file():
268
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
269
+ return checkpoint_path
270
+
271
+ return Path(
272
+ hf_hub_download(
273
+ repo_id=os.environ.get("INFINISPLAT_MODEL_REPO", MODEL_REPO_ID),
274
+ filename=RGB_CHECKPOINT_FILE,
275
+ revision=os.environ.get("INFINISPLAT_MODEL_REVISION", "main"),
276
+ )
277
+ )
278
+
279
+
280
+ class InfiniSplatRuntime:
281
+ """One process-wide RGB encoder reused by all web requests."""
282
+
283
+ def __init__(self, encoder: Encoder, device: torch.device) -> None:
284
+ self.encoder = encoder
285
+ self.device = device
286
+
287
+ @classmethod
288
+ def load(
289
+ cls,
290
+ checkpoint_path: Path | None = None,
291
+ device: str | torch.device | None = None,
292
+ ) -> "InfiniSplatRuntime":
293
+ resolved_device = torch.device(
294
+ device or ("cuda" if torch.cuda.is_available() else "cpu")
295
+ )
296
+ cfg = load_demo_config(RGB_EXPERIMENT)
297
+ encoder = load_demo_encoder(
298
+ cfg=cfg,
299
+ checkpoint_path=checkpoint_path or resolve_rgb_checkpoint(),
300
+ device=resolved_device,
301
+ )
302
+ return cls(encoder=encoder, device=resolved_device)
303
+
304
+ @torch.inference_mode()
305
+ def infer_to_artifact(self, image_path: Path, artifact_path: Path) -> Path:
306
+ """Run one RGB reconstruction and persist CPU tensors for post-processing."""
307
+ image_bundle = load_demo_image_bundle(image_path=image_path)
308
+ encoder_output = run_single_image_inference(
309
+ encoder=self.encoder,
310
+ image=image_bundle.inference_image,
311
+ intrinsics_px=image_bundle.inference_intrinsics.intrinsics_px,
312
+ device=self.device,
313
+ )
314
+ _, height, width = image_bundle.inference_image.shape
315
+ artifact = GaussianArtifact(
316
+ gaussians=encoder_output["gaussians"].to("cpu"),
317
+ focal_length_px=image_bundle.inference_intrinsics.focal_length_px,
318
+ image_shape=(height, width),
319
+ )
320
+ return save_gaussian_artifact(artifact, artifact_path)
src/demo/hf_ui.py ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import uuid
5
+ from pathlib import Path
6
+ from urllib.parse import quote
7
+
8
+ import spaces
9
+ import gradio as gr
10
+ from gradio.utils import get_upload_folder
11
+
12
+ from src.demo.hf_runtime import InfiniSplatRuntime, export_gaussian_artifact
13
+
14
+ OUTPUT_ROOT = Path(get_upload_folder()).resolve() / "infinisplat"
15
+ OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
16
+ _runtime: InfiniSplatRuntime | None = None
17
+ RGB_EXAMPLES = [
18
+ ("examples/data/rgb_demo/eth3d_courtyard.png", "Courtyard"),
19
+ ("examples/data/rgb_demo/maksim-shutov-unsplash.jpg", "Unsplash"),
20
+ ("examples/data/rgb_demo/pexels-masi.jpg", "Pexels"),
21
+ ("examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG", "ScanNet++"),
22
+ ]
23
+ APP_THEME = gr.themes.Soft(
24
+ primary_hue="emerald",
25
+ secondary_hue="amber",
26
+ neutral_hue="zinc",
27
+ spacing_size="sm",
28
+ radius_size="sm",
29
+ font=("ui-sans-serif", "system-ui", "sans-serif"),
30
+ ).set(
31
+ body_background_fill="#f5f6f6",
32
+ body_background_fill_dark="#f5f6f6",
33
+ body_text_color="#17201e",
34
+ body_text_color_dark="#17201e",
35
+ body_text_color_subdued="#68716f",
36
+ body_text_color_subdued_dark="#68716f",
37
+ background_fill_primary="#ffffff",
38
+ background_fill_primary_dark="#ffffff",
39
+ background_fill_secondary="#f8f9f9",
40
+ background_fill_secondary_dark="#f8f9f9",
41
+ block_background_fill="#ffffff",
42
+ block_background_fill_dark="#ffffff",
43
+ block_label_text_color="#17201e",
44
+ block_label_text_color_dark="#17201e",
45
+ block_title_text_color="#17201e",
46
+ block_title_text_color_dark="#17201e",
47
+ input_background_fill="#ffffff",
48
+ input_background_fill_dark="#ffffff",
49
+ input_placeholder_color="#68716f",
50
+ input_placeholder_color_dark="#68716f",
51
+ button_primary_background_fill="#087f62",
52
+ button_primary_background_fill_dark="#087f62",
53
+ button_primary_text_color="#ffffff",
54
+ button_primary_text_color_dark="#ffffff",
55
+ button_secondary_background_fill="#f8f9f9",
56
+ button_secondary_background_fill_dark="#f8f9f9",
57
+ button_secondary_text_color="#17201e",
58
+ button_secondary_text_color_dark="#17201e",
59
+ )
60
+ APP_CSS = """
61
+ :root {
62
+ color-scheme: light;
63
+ --app-bg: #f5f6f6;
64
+ --surface: #ffffff;
65
+ --surface-muted: #f8f9f9;
66
+ --border: #dfe3e1;
67
+ --border-strong: #c6ceca;
68
+ --text: #17201e;
69
+ --muted: #68716f;
70
+ --viewer: #101413;
71
+ --primary: #087f62;
72
+ --primary-hover: #066b53;
73
+ --accent: #c27616;
74
+ }
75
+ html,
76
+ body {
77
+ background: var(--app-bg) !important;
78
+ color: var(--text) !important;
79
+ }
80
+ .gradio-container {
81
+ max-width: none !important;
82
+ min-height: 100vh;
83
+ padding: 0 !important;
84
+ background: var(--app-bg) !important;
85
+ color: var(--text);
86
+ }
87
+ .gradio-container *,
88
+ .dark .gradio-container * {
89
+ letter-spacing: 0 !important;
90
+ }
91
+ #app-title {
92
+ padding: 28px 2px 18px;
93
+ background: transparent !important;
94
+ }
95
+ #app-title h1 {
96
+ margin: 0 !important;
97
+ color: var(--text);
98
+ font-size: clamp(1.75rem, 2.5vw, 2.2rem);
99
+ font-weight: 720;
100
+ line-height: 1;
101
+ }
102
+ #app-main {
103
+ width: calc(100% - clamp(28px, 6vw, 80px));
104
+ max-width: 1440px;
105
+ margin: 0 auto !important;
106
+ gap: 0 !important;
107
+ }
108
+ .section-heading {
109
+ display: flex;
110
+ align-items: baseline;
111
+ justify-content: space-between;
112
+ gap: 16px;
113
+ }
114
+ .section-heading h2 {
115
+ margin: 0;
116
+ color: var(--text);
117
+ font-size: 0.9rem;
118
+ font-weight: 700;
119
+ }
120
+ .section-heading span {
121
+ color: var(--muted);
122
+ font-size: 0.75rem;
123
+ }
124
+ #workspace {
125
+ margin: 0 0 26px !important;
126
+ gap: 18px;
127
+ align-items: stretch;
128
+ }
129
+ .tool-panel {
130
+ min-width: 0 !important;
131
+ overflow: hidden;
132
+ gap: 0 !important;
133
+ border: 1px solid var(--border) !important;
134
+ border-radius: 8px !important;
135
+ background: var(--surface) !important;
136
+ box-shadow: 0 8px 28px rgba(23, 32, 30, 0.055);
137
+ }
138
+ .panel-heading {
139
+ display: flex;
140
+ align-items: baseline;
141
+ justify-content: space-between;
142
+ min-height: 56px;
143
+ padding: 17px 18px 15px;
144
+ border-bottom: 1px solid var(--border);
145
+ }
146
+ .panel-heading h2 {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 9px;
150
+ margin: 0;
151
+ color: var(--text);
152
+ font-size: 0.9rem;
153
+ font-weight: 700;
154
+ line-height: 1.2;
155
+ }
156
+ .panel-heading span {
157
+ color: var(--muted);
158
+ font-size: 0.75rem;
159
+ }
160
+ .panel-heading .section-index {
161
+ color: var(--accent);
162
+ font-size: 0.7rem;
163
+ font-variant-numeric: tabular-nums;
164
+ }
165
+ .source-content,
166
+ .viewer-content {
167
+ margin: 0 !important;
168
+ padding: 12px !important;
169
+ gap: 0 !important;
170
+ background: var(--surface) !important;
171
+ }
172
+ #source-image {
173
+ min-height: 510px;
174
+ overflow: hidden;
175
+ border: 1px solid var(--border) !important;
176
+ border-radius: 6px !important;
177
+ background: var(--surface-muted) !important;
178
+ }
179
+ #source-image > div,
180
+ #source-image .wrap {
181
+ border-radius: 6px !important;
182
+ background: var(--surface-muted) !important;
183
+ }
184
+ #source-image p,
185
+ #source-image span,
186
+ #source-image button:not(.primary) {
187
+ color: var(--muted) !important;
188
+ }
189
+ .source-actions {
190
+ margin: 0 !important;
191
+ padding: 0 12px 12px;
192
+ border: 0 !important;
193
+ background: var(--surface) !important;
194
+ }
195
+ #reconstruct-button {
196
+ min-height: 46px;
197
+ border-color: var(--primary) !important;
198
+ background: var(--primary) !important;
199
+ color: #ffffff !important;
200
+ font-weight: 700;
201
+ box-shadow: 0 4px 12px rgba(8, 127, 98, 0.16);
202
+ }
203
+ #reconstruct-button:hover {
204
+ border-color: var(--primary-hover) !important;
205
+ background: var(--primary-hover) !important;
206
+ }
207
+ .splat-shell,
208
+ .viewer-state,
209
+ .splat-frame {
210
+ width: 100%;
211
+ height: min(62vh, 510px);
212
+ min-height: 510px;
213
+ background: var(--viewer);
214
+ }
215
+ .splat-shell {
216
+ overflow: hidden;
217
+ border-radius: 6px;
218
+ }
219
+ .viewer-host {
220
+ position: relative;
221
+ }
222
+ .viewer-state {
223
+ display: flex;
224
+ flex-direction: column;
225
+ align-items: center;
226
+ justify-content: center;
227
+ width: 100%;
228
+ height: 100%;
229
+ gap: 8px;
230
+ color: #f4f7f6;
231
+ text-align: center;
232
+ }
233
+ .viewer-state strong {
234
+ color: #f4f7f6 !important;
235
+ font-size: 0.9rem;
236
+ font-weight: 650;
237
+ }
238
+ .viewer-state span {
239
+ color: #94a19d !important;
240
+ font-size: 0.75rem;
241
+ }
242
+ .viewer-idle {
243
+ width: 32px;
244
+ height: 2px;
245
+ margin-bottom: 7px;
246
+ background: #46514e;
247
+ }
248
+ .viewer-loader {
249
+ width: 34px;
250
+ height: 34px;
251
+ margin-bottom: 7px;
252
+ border: 3px solid #35413d;
253
+ border-top-color: #34d399;
254
+ border-radius: 50%;
255
+ animation: viewer-spin 0.9s linear infinite;
256
+ }
257
+ .viewer-error {
258
+ width: 32px;
259
+ height: 3px;
260
+ margin-bottom: 7px;
261
+ background: #f87171;
262
+ }
263
+ @keyframes viewer-spin {
264
+ to { transform: rotate(360deg); }
265
+ }
266
+ @media (prefers-reduced-motion: reduce) {
267
+ .viewer-loader { animation-duration: 1.8s; }
268
+ }
269
+ .splat-frame {
270
+ display: block;
271
+ border: 0;
272
+ }
273
+ .viewer-host .splat-frame {
274
+ opacity: 0;
275
+ transition: opacity 180ms ease-out;
276
+ }
277
+ .viewer-client-state {
278
+ position: absolute;
279
+ inset: 0;
280
+ z-index: 2;
281
+ transition: opacity 180ms ease-out, visibility 180ms ease-out;
282
+ }
283
+ .viewer-client-error,
284
+ .viewer-client-timeout {
285
+ display: none;
286
+ }
287
+ .viewer-host[data-viewer-state="ready"] .splat-frame {
288
+ opacity: 1;
289
+ }
290
+ .viewer-host[data-viewer-state="ready"] .viewer-client-state {
291
+ visibility: hidden;
292
+ opacity: 0;
293
+ }
294
+ .viewer-host[data-viewer-state="error"] .viewer-client-loading,
295
+ .viewer-host[data-viewer-state="timeout"] .viewer-client-loading {
296
+ display: none;
297
+ }
298
+ .viewer-host[data-viewer-state="error"] .viewer-client-error,
299
+ .viewer-host[data-viewer-state="timeout"] .viewer-client-timeout {
300
+ display: flex;
301
+ }
302
+ #viewer-output {
303
+ margin: 0 !important;
304
+ padding: 0 !important;
305
+ border: 0 !important;
306
+ }
307
+ .download-bar {
308
+ margin: 0 !important;
309
+ padding: 0 12px 12px;
310
+ border: 0 !important;
311
+ gap: 10px;
312
+ background: var(--surface) !important;
313
+ }
314
+ .artifact-button {
315
+ min-height: 42px;
316
+ border-color: var(--border-strong) !important;
317
+ background: var(--surface-muted) !important;
318
+ color: var(--text) !important;
319
+ font-weight: 650;
320
+ }
321
+ .artifact-button:hover {
322
+ border-color: var(--accent) !important;
323
+ background: #ffffff !important;
324
+ }
325
+ #examples-section {
326
+ gap: 11px !important;
327
+ margin: 0 0 36px !important;
328
+ padding: 4px 2px 0;
329
+ }
330
+ #example-gallery {
331
+ overflow: hidden;
332
+ padding: 0 !important;
333
+ border: 0 !important;
334
+ border-radius: 8px !important;
335
+ background: transparent !important;
336
+ }
337
+ #example-gallery .gallery-container,
338
+ #example-gallery .grid-wrap {
339
+ height: 180px !important;
340
+ min-height: 0 !important;
341
+ }
342
+ #example-gallery .grid-wrap {
343
+ overflow: hidden !important;
344
+ padding: 0 !important;
345
+ }
346
+ #example-gallery .grid-container {
347
+ height: 100% !important;
348
+ grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
349
+ grid-template-rows: minmax(0, 1fr) !important;
350
+ grid-auto-rows: minmax(0, 1fr) !important;
351
+ gap: 10px !important;
352
+ }
353
+ #example-gallery .gallery-item {
354
+ min-width: 0 !important;
355
+ overflow: hidden;
356
+ border-radius: 7px !important;
357
+ }
358
+ #example-gallery img {
359
+ object-fit: cover !important;
360
+ }
361
+ #example-gallery button {
362
+ color: var(--text) !important;
363
+ }
364
+ @media (max-width: 980px) {
365
+ #app-main { width: calc(100% - 28px); }
366
+ #workspace { flex-direction: column; }
367
+ #workspace > .tool-panel { width: 100% !important; }
368
+ .splat-shell, .viewer-state, .splat-frame {
369
+ height: 56vh;
370
+ min-height: 420px;
371
+ }
372
+ #source-image { min-height: 420px; }
373
+ }
374
+ @media (max-width: 560px) {
375
+ #app-main { width: calc(100% - 20px); }
376
+ #app-title { padding: 22px 2px 15px; }
377
+ #app-title h1 { font-size: 1.7rem; }
378
+ #workspace { margin: 14px 0 22px !important; gap: 12px; }
379
+ .panel-heading { min-height: 52px; padding: 14px; }
380
+ .source-content, .viewer-content { padding: 9px !important; }
381
+ .source-actions, .download-bar { padding: 0 9px 9px; }
382
+ .splat-shell, .viewer-state, .splat-frame { min-height: 360px; }
383
+ #source-image { min-height: 360px; }
384
+ #examples-section { margin-bottom: 24px !important; padding: 0; }
385
+ }
386
+ """
387
+
388
+
389
+ def build_viewer_iframe(viewer_html: Path) -> str:
390
+ """Build a same-origin iframe for one generated standalone viewer."""
391
+ encoded_path = quote(str(viewer_html.resolve()), safe="/")
392
+ source = html.escape(f"/gradio_api/file={encoded_path}", quote=True)
393
+ return (
394
+ '<div class="splat-shell viewer-host" '
395
+ 'data-infinisplat-viewer data-viewer-state="loading">'
396
+ '<div class="viewer-state viewer-client-state viewer-client-loading">'
397
+ '<div class="viewer-loader"></div>'
398
+ "<strong>Initializing 3D viewer</strong>"
399
+ "<span>Uploading scene data to WebGL</span>"
400
+ "</div>"
401
+ '<div class="viewer-state viewer-client-state viewer-client-error">'
402
+ '<div class="viewer-error"></div>'
403
+ "<strong>Viewer initialization failed</strong>"
404
+ "<span>Check the browser console for details</span>"
405
+ "</div>"
406
+ '<div class="viewer-state viewer-client-state viewer-client-timeout">'
407
+ '<div class="viewer-loader"></div>'
408
+ "<strong>Viewer is still loading</strong>"
409
+ "<span>Large scenes can take longer on this device</span>"
410
+ "</div>"
411
+ '<iframe class="splat-frame" '
412
+ f'src="{source}" '
413
+ 'allow="fullscreen; xr-spatial-tracking" '
414
+ 'title="Interactive Gaussian scene" '
415
+ 'loading="eager"></iframe>'
416
+ "</div>"
417
+ )
418
+
419
+
420
+ def build_viewer_status(title: str, detail: str, indicator: str) -> str:
421
+ """Build one fixed-height viewer status surface."""
422
+ return (
423
+ '<div class="splat-shell">'
424
+ '<div class="viewer-state">'
425
+ f'<div class="viewer-{indicator}"></div>'
426
+ f"<strong>{html.escape(title)}</strong>"
427
+ f"<span>{html.escape(detail)}</span>"
428
+ "</div>"
429
+ "</div>"
430
+ )
431
+
432
+
433
+ def show_reconstructing_viewer() -> str:
434
+ """Show the model inference stage before entering the GPU queue."""
435
+ return build_viewer_status(
436
+ "Reconstructing scene",
437
+ "Running model inference",
438
+ "loader",
439
+ )
440
+
441
+
442
+ def show_exporting_viewer() -> str:
443
+ """Show the CPU export stage after inference completes."""
444
+ return build_viewer_status(
445
+ "Preparing viewer",
446
+ "Encoding PLY and optimized viewer",
447
+ "loader",
448
+ )
449
+
450
+
451
+ def show_failed_viewer() -> str:
452
+ """Show a terminal viewer state when a queued stage fails."""
453
+ return build_viewer_status(
454
+ "Reconstruction stopped",
455
+ "See the error message for details",
456
+ "error",
457
+ )
458
+
459
+
460
+ def select_example(evt: gr.SelectData) -> str:
461
+ """Return the source path selected from the example gallery."""
462
+ index = evt.index[0] if isinstance(evt.index, tuple) else evt.index
463
+ return RGB_EXAMPLES[int(index)][0]
464
+
465
+
466
+ def configure_runtime(runtime: InfiniSplatRuntime) -> None:
467
+ """Register the process-wide read-only inference runtime before serving."""
468
+ global _runtime
469
+ if _runtime is not None and _runtime is not runtime:
470
+ raise RuntimeError("The InfiniSplat runtime is already configured.")
471
+ _runtime = runtime
472
+
473
+
474
+ @spaces.GPU(duration=30)
475
+ def reconstruct(image_path: str | None) -> str:
476
+ """Run one GPU reconstruction and return a CPU artifact path."""
477
+ if image_path is None:
478
+ raise gr.Error("Please upload an image.")
479
+ if _runtime is None:
480
+ raise RuntimeError("The InfiniSplat runtime is not configured.")
481
+ request_dir = OUTPUT_ROOT / uuid.uuid4().hex
482
+ artifact_path = _runtime.infer_to_artifact(
483
+ image_path=Path(image_path),
484
+ artifact_path=request_dir / "gaussians.pt",
485
+ )
486
+ return str(artifact_path)
487
+
488
+
489
+ def export_results(artifact_path: str) -> tuple[str, str, str]:
490
+ """Export one CPU artifact for browser viewing and download."""
491
+ internal_artifact = Path(artifact_path)
492
+ exported = export_gaussian_artifact(
493
+ artifact_path=internal_artifact,
494
+ output_dir=internal_artifact.parent,
495
+ )
496
+ internal_artifact.unlink()
497
+ return (
498
+ build_viewer_iframe(exported.viewer_html),
499
+ str(exported.scene_ply),
500
+ str(exported.standalone_html),
501
+ )
502
+
503
+
504
+ def create_demo(runtime: InfiniSplatRuntime) -> gr.Blocks:
505
+ """Create the public RGB reconstruction interface."""
506
+ configure_runtime(runtime)
507
+ empty_viewer = build_viewer_status(
508
+ "Ready",
509
+ "No reconstruction yet",
510
+ "idle",
511
+ )
512
+
513
+ with gr.Blocks(
514
+ title="InfiniSplat",
515
+ delete_cache=(3600, 3600),
516
+ analytics_enabled=False,
517
+ ) as demo:
518
+ artifact_state = gr.State()
519
+ with gr.Column(elem_id="app-main"):
520
+ gr.HTML("<h1>InfiniSplat</h1>", elem_id="app-title")
521
+
522
+ with gr.Row(equal_height=True, elem_id="workspace"):
523
+ with gr.Column(
524
+ scale=2,
525
+ min_width=300,
526
+ elem_classes=["tool-panel", "source-panel"],
527
+ ):
528
+ gr.HTML(
529
+ '<div class="panel-heading">'
530
+ '<h2><span class="section-index">01</span>Input image</h2>'
531
+ "<span>RGB</span>"
532
+ "</div>"
533
+ )
534
+ with gr.Column(elem_classes="source-content"):
535
+ image_input = gr.Image(
536
+ label="Source image",
537
+ show_label=False,
538
+ type="filepath",
539
+ sources=["upload"],
540
+ buttons=["fullscreen"],
541
+ height=510,
542
+ elem_id="source-image",
543
+ )
544
+ with gr.Row(elem_classes="source-actions"):
545
+ reconstruct_button = gr.Button(
546
+ "Reconstruct scene",
547
+ variant="primary",
548
+ elem_id="reconstruct-button",
549
+ )
550
+ with gr.Column(
551
+ scale=3,
552
+ min_width=360,
553
+ elem_classes=["tool-panel", "viewer-panel"],
554
+ ):
555
+ gr.HTML(
556
+ '<div class="panel-heading">'
557
+ '<h2><span class="section-index">02</span>Scene viewer</h2>'
558
+ "<span>Interactive</span>"
559
+ "</div>"
560
+ )
561
+ with gr.Column(elem_classes="viewer-content"):
562
+ viewer = gr.HTML(
563
+ empty_viewer,
564
+ label="Interactive Gaussian scene",
565
+ elem_id="viewer-output",
566
+ )
567
+ with gr.Row(elem_classes="download-bar"):
568
+ ply_download = gr.DownloadButton(
569
+ "Download PLY",
570
+ size="sm",
571
+ elem_classes="artifact-button",
572
+ )
573
+ html_download = gr.DownloadButton(
574
+ "Download HTML viewer",
575
+ size="sm",
576
+ elem_classes="artifact-button",
577
+ )
578
+
579
+ with gr.Column(elem_id="examples-section"):
580
+ gr.HTML(
581
+ '<div class="section-heading">'
582
+ "<h2>Examples</h2><span>4 scenes</span>"
583
+ "</div>"
584
+ )
585
+ example_gallery = gr.Gallery(
586
+ value=RGB_EXAMPLES,
587
+ label="Examples",
588
+ show_label=False,
589
+ container=False,
590
+ columns=4,
591
+ rows=1,
592
+ height=180,
593
+ allow_preview=False,
594
+ object_fit="cover",
595
+ buttons=[],
596
+ interactive=False,
597
+ elem_id="example-gallery",
598
+ )
599
+
600
+ example_gallery.select(
601
+ fn=select_example,
602
+ inputs=None,
603
+ outputs=[image_input],
604
+ queue=False,
605
+ show_progress="hidden",
606
+ api_name=False,
607
+ )
608
+ loading_event = reconstruct_button.click(
609
+ fn=show_reconstructing_viewer,
610
+ inputs=None,
611
+ outputs=[viewer],
612
+ queue=False,
613
+ show_progress="hidden",
614
+ api_name=False,
615
+ )
616
+ reconstruction_event = loading_event.then(
617
+ fn=reconstruct,
618
+ inputs=[image_input],
619
+ outputs=[artifact_state],
620
+ concurrency_limit=1,
621
+ concurrency_id="infinisplat-gpu",
622
+ show_progress="hidden",
623
+ api_name=False,
624
+ )
625
+ exporting_event = reconstruction_event.success(
626
+ fn=show_exporting_viewer,
627
+ inputs=None,
628
+ outputs=[viewer],
629
+ queue=False,
630
+ show_progress="hidden",
631
+ api_name=False,
632
+ )
633
+ export_event = exporting_event.success(
634
+ fn=export_results,
635
+ inputs=[artifact_state],
636
+ outputs=[viewer, ply_download, html_download],
637
+ show_progress="hidden",
638
+ api_name=False,
639
+ )
640
+ reconstruction_event.failure(
641
+ fn=show_failed_viewer,
642
+ inputs=None,
643
+ outputs=[viewer],
644
+ queue=False,
645
+ show_progress="hidden",
646
+ api_name=False,
647
+ )
648
+ export_event.failure(
649
+ fn=show_failed_viewer,
650
+ inputs=None,
651
+ outputs=[viewer],
652
+ queue=False,
653
+ show_progress="hidden",
654
+ api_name=False,
655
+ )
656
+
657
+ demo.queue(max_size=8, default_concurrency_limit=1)
658
+ return demo
src/demo/infer_batch_images.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import math
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import torch
14
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
15
+
16
+ from src.demo.infer_single_image import (
17
+ _resolve_checkpoint_path,
18
+ _resolve_device,
19
+ filter_final_gaussian_floaters,
20
+ load_demo_config,
21
+ load_demo_image_bundle,
22
+ load_demo_model,
23
+ load_prompt_depth_tensors,
24
+ patch_supersplat_html_auto_rotate,
25
+ render_novel_view_video_from_single_view,
26
+ run_single_image_inference,
27
+ scale_intrinsics_px,
28
+ validate_prompt_configuration,
29
+ )
30
+ from src.model.decoder.decoder_gsplat import is_gsplat_available
31
+ from src.utils.gaussians import save_ply
32
+
33
+ MODE_EXPERIMENTS = {
34
+ "rgb": "infinisplat_hypersim_rgb",
35
+ "lidar": "infinisplat_hypersim_lidar",
36
+ }
37
+ MODE_CHECKPOINTS = {
38
+ "rgb": Path("checkpoints/infinisplat_rgb.ckpt"),
39
+ "lidar": Path("checkpoints/infinisplat_lidar.ckpt"),
40
+ }
41
+ MODE_INPUT_DIRS = {
42
+ "rgb": Path("examples/data/rgb_demo"),
43
+ "lidar": Path("examples/data/lidar_demo"),
44
+ }
45
+ DEFAULT_OUTPUT_ROOT = Path("outputs/demo")
46
+ DEFAULT_MAX_RENDER_LONG_EDGE = 3840
47
+ DEFAULT_MAX_RENDER_PIXELS = 3840 * 2160
48
+ SPLAT_TRANSFORM = os.environ.get("SPLAT_TRANSFORM", "splat-transform")
49
+ VIEWER_SETTINGS = Path(__file__).resolve().parents[2] / "config" / "viewer_settings.json"
50
+ IMAGE_EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
51
+ PROMPT_DEPTH_EXTENSIONS = (".npz", ".npy", ".png", ".h5", ".hdf5", ".exr")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class CasePaths:
56
+ """Output paths for one image case.
57
+
58
+ Args:
59
+ case_dir: Directory containing all artifacts for this input image.
60
+ scene_ply: Final Gaussian PLY path.
61
+ video: Novel-view video path.
62
+ html: SuperSplat HTML path.
63
+ """
64
+
65
+ case_dir: Path
66
+ scene_ply: Path
67
+ video: Path
68
+ html: Path
69
+
70
+
71
+ def _parse_args() -> argparse.Namespace:
72
+ parser = argparse.ArgumentParser(
73
+ description=(
74
+ "Run InfiniSplat inference on one image or a directory of images "
75
+ "without reloading model weights for every image."
76
+ )
77
+ )
78
+ parser.add_argument(
79
+ "--mode",
80
+ choices=tuple(MODE_EXPERIMENTS),
81
+ default="rgb",
82
+ help="Inference mode. Defaults to rgb.",
83
+ )
84
+ parser.add_argument(
85
+ "--checkpoint",
86
+ type=Path,
87
+ default=None,
88
+ help="Checkpoint override. Defaults to the released checkpoint for --mode.",
89
+ )
90
+ parser.add_argument(
91
+ "--input",
92
+ dest="input_path",
93
+ type=Path,
94
+ default=None,
95
+ metavar="PATH",
96
+ help="Input image or directory. Defaults to the bundled examples for --mode.",
97
+ )
98
+ parser.add_argument(
99
+ "--output-dir",
100
+ type=Path,
101
+ default=None,
102
+ help="Root output directory. Defaults to outputs/demo/<mode>.",
103
+ )
104
+ parser.add_argument("--recursive", action="store_true", help="Search an input directory recursively.")
105
+ parser.add_argument("--limit", type=int, default=0, help="Maximum number of selected images to process; 0 means no cap.")
106
+ parser.add_argument("--overwrite", action="store_true", help="Recompute outputs that already exist.")
107
+
108
+ parser.add_argument("--device", type=str, default="auto")
109
+ camera_group = parser.add_mutually_exclusive_group()
110
+ camera_group.add_argument("--intrinsics-file", type=Path, default=None)
111
+ camera_group.add_argument("--focal-px", type=float, default=None)
112
+ camera_group.add_argument("--focal-mm", type=float, default=None)
113
+
114
+ prompt_group = parser.add_mutually_exclusive_group()
115
+ prompt_group.add_argument("--prompt-depth", type=Path, default=None)
116
+ prompt_group.add_argument(
117
+ "--prompt-depth-dir",
118
+ type=Path,
119
+ default=None,
120
+ help="Directory containing per-image prompt depth files with matching stems.",
121
+ )
122
+ parser.add_argument(
123
+ "--disable-floater-filter",
124
+ action="store_true",
125
+ help="Disable final Gaussian floater filtering.",
126
+ )
127
+
128
+ parser.add_argument("--no-video", action="store_true", help="Skip novel-view video rendering.")
129
+ parser.add_argument(
130
+ "--no-export-html",
131
+ dest="export_html",
132
+ action="store_false",
133
+ default=True,
134
+ help="Disable SuperSplat HTML export.",
135
+ )
136
+ return parser.parse_args()
137
+
138
+
139
+ def _resolve_output_dir(args: argparse.Namespace) -> Path:
140
+ """Resolve the root output directory for this batch run."""
141
+ if args.output_dir is not None:
142
+ return args.output_dir
143
+ return DEFAULT_OUTPUT_ROOT / args.mode
144
+
145
+
146
+ def _collect_images(args: argparse.Namespace) -> list[Path]:
147
+ """Collect input image paths in deterministic order."""
148
+ input_path = args.input_path or MODE_INPUT_DIRS[args.mode]
149
+ if input_path.is_file():
150
+ if input_path.suffix.lower() not in IMAGE_EXTENSIONS:
151
+ raise ValueError(f"Unsupported input image extension: {input_path}")
152
+ images = [input_path]
153
+ elif input_path.is_dir():
154
+ iterator = input_path.rglob("*") if args.recursive else input_path.iterdir()
155
+ images = [path for path in iterator if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS]
156
+ else:
157
+ raise FileNotFoundError(f"Input image or directory not found: {input_path}")
158
+
159
+ images = sorted(images)
160
+ if not images:
161
+ raise FileNotFoundError("No input images were found.")
162
+
163
+ if args.limit < 0:
164
+ raise ValueError("--limit must be non-negative.")
165
+ if args.limit > 0:
166
+ images = images[: args.limit]
167
+ return images
168
+
169
+
170
+ def _resolve_prompt_depth_path(args: argparse.Namespace, image_path: Path) -> Path | None:
171
+ """Resolve the prompt depth path for one image.
172
+
173
+ Args:
174
+ args: Parsed batch arguments.
175
+ image_path: RGB image path.
176
+
177
+ Returns:
178
+ Prompt depth path for this image, or None when prompt depth is disabled.
179
+ """
180
+ if args.prompt_depth is not None:
181
+ return args.prompt_depth
182
+ prompt_depth_dir = args.prompt_depth_dir
183
+ if prompt_depth_dir is None and args.mode == "lidar":
184
+ input_path = args.input_path or MODE_INPUT_DIRS[args.mode]
185
+ prompt_depth_dir = input_path if input_path.is_dir() else input_path.parent
186
+ if prompt_depth_dir is None:
187
+ return None
188
+
189
+ candidates = [
190
+ prompt_depth_dir / f"{image_path.stem}{ext}"
191
+ for ext in PROMPT_DEPTH_EXTENSIONS
192
+ ]
193
+ for candidate in candidates:
194
+ if candidate.exists() and candidate.resolve() != image_path.resolve():
195
+ return candidate
196
+ raise FileNotFoundError(
197
+ "Prompt depth file not found for "
198
+ f"{image_path}. Tried: {', '.join(str(candidate) for candidate in candidates)}"
199
+ )
200
+
201
+
202
+ def _case_paths(output_dir: Path, image_path: Path) -> CasePaths:
203
+ """Create deterministic artifact paths for one image."""
204
+ case_dir = output_dir / image_path.stem
205
+ stem = image_path.stem
206
+ return CasePaths(
207
+ case_dir=case_dir,
208
+ scene_ply=case_dir / f"{stem}.ply",
209
+ video=case_dir / f"{stem}.mp4",
210
+ html=case_dir / f"{stem}.html",
211
+ )
212
+
213
+
214
+ def _resolve_video_render_geometry(
215
+ intrinsics_px: torch.Tensor,
216
+ original_width: int,
217
+ original_height: int,
218
+ ) -> tuple[tuple[int, int], torch.Tensor]:
219
+ """Resolve capped video render shape and scaled camera intrinsics.
220
+
221
+ Args:
222
+ intrinsics_px: Pixel-space camera intrinsics with shape [3, 3].
223
+ original_width: Original image width in pixels.
224
+ original_height: Original image height in pixels.
225
+ Returns:
226
+ A tuple containing render image shape as (height, width) and pixel-space
227
+ intrinsics with shape [3, 3].
228
+ """
229
+ if original_width <= 0 or original_height <= 0:
230
+ raise ValueError(f"Invalid original image size: {original_width}x{original_height}")
231
+
232
+ scale = min(
233
+ 1.0,
234
+ float(DEFAULT_MAX_RENDER_LONG_EDGE) / float(max(original_width, original_height)),
235
+ math.sqrt(float(DEFAULT_MAX_RENDER_PIXELS) / float(original_width * original_height)),
236
+ )
237
+
238
+ if scale >= 1.0:
239
+ return (original_height, original_width), intrinsics_px
240
+
241
+ render_width = max(2, int(math.floor(original_width * scale)))
242
+ render_height = max(2, int(math.floor(original_height * scale)))
243
+ # Keep video dimensions even for common yuv420 encoders.
244
+ render_width = max(2, render_width - (render_width % 2))
245
+ render_height = max(2, render_height - (render_height % 2))
246
+ render_intrinsics_px = scale_intrinsics_px(
247
+ intrinsics_px=intrinsics_px,
248
+ src_width=original_width,
249
+ src_height=original_height,
250
+ dst_width=render_width,
251
+ dst_height=render_height,
252
+ )
253
+ return (render_height, render_width), render_intrinsics_px
254
+
255
+
256
+ def _expected_artifacts_done(paths: CasePaths, args: argparse.Namespace) -> bool:
257
+ """Return whether all requested artifacts already exist."""
258
+ expected = [paths.scene_ply]
259
+ if not args.no_video:
260
+ expected.append(paths.video)
261
+ if args.export_html:
262
+ expected.append(paths.html)
263
+ return all(path.exists() for path in expected)
264
+
265
+
266
+ def _needs_only_conversion(paths: CasePaths, args: argparse.Namespace) -> bool:
267
+ """Return whether inference is done but requested converted outputs are missing."""
268
+ if not paths.scene_ply.exists():
269
+ return False
270
+ if not args.no_video and not paths.video.exists():
271
+ return False
272
+ return not _expected_artifacts_done(paths, args)
273
+
274
+
275
+ def _disable_unavailable_optional_outputs(args: argparse.Namespace) -> None:
276
+ """Skip optional outputs whose external dependencies are unavailable."""
277
+ if not args.no_video and not is_gsplat_available():
278
+ print("[batch] Skipping video rendering; optional gsplat is unavailable.")
279
+ args.no_video = True
280
+ if (
281
+ args.export_html
282
+ and shutil.which(SPLAT_TRANSFORM) is None
283
+ and not Path(SPLAT_TRANSFORM).exists()
284
+ ):
285
+ print("[batch] Skipping HTML export; optional converter is unavailable.")
286
+ args.export_html = False
287
+
288
+
289
+ def _build_splat_transform_command(
290
+ scene_ply: Path,
291
+ output_path: Path,
292
+ viewer_settings: Path,
293
+ ) -> list[str]:
294
+ """Build the fixed SH0 HTML conversion command."""
295
+ command = [
296
+ SPLAT_TRANSFORM,
297
+ "-w",
298
+ "--viewer-settings",
299
+ str(viewer_settings),
300
+ str(scene_ply),
301
+ "--filter-harmonics",
302
+ "0",
303
+ str(output_path),
304
+ ]
305
+ return command
306
+
307
+
308
+ def _run_splat_transform(
309
+ scene_ply: Path,
310
+ output_path: Path,
311
+ viewer_settings: Path,
312
+ ) -> None:
313
+ """Convert one Gaussian PLY into a quiet, paused HTML viewer."""
314
+ command = _build_splat_transform_command(scene_ply, output_path, viewer_settings)
315
+ subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
316
+ patch_supersplat_html_auto_rotate(output_path)
317
+
318
+
319
+ def _convert_scene_if_requested(
320
+ export_html: bool,
321
+ paths: CasePaths,
322
+ viewer_settings: Path | None,
323
+ ) -> None:
324
+ """Convert scene.ply into the default HTML viewer when requested."""
325
+ if not export_html:
326
+ return
327
+ if viewer_settings is None:
328
+ raise RuntimeError("Viewer settings are required for HTML export.")
329
+ _run_splat_transform(paths.scene_ply, paths.html, viewer_settings)
330
+
331
+
332
+ def _validate_batch_prompt_configuration(cfg: Any, args: argparse.Namespace) -> bool:
333
+ """Validate prompt configuration for batch inference."""
334
+ prompt_depth_enabled = (
335
+ args.mode == "lidar"
336
+ or args.prompt_depth is not None
337
+ or args.prompt_depth_dir is not None
338
+ )
339
+ return validate_prompt_configuration(cfg, prompt_depth_enabled)
340
+
341
+
342
+ @torch.inference_mode()
343
+ def _run_one_image(
344
+ args: argparse.Namespace,
345
+ image_path: Path,
346
+ paths: CasePaths,
347
+ encoder: Any,
348
+ decoder: Any,
349
+ prompt_enabled: bool,
350
+ device: torch.device,
351
+ viewer_settings: Path | None,
352
+ ) -> None:
353
+ """Run inference and export artifacts for one input image."""
354
+ paths.case_dir.mkdir(parents=True, exist_ok=True)
355
+
356
+ image_bundle = load_demo_image_bundle(
357
+ image_path=image_path,
358
+ focal_length_px=args.focal_px,
359
+ focal_length_mm=args.focal_mm,
360
+ intrinsics_override_path=args.intrinsics_file,
361
+ )
362
+ original_height, original_width = image_bundle.original_image_shape
363
+ _, inference_height, inference_width = image_bundle.inference_image.shape
364
+ render_image_shape, render_intrinsics_px = _resolve_video_render_geometry(
365
+ intrinsics_px=image_bundle.original_intrinsics.intrinsics_px,
366
+ original_width=original_width,
367
+ original_height=original_height,
368
+ )
369
+ prompt_inputs = None
370
+ if prompt_enabled:
371
+ prompt_depth_path = _resolve_prompt_depth_path(args, image_path)
372
+ if prompt_depth_path is None:
373
+ raise ValueError("Prompt-conditioned inference requires --prompt-depth or --prompt-depth-dir.")
374
+ prompt_inputs = load_prompt_depth_tensors(
375
+ prompt_depth_path=prompt_depth_path,
376
+ image_shape=(inference_height, inference_width),
377
+ )
378
+
379
+ encoder_output = run_single_image_inference(
380
+ encoder=encoder,
381
+ image=image_bundle.inference_image,
382
+ intrinsics_px=image_bundle.inference_intrinsics.intrinsics_px,
383
+ device=device,
384
+ prompt_inputs=prompt_inputs,
385
+ )
386
+
387
+ final_gaussians = encoder_output["gaussians"]
388
+ if not args.disable_floater_filter:
389
+ final_gaussians = filter_final_gaussian_floaters(final_gaussians)
390
+
391
+ save_ply(
392
+ gaussians=final_gaussians,
393
+ f_px=image_bundle.inference_intrinsics.focal_length_px,
394
+ image_shape=(inference_height, inference_width),
395
+ path=paths.scene_ply,
396
+ )
397
+
398
+ if not args.no_video:
399
+ render_novel_view_video_from_single_view(
400
+ decoder=decoder,
401
+ gaussians=final_gaussians,
402
+ render_intrinsics_px=render_intrinsics_px,
403
+ render_image_shape=render_image_shape,
404
+ output_path=paths.video,
405
+ )
406
+
407
+ _convert_scene_if_requested(args.export_html, paths, viewer_settings)
408
+
409
+
410
+ def run_batch(args: argparse.Namespace) -> dict[str, Any]:
411
+ """Run the batch inference pipeline."""
412
+ images = _collect_images(args)
413
+ output_dir = _resolve_output_dir(args)
414
+ output_dir.mkdir(parents=True, exist_ok=True)
415
+ _disable_unavailable_optional_outputs(args)
416
+ viewer_settings = VIEWER_SETTINGS if args.export_html else None
417
+
418
+ succeeded = 0
419
+ skipped = 0
420
+ progress = Progress(
421
+ SpinnerColumn(style="cyan"),
422
+ TextColumn("[bold cyan]{task.description}"),
423
+ BarColumn(bar_width=36, complete_style="cyan", finished_style="green"),
424
+ TaskProgressColumn(),
425
+ TextColumn("{task.completed:.0f}/{task.total:.0f}"),
426
+ )
427
+ with progress:
428
+ task = progress.add_task("Preparing model", total=len(images))
429
+ cfg = load_demo_config(MODE_EXPERIMENTS[args.mode])
430
+ prompt_enabled = _validate_batch_prompt_configuration(cfg, args)
431
+ checkpoint_path = _resolve_checkpoint_path(
432
+ args.checkpoint or MODE_CHECKPOINTS[args.mode]
433
+ )
434
+ device = _resolve_device(args.device)
435
+ encoder, decoder = load_demo_model(cfg=cfg, checkpoint_path=checkpoint_path, device=device)
436
+ progress.update(task, description="Running inference")
437
+
438
+ for image_path in images:
439
+ paths = _case_paths(output_dir, image_path)
440
+ if not args.overwrite and _expected_artifacts_done(paths, args):
441
+ skipped += 1
442
+ progress.advance(task)
443
+ continue
444
+
445
+ try:
446
+ if not args.overwrite and _needs_only_conversion(paths, args):
447
+ _convert_scene_if_requested(
448
+ args.export_html,
449
+ paths,
450
+ viewer_settings,
451
+ )
452
+ else:
453
+ _run_one_image(
454
+ args=args,
455
+ image_path=image_path,
456
+ paths=paths,
457
+ encoder=encoder,
458
+ decoder=decoder,
459
+ prompt_enabled=prompt_enabled,
460
+ device=device,
461
+ viewer_settings=viewer_settings,
462
+ )
463
+ succeeded += 1
464
+ finally:
465
+ if device.type == "cuda":
466
+ torch.cuda.empty_cache()
467
+ progress.advance(task)
468
+
469
+ progress.update(task, description="Complete")
470
+
471
+ return {
472
+ "status": "success",
473
+ "succeeded": succeeded,
474
+ "skipped": skipped,
475
+ "output_dir": str(output_dir),
476
+ }
477
+
478
+
479
+ def main() -> None:
480
+ args = _parse_args()
481
+ result = run_batch(args)
482
+ print(json.dumps(result, indent=2, ensure_ascii=False))
483
+
484
+
485
+ if __name__ == "__main__":
486
+ main()
src/demo/infer_single_image.py ADDED
@@ -0,0 +1,889 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import imageio.v2 as imageio
10
+ import numpy as np
11
+
12
+ os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
13
+
14
+ import cv2
15
+ import torch
16
+ import torchvision.transforms as tf
17
+ from einops import rearrange, repeat
18
+ from hydra import compose, initialize_config_dir
19
+ from hydra.core.global_hydra import GlobalHydra
20
+ from omegaconf import OmegaConf
21
+ from PIL import ExifTags, Image, ImageOps, TiffTags
22
+ from scipy.spatial import cKDTree
23
+
24
+ from src.demo.config import RootCfg, load_typed_root_config
25
+ from src.model.decoder import Decoder, get_decoder
26
+ from src.model.encoder import Encoder, get_encoder
27
+ from src.utils.gaussians import Gaussians3D
28
+ from src.utils.io import save_video
29
+
30
+ DEFAULT_FOCAL_35MM_MM = 30.0
31
+ INFERENCE_HEIGHT = 1152
32
+ INFERENCE_WIDTH = 1536
33
+ DEFAULT_VIDEO_FPS = 10
34
+ DEFAULT_VIDEO_FRAMES = 60
35
+ DEFAULT_RENDER_CHUNK_SIZE = 8
36
+ PROMPT_DEPTH_EPS = 1e-6
37
+ PROMPT_DISPARITY_MIN_DEPTH = 1e-2
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ResolvedIntrinsics:
42
+ """Resolved intrinsics for one input image.
43
+
44
+ Args:
45
+ intrinsics_px: Pixel-space intrinsics with shape [3, 3].
46
+ focal_length_px: Pixel focal length used for export and trajectory sizing.
47
+ """
48
+
49
+ intrinsics_px: torch.Tensor
50
+ focal_length_px: float
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class PromptInputs:
55
+ """Prompt-conditioned depth inputs for one single-view demo sample.
56
+
57
+ Args:
58
+ prompt_disparity: Sparse prompt disparity with shape [1, H, W].
59
+ prompt_mask: Prompt validity mask with shape [1, H, W].
60
+ """
61
+
62
+ prompt_disparity: torch.Tensor
63
+ prompt_mask: torch.Tensor
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class DemoImageBundle:
68
+ """Original/render image data and inference image data for one demo sample.
69
+
70
+ Args:
71
+ original_image_shape: Original image shape as (height, width).
72
+ inference_image: Inference-resolution image tensor with shape [3, H_inf, W_inf].
73
+ original_intrinsics: Pixel-space intrinsics resolved in original image space.
74
+ inference_intrinsics: Pixel-space intrinsics resolved in inference image space.
75
+ """
76
+
77
+ original_image_shape: tuple[int, int]
78
+ inference_image: torch.Tensor
79
+ original_intrinsics: ResolvedIntrinsics
80
+ inference_intrinsics: ResolvedIntrinsics
81
+
82
+
83
+ def _resolve_device(device_name: str) -> torch.device:
84
+ if device_name == "auto":
85
+ return torch.device("cuda" if torch.cuda.is_available() else "cpu")
86
+ return torch.device(device_name)
87
+
88
+
89
+ def _as_float(value: Any) -> float | None:
90
+ if value is None:
91
+ return None
92
+ if isinstance(value, tuple) and len(value) == 2:
93
+ numerator, denominator = value
94
+ if float(denominator) == 0:
95
+ return None
96
+ return float(numerator) / float(denominator)
97
+ try:
98
+ return float(value)
99
+ except (TypeError, ValueError):
100
+ return None
101
+
102
+
103
+ def convert_focallength_mm_to_px(width: float, height: float, focal_mm: float) -> float:
104
+ """Convert 35mm-equivalent focal length in millimeters to pixels."""
105
+ return focal_mm * math.sqrt(width**2.0 + height**2.0) / math.sqrt(36.0**2 + 24.0**2)
106
+
107
+
108
+ def build_intrinsics_from_focal_px(
109
+ focal_length_px: float,
110
+ width: int,
111
+ height: int,
112
+ ) -> torch.Tensor:
113
+ """Create a pixel-space OpenCV intrinsics matrix."""
114
+ return torch.tensor(
115
+ [
116
+ [focal_length_px, 0.0, (width - 1) / 2.0],
117
+ [0.0, focal_length_px, (height - 1) / 2.0],
118
+ [0.0, 0.0, 1.0],
119
+ ],
120
+ dtype=torch.float32,
121
+ )
122
+
123
+
124
+ def normalize_intrinsics(
125
+ intrinsics_px: torch.Tensor,
126
+ width: int,
127
+ height: int,
128
+ ) -> torch.Tensor:
129
+ """Convert pixel-space intrinsics into the normalized InfiniSplat convention."""
130
+ intrinsics_norm = intrinsics_px.clone()
131
+ intrinsics_norm[0] = intrinsics_norm[0] / float(width)
132
+ intrinsics_norm[1] = intrinsics_norm[1] / float(height)
133
+ return intrinsics_norm
134
+
135
+
136
+ def scale_intrinsics_px(
137
+ intrinsics_px: torch.Tensor,
138
+ src_width: int,
139
+ src_height: int,
140
+ dst_width: int,
141
+ dst_height: int,
142
+ ) -> torch.Tensor:
143
+ """Scale pixel-space intrinsics from one image resolution to another."""
144
+ scaled = intrinsics_px.clone()
145
+ scale_x = float(dst_width) / float(src_width)
146
+ scale_y = float(dst_height) / float(src_height)
147
+ scaled[0] = scaled[0] * scale_x
148
+ scaled[1] = scaled[1] * scale_y
149
+ return scaled
150
+
151
+
152
+ def _extract_exif_dict(image: Image.Image) -> dict[str, Any]:
153
+ exif_block = image.getexif().get_ifd(0x8769)
154
+ exif_dict = {ExifTags.TAGS[k]: v for k, v in exif_block.items() if k in ExifTags.TAGS}
155
+ tiff_tags = image.getexif()
156
+ tiff_dict = {TiffTags.TAGS_V2[k].name: v for k, v in tiff_tags.items() if k in TiffTags.TAGS_V2}
157
+ return {**exif_dict, **tiff_dict}
158
+
159
+
160
+ def _extract_exif_focal_length_px(
161
+ image: Image.Image,
162
+ width: int,
163
+ height: int,
164
+ ) -> float | None:
165
+ exif_dict = _extract_exif_dict(image)
166
+ focal_35mm = _as_float(
167
+ exif_dict.get("FocalLengthIn35mmFilm", exif_dict.get("FocalLenIn35mmFilm"))
168
+ )
169
+ if focal_35mm is None or focal_35mm < 1.0:
170
+ focal_mm = _as_float(exif_dict.get("FocalLength"))
171
+ if focal_mm is None:
172
+ return None
173
+ if focal_mm < 10.0:
174
+ focal_35mm = focal_mm * 8.4
175
+ else:
176
+ focal_35mm = focal_mm
177
+ return convert_focallength_mm_to_px(width, height, focal_35mm)
178
+
179
+
180
+ def _load_intrinsics_override(path: Path) -> torch.Tensor:
181
+ cfg = OmegaConf.load(path)
182
+ container = OmegaConf.to_container(cfg, resolve=True)
183
+ if isinstance(container, dict):
184
+ if "intrinsics_px" in container:
185
+ matrix = container["intrinsics_px"]
186
+ elif "camera" in container and isinstance(container["camera"], dict) and "intrinsics_px" in container["camera"]:
187
+ matrix = container["camera"]["intrinsics_px"]
188
+ else:
189
+ raise KeyError(
190
+ f"Could not find a 3x3 intrinsics matrix in {path}. Expected `intrinsics_px` or `camera.intrinsics_px`."
191
+ )
192
+ else:
193
+ matrix = container
194
+ intrinsics_px = torch.tensor(matrix, dtype=torch.float32)
195
+ if intrinsics_px.shape != (3, 3):
196
+ raise ValueError(f"Expected a 3x3 intrinsics matrix in {path}, got {tuple(intrinsics_px.shape)}.")
197
+ return intrinsics_px
198
+
199
+
200
+ def resolve_intrinsics(
201
+ image: Image.Image,
202
+ focal_length_px: float | None = None,
203
+ focal_length_mm: float | None = None,
204
+ intrinsics_override_path: Path | None = None,
205
+ default_focal_35mm_mm: float = DEFAULT_FOCAL_35MM_MM,
206
+ ) -> ResolvedIntrinsics:
207
+ """Resolve pixel-space and normalized intrinsics for one image."""
208
+ width, height = image.size
209
+ if intrinsics_override_path is not None:
210
+ intrinsics_px = _load_intrinsics_override(intrinsics_override_path)
211
+ resolved_focal_px = float(intrinsics_px[0, 0].item())
212
+ elif focal_length_px is not None:
213
+ intrinsics_px = build_intrinsics_from_focal_px(focal_length_px, width, height)
214
+ resolved_focal_px = float(focal_length_px)
215
+ elif focal_length_mm is not None:
216
+ resolved_focal_px = convert_focallength_mm_to_px(width, height, focal_length_mm)
217
+ intrinsics_px = build_intrinsics_from_focal_px(resolved_focal_px, width, height)
218
+ else:
219
+ exif_focal_px = _extract_exif_focal_length_px(image, width, height)
220
+ if exif_focal_px is not None:
221
+ resolved_focal_px = exif_focal_px
222
+ intrinsics_px = build_intrinsics_from_focal_px(resolved_focal_px, width, height)
223
+ else:
224
+ resolved_focal_px = convert_focallength_mm_to_px(width, height, default_focal_35mm_mm)
225
+ intrinsics_px = build_intrinsics_from_focal_px(resolved_focal_px, width, height)
226
+
227
+ return ResolvedIntrinsics(
228
+ intrinsics_px=intrinsics_px,
229
+ focal_length_px=resolved_focal_px,
230
+ )
231
+
232
+
233
+ def load_demo_image_bundle(
234
+ image_path: Path,
235
+ focal_length_px: float | None = None,
236
+ focal_length_mm: float | None = None,
237
+ intrinsics_override_path: Path | None = None,
238
+ default_focal_35mm_mm: float = DEFAULT_FOCAL_35MM_MM,
239
+ ) -> DemoImageBundle:
240
+ """Load both original-resolution and inference-resolution image/intrinsics bundles."""
241
+ with Image.open(image_path) as image_pil:
242
+ image_rgb = ImageOps.exif_transpose(image_pil).convert("RGB")
243
+ original_width, original_height = image_rgb.size
244
+ original_intrinsics = resolve_intrinsics(
245
+ image=image_rgb,
246
+ focal_length_px=focal_length_px,
247
+ focal_length_mm=focal_length_mm,
248
+ intrinsics_override_path=intrinsics_override_path,
249
+ default_focal_35mm_mm=default_focal_35mm_mm,
250
+ )
251
+
252
+ target_height = INFERENCE_HEIGHT
253
+ target_width = INFERENCE_WIDTH
254
+
255
+ inference_image_rgb = image_rgb
256
+ inference_intrinsics = original_intrinsics
257
+ if (original_height, original_width) != (target_height, target_width):
258
+ inference_image_rgb = image_rgb.resize((target_width, target_height), Image.Resampling.BILINEAR)
259
+ inference_intrinsics_px = scale_intrinsics_px(
260
+ original_intrinsics.intrinsics_px,
261
+ src_width=original_width,
262
+ src_height=original_height,
263
+ dst_width=target_width,
264
+ dst_height=target_height,
265
+ )
266
+ inference_intrinsics = ResolvedIntrinsics(
267
+ intrinsics_px=inference_intrinsics_px,
268
+ focal_length_px=float(inference_intrinsics_px[0, 0].item()),
269
+ )
270
+
271
+ return DemoImageBundle(
272
+ original_image_shape=(original_height, original_width),
273
+ inference_image=tf.ToTensor()(inference_image_rgb),
274
+ original_intrinsics=original_intrinsics,
275
+ inference_intrinsics=inference_intrinsics,
276
+ )
277
+
278
+
279
+ def _to_single_channel_depth(depth: np.ndarray, depth_path: Path) -> np.ndarray:
280
+ """Convert loaded depth arrays to a single-channel float32 map."""
281
+ depth = np.asarray(depth)
282
+ while depth.ndim > 3:
283
+ singleton_axes = [axis for axis, size in enumerate(depth.shape) if size == 1]
284
+ if not singleton_axes:
285
+ break
286
+ depth = np.squeeze(depth, axis=singleton_axes[0])
287
+ if depth.ndim == 3 and depth.shape[0] in (1, 3) and depth.shape[2] not in (1, 3):
288
+ depth = np.moveaxis(depth, 0, -1)
289
+ if depth.ndim == 2:
290
+ return depth.astype(np.float32)
291
+ if depth.ndim == 3:
292
+ if depth.shape[2] == 1:
293
+ return depth[:, :, 0].astype(np.float32)
294
+ if np.issubdtype(depth.dtype, np.integer) and depth.shape[2] >= 3:
295
+ ch0 = depth[:, :, 0]
296
+ ch1 = depth[:, :, 1]
297
+ ch2 = depth[:, :, 2]
298
+ if np.array_equal(ch0, ch1) and np.array_equal(ch1, ch2):
299
+ return ch0.astype(np.float32)
300
+ return (
301
+ ch0.astype(np.float32) * (256.0**2)
302
+ + ch1.astype(np.float32) * 256.0
303
+ + ch2.astype(np.float32)
304
+ )
305
+ return depth[:, :, 0].astype(np.float32)
306
+ raise ValueError(f"Unsupported depth shape {depth.shape} for file: {depth_path}")
307
+
308
+
309
+ def _load_depth_from_png(depth_path: Path) -> np.ndarray:
310
+ raw = imageio.imread(depth_path)
311
+ depth = _to_single_channel_depth(raw, depth_path)
312
+ if np.issubdtype(raw.dtype, np.integer) and float(np.nanmax(depth)) > 255.0:
313
+ depth = depth / 1000.0
314
+ return depth.astype(np.float32)
315
+
316
+
317
+ def _decode_sparse_depth(
318
+ mask: np.ndarray,
319
+ value: np.ndarray,
320
+ depth_path: Path,
321
+ ) -> np.ndarray:
322
+ mask = np.asarray(mask).astype(bool)
323
+ value = np.asarray(value, dtype=np.float32)
324
+ depth = np.zeros(mask.shape, dtype=np.float32)
325
+ if value.shape == mask.shape:
326
+ depth[mask] = value[mask]
327
+ return depth
328
+
329
+ value_flat = value.reshape(-1)
330
+ valid_count = int(mask.sum())
331
+ if value_flat.size < valid_count:
332
+ raise ValueError(
333
+ f"Depth value count ({value_flat.size}) is smaller than the mask valid count "
334
+ f"({valid_count}): {depth_path}"
335
+ )
336
+ depth[mask] = value_flat[:valid_count]
337
+ return depth
338
+
339
+
340
+ def _load_depth_from_npz(depth_path: Path) -> np.ndarray:
341
+ with np.load(depth_path, allow_pickle=False) as npz_data:
342
+ if len(npz_data.files) == 0:
343
+ raise ValueError(f"Empty npz depth file: {depth_path}")
344
+ if "mask" in npz_data.files and "value" in npz_data.files:
345
+ return _decode_sparse_depth(
346
+ npz_data["mask"],
347
+ npz_data["value"],
348
+ depth_path,
349
+ )
350
+ preferred_keys = ("depth", "data", "depth_map", "arr_0")
351
+ key = next((k for k in preferred_keys if k in npz_data.files), npz_data.files[0])
352
+ depth = np.asarray(npz_data[key])
353
+ return _to_single_channel_depth(depth, depth_path)
354
+
355
+
356
+ def _find_h5_dataset(node: Any) -> Any:
357
+ preferred_keys = ("dataset", "depth", "data")
358
+ for key in preferred_keys:
359
+ if key in node:
360
+ child = node[key]
361
+ if child.__class__.__name__ == "Dataset":
362
+ return child
363
+ for key in node.keys():
364
+ child = node[key]
365
+ if child.__class__.__name__ == "Dataset":
366
+ return child
367
+ for key in node.keys():
368
+ child = node[key]
369
+ if child.__class__.__name__ == "Group":
370
+ found = _find_h5_dataset(child)
371
+ if found is not None:
372
+ return found
373
+ return None
374
+
375
+
376
+ def _load_depth_from_h5(depth_path: Path) -> np.ndarray:
377
+ import h5py
378
+
379
+ with h5py.File(depth_path, "r") as h5_file:
380
+ dataset = _find_h5_dataset(h5_file)
381
+ if dataset is None:
382
+ raise ValueError(f"No dataset found in h5 file: {depth_path}")
383
+ depth = np.asarray(dataset)
384
+ return _to_single_channel_depth(depth, depth_path)
385
+
386
+
387
+ def _load_depth_from_npy(depth_path: Path) -> np.ndarray:
388
+ return _to_single_channel_depth(np.load(depth_path, allow_pickle=False), depth_path)
389
+
390
+
391
+ def _load_depth_from_exr(depth_path: Path) -> np.ndarray:
392
+ depth = cv2.imread(str(depth_path), cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
393
+ if depth is None:
394
+ raise ValueError(f"Failed to read EXR depth file: {depth_path}")
395
+ return _to_single_channel_depth(depth, depth_path)
396
+
397
+
398
+ def _read_depth_array(depth_path: Path) -> np.ndarray:
399
+ """Load a raw depth array from a supported prompt depth file."""
400
+ ext = depth_path.suffix.lower()
401
+ if ext == ".png":
402
+ return _load_depth_from_png(depth_path)
403
+ if ext == ".npz":
404
+ return _load_depth_from_npz(depth_path)
405
+ if ext in (".hdf5", ".h5"):
406
+ return _load_depth_from_h5(depth_path)
407
+ if ext == ".npy":
408
+ return _load_depth_from_npy(depth_path)
409
+ if ext == ".exr":
410
+ return _load_depth_from_exr(depth_path)
411
+ raise ValueError(f"Unsupported prompt depth extension `{ext}`: {depth_path}")
412
+
413
+
414
+ def load_depth(
415
+ depth_path: Path,
416
+ tar_size: tuple[int, int],
417
+ num_samples: int = 1500,
418
+ min_prompt: int = 1,
419
+ max_prompt: int = 100,
420
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
421
+ """Load dense depth and build a sparse prompt map for InfiniDepth."""
422
+ depth = _read_depth_array(depth_path).astype(np.float32)
423
+ depth[~np.isfinite(depth)] = 0.0
424
+ depth = cv2.resize(depth, tar_size[::-1], interpolation=cv2.INTER_NEAREST)
425
+
426
+ depth_mask = ((depth > min_prompt) & (depth < max_prompt)).astype(np.float32)
427
+ valid_depth = depth * depth_mask
428
+ if (valid_depth > 0.1).sum() > num_samples:
429
+ height, width = depth.shape
430
+ sample_depth = valid_depth.reshape(-1)
431
+ nonzero_index = np.flatnonzero(sample_depth > 0.1)
432
+ index = np.random.permutation(nonzero_index)[:num_samples]
433
+ sample_mask = np.ones_like(sample_depth)
434
+ sample_mask[index] = 0.0
435
+ sample_depth[sample_mask.astype(bool)] = 0.0
436
+ sample_depth = sample_depth.reshape(height, width)
437
+ else:
438
+ sample_depth = valid_depth
439
+
440
+ depth_tensor = torch.from_numpy(depth).unsqueeze(0).unsqueeze(0).float()
441
+ sample_depth_tensor = torch.from_numpy(sample_depth).unsqueeze(0).unsqueeze(0).float()
442
+ depth_mask_tensor = torch.from_numpy(depth_mask).unsqueeze(0).unsqueeze(0)
443
+ return depth_tensor, sample_depth_tensor, depth_mask_tensor
444
+
445
+
446
+ def load_prompt_depth_tensors(
447
+ prompt_depth_path: Path,
448
+ image_shape: tuple[int, int],
449
+ ) -> PromptInputs:
450
+ """Load one prompt depth file and build prompt tensors for InfiniDepth demo inference."""
451
+ if not prompt_depth_path.exists():
452
+ raise FileNotFoundError(f"Prompt depth file not found: {prompt_depth_path}")
453
+
454
+ _, prompt_depth_tensor, _ = load_depth(
455
+ depth_path=prompt_depth_path,
456
+ tar_size=image_shape,
457
+ )
458
+
459
+ prompt_depth = prompt_depth_tensor.squeeze(0).squeeze(0).numpy()
460
+ prompt_mask = (prompt_depth > PROMPT_DISPARITY_MIN_DEPTH).astype(np.float32)
461
+ valid_mask = prompt_mask > 0.0
462
+ if not valid_mask.any():
463
+ raise ValueError(
464
+ f"Prompt depth file `{prompt_depth_path}` does not contain any valid prompt pixels after resizing."
465
+ )
466
+
467
+ prompt_disparity = np.zeros_like(prompt_depth, dtype=np.float32)
468
+ prompt_disparity[valid_mask] = 1.0 / np.clip(
469
+ prompt_depth[valid_mask],
470
+ a_min=PROMPT_DEPTH_EPS,
471
+ a_max=None,
472
+ )
473
+
474
+ return PromptInputs(
475
+ prompt_disparity=torch.from_numpy(prompt_disparity).unsqueeze(0),
476
+ prompt_mask=torch.from_numpy(prompt_mask).unsqueeze(0),
477
+ )
478
+
479
+
480
+ def _requires_prompt_depth(cfg: RootCfg) -> bool:
481
+ """Return whether the current encoder requires an explicit prompt depth input."""
482
+ return cfg.model.encoder.name == "infinisplat_infinidepth"
483
+
484
+
485
+ def validate_prompt_configuration(cfg: RootCfg, prompt_depth_enabled: bool) -> bool:
486
+ """Validate whether prompt inputs match the selected encoder configuration."""
487
+ requires_prompt = _requires_prompt_depth(cfg)
488
+ if requires_prompt and not prompt_depth_enabled:
489
+ raise ValueError(
490
+ "`infinisplat_infinidepth` demo inference requires `--prompt-depth` because the encoder uses prompt-conditioned InfiniDepth."
491
+ )
492
+ if not requires_prompt and prompt_depth_enabled:
493
+ raise ValueError(
494
+ "`--prompt-depth` is only supported when the resolved encoder is `infinisplat_infinidepth`."
495
+ )
496
+ return requires_prompt
497
+
498
+
499
+ def build_single_view_batch(
500
+ image: torch.Tensor,
501
+ intrinsics_px: torch.Tensor,
502
+ device: torch.device | str = "cpu",
503
+ prompt_inputs: PromptInputs | None = None,
504
+ ) -> dict[str, torch.Tensor]:
505
+ """Build the minimal single-view batch required by the encoder."""
506
+ device = torch.device(device)
507
+ _, height, width = image.shape
508
+ intrinsics_norm = normalize_intrinsics(intrinsics_px, width, height)
509
+ eye = torch.eye(4, dtype=torch.float32, device=device)
510
+ context = {
511
+ "image": rearrange(image.to(device=device, dtype=torch.float32), "c h w -> 1 1 c h w"),
512
+ "intrinsics": rearrange(intrinsics_norm.to(device=device, dtype=torch.float32), "i j -> 1 1 i j"),
513
+ "extrinsics": rearrange(eye, "i j -> 1 1 i j"),
514
+ }
515
+ if prompt_inputs is not None:
516
+ context["prompt_disparity"] = rearrange(
517
+ prompt_inputs.prompt_disparity.to(device=device, dtype=torch.float32),
518
+ "c h w -> 1 1 c h w",
519
+ )
520
+ context["prompt_mask"] = rearrange(
521
+ prompt_inputs.prompt_mask.to(device=device, dtype=torch.float32),
522
+ "c h w -> 1 1 c h w",
523
+ )
524
+ return context
525
+
526
+
527
+ def load_demo_config(
528
+ experiment_name: str,
529
+ ) -> RootCfg:
530
+ """Compose the Hydra config used for demo inference."""
531
+ config_dir = Path(__file__).resolve().parents[2] / "config"
532
+ if GlobalHydra.instance().is_initialized():
533
+ GlobalHydra.instance().clear()
534
+ with initialize_config_dir(version_base=None, config_dir=str(config_dir)):
535
+ cfg_dict = compose(
536
+ config_name="inference",
537
+ overrides=[
538
+ f"+experiment={experiment_name}",
539
+ ],
540
+ )
541
+ return load_typed_root_config(cfg_dict)
542
+
543
+
544
+ def _extract_state_dict(checkpoint_path: Path) -> dict[str, torch.Tensor]:
545
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
546
+ if "state_dict" in checkpoint:
547
+ return checkpoint["state_dict"]
548
+ if isinstance(checkpoint, dict):
549
+ return checkpoint
550
+ raise TypeError(f"Unexpected checkpoint format at {checkpoint_path}.")
551
+
552
+
553
+ def _load_prefixed_state_dict(
554
+ module: torch.nn.Module,
555
+ state_dict: dict[str, torch.Tensor],
556
+ prefix: str,
557
+ ) -> None:
558
+ module_state = {
559
+ key[len(prefix):]: value
560
+ for key, value in state_dict.items()
561
+ if key.startswith(prefix)
562
+ }
563
+ if not module_state:
564
+ if len(module.state_dict()) == 0:
565
+ return
566
+ raise KeyError(f"Did not find any weights with prefix `{prefix}` in the checkpoint.")
567
+ missing_keys, unexpected_keys = module.load_state_dict(module_state, strict=True)
568
+ if missing_keys or unexpected_keys:
569
+ raise RuntimeError(
570
+ f"Unexpected state-dict mismatch for prefix `{prefix}`: "
571
+ f"missing={missing_keys}, unexpected={unexpected_keys}"
572
+ )
573
+
574
+
575
+ def load_demo_model(
576
+ cfg: RootCfg,
577
+ checkpoint_path: Path,
578
+ device: torch.device,
579
+ ) -> tuple[Encoder, Decoder]:
580
+ """Instantiate encoder/decoder and load checkpoint weights for demo inference."""
581
+ encoder = get_encoder(cfg.model.encoder)
582
+ decoder = get_decoder(cfg.model.decoder)
583
+
584
+ state_dict = _extract_state_dict(checkpoint_path)
585
+ _load_prefixed_state_dict(encoder, state_dict, "encoder.")
586
+ _load_prefixed_state_dict(decoder, state_dict, "decoder.")
587
+
588
+ encoder = encoder.to(device)
589
+ decoder = decoder.to(device)
590
+ encoder.eval()
591
+ decoder.eval()
592
+ return encoder, decoder
593
+
594
+
595
+ def load_demo_encoder(
596
+ cfg: RootCfg,
597
+ checkpoint_path: Path,
598
+ device: torch.device,
599
+ ) -> Encoder:
600
+ """Instantiate the encoder and restore only its checkpoint weights."""
601
+ encoder = get_encoder(cfg.model.encoder)
602
+ state_dict = _extract_state_dict(checkpoint_path)
603
+ _load_prefixed_state_dict(encoder, state_dict, "encoder.")
604
+ encoder = encoder.to(device)
605
+ encoder.eval()
606
+ return encoder
607
+
608
+
609
+ @torch.inference_mode()
610
+ def run_single_image_inference(
611
+ encoder: Encoder,
612
+ image: torch.Tensor,
613
+ intrinsics_px: torch.Tensor,
614
+ device: torch.device,
615
+ prompt_inputs: PromptInputs | None = None,
616
+ ) -> dict[str, torch.Tensor]:
617
+ """Run encoder-only inference for one image."""
618
+ context = build_single_view_batch(
619
+ image=image,
620
+ intrinsics_px=intrinsics_px,
621
+ device=device,
622
+ prompt_inputs=prompt_inputs,
623
+ )
624
+ return encoder(context)
625
+
626
+
627
+ def _filter_gaussians_by_mask(
628
+ gaussians: Gaussians3D,
629
+ keep_mask: torch.Tensor,
630
+ ) -> Gaussians3D:
631
+ """Select a single-image subset of Gaussians.
632
+
633
+ Args:
634
+ gaussians: Gaussian container with shape [1, N, ...].
635
+ keep_mask: Boolean keep mask with shape [N].
636
+
637
+ Returns:
638
+ Filtered Gaussian container with shape [1, M, ...].
639
+ """
640
+ return Gaussians3D(
641
+ mean_vectors=gaussians.mean_vectors[:, keep_mask],
642
+ singular_values=gaussians.singular_values[:, keep_mask],
643
+ quaternions=gaussians.quaternions[:, keep_mask],
644
+ colors=gaussians.colors[:, keep_mask],
645
+ opacities=gaussians.opacities[:, keep_mask],
646
+ covariances=gaussians.covariances[:, keep_mask] if gaussians.covariances is not None else None,
647
+ )
648
+
649
+
650
+ @torch.inference_mode()
651
+ def filter_final_gaussian_floaters(gaussians: Gaussians3D) -> Gaussians3D:
652
+ """Remove spatial outliers from final single-image Gaussians."""
653
+ if gaussians.mean_vectors.shape[0] != 1:
654
+ raise ValueError("Single-image floater filtering expects batch size 1.")
655
+
656
+ total = int(gaussians.mean_vectors.shape[1])
657
+ points = gaussians.mean_vectors[0]
658
+ candidate_indices = torch.nonzero(
659
+ torch.isfinite(points).all(dim=-1),
660
+ as_tuple=False,
661
+ ).flatten()
662
+ if candidate_indices.numel() == 0:
663
+ return gaussians
664
+
665
+ candidate_points = points[candidate_indices].detach().float().cpu().numpy()
666
+ if len(candidate_points) < 2:
667
+ return gaussians
668
+ neighbor_count = min(16, len(candidate_points) - 1)
669
+ distances, _ = cKDTree(candidate_points).query(
670
+ candidate_points,
671
+ k=neighbor_count + 1,
672
+ )
673
+ mean_distances = distances[:, 1:].mean(axis=1)
674
+ threshold = mean_distances.mean() + 2.5 * mean_distances.std()
675
+ inlier_indices = np.flatnonzero(mean_distances <= threshold)
676
+
677
+ keep_mask = torch.zeros(total, device=points.device, dtype=torch.bool)
678
+ if inlier_indices.size > 0:
679
+ inlier_indices_tensor = torch.as_tensor(
680
+ inlier_indices,
681
+ device=points.device,
682
+ dtype=torch.long,
683
+ )
684
+ keep_mask[candidate_indices[inlier_indices_tensor]] = True
685
+ if not keep_mask.any():
686
+ return gaussians
687
+ return _filter_gaussians_by_mask(gaussians, keep_mask)
688
+
689
+
690
+ def _normalize_vector(vectors: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
691
+ return vectors / vectors.norm(dim=-1, keepdim=True).clamp_min(eps)
692
+
693
+
694
+ def _build_look_at_c2w(
695
+ eye_positions: torch.Tensor,
696
+ look_at: torch.Tensor,
697
+ world_up: torch.Tensor,
698
+ ) -> torch.Tensor:
699
+ """Construct OpenCV-style camera-to-world matrices from eye positions."""
700
+ forward = _normalize_vector(look_at.unsqueeze(0) - eye_positions)
701
+ right = torch.cross(forward, world_up.unsqueeze(0).expand_as(forward), dim=-1)
702
+ near_parallel = right.norm(dim=-1) < 1e-6
703
+ if near_parallel.any():
704
+ fallback_up = torch.tensor([0.0, 0.0, -1.0], device=eye_positions.device, dtype=eye_positions.dtype)
705
+ right = torch.where(
706
+ near_parallel[:, None],
707
+ torch.cross(forward, fallback_up.unsqueeze(0).expand_as(forward), dim=-1),
708
+ right,
709
+ )
710
+ right = _normalize_vector(right)
711
+ down = _normalize_vector(torch.cross(forward, right, dim=-1))
712
+
713
+ c2w = repeat(
714
+ torch.eye(4, dtype=eye_positions.dtype, device=eye_positions.device),
715
+ "i j -> t i j",
716
+ t=eye_positions.shape[0],
717
+ ).clone()
718
+ c2w[:, :3, 0] = right
719
+ c2w[:, :3, 1] = down
720
+ c2w[:, :3, 2] = forward
721
+ c2w[:, :3, 3] = eye_positions
722
+ return c2w
723
+
724
+
725
+ def create_demo_trajectory(
726
+ gaussians: Gaussians3D,
727
+ intrinsics_px: torch.Tensor,
728
+ image_shape: tuple[int, int],
729
+ ) -> tuple[torch.Tensor, torch.Tensor]:
730
+ """Create a single-image novel-view trajectory."""
731
+ device = gaussians.mean_vectors.device
732
+ height, width = image_shape
733
+ focal_length_px = float(intrinsics_px[0, 0].item())
734
+
735
+ points = gaussians.mean_vectors[0].detach().to(device=device, dtype=torch.float32)
736
+ valid_mask = torch.isfinite(points).all(dim=-1) & (points[:, 2] > 1e-4)
737
+ if valid_mask.any():
738
+ valid_points = points[valid_mask]
739
+ else:
740
+ valid_points = points[torch.isfinite(points).all(dim=-1)]
741
+ if valid_points.numel() == 0:
742
+ raise ValueError("Could not derive a valid trajectory because all gaussian centers are invalid.")
743
+
744
+ look_at = valid_points.median(dim=0).values
745
+ min_depth = torch.quantile(valid_points[:, 2], 0.1).clamp_min(1e-3)
746
+
747
+ diagonal = math.sqrt((width / focal_length_px) ** 2 + (height / focal_length_px) ** 2)
748
+ max_lateral_offset = 0.08 * diagonal * float(min_depth.item())
749
+ max_medial_offset = 0.15 * float(min_depth.item())
750
+
751
+ phase = torch.linspace(
752
+ 0.0,
753
+ 1.0,
754
+ steps=DEFAULT_VIDEO_FRAMES,
755
+ device=device,
756
+ dtype=torch.float32,
757
+ )
758
+ eye_positions = torch.stack(
759
+ [
760
+ max_lateral_offset * torch.sin(2.0 * torch.pi * phase),
761
+ torch.zeros(DEFAULT_VIDEO_FRAMES, device=device),
762
+ max_medial_offset * (1.0 - torch.cos(2.0 * torch.pi * phase)) / 2.0,
763
+ ],
764
+ dim=-1,
765
+ )
766
+
767
+ c2w = _build_look_at_c2w(
768
+ eye_positions=eye_positions,
769
+ look_at=look_at,
770
+ world_up=torch.tensor([0.0, -1.0, 0.0], device=device, dtype=torch.float32),
771
+ )
772
+ extrinsics = torch.linalg.inv(c2w)
773
+ intrinsics_norm = normalize_intrinsics(intrinsics_px, width, height).to(device=device, dtype=torch.float32)
774
+ intrinsics_norm = repeat(intrinsics_norm, "i j -> 1 t i j", t=DEFAULT_VIDEO_FRAMES)
775
+ return extrinsics.unsqueeze(0), intrinsics_norm
776
+
777
+
778
+ @torch.inference_mode()
779
+ def render_novel_view_video_from_single_view(
780
+ decoder: Decoder,
781
+ gaussians: Gaussians3D,
782
+ render_intrinsics_px: torch.Tensor,
783
+ render_image_shape: tuple[int, int],
784
+ output_path: Path,
785
+ ) -> Path:
786
+ """Render an RGB novel-view video for a single-image gaussian scene."""
787
+ device = gaussians.mean_vectors.device
788
+ extrinsics, intrinsics_norm = create_demo_trajectory(
789
+ gaussians=gaussians,
790
+ intrinsics_px=render_intrinsics_px.to(device=device, dtype=torch.float32),
791
+ image_shape=render_image_shape,
792
+ )
793
+
794
+ frames: list[torch.Tensor] = []
795
+ num_frames = extrinsics.shape[1]
796
+ for start in range(0, num_frames, DEFAULT_RENDER_CHUNK_SIZE):
797
+ end = min(start + DEFAULT_RENDER_CHUNK_SIZE, num_frames)
798
+ rendered = decoder.forward(
799
+ gaussians=gaussians,
800
+ extrinsics=extrinsics[:, start:end],
801
+ intrinsics=intrinsics_norm[:, start:end],
802
+ image_shape=render_image_shape,
803
+ )
804
+ frames.extend(frame.detach().cpu() for frame in rendered[0])
805
+
806
+ save_video(frames, output_path, fps=DEFAULT_VIDEO_FPS)
807
+ return output_path
808
+
809
+
810
+ def _resolve_checkpoint_path(checkpoint: str | Path) -> Path:
811
+ checkpoint_path = Path(checkpoint)
812
+ if not checkpoint_path.exists():
813
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
814
+ if not checkpoint_path.is_file():
815
+ raise ValueError(f"Checkpoint path is not a file: {checkpoint_path}")
816
+ return checkpoint_path
817
+
818
+
819
+ def patch_supersplat_html_auto_rotate(html_path: Path) -> None:
820
+ """Patch SuperSplat HTML so generated camera animation starts paused.
821
+
822
+ Args:
823
+ html_path: Generated SuperSplat HTML path.
824
+
825
+ Returns:
826
+ None.
827
+ """
828
+ source = html_path.read_text(encoding="utf-8")
829
+ old = "noanim: url.searchParams.has('noanim'),"
830
+ new = "noanim: true,"
831
+ if new in source:
832
+ return
833
+ if old not in source:
834
+ raise RuntimeError(
835
+ "Could not pause SuperSplat HTML animation; the viewer bundle changed."
836
+ )
837
+ html_path.write_text(source.replace(old, new, 1), encoding="utf-8")
838
+
839
+
840
+ def patch_supersplat_html_viewer_bridge(
841
+ html_path: Path,
842
+ viewer_script_path: Path | None = None,
843
+ ) -> None:
844
+ """Report SuperSplat's first rendered frame to its embedding container."""
845
+ source = html_path.read_text(encoding="utf-8")
846
+ marker = "data-infinisplat-viewer-bridge"
847
+ first_frame_hook = "window.firstFrame?.();"
848
+ if marker in source:
849
+ return
850
+ hook_source = source
851
+ if viewer_script_path is not None:
852
+ hook_source += viewer_script_path.read_text(encoding="utf-8")
853
+ if first_frame_hook not in hook_source or "</head>" not in source:
854
+ raise RuntimeError(
855
+ "Could not install the SuperSplat viewer bridge; the viewer bundle changed."
856
+ )
857
+
858
+ bridge = f"""
859
+ <script {marker}>
860
+ (() => {{
861
+ let viewerState = "loading";
862
+ const setViewerState = (state) => {{
863
+ if (viewerState === "ready" && state !== "ready") return;
864
+ viewerState = state;
865
+ const frame = window.frameElement;
866
+ const host = frame?.closest("[data-infinisplat-viewer]");
867
+ if (host) host.dataset.viewerState = state;
868
+ window.parent.postMessage(
869
+ {{ type: "infinisplat:viewer-state", state }},
870
+ "*"
871
+ );
872
+ }};
873
+
874
+ window.firstFrame = () => setViewerState("ready");
875
+ window.addEventListener("error", () => setViewerState("error"));
876
+ window.addEventListener(
877
+ "unhandledrejection",
878
+ () => setViewerState("error")
879
+ );
880
+ window.setTimeout(() => {{
881
+ if (viewerState !== "ready") setViewerState("timeout");
882
+ }}, 60000);
883
+ }})();
884
+ </script>
885
+ """
886
+ html_path.write_text(
887
+ source.replace("</head>", f"{bridge}\n </head>", 1),
888
+ encoding="utf-8",
889
+ )
src/model/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Model package for InfiniSplat."""
src/model/decoder/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.model.decoder.decoder import Decoder
2
+ from src.model.decoder.decoder_gsplat import DecoderGsplat, DecoderGsplatCfg
3
+
4
+ DECODERS = {
5
+ "gsplat": DecoderGsplat,
6
+ }
7
+
8
+ DecoderCfg = DecoderGsplatCfg
9
+
10
+
11
+ def get_decoder(cfg: DecoderCfg) -> Decoder:
12
+ decoder = DECODERS[cfg.name]
13
+ decoder = decoder(cfg)
14
+ return decoder
15
+
16
+
17
+ __all__ = [
18
+ "Decoder",
19
+ "DecoderCfg",
20
+ "DecoderGsplat",
21
+ "DecoderGsplatCfg",
22
+ "get_decoder",
23
+ ]
src/model/decoder/decoder.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, TypeVar
3
+
4
+ from jaxtyping import Float
5
+ from torch import Tensor, nn
6
+
7
+ from src.utils.gaussians import Gaussians3D
8
+
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class Decoder(nn.Module, ABC, Generic[T]):
14
+ cfg: T
15
+
16
+ def __init__(self, cfg: T) -> None:
17
+ super().__init__()
18
+ self.cfg = cfg
19
+
20
+ @abstractmethod
21
+ def forward(
22
+ self,
23
+ gaussians: Gaussians3D,
24
+ extrinsics: Float[Tensor, "batch view 4 4"],
25
+ intrinsics: Float[Tensor, "batch view 3 3"],
26
+ image_shape: tuple[int, int],
27
+ ) -> Float[Tensor, "batch view 3 height width"]:
28
+ raise NotImplementedError
src/model/decoder/decoder_gsplat.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ import torch
5
+ from einops import rearrange
6
+ from jaxtyping import Float
7
+ from torch import Tensor
8
+
9
+ from src.model.decoder.decoder import Decoder
10
+ from src.utils.color_space import linearRGB2sRGB
11
+ from src.utils.gaussians import Gaussians3D
12
+
13
+ try:
14
+ from gsplat import rasterization
15
+ except ImportError:
16
+ rasterization = None
17
+
18
+
19
+ def is_gsplat_available() -> bool:
20
+ """Return whether optional novel-view rendering is available."""
21
+ return rasterization is not None
22
+
23
+
24
+ @dataclass
25
+ class DecoderGsplatCfg:
26
+ name: Literal["gsplat"]
27
+ background_color: list[float]
28
+
29
+
30
+ class DecoderGsplat(Decoder[DecoderGsplatCfg]):
31
+ background_color: Float[Tensor, "3"]
32
+
33
+ def __init__(
34
+ self,
35
+ cfg: DecoderGsplatCfg,
36
+ ) -> None:
37
+ super().__init__(cfg)
38
+ self.register_buffer(
39
+ "background_color",
40
+ torch.tensor(cfg.background_color, dtype=torch.float32),
41
+ persistent=False,
42
+ )
43
+
44
+ def forward(
45
+ self,
46
+ gaussians: Gaussians3D,
47
+ extrinsics: Float[Tensor, "batch view 4 4"],
48
+ intrinsics: Float[Tensor, "batch view 3 3"],
49
+ image_shape: tuple[int, int],
50
+ ) -> Float[Tensor, "batch view 3 height width"]:
51
+ if rasterization is None:
52
+ raise RuntimeError(
53
+ "Novel-view rendering requires the optional `gsplat` package."
54
+ )
55
+
56
+ h, w = image_shape
57
+
58
+ xyzs = gaussians.mean_vectors.float()
59
+ opacitys = gaussians.opacities.float()
60
+ rotations = gaussians.quaternions.float()
61
+ scales = gaussians.singular_values.float()
62
+ colors = gaussians.colors.float()
63
+ covariances = gaussians.covariances.float() if gaussians.covariances is not None else None
64
+
65
+ # The public batch interface stores extrinsics as OpenCV world-to-camera.
66
+ test_w2c = extrinsics.float()
67
+ test_intr_normalized = intrinsics.float()
68
+ test_intr = test_intr_normalized.clone()
69
+ test_intr[:, :, 0] = test_intr_normalized[:, :, 0] * w
70
+ test_intr[:, :, 1] = test_intr_normalized[:, :, 1] * h
71
+
72
+ rendering, alpha, _ = rasterization(
73
+ xyzs,
74
+ rotations,
75
+ scales,
76
+ opacitys,
77
+ colors,
78
+ test_w2c,
79
+ test_intr,
80
+ w,
81
+ h,
82
+ sh_degree=None,
83
+ render_mode="RGB",
84
+ packed=True,
85
+ covars=covariances,
86
+ eps2d=1e-8,
87
+ )
88
+
89
+ rendered_rgb = rearrange(rendering, "b v h w c -> b v c h w")
90
+ alpha_rgb = rearrange(alpha, "b v h w 1 -> b v 1 h w")
91
+
92
+ rendered_rgb = linearRGB2sRGB(rendered_rgb)
93
+
94
+ backgrounds = rearrange(self.background_color.to(rendered_rgb), "c -> 1 1 c 1 1")
95
+ rendered_rgb = rendered_rgb + backgrounds * (1.0 - alpha_rgb)
96
+ rendered_rgb = rendered_rgb.clamp(0.0, 1.0)
97
+
98
+ return rendered_rgb
src/model/encoder/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.model.encoder.encoder import Encoder
2
+ from src.model.encoder.encoder_infinidepth_query import (
3
+ EncoderInfiniDepthQuery,
4
+ EncoderInfiniDepthQueryCfg,
5
+ )
6
+ from src.model.encoder.encoder_infinisplat import EncoderInfiniSplat, EncoderInfiniSplatCfg
7
+
8
+ ENCODERS = {
9
+ "infinisplat": EncoderInfiniSplat,
10
+ "infinisplat_infinidepth": EncoderInfiniDepthQuery,
11
+ }
12
+
13
+ EncoderCfg = EncoderInfiniSplatCfg | EncoderInfiniDepthQueryCfg
14
+
15
+
16
+ def get_encoder(cfg: EncoderCfg) -> Encoder:
17
+ encoder = ENCODERS[cfg.name]
18
+ encoder = encoder(cfg)
19
+ return encoder
src/model/encoder/blocks/torchhub/dinov3/.docstr.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ paths:
2
+ - dinov3
3
+ exclude: dinov3/tests
4
+ skip_init: True
5
+ skip_private: True
6
+ fail_under: 0
src/model/encoder/blocks/torchhub/dinov3/.gitignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ build/
2
+ dist/
3
+ *.egg-info/
4
+ **/__pycache__/
5
+
6
+ **/.ipynb_checkpoints
7
+ **/.ipynb_checkpoints/**
8
+
9
+ **/notebooks
10
+
11
+ # Ignore shell scripts
12
+ *.sh
13
+
14
+ # Ignore swap files
15
+ *.swp
16
+
17
+ # Ignore vscode directory
18
+ .vscode/
src/model/encoder/blocks/torchhub/dinov3/CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to make participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all project spaces, and it also applies when
49
+ an individual is representing the project or its community in public spaces.
50
+ Examples of representing a project or community include using an official
51
+ project e-mail address, posting via an official social media account, or acting
52
+ as an appointed representative at an online or offline event. Representation of
53
+ a project may be further defined and clarified by project maintainers.
54
+
55
+ This Code of Conduct also applies outside the project spaces when there is a
56
+ reasonable belief that an individual's behavior may have a negative impact on
57
+ the project or its community.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported by contacting the project team at <opensource-conduct@meta.com>. All
63
+ complaints will be reviewed and investigated and will result in a response that
64
+ is deemed necessary and appropriate to the circumstances. The project team is
65
+ obligated to maintain confidentiality with regard to the reporter of an incident.
66
+ Further details of specific enforcement policies may be posted separately.
67
+
68
+ Project maintainers who do not follow or enforce the Code of Conduct in good
69
+ faith may face temporary or permanent repercussions as determined by other
70
+ members of the project's leadership.
71
+
72
+ ## Attribution
73
+
74
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
75
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
76
+
77
+ [homepage]: https://www.contributor-covenant.org
78
+
79
+ For answers to common questions about this code of conduct, see
80
+ https://www.contributor-covenant.org/faq
src/model/encoder/blocks/torchhub/dinov3/CONTRIBUTING.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to DINOv3
2
+ We want to make contributing to this project as easy and transparent as
3
+ possible.
4
+
5
+ ## Pull Requests
6
+ We actively welcome your pull requests.
7
+
8
+ 1. Fork the repo and create your branch from `main`.
9
+ 2. If you've added code that should be tested, add tests.
10
+ 3. If you've changed APIs, update the documentation.
11
+ 4. Ensure the test suite passes.
12
+ 5. Make sure your code lints.
13
+ 6. If you haven't already, complete the Contributor License Agreement ("CLA").
14
+
15
+ ## Contributor License Agreement ("CLA")
16
+ In order to accept your pull request, we need you to submit a CLA. You only need
17
+ to do this once to work on any of Meta's open source projects.
18
+
19
+ Complete your CLA here: <https://code.facebook.com/cla>
20
+
21
+ ## Issues
22
+ We use GitHub issues to track public bugs. Please ensure your description is
23
+ clear and has sufficient instructions to be able to reproduce the issue.
24
+
25
+ Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe
26
+ disclosure of security bugs. In those cases, please go through the process
27
+ outlined on that page and do not file a public issue.
28
+
29
+ ## License
30
+ By contributing to DINOv3, you agree that your contributions will be licensed
31
+ under the LICENSE.md file in the root directory of this source tree.
src/model/encoder/blocks/torchhub/dinov3/DATASETS.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dataset setups for DINOv3
2
+
3
+ ## Evaluations
4
+
5
+ ### Depth Estimation on NYU
6
+
7
+ Create a folder to host the [NYU dataset](https://cs.nyu.edu/~fergus/datasets/nyu_depth_v2.html) for example:
8
+
9
+ ```
10
+ export DEPTH_DATASETS_ROOT=${HOME}/datasets
11
+ mkdir -p ${DEPTH_DATASETS_ROOT}/NYU
12
+ ```
13
+
14
+ We use the NYU subset extracted by [BTS](https://github.com/cleinc/bts/blob/master/tensorflow/README.md) from the 120k samples of the original NYU raw dataset.
15
+
16
+ #### Option 1 -- Follow BTS's instructions
17
+ Please follow BTS instructions to create the dataset:
18
+ - [train set](https://github.com/cleinc/bts/blob/master/tensorflow/README.md#nyu-depvh-v2)
19
+ - [test set](https://github.com/cleinc/bts/blob/master/README.md#prepare-nyu-depth-v2-test-set).
20
+
21
+ Make sure you also download the train and test splits:
22
+ ```
23
+ wget https://github.com/cleinc/bts/blob/master/train_test_inputs/nyudepthv2_train_files_with_gt.txt -O ${DEPTH_DATASETS_ROOT}/NYU/nyu_train.txt
24
+ wget https://github.com/cleinc/bts/blob/master/train_test_inputs/nyudepthv2_test_files_with_gt.txt -O ${DEPTH_DATASETS_ROOT}/NYU/nyu_test.txt
25
+ ```
26
+
27
+ #### Option 2 (preferred) -- Download the readily availble dataset from BinsFormer
28
+ Alternatively, one can download the dataset from the following Google Drive [link](https://drive.google.com/file/d/1xI9ksHzCC_kUz6Z4FL_b1ttgj3RVHGwW/view?usp=sharing). If the Google Drive link is not available anymore, try Option 1.
29
+
30
+ Expected contents:
31
+ - `$DEPTH_DATASETS_ROOT/NYU/basement/[...]`
32
+ - `$DEPTH_DATASETS_ROOT/NYU/basement_0001a/[...]`
33
+ - `$DEPTH_DATASETS_ROOT/NYU/basement_0001b/[...]`
34
+ - `$DEPTH_DATASETS_ROOT/NYU/bathroom/[...]`
35
+ - `$DEPTH_DATASETS_ROOT/NYU/[...]`
36
+ - `$DEPTH_DATASETS_ROOT/NYU/study_room_0004/[...]`
37
+ - `$DEPTH_DATASETS_ROOT/NYU/study_room_0005a/[...]`
38
+ - `$DEPTH_DATASETS_ROOT/NYU/study_room_0005b/[...]`
39
+ - `$DEPTH_DATASETS_ROOT/NYU/nyu_test.txt`
40
+ - `$DEPTH_DATASETS_ROOT/NYU/nyu_train.txt`
41
+
42
+ Note: if data is downloaded with Option 2 make sure to rename `nyu` into `NYU`.
43
+
src/model/encoder/blocks/torchhub/dinov3/LICENSE.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DINOv3 License
2
+
3
+ *Last Updated: August 19, 2025*
4
+
5
+ **“Agreement”** means the terms and conditions for use, reproduction, distribution and modification of the DINO Materials set forth herein.
6
+
7
+ **“DINO Materials”** means, collectively, Documentation and the models, software and algorithms, including machine-learning model code, trained model weights, inference-enabling code, training-enabling code, fine-tuning enabling code, and other elements of the foregoing distributed by Meta and made available under this Agreement.
8
+
9
+ **“Documentation”** means the specifications, manuals and documentation accompanying
10
+ DINO Materials distributed by Meta.
11
+
12
+ **“Licensee”** or **“you”** means you, or your employer or any other person or entity (if you are entering into this Agreement on such person or entity’s behalf), of the age required under applicable laws, rules or regulations to provide legal consent and that has legal authority to bind your employer or such other person or entity if you are entering in this Agreement on their behalf.
13
+
14
+ **“Meta”** or **“we”** means Meta Platforms Ireland Limited (if you are located in or, if you are an entity, your principal place of business is in the EEA or Switzerland) or Meta Platforms, Inc. (if you are located outside of the EEA or Switzerland).
15
+
16
+ **“Sanctions”** means any economic or trade sanctions or restrictions administered or enforced by the United States (including the Office of Foreign Assets Control of the U.S. Department of the Treasury (“OFAC”), the U.S. Department of State and the U.S. Department of Commerce), the United Nations, the European Union, or the United Kingdom.
17
+
18
+ **“Trade Controls”** means any of the following: Sanctions and applicable export and import controls.
19
+
20
+ By clicking “I Accept” below or by using or distributing any portion or element of the DINO Materials, you agree to be bound by this Agreement.
21
+
22
+ ## 1. License Rights and Redistribution.
23
+
24
+ a. <ins>Grant of Rights</ins>. You are granted a non-exclusive, worldwide, non-transferable and royalty-free limited license under Meta’s intellectual property or other rights owned by Meta embodied in the DINO Materials to use, reproduce, distribute, copy, create derivative works of, and make modifications to the DINO Materials.
25
+
26
+ b. <ins>Redistribution and Use</ins>.
27
+
28
+ i. Distribution of DINO Materials, and any derivative works thereof, are subject to the terms of this Agreement. If you distribute or make the DINO Materials, or any derivative works thereof, available to a third party, you may only do so under the terms of this Agreement and you shall provide a copy of this Agreement with any such DINO Materials.
29
+
30
+ ii. If you submit for publication the results of research you perform on, using, or otherwise in connection with DINO Materials, you must acknowledge the use of DINO Materials in your publication.
31
+
32
+ iii. Your use of the DINO Materials must comply with applicable laws and regulations, including Trade Control Laws and applicable privacy and data protection laws.
33
+
34
+ iv. Your use of the DINO Materials will not involve or encourage others to reverse engineer, decompile or discover the underlying components of the DINO Materials.
35
+
36
+ v. You are not the target of Trade Controls and your use of DINO Materials must comply with Trade Controls. You agree not to use, or permit others to use, DINO Materials for any activities subject to the International Traffic in Arms Regulations (ITAR) or end uses prohibited by Trade Controls, including those related to military or warfare purposes, nuclear industries or applications, espionage, or the development or use of guns or illegal weapons.
37
+
38
+ ## 2. User Support.
39
+
40
+ Your use of the DINO Materials is done at your own discretion; Meta does not process any information nor provide any service in relation to such use. Meta is under no obligation to provide any support services for the DINO Materials. Any support provided is “as is”, “with all faults”, and without warranty of any kind.
41
+
42
+ ## 3. Disclaimer of Warranty.
43
+
44
+ UNLESS REQUIRED BY APPLICABLE LAW, THE DINO MATERIALS AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OF ANY KIND, AND META DISCLAIMS ALL WARRANTIES OF ANY KIND, BOTH EXPRESS AND IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING OR REDISTRIBUTING THE DINO MATERIALS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR USE OF THE DINO MATERIALS AND ANY OUTPUT AND RESULTS.
45
+
46
+ ## 4. Limitation of Liability.
47
+
48
+ IN NO EVENT WILL META OR ITS AFFILIATES BE LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, ARISING OUT OF THIS AGREEMENT, FOR ANY LOST PROFITS OR ANY DIRECT OR INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, EXEMPLARY OR PUNITIVE DAMAGES, EVEN IF META OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY OF THE FOREGOING.
49
+
50
+ ## 5. Intellectual Property.
51
+
52
+ a. Subject to Meta’s ownership of DINO Materials and derivatives made by or for Meta, with respect to any derivative works and modifications of the DINO Materials that are made by you, as between you and Meta, you are and will be the owner of such derivative works and modifications.
53
+
54
+ b. If you institute litigation or other proceedings against Meta or any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the DINO Materials, outputs or results, or any portion of any of the foregoing, constitutes infringement of intellectual property or other rights owned or licensable by you, then any licenses granted to you under this Agreement shall terminate as of the date such litigation or claim is filed or instituted. You will indemnify and hold harmless Meta from and against any claim by any third party arising out of or related to your use or distribution of the DINO Materials.
55
+
56
+ ## 6. Term and Termination.
57
+
58
+ The term of this Agreement will commence upon your acceptance of this Agreement or access to the DINO Materials and will continue in full force and effect until terminated in accordance with the terms and conditions herein. Meta may terminate this Agreement if you are in breach of any term or condition of this Agreement. Upon termination of this Agreement, you shall delete and cease use of the DINO Materials. Sections 3, 4 and 7 shall survive the termination of this Agreement.
59
+
60
+ ## 7. Governing Law and Jurisdiction.
61
+
62
+ This Agreement will be governed and construed under the laws of the State of California without regard to choice of law principles, and the UN Convention on Contracts for the International Sale of Goods does not apply to this Agreement. The courts of California shall have exclusive jurisdiction of any dispute arising out of this Agreement.
63
+
64
+ ## 8. Modifications and Amendments.
65
+
66
+ Meta may modify this Agreement from time to time; provided that they are similar in spirit to the current version of the Agreement, but may differ in detail to address new problems or concerns. All such changes will be effective immediately. Your continued use of the DINO Materials after any modification to this Agreement constitutes your agreement to such modification. Except as provided in this Agreement, no modification or addition to any provision of this Agreement will be binding unless it is in writing and signed by an authorized representative of both you and Meta.
src/model/encoder/blocks/torchhub/dinov3/MODEL_CARD.md ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card for DINOv3
2
+
3
+ DINOv3 is a family of versatile vision foundation models that outperforms the specialized state of the art across a broad range of settings, without fine-tuning. DINOv3 produces high-quality dense features that achieve outstanding performance on various vision tasks, significantly surpassing previous self- and weakly-supervised foundation models.
4
+
5
+ ## Model Details
6
+
7
+ These are Vision Transformer and ConvNeXt models trained following the method described in the DINOv3 paper. 12 models are provided:
8
+
9
+ - 10 models pretrained on web data (LVD-1689M dataset)
10
+ - 1 ViT-7B trained from scratch,
11
+ - 5 ViT-S/S+/B/L/H+ models distilled from the ViT-7B,
12
+ - 4 ConvNeXt-{T/S/B/L} models distilled from the ViT-7B,
13
+ - 2 models pretrained on satellite data (SAT-493M dataset)
14
+ - 1 ViT-7B trained from scratch
15
+ - 1 ViT-L distilled from the ViT-7B
16
+
17
+
18
+ Each Transformer-based model takes an image as input and returns a class token, patch tokens (and register tokens). These models follow a ViT architecture, with a patch size of 16. For a 224x224 image, this results in 1 class token + 4 register tokens + 196 patch tokens = 201 tokens (for DINOv2 with registers this resulted in 1 + 4 + 256 = 261 tokens).
19
+
20
+ The models can accept larger images provided the image shapes are multiples of the patch size (16). If this condition is not verified, the model will crop to the closest smaller multiple of the patch size.
21
+
22
+ ### Model Description
23
+
24
+ - **Developed by:** Meta AI
25
+ - **Model type:** Vision Transformer, ConvNeXt
26
+ - **License:** [DINOv3 License](https://ai.meta.com/resources/models-and-libraries/dinov3-license/)
27
+
28
+ ### Model Sources
29
+
30
+ - **Repository:** [https://github.com/facebookresearch/dinov3](https://github.com/facebookresearch/dinov3)
31
+ - **Paper:** [https://arxiv.org/abs/2508.10104](https://arxiv.org/abs/2508.10104)
32
+
33
+ ## Uses
34
+
35
+ The models are vision backbones providing multi-purpose features for downstream tasks.
36
+
37
+ ### Direct Use
38
+
39
+ The models can be used without fine-tuning, with downstream classifiers as simple as linear layers, to obtain competitive results:
40
+
41
+ - on image classification, using k-NN classifiers on the class token
42
+ - on image classification, with logistic regression classifiers applied on the class token
43
+ - on image classification, with a linear layer applied on the class token and the average of the patch tokens
44
+ - on image retrieval using nearest neighbors
45
+ - on geometric and semantic 3D keypoint correspondances
46
+ - on depth estimation, semantic segmentation, using linear layers
47
+ - on unsupervised object discovery
48
+ - on video segmentation tracking
49
+ - on video classification, using a small 4-layer attentive probe
50
+
51
+ ### Downstream Use
52
+
53
+ While fine-tuning the models can yield some gains, it is recommended to keep this option as a last resort: the frozen features are expected to provide good performance out-of-the-box.
54
+
55
+ ## Bias, Risks, and Limitations
56
+
57
+ Compared to DINOv2 and SEERv2, DINOv3 delivers somewhat consistent performance across income categories on geographical fairness and diversity, although with a notable performance drop in the low-income bucket compared to the highest-income bucket.
58
+
59
+ DINOv3 also achieves relatively good scores across different regions, improving over its predecessor DINOv2. However, a relative difference is still observed between Europe and Africa.
60
+
61
+ ### Recommendations
62
+
63
+ Fine-tuning is expected to increase the biases in the features produced by the model as they will be tuned to the fine-tuning labels.
64
+
65
+ ## How to Get Started with the Model
66
+
67
+ Use the code below to get started with the model.
68
+
69
+ ```python
70
+ import torch
71
+
72
+ model = torch.hub.load(
73
+ repo_or_dir='facebookresearch/dinov3',
74
+ model='<MODEL_NAME>',
75
+ weights='<PATH/OR/URL/TO/CHECKPOINT>',
76
+ )
77
+
78
+ # where MODEL_NAME can be one of:
79
+ # - dinov3_vits16
80
+ # - dinov3_vits16plus
81
+ # - dinov3_vitb16
82
+ # - dinov3_vitl16
83
+ # - dinov3_vith16plus
84
+ # - dinov3_vit7b16
85
+ # - dinov3_convnext_tiny
86
+ # - dinov3_convnext_small
87
+ # - dinov3_convnext_base
88
+ # - dinov3_convnext_large
89
+
90
+ # For instance
91
+ dinov3_vits16 = torch.hub.load(
92
+ repo_or_dir='facebookresearch/dinov3',
93
+ model='dinov3_vits16',
94
+ weights='<PATH/OR/URL/TO/DINOV3/VITS16/LVD1689M/CHECKPOINT>',
95
+ )
96
+ ```
97
+
98
+ ## Training Details
99
+
100
+ ### Training Data
101
+
102
+ - Web dataset (LVD-1689M): a curated dataset of 1,689 millions of images extracted from a large data
103
+ pool of 17 billions web images collected from public posts on Instagram
104
+
105
+ - Satellite dataset (SAT-493M): a dataset of 493 millions of 512x512 images sampled randomly from Maxar RGB ortho-rectified imagery at 0.6 meter resolution
106
+
107
+ ### Training Procedure
108
+
109
+ **Training objective:**
110
+
111
+ - DINO self-distillation loss with multi-crop
112
+ - iBOT masked-image modeling loss
113
+ - KoLeo regularization on [CLS] tokens
114
+ - Gram anchoring
115
+
116
+ - **Training regime:** PyTorch FSDP2 (with bf16 and fp8 matrix multiplications)
117
+
118
+ **Distillation:**
119
+
120
+ - Distillation follows the standard DINOv3 pretraining procedure, except the teacher is a frozen pretrained ViT-7B.
121
+
122
+ ## Evaluation
123
+
124
+ **Results**
125
+
126
+ The reader is referred to the associated paper for details on the evaluation protocols
127
+
128
+ *Results for ViT backbones pretrained (or distilled) on web (LVD-1689M)*
129
+
130
+ <table>
131
+ <tr>
132
+ <th></th>
133
+ <!-- <th></th> -->
134
+ <th colspan="4">Global Tasks</th>
135
+ <th colspan="5">Dense Tasks</th>
136
+ </tr>
137
+ <tr>
138
+ <th>Model</th>
139
+ <!-- <th>Dataset</th> -->
140
+ <th>IN-ReaL</th>
141
+ <th>IN-R</th>
142
+ <th>Obj.Net</th>
143
+ <th>Ox.-H</th>
144
+ <th>ADE20k</th>
145
+ <th>NYU↓</th>
146
+ <th>DAVIS</th>
147
+ <th>NAVI</th>
148
+ <th>SPair</th>
149
+ </tr>
150
+ <tr>
151
+ <td>DINOv3 ViT-S/16</td>
152
+ <!-- <td>LVD-1689M</td> -->
153
+ <td align="right">87.0</td>
154
+ <td align="right">60.4</td>
155
+ <td align="right">50.9</td>
156
+ <td align="right">49.5</td>
157
+ <td align="right">47.0</td>
158
+ <td align="right">0.403</td>
159
+ <td align="right">72.7</td>
160
+ <td align="right">56.3</td>
161
+ <td align="right">50.4</td>
162
+ </tr>
163
+ <tr>
164
+ <td>DINOv3 ViT-S+/16</td>
165
+ <!-- <td>LVD-1689M</td> -->
166
+ <td align="right">88.0</td>
167
+ <td align="right">68.8</td>
168
+ <td align="right">54.6</td>
169
+ <td align="right">50.0</td>
170
+ <td align="right">48.8</td>
171
+ <td align="right">0.399</td>
172
+ <td align="right">75.5</td>
173
+ <td align="right">57.1</td>
174
+ <td align="right">55.2</td>
175
+ </tr>
176
+ <tr>
177
+ <td>DINOv3 ViT-B/16</td>
178
+ <!-- <td>LVD-1689M</td> -->
179
+ <td align="right">89.3</td>
180
+ <td align="right">76.7</td>
181
+ <td align="right">64.1</td>
182
+ <td align="right">58.5</td>
183
+ <td align="right">51.8</td>
184
+ <td align="right">0.373</td>
185
+ <td align="right">77.2</td>
186
+ <td align="right">58.8</td>
187
+ <td align="right">57.2</td>
188
+ </tr>
189
+ <tr>
190
+ <td>DINOv3 ViT-L/16</td>
191
+ <!-- <td>LVD-1689M</td> -->
192
+ <td align="right">90.2</td>
193
+ <td align="right">88.1</td>
194
+ <td align="right">74.8</td>
195
+ <td align="right">63.1</td>
196
+ <td align="right">54.9</td>
197
+ <td align="right">0.352</td>
198
+ <td align="right">79.9</td>
199
+ <td align="right">62.3</td>
200
+ <td align="right">61.3</td>
201
+ </tr>
202
+ <tr>
203
+ <td>DINOv3 ViT-H+/16</td>
204
+ <!-- <td>LVD-1689M</td> -->
205
+ <td align="right">90.3</td>
206
+ <td align="right">90.0</td>
207
+ <td align="right">78.6</td>
208
+ <td align="right">64.5</td>
209
+ <td align="right">54.8</td>
210
+ <td align="right">0.352</td>
211
+ <td align="right">79.3</td>
212
+ <td align="right">63.3</td>
213
+ <td align="right">56.3</td>
214
+ </tr>
215
+ <tr>
216
+ <td>DINOv3 ViT-7B/16</td>
217
+ <!-- <td>LVD-1689M</td> -->
218
+ <td align="right">90.4</td>
219
+ <td align="right">91.1</td>
220
+ <td align="right">91.1</td>
221
+ <td align="right">72.8</td>
222
+ <td align="right">55.9</td>
223
+ <td align="right">0.309</td>
224
+ <td align="right">79.7</td>
225
+ <td align="right">64.4</td>
226
+ <td align="right">58.7</td>
227
+ </tr>
228
+ </table>
229
+
230
+ *Results for ConvNeXt backbones distilled on web (LVD-1689M)*
231
+
232
+ <table>
233
+ <tr>
234
+ <th></th>
235
+ <th colspan="6">Global Tasks</th>
236
+ <th colspan="2">Dense Tasks</th>
237
+ </tr>
238
+ <tr>
239
+ <th>Model</th>
240
+ <th colspan="2">IN-ReaL</th>
241
+ <th colspan="2">IN-R</th>
242
+ <th colspan="2">Obj.Net</th>
243
+ <th>ADE20k</th>
244
+ <th>NYU↓</th>
245
+ </tr>
246
+ <tr>
247
+ <td></th>
248
+ <td>@256px</td>
249
+ <td>@512px</td>
250
+ <td>@256px</td>
251
+ <td>@512px</td>
252
+ <td>@256px</td>
253
+ <td>@512px</td>
254
+ <td colspan="2"></td>
255
+ </tr>
256
+ <tr>
257
+ <td>DINOv3 ConvNeXt Tiny</td>
258
+ <td align="right">86.6</td>
259
+ <td align="right">87.7</td>
260
+ <td align="right">73.7</td>
261
+ <td align="right">74.1</td>
262
+ <td align="right">52.6</td>
263
+ <td align="right">58.7</td>
264
+ <td align="right">42.7</td>
265
+ <td align="right">0.448</td>
266
+ </tr>
267
+ <tr>
268
+ <td>DINOv3 ConvNeXt Small</td>
269
+ <td align="right">87.9</td>
270
+ <td align="right">88.7</td>
271
+ <td align="right">73.7</td>
272
+ <td align="right">74.1</td>
273
+ <td align="right">52.6</td>
274
+ <td align="right">58.7</td>
275
+ <td align="right">44.8</td>
276
+ <td align="right">0.432</td>
277
+ </tr>
278
+ <tr>
279
+ <td>DINOv3 ConvNeXt Base</td>
280
+ <td align="right">88.5</td>
281
+ <td align="right">89.2</td>
282
+ <td align="right">77.2</td>
283
+ <td align="right">78.2</td>
284
+ <td align="right">56.2</td>
285
+ <td align="right">61.3</td>
286
+ <td align="right">46.3</td>
287
+ <td align="right">0.420</td>
288
+ </tr>
289
+ <tr>
290
+ <td>DINOv3 ConvNeXt Large</td>
291
+ <td align="right">88.9</td>
292
+ <td align="right">89.4</td>
293
+ <td align="right">81.3</td>
294
+ <td align="right">82.4</td>
295
+ <td align="right">59.3</td>
296
+ <td align="right">65.2</td>
297
+ <td align="right">47.8</td>
298
+ <td align="right">0.403</td>
299
+ </tr>
300
+ </table>
301
+
302
+ *Results for ViT backbones pretrained (or distilled) on satellite (SAT-493M)*
303
+
304
+ <table>
305
+ <tr>
306
+ <th></th>
307
+ <th colspan="7">(GEO-Bench) Classification</th>
308
+ </tr>
309
+ <tr>
310
+ <th>Model</ht>
311
+ <th>m-BEnet</th>
312
+ <th>m-brick-kiln
313
+ <th>m-eurosat</th>
314
+ <th>m-forestnet</th>
315
+ <th>m-pv4ger</th>
316
+ <th>m-so2sat</th>
317
+ <th>mean</th>
318
+ </tr>
319
+ <tr>
320
+ <td>DINOv3 ViT-L/16</td>
321
+ <td>73.0</td>
322
+ <td>96.5</td>
323
+ <td>94.1</td>
324
+ <td>60.6</td>
325
+ <td>96.0</td>
326
+ <td>57.4</td>
327
+ <td>79.6</td>
328
+ </tr>
329
+ <tr>
330
+ <td>DINOv3 ViT-7B/16</td>
331
+ <td>74.0</td>
332
+ <td>97.2</td>
333
+ <td>94.8</td>
334
+ <td>62.3</td>
335
+ <td>96.1</td>
336
+ <td>62.1</td>
337
+ <td>81.1</td>
338
+ </tr>
339
+ <tr>
340
+ <th></th>
341
+ <th colspan="7">(GEO-Bench) Segmentation</th>
342
+ </tr>
343
+ <tr>
344
+ <th>Model</th>
345
+ <th>m-cashew</th>
346
+ <th>m-chesapeake</th>
347
+ <th>m-NeonTree</th>
348
+ <th>m-nz-cattle</th>
349
+ <th>m-pv4ger-seg</th>
350
+ <th>m-SA-crop</th>
351
+ <th>mean</th>
352
+ </tr>
353
+ <tr>
354
+ <td>DINOv3 ViT-L/16</td>
355
+ <td>94.2</td>
356
+ <td>75.6</td>
357
+ <td>61.8</td>
358
+ <td>83.7</td>
359
+ <td>95.2</td>
360
+ <td>36.8</td>
361
+ <td>74.5</td>
362
+ </tr>
363
+ <tr>
364
+ <td>DINOv3 ViT-7B/16</td>
365
+ <td>94.1</td>
366
+ <td>76.6</td>
367
+ <td>62.6</td>
368
+ <td>83.4</td>
369
+ <td>95.5</td>
370
+ <td>37.6</td>
371
+ <td>75.0</td>
372
+ </tr>
373
+ </table>
374
+
375
+
376
+ ## Environmental Impact
377
+
378
+ - **Hardware Type:** Nvidia H100
379
+ - **Hours used:** 61,440 hours for ViT-7B model training
380
+ - **Cloud Provider:** Private infrastructure
381
+ - **Compute Region:** USA
382
+ - **Carbon Emitted:** 18t CO2eq
383
+
384
+ ## Technical Specifications
385
+
386
+ ### Model Architecture and Objective
387
+
388
+ Vision Transformer models:
389
+
390
+ - ViT-S (21M parameters): patch size 16, embedding dimension 384, 4 register tokens, 6 heads, MLP FFN, RoPE
391
+ - ViT-S+ (29M parameters): patch size 16, embedding dimension 384, 4 register tokens, 6 heads, SwiGLU FFN, RoPE
392
+ - ViT-B (86M parameters): patch size 16, embedding dimension 768, 4 register tokens, 12 heads, MLP FFN, RoPE
393
+ - ViT-L (300M parameters): patch size 16, embedding dimension 1024, 4 register tokens, 16 heads, MLP FFN, RoPE
394
+ - ViT-H+ (840M parameters): patch size 16, embedding dimension 1280, 4 register tokens, 20 heads, SwiGLU FFN, RoPE
395
+ - ViT-7B (6716M parameters): patch size 16, embedding dimension 4096, 4 register tokens, 32 heads, SwiGLU FFN, RoPE
396
+
397
+ ConvNeXt models:
398
+
399
+ - ConvNeXt Tiny (29M parameters)
400
+ - ConvNeXt Small (50M parameters)
401
+ - ConvNeXt Base (89M parameters)
402
+ - ConvNeXt Large (198M parameters)
403
+
404
+ ### Compute Infrastructure
405
+
406
+ #### Hardware
407
+
408
+ Nvidia H100 GPUs
409
+
410
+ #### Software
411
+
412
+ PyTorch 2.7
413
+
414
+ ## More Information
415
+
416
+ See the [blog post](https://ai.meta.com/blog/dinov3-self-supervised-vision-model/) and the associated [website](https://ai.meta.com/dinov3/).
417
+
418
+ ## Citation
419
+
420
+ **BibTeX**
421
+
422
+ ```
423
+ @misc{simeoni2025dinov3,
424
+ title={{DINOv3}},
425
+ author={Sim{\'e}oni, Oriane and Vo, Huy V. and Seitzer, Maximilian and Baldassarre, Federico and Oquab, Maxime and Jose, Cijo and Khalidov, Vasil and Szafraniec, Marc and Yi, Seungeun and Ramamonjisoa, Micha{\"e}l and Massa, Francisco and Haziza, Daniel and Wehrstedt, Luca and Wang, Jianyuan and Darcet, Timoth{\'e}e and Moutakanni, Th{\'e}o and Sentana, Leonel and Roberts, Claire and Vedaldi, Andrea and Tolan, Jamie and Brandt, John and Couprie, Camille and Mairal, Julien and J{\'e}gou, Herv{\'e} and Labatut, Patrick and Bojanowski, Piotr},
426
+ year={2025},
427
+ eprint={2508.10104},
428
+ archivePrefix={arXiv},
429
+ primaryClass={cs.CV},
430
+ url={https://arxiv.org/abs/2508.10104},
431
+ }
432
+ ```
src/model/encoder/blocks/torchhub/dinov3/README.md ADDED
@@ -0,0 +1,882 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :new: [2026-03-10] :fire: The [Canopy Height Maps v2 (CHMv2) model](https://arxiv.org/abs/2603.06382) and inference code are now available (more details on downloading the model weights and using the code [here](#canopy-height-maps-v2-chmv2)). The model weights are also available in [Hugging Face Hub](https://huggingface.co/facebook/dinov3-vitl16-chmv2-dpt-head) and [supported](https://github.com/huggingface/transformers/blob/main/docs/source/en/model_doc/chmv2.md) by the Hugging Face [Transformers](https://huggingface.co/docs/transformers/index) library. Building on our original high-resolution canopy height maps released in 2024, CHMv2 delivers substantial improvements in accuracy, detail, and global consistency by leveraging DINOv3.
2
+
3
+ [2025-11-20] Distillation code and configurations for ConvNeXt backbones are now released!
4
+
5
+ [2025-10-13] [Semantic segmentation](https://github.com/facebookresearch/dinov3?tab=readme-ov-file#linear-segmentation-with-data-augmentation-on-ade20k) (ADE20K) and [monocular depth estimation](https://github.com/facebookresearch/dinov3?tab=readme-ov-file#linear-depth-estimation-on-nyuv2-depth) (NYUv2-Depth) linear probing code are now released!
6
+
7
+ [2025-09-17] DINOv3 backbones are now supported by the [PyTorch Image Models / timm](https://github.com/huggingface/pytorch-image-models/) library starting with version [1.0.20](https://github.com/huggingface/pytorch-image-models/releases/tag/v1.0.20)
8
+
9
+ [2025-08-29] DINOv3 backbones are [supported](https://huggingface.co/docs/transformers/model_doc/dinov3) by released versions of the Hugging Face [Transformers](https://huggingface.co/docs/transformers/index) library starting with version [4.56.0](https://github.com/huggingface/transformers/releases/tag/v4.56.0)
10
+
11
+ [2025-08-14] DINOv3 backbones are now available in [Hugging Face Hub](https://huggingface.co/collections/facebook/dinov3-68924841bd6b561778e31009) and [supported](https://huggingface.co/docs/transformers/model_doc/dinov3) by the [development](https://github.com/huggingface/transformers/) version of the Hugging Face [Transformers](https://huggingface.co/docs/transformers/index) library
12
+
13
+ # DINOv3 🦖🦖🦖
14
+
15
+ **[Meta AI Research, FAIR](https://ai.meta.com/research/)**
16
+
17
+ Oriane Siméoni, Huy V. Vo, Maximilian Seitzer, Federico Baldassarre, Maxime Oquab, <br/>
18
+ Cijo Jose, Vasil Khalidov, Marc Szafraniec, Seungeun Yi, Michaël Ramamonjisoa, <br/>
19
+ Francisco Massa, Daniel Haziza, Luca Wehrstedt, Jianyuan Wang, <br/>
20
+ Timothée Darcet, Théo Moutakanni, Leonel Sentana, Claire Roberts, <br/>
21
+ Andrea Vedaldi, Jamie Tolan, John Brandt, Camille Couprie, <br/>
22
+ Julien Mairal, Hervé Jégou, Patrick Labatut, Piotr Bojanowski
23
+
24
+ [ :scroll: [`Paper`](https://arxiv.org/abs/2508.10104)] [ :newspaper: [`Blog`](https://ai.meta.com/blog/dinov3-self-supervised-vision-model/)] [ :globe_with_meridians: [`Website`](https://ai.meta.com/dinov3/)] [ :book: [`BibTeX`](#citing-dinov3)]
25
+
26
+ Reference PyTorch implementation and models for DINOv3. For details, see the **[DINOv3](https://arxiv.org/abs/2508.10104)** paper.
27
+
28
+ ## Overview
29
+
30
+ <div align="center">
31
+ <img width="1364" height="1024" alt="market" src="https://github.com/user-attachments/assets/1411f491-988e-49cb-95ae-d03fe6e3c268" />
32
+
33
+ <i></em><b>High-resolution dense features.</b><br/>We visualize the cosine similarity maps obtained with DINOv3 output features<br/> between the patches marked with a red cross and all other patches.</i>
34
+ </div>
35
+
36
+ <br/>
37
+
38
+ An extended family of versatile vision foundation models producing high-quality dense features and achieving outstanding performance on various vision tasks including outperforming the specialized state of the art across a broad range of settings, without fine-tuning
39
+
40
+ ## Pretrained models
41
+
42
+ :information_source: Please follow the link provided below to get access to all the model weights: once accepted, an e-mail will be sent with the complete list of URLs pointing to all the available model weights (both backbones and adapters). These URLs can then be used to either:
43
+ - download the model or adapter weights to a local filesystem and point `torch.hub.load()` to these local weights via the `weights` or `backbone_weights` parameters, or
44
+ - directly invoke `torch.hub.load()` to download and load a backbone or an adapter from its URL via also the `weights` or `backbone_weights` parameters.
45
+
46
+ See the example code snippets below.
47
+
48
+ :warning: Please use `wget` instead of a web browser to download the weights.
49
+
50
+ ViT models pretrained on web dataset (LVD-1689M):
51
+ <table style="margin: auto">
52
+ <thead>
53
+ <tr>
54
+ <th>Model</th>
55
+ <th>Parameters</th>
56
+ <th>Pretraining<br/>Dataset</th>
57
+ <th>Download</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ <tr>
62
+ <td>ViT-S/16 distilled </td>
63
+ <td align="right">21M</td>
64
+ <td align="center">LVD-1689M</td>
65
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
66
+ </tr>
67
+ <tr>
68
+ <td>ViT-S+/16 distilled</td>
69
+ <td align="right">29M</td>
70
+ <td align="center">LVD-1689M</td>
71
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
72
+ </tr>
73
+ <tr>
74
+ <td>ViT-B/16 distilled</td>
75
+ <td align="right">86M</td>
76
+ <td align="center">LVD-1689M</td>
77
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
78
+ </tr>
79
+ <tr>
80
+ <td>ViT-L/16 distilled</td>
81
+ <td align="right">300M</td>
82
+ <td align="center">LVD-1689M</td>
83
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
84
+ </tr>
85
+ <tr>
86
+ <td>ViT-H+/16 distilled</td>
87
+ <td align="right">840M</td>
88
+ <td align="center">LVD-1689M</td>
89
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
90
+ </tr>
91
+ <tr>
92
+ <td>ViT-7B/16</td>
93
+ <td align="right">6,716M</td>
94
+ <td align="center">LVD-1689M</td>
95
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
96
+ </tr>
97
+ </tbody>
98
+ </table>
99
+
100
+ ConvNeXt models pretrained on web dataset (LVD-1689M):
101
+ <table style="margin: auto">
102
+ <thead>
103
+ <tr>
104
+ <th>Model</th>
105
+ <th>Parameters</th>
106
+ <th>Pretraining<br/>Dataset</th>
107
+ <th>Download</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <tr>
112
+ <td>ConvNeXt Tiny</td>
113
+ <td align="right">29M</td>
114
+ <td align="center">LVD-1689M</td>
115
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
116
+ </tr>
117
+ <tr>
118
+ <td>ConvNeXt Small</td>
119
+ <td align="right">50M</td>
120
+ <td align="center">LVD-1689M</td>
121
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
122
+ </tr>
123
+ <tr>
124
+ <td>ConvNeXt Base</td>
125
+ <td align="right">89M</td>
126
+ <td align="center">LVD-1689M</td>
127
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
128
+ </tr>
129
+ <tr>
130
+ <td>ConvNeXt Large</td>
131
+ <td align="right">198M</td>
132
+ <td align="center">LVD-1689M</td>
133
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
134
+ </tr>
135
+ </tbody>
136
+ </table>
137
+
138
+ ViT models pretrained on satellite dataset (SAT-493M):
139
+ <table style="margin: auto">
140
+ <thead>
141
+ <tr>
142
+ <th>Model</th>
143
+ <th>Parameters</th>
144
+ <th>Pretraining<br/>Dataset</th>
145
+ <th>Download</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ <tr>
150
+ <td>ViT-L/16 distilled</td>
151
+ <td align="right">300M</td>
152
+ <td align="center">SAT-493M</td>
153
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
154
+ </tr>
155
+ <tr>
156
+ <td>ViT-7B/16</td>
157
+ <td align="right">6,716M</td>
158
+ <td align="center">SAT-493M</td>
159
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
160
+ </tr>
161
+ </tbody>
162
+ </table>
163
+
164
+
165
+ ### Pretrained backbones (via PyTorch [Hub](https://docs.pytorch.org/docs/stable/hub.html))
166
+
167
+ Please follow the instructions [here](https://pytorch.org/get-started/locally/) to install PyTorch (the only required dependency for loading the model). Installing PyTorch with CUDA support is strongly recommended.
168
+
169
+ ```python
170
+ import torch
171
+
172
+ REPO_DIR = <PATH/TO/A/LOCAL/DIRECTORY/WHERE/THE/DINOV3/REPO/WAS/CLONED>
173
+
174
+ # DINOv3 ViT models pretrained on web images
175
+ dinov3_vits16 = torch.hub.load(REPO_DIR, 'dinov3_vits16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
176
+ dinov3_vits16plus = torch.hub.load(REPO_DIR, 'dinov3_vits16plus', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
177
+ dinov3_vitb16 = torch.hub.load(REPO_DIR, 'dinov3_vitb16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
178
+ dinov3_vitl16 = torch.hub.load(REPO_DIR, 'dinov3_vitl16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
179
+ dinov3_vith16plus = torch.hub.load(REPO_DIR, 'dinov3_vith16plus', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
180
+ dinov3_vit7b16 = torch.hub.load(REPO_DIR, 'dinov3_vit7b16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
181
+
182
+ # DINOv3 ConvNeXt models pretrained on web images
183
+ dinov3_convnext_tiny = torch.hub.load(REPO_DIR, 'dinov3_convnext_tiny', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
184
+ dinov3_convnext_small = torch.hub.load(REPO_DIR, 'dinov3_convnext_small', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
185
+ dinov3_convnext_base = torch.hub.load(REPO_DIR, 'dinov3_convnext_base', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
186
+ dinov3_convnext_large = torch.hub.load(REPO_DIR, 'dinov3_convnext_large', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
187
+
188
+ # DINOv3 ViT models pretrained on satellite imagery
189
+ dinov3_vitl16 = torch.hub.load(REPO_DIR, 'dinov3_vitl16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
190
+ dinov3_vit7b16 = torch.hub.load(REPO_DIR, 'dinov3_vit7b16', source='local', weights=<CHECKPOINT/URL/OR/PATH>)
191
+ ```
192
+
193
+ ### Pretrained backbones (via Hugging Face [Transformers](https://huggingface.co/docs/transformers/))
194
+
195
+ All the backbones are available in the [DINOv3](https://huggingface.co/collections/facebook/dinov3-68924841bd6b561778e31009) collection on Hugging Face Hub and supported via the Hugging Face [Transformers](https://huggingface.co/docs/transformers/index) library (with released packages from version 4.56.0). Please refer to the corresponding documentation for usage, but below is a short example that demonstrates how to obtain an image embedding with either [Pipeline] or the [AutoModel] class.
196
+
197
+ ```python
198
+ from transformers import pipeline
199
+ from transformers.image_utils import load_image
200
+
201
+ url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
202
+ image = load_image(url)
203
+
204
+ feature_extractor = pipeline(
205
+ model="facebook/dinov3-convnext-tiny-pretrain-lvd1689m",
206
+ task="image-feature-extraction",
207
+ )
208
+ features = feature_extractor(image)
209
+ ```
210
+
211
+ ```python
212
+ import torch
213
+ from transformers import AutoImageProcessor, AutoModel
214
+ from transformers.image_utils import load_image
215
+
216
+ url = "http://images.cocodataset.org/val2017/000000039769.jpg"
217
+ image = load_image(url)
218
+
219
+ pretrained_model_name = "facebook/dinov3-convnext-tiny-pretrain-lvd1689m"
220
+ processor = AutoImageProcessor.from_pretrained(pretrained_model_name)
221
+ model = AutoModel.from_pretrained(
222
+ pretrained_model_name,
223
+ device_map="auto",
224
+ )
225
+
226
+ inputs = processor(images=image, return_tensors="pt").to(model.device)
227
+ with torch.inference_mode():
228
+ outputs = model(**inputs)
229
+
230
+ pooled_output = outputs.pooler_output
231
+ print("Pooled output shape:", pooled_output.shape)
232
+ ```
233
+
234
+ where `model` and `pretrained_model_name` above can be one of:
235
+ - `facebook/dinov3-vits16-pretrain-lvd1689m`
236
+ - `facebook/dinov3-vits16plus-pretrain-lvd1689m`
237
+ - `facebook/dinov3-vitb16-pretrain-lvd1689m`
238
+ - `facebook/dinov3-vitl16-pretrain-lvd1689m`
239
+ - `facebook/dinov3-vith16plus-pretrain-lvd1689m`
240
+ - `facebook/dinov3-vit7b16-pretrain-lvd1689m`
241
+ - `facebook/dinov3-convnext-base-pretrain-lvd1689m`
242
+ - `facebook/dinov3-convnext-large-pretrain-lvd1689m`
243
+ - `facebook/dinov3-convnext-small-pretrain-lvd1689m`
244
+ - `facebook/dinov3-convnext-tiny-pretrain-lvd1689m`
245
+ - `facebook/dinov3-vitl16-pretrain-sat493m`
246
+ - `facebook/dinov3-vit7b16-pretrain-sat493m`
247
+
248
+ ### Image transforms
249
+
250
+ For models using the LVD-1689M weights (pretrained on web images), please use the following transform (standard ImageNet evaluation transform):
251
+
252
+ ```python
253
+ import torchvision
254
+ from torchvision.transforms import v2
255
+
256
+ def make_transform(resize_size: int = 256):
257
+ to_tensor = v2.ToImage()
258
+ resize = v2.Resize((resize_size, resize_size), antialias=True)
259
+ to_float = v2.ToDtype(torch.float32, scale=True)
260
+ normalize = v2.Normalize(
261
+ mean=(0.485, 0.456, 0.406),
262
+ std=(0.229, 0.224, 0.225),
263
+ )
264
+ return v2.Compose([to_tensor, resize, to_float, normalize])
265
+ ```
266
+
267
+
268
+ For models using the SAT-493M weights (pretrained on satellite imagery), please use the following transform:
269
+
270
+
271
+ ```python
272
+ import torchvision
273
+ from torchvision.transforms import v2
274
+
275
+ def make_transform(resize_size: int = 256):
276
+ to_tensor = v2.ToImage()
277
+ resize = v2.Resize((resize_size, resize_size), antialias=True)
278
+ to_float = v2.ToDtype(torch.float32, scale=True)
279
+ normalize = v2.Normalize(
280
+ mean=(0.430, 0.411, 0.296),
281
+ std=(0.213, 0.156, 0.143),
282
+ )
283
+ return v2.Compose([to_tensor, resize, to_float, normalize])
284
+ ```
285
+
286
+ ### Pretrained heads - Image classification
287
+
288
+ <table style="margin: auto">
289
+ <thead>
290
+ <tr>
291
+ <th>Backbone</th>
292
+ <th>Pretraining<br/>Dataset</th>
293
+ <th>Head<br/>Dataset</th>
294
+ <th>Download</th>
295
+ </tr>
296
+ </thead>
297
+ <tbody>
298
+ <tr>
299
+ <td>ViT-7B/16</td>
300
+ <td align="center">LVD-1689M</td>
301
+ <td align="center">ImageNet</td>
302
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
303
+ </tr>
304
+ </tbody>
305
+ </table>
306
+
307
+
308
+ The (full) classifier models can be loaded via PyTorch Hub:
309
+
310
+ ```python
311
+ import torch
312
+
313
+ # DINOv3
314
+ dinov3_vit7b16_lc = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_lc', source="local", weights=<DEPTHER/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
315
+
316
+ ```
317
+
318
+ ### Pretrained heads - Depther trained on SYNTHMIX dataset
319
+
320
+ <table style="margin: auto">
321
+ <thead>
322
+ <tr>
323
+ <th>Backbone</th>
324
+ <th>Pretraining<br/>Dataset</th>
325
+ <th>Head<br/>Dataset</th>
326
+ <th>Download</th>
327
+ </tr>
328
+ </thead>
329
+ <tbody>
330
+ <tr>
331
+ <td>ViT-7B/16</td>
332
+ <td align="center">LVD-1689M</td>
333
+ <td align="center">SYNTHMIX</td>
334
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
335
+ </tr>
336
+ </tbody>
337
+ </table>
338
+
339
+
340
+ ```python
341
+ depther = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_dd', source="local", weights=<DEPTHER/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
342
+ ```
343
+
344
+ Full example code of depther on an image
345
+
346
+ ```python
347
+ from PIL import Image
348
+ import torch
349
+ from torchvision.transforms import v2
350
+ import matplotlib.pyplot as plt
351
+ from matplotlib import colormaps
352
+
353
+ def get_img():
354
+ import requests
355
+ url = "http://images.cocodataset.org/val2017/000000039769.jpg"
356
+ image = Image.open(requests.get(url, stream=True).raw).convert("RGB")
357
+ return image
358
+
359
+ def make_transform(resize_size: int | list[int] = 768):
360
+ to_tensor = v2.ToImage()
361
+ resize = v2.Resize((resize_size, resize_size), antialias=True)
362
+ to_float = v2.ToDtype(torch.float32, scale=True)
363
+ normalize = v2.Normalize(
364
+ mean=(0.485, 0.456, 0.406),
365
+ std=(0.229, 0.224, 0.225),
366
+ )
367
+ return v2.Compose([to_tensor, resize, to_float, normalize])
368
+
369
+ depther = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_dd', source="local", weights=<DEPTHER/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
370
+
371
+ img_size = 1024
372
+ img = get_img()
373
+ transform = make_transform(img_size)
374
+ with torch.inference_mode():
375
+ with torch.autocast('cuda', dtype=torch.bfloat16):
376
+ batch_img = transform(img)[None]
377
+ batch_img = batch_img
378
+ depths = depther(batch_img)
379
+
380
+ plt.figure(figsize=(12, 6))
381
+ plt.subplot(121)
382
+ plt.imshow(img)
383
+ plt.axis("off")
384
+ plt.subplot(122)
385
+ plt.imshow(depths[0,0].cpu(), cmap=colormaps["Spectral"])
386
+ plt.axis("off")
387
+
388
+ ```
389
+
390
+ #### Reproduce paper results
391
+
392
+ Make sure the NYU dataset is setup following [this](DATASETS.md#depth-estimation-on-nyu).
393
+
394
+ Launch the following to reproduce our paper's depth estimation results on NYUv2 with the pretrained Depther trained on SYNTHMIX:
395
+
396
+ ```shell
397
+ PYTHONPATH=. python -m dinov3.run.submit dinov3/eval/depth/run.py \
398
+ config=dinov3/eval/depth/configs/config-nyu-synthmix-dpt-inference.yaml \
399
+ datasets.root=<PATH/TO/DATASET> \
400
+ load_from=dinov3_vit7b16_dd \
401
+ --output-dir <PATH/TO/OUTPUT/DIR>
402
+ ```
403
+
404
+ Notes:
405
+ - if you want to launch the code without dinov3.run.submit, you can do so using python directly or torchrun:
406
+
407
+ ```shell
408
+ PYTHONPATH=. python dinov3/eval/depth/run.py \
409
+ config=dinov3/eval/depth/configs/config-nyu-synthmix-dpt-inference.yaml \
410
+ datasets.root=<PATH/TO/DATASET> \
411
+ load_from=dinov3_vit7b16_dd \
412
+ output_dir=<PATH/TO/OUTPUT/DIR>
413
+ ```
414
+
415
+ - One can also save prediction results using `result_config.save_results=true`.
416
+
417
+
418
+ ### Pretrained heads - Detector trained on COCO2017 dataset
419
+
420
+ <table style="margin: auto">
421
+ <thead>
422
+ <tr>
423
+ <th>Backbone</th>
424
+ <th>Pretraining<br/>Dataset</th>
425
+ <th>Head<br/>Dataset</th>
426
+ <th>Download</th>
427
+ </tr>
428
+ </thead>
429
+ <tbody>
430
+ <tr>
431
+ <td>ViT-7B/16</td>
432
+ <td align="center">LVD-1689M</td>
433
+ <td align="center">COCO2017</td>
434
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
435
+ </tr>
436
+ </tbody>
437
+ </table>
438
+
439
+
440
+ ```python
441
+ detector = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_de', source="local", weights=<DETECTOR/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
442
+ ```
443
+
444
+ ### Pretrained heads - Segmentor trained on ADE20K dataset
445
+
446
+ <table style="margin: auto">
447
+ <thead>
448
+ <tr>
449
+ <th>Backbone</th>
450
+ <th>Pretraining<br/>Dataset</th>
451
+ <th>Head<br/>Dataset</th>
452
+ <th>Download</th>
453
+ </tr>
454
+ </thead>
455
+ <tbody>
456
+ <tr>
457
+ <td>ViT-7B/16</td>
458
+ <td align="center">LVD-1689M</td>
459
+ <td align="center">ADE20K</td>
460
+ <td align="center"><a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a></td>
461
+ </tr>
462
+ </tbody>
463
+ </table>
464
+
465
+ ```python
466
+ segmentor = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_ms', source="local", weights=<SEGMENTOR/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
467
+ ```
468
+
469
+ Example command to run a full inference on ADE20K with the provided segmentor (ViT-7B + M2F):
470
+
471
+ ```shell
472
+ PYTHONPATH=. python -m dinov3.run.submit dinov3/eval/segmentation/run.py \
473
+ config=dinov3/eval/segmentation/configs/config-ade20k-m2f-inference.yaml \
474
+ datasets.root=<PATH/TO/DATASET> \
475
+ load_from=dinov3_vit7b16_ms \
476
+ --output-dir <PATH/TO/OUTPUT/DIR>
477
+ ```
478
+
479
+ Full example code of segmentator on an image
480
+
481
+ ```python
482
+ import sys
483
+ sys.path.append(REPO_DIR)
484
+
485
+ from PIL import Image
486
+ import torch
487
+ from torchvision import transforms
488
+ import matplotlib.pyplot as plt
489
+ from matplotlib import colormaps
490
+ from functools import partial
491
+ from dinov3.eval.segmentation.inference import make_inference
492
+
493
+
494
+ def get_img():
495
+ import requests
496
+ url = "http://images.cocodataset.org/val2017/000000039769.jpg"
497
+ image = Image.open(requests.get(url, stream=True).raw).convert("RGB")
498
+ return image
499
+
500
+ def make_transform(resize_size: int | list[int] = 768):
501
+ to_tensor = v2.ToImage()
502
+ resize = v2.Resize((resize_size, resize_size), antialias=True)
503
+ to_float = v2.ToDtype(torch.float32, scale=True)
504
+ normalize = v2.Normalize(
505
+ mean=(0.485, 0.456, 0.406),
506
+ std=(0.229, 0.224, 0.225),
507
+ )
508
+ return v2.Compose([to_tensor, resize, to_float, normalize])
509
+
510
+ segmentor = torch.hub.load(REPO_DIR, 'dinov3_vit7b16_ms', source="local", weights=<SEGMENTOR/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
511
+
512
+ img_size = 896
513
+ img = get_img()
514
+ transform = make_transform(img_size)
515
+ with torch.inference_mode():
516
+ with torch.autocast('cuda', dtype=torch.bfloat16):
517
+ batch_img = transform(img)[None]
518
+ pred_vit7b = segmentor(batch_img) # raw predictions
519
+ # actual segmentation map
520
+ segmentation_map_vit7b = make_inference(
521
+ batch_img,
522
+ segmentor,
523
+ inference_mode="slide",
524
+ decoder_head_type="m2f",
525
+ rescale_to=(img.size[-1], img.size[-2]),
526
+ n_output_channels=150,
527
+ crop_size=(img_size, img_size),
528
+ stride=(img_size, img_size),
529
+ output_activation=partial(torch.nn.functional.softmax, dim=1),
530
+ ).argmax(dim=1, keepdim=True)
531
+ plt.figure(figsize=(12, 6))
532
+ plt.subplot(121)
533
+ plt.imshow(img)
534
+ plt.axis("off")
535
+ plt.subplot(122)
536
+ plt.imshow(segmentation_map_vit7b[0,0].cpu(), cmap=colormaps["Spectral"])
537
+ plt.axis("off")
538
+ ```
539
+
540
+
541
+
542
+
543
+ ### Pretrained heads - Zero-shot tasks with `dino.txt`
544
+
545
+ <table style="margin: auto">
546
+ <thead>
547
+ <tr>
548
+ <th rowspan="2">Backbone</th>
549
+ <th>Download</th>
550
+ </tr>
551
+ </thead>
552
+ <tbody>
553
+ <tr>
554
+ <td>ViT-L/16 distilled</td>
555
+ <td align="center">
556
+ <a href="https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/">[link]</a>,
557
+ <a href="https://dl.fbaipublicfiles.com/dinov3/thirdparty/bpe_simple_vocab_16e6.txt.gz">vocabulary</a>,
558
+ <a href="https://dl.fbaipublicfiles.com/dinov2/thirdparty/LICENSE">vocabulary license</a>
559
+ </td>
560
+ </tr>
561
+ </tbody>
562
+ </table>
563
+
564
+ The (full) dino.txt model can be loaded via PyTorch Hub:
565
+
566
+ ```python
567
+ import torch
568
+ # DINOv3
569
+ dinov3_vitl16_dinotxt_tet1280d20h24l, tokenizer = torch.hub.load(REPO_DIR, 'dinov3_vitl16_dinotxt_tet1280d20h24l', weights=<SEGMENTOR/CHECKPOINT/URL/OR/PATH>, backbone_weights=<BACKBONE/CHECKPOINT/URL/OR/PATH>)
570
+ ```
571
+
572
+
573
+ ## Installation
574
+
575
+ The training and evaluation code requires PyTorch version >= 2.7.1 as well as a few other 3rd party packages. Note that the code has only been tested with the specified versions and also expects a Linux environment. To setup all the required dependencies for training and evaluation, please follow the instructions below:
576
+
577
+ *[micromamba](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html)* **(Recommended)** - Clone the repository and then create and activate a `dinov3` conda environment using the provided environment definition:
578
+
579
+ ```shell
580
+ micromamba env create -f conda.yaml
581
+ micromamba activate dinov3
582
+ ```
583
+
584
+ ## Getting started
585
+
586
+ Several notebooks are provided to get started applying DINOv3:
587
+ - [PCA of patch features](notebooks/pca.ipynb): display the PCA of DINOv3 patch features on a foreground object (rainbow visualizations from the paper) [[Run in Google Colab]](https://colab.research.google.com/github/facebookresearch/dinov3/blob/main/notebooks/pca.ipynb)
588
+ - [Foreground segmentation](notebooks/foreground_segmentation.ipynb): train a linear foreground segmentation model based on DINOv3 features [[Run in Google Colab]](https://colab.research.google.com/github/facebookresearch/dinov3/blob/main/notebooks/foreground_segmentation.ipynb)
589
+ - [Dense and sparse matching](notebooks/dense_sparse_matching.ipynb): match patches from objects on two different images based on DINOv3 features [[Run in Google Colab]](https://colab.research.google.com/github/facebookresearch/dinov3/blob/main/notebooks/dense_sparse_matching.ipynb)
590
+ - [Segmentation tracking](notebooks/segmentation_tracking.ipynb): video segmentation tracking using a non-parametric method based on DINOv3 features [[Run in Google Colab]](https://colab.research.google.com/github/facebookresearch/dinov3/blob/main/notebooks/segmentation_tracking.ipynb)
591
+ - [Zero-shot segmentation with DINOv3-based dino.txt](notebooks/dinotxt_segmentation_inference.ipynb): compute the open-vocabulary segmentation results with dino.txt strategy.
592
+
593
+ ## Data preparation
594
+
595
+ ### ImageNet-1k
596
+
597
+ The root directory of the dataset should hold the following contents:
598
+
599
+ - `<ROOT>/test/ILSVRC2012_test_00000001.JPEG`
600
+ - `<ROOT>/test/[..]`
601
+ - `<ROOT>/test/ILSVRC2012_test_00100000.JPEG`
602
+ - `<ROOT>/train/n01440764/n01440764_10026.JPEG`
603
+ - `<ROOT>/train/[...]`
604
+ - `<ROOT>/train/n15075141/n15075141_9993.JPEG`
605
+ - `<ROOT>/val/n01440764/ILSVRC2012_val_00000293.JPEG`
606
+ - `<ROOT>/val/[...]`
607
+ - `<ROOT>/val/n15075141/ILSVRC2012_val_00049174.JPEG`
608
+ - `<ROOT>/labels.txt`
609
+
610
+ The provided dataset implementation expects a few additional metadata files to be present under the extra directory:
611
+
612
+ - `<EXTRA>/class-ids-TRAIN.npy`
613
+ - `<EXTRA>/class-ids-VAL.npy`
614
+ - `<EXTRA>/class-names-TRAIN.npy`
615
+ - `<EXTRA>/class-names-VAL.npy`
616
+ - `<EXTRA>/entries-TEST.npy`
617
+ - `<EXTRA>/entries-TRAIN.npy`
618
+ - `<EXTRA>/entries-VAL.npy`
619
+
620
+ These metadata files can be generated (once) with the following lines of Python code:
621
+
622
+ ```python
623
+ from dinov3.data.datasets import ImageNet
624
+
625
+ for split in ImageNet.Split:
626
+ dataset = ImageNet(split=split, root="<ROOT>", extra="<EXTRA>")
627
+ dataset.dump_extra()
628
+ ```
629
+
630
+ Note that the root and extra directories do not have to be distinct directories.
631
+
632
+ ### ImageNet-22k
633
+
634
+ Please adapt the [dataset class](dinov3/data/datasets/image_net_22k.py) to match your local setup.
635
+
636
+ <br />
637
+
638
+ :warning: To execute the commands provided in the next sections for training and evaluation, the `dinov3` package should be included in the Python module search path, i.e. simply prefix the command to run with `PYTHONPATH=.`.
639
+
640
+ ## Training
641
+
642
+ ### Fast setup: training DINOv3 ViT-L/16 on ImageNet-1k
643
+
644
+ Run DINOv3 pre-training on 4 H100-80GB nodes (32 GPUs) in a SLURM cluster environment with submitit:
645
+
646
+ ```shell
647
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/train/train.py \
648
+ --nodes 4 \
649
+ --config-file dinov3/configs/train/vitl_im1k_lin834.yaml \
650
+ --output-dir <PATH/TO/OUTPUT/DIR> \
651
+ train.dataset_path=ImageNet22k:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
652
+ ```
653
+ Training time is approximately 14 hours and the resulting checkpoint should reach 82.0% on k-NN eval and 83.5% on linear eval.
654
+
655
+ The training code saves the weights of the teacher in the eval folder every 12500 iterations for evaluation.
656
+
657
+ ### Exact DINOv3 setup: training DINOv3 ViT-7B/16
658
+
659
+ DINOv3 ViT-7B/16 is trained on a private dataset. The training involves 3 stages:
660
+ - Pretraining
661
+ - Gram anchoring
662
+ - High resolution adaptation
663
+
664
+ #### Pretraining
665
+
666
+ Launch DINOV3 ViT-7B/16 pretraining on 32 nodes (256 GPUs) in a SLURM cluster environment with submitit.
667
+
668
+ ```shell
669
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/train/train.py \
670
+ --nodes 32 \
671
+ --config-file dinov3/configs/train/dinov3_vit7b16_pretrain.yaml \
672
+ --output-dir <PATH/TO/OUTPUT/DIR> \
673
+ train.dataset_path=<DATASET>:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
674
+ ```
675
+
676
+ #### Gram anchoring
677
+
678
+ ```shell
679
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/train/train.py \
680
+ --nodes 32 \
681
+ --config-file dinov3/configs/train/dinov3_vit7b16_gram_anchor.yaml \
682
+ --output-dir <PATH/TO/OUTPUT/DIR> \
683
+ train.dataset_path=<DATASET>:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET> \
684
+ gram.ckpt=<PATH/TO/GRAM_TEACHER_FROM_PREVIOUS_STEP>
685
+ ```
686
+
687
+ #### High-resolution adaptation
688
+
689
+
690
+ ```shell
691
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/train/train.py \
692
+ --nodes 32 \
693
+ --config-file dinov3/configs/train/dinov3_vit7b16_high_res_adapt.yaml \
694
+ --output-dir <PATH/TO/OUTPUT/DIR> \
695
+ train.dataset_path=<DATASET>:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET> \
696
+ gram.ckpt=<PATH/TO/TEACHER_FROM_GRAM> \
697
+ student.resume_from_teacher_chkpt=<PATH/TO/TEACHER_FROM_GRAM>
698
+ ```
699
+
700
+ ## Multi-distillation
701
+
702
+ ### Test setup:
703
+
704
+ ```shell
705
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/train/train.py \
706
+ --nodes 1 \
707
+ --config-file dinov3/configs/train/multi_distillation_test.yaml \
708
+ --output-dir <PATH/TO/OUTPUT/DIR> \
709
+ --multi-distillation \
710
+ train.dataset_path=<DATASET>:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
711
+ ```
712
+
713
+ ## Evaluation
714
+
715
+ The training code regularly saves the teacher weights. In order to evaluate the model, run the following evaluation on a single node:
716
+
717
+
718
+ ### Logistic regression classification on ImageNet-1k
719
+
720
+ ```shell
721
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/eval/log_regression.py \
722
+ model.config_file=<PATH/TO/OUTPUT/DIR>/config.yaml \
723
+ model.pretrained_weights=<PATH/TO/OUTPUT/DIR>/teacher_checkpoint.pth \
724
+ output_dir=<PATH/TO/OUTPUT/DIR> \
725
+ train.dataset=ImageNet:split=TRAIN:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET> \
726
+ eval.test_dataset=ImageNet:split=VAL:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
727
+ ```
728
+
729
+ ### k-NN classification on ImageNet-1k
730
+
731
+ ```shell
732
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/eval/knn.py \
733
+ model.config_file=<PATH/TO/OUTPUT/DIR>/config.yaml \
734
+ model.pretrained_weights=<PATH/TO/OUTPUT/DIR>/teacher_checkpoint.pth \
735
+ output_dir=<PATH/TO/OUTPUT/DIR> \
736
+ train.dataset=ImageNet:split=TRAIN:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET> \
737
+ eval.test_dataset=ImageNet:split=VAL:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
738
+ ```
739
+
740
+ ### Linear classification with data augmentation on ImageNet-1k
741
+
742
+ ```shell
743
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/eval/linear.py \
744
+ model.config_file=<PATH/TO/OUTPUT/DIR>/config.yaml \
745
+ model.pretrained_weights=<PATH/TO/OUTPUT/DIR>/teacher_checkpoint.pth \
746
+ output_dir=<PATH/TO/OUTPUT/DIR> \
747
+ train.dataset=ImageNet:split=TRAIN:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET> \
748
+ train.val_dataset=ImageNet:split=VAL:root=<PATH/TO/DATASET>:extra=<PATH/TO/DATASET>
749
+ ```
750
+
751
+ ### Linear segmentation with data augmentation on ADE20K
752
+
753
+ ```shell
754
+ PYTHONPATH=. python -m dinov3.run.submit dinov3/eval/segmentation/run.py \
755
+ model.dino_hub=dinov3_vit7b16 \
756
+ config=dinov3/eval/segmentation/configs/config-ade20k-linear-training.yaml \
757
+ datasets.root=<PATH/TO/DATASET> \
758
+ --output-dir <PATH/TO/OUTPUT/DIR>
759
+ ```
760
+
761
+ After the job completes, you will find in the output path directory you specified
762
+ - `segmentation_config.yaml` that contains the config you trained the model with;
763
+ - `model_final.pth`, the final linear head checkpoint at the end of training; and
764
+ - `results-semantic-segmentation.csv` with the final metrics.
765
+
766
+
767
+ #### Linear depth estimation on NYUv2 Depth
768
+ ```shell
769
+ PYTHONPATH=. python -m dinov3.run.submit dinov3/eval/depth/run.py \
770
+ model.dino_hub=dinov3_vit7b16 \
771
+ config=dinov3/eval/depth/configs/config-nyu.yaml \
772
+ datasets.root=<PATH/TO/DATASET> \
773
+ --output-dir <PATH/TO/OUTPUT/DIR>
774
+ ```
775
+
776
+ After the job completes, you will find in the output path directory you specified
777
+ - `depth_config.yaml` that contains the config you trained the model with;
778
+ - `model_final.pth`, the final linear head checkpoint at the end of training; and
779
+ - `results-depth.csv` with the final metrics.
780
+
781
+ ### Text alignment on DINOv3 using dino.txt
782
+
783
+ Text alignment can be done following the method from `dino.txt` aka [DINOv2 Meets Text](https://arxiv.org/abs/2412.16334).
784
+
785
+ ```shell
786
+ PYTHONPATH=${PWD} python -m dinov3.run.submit dinov3/eval/text/train_dinotxt.py \
787
+ --nodes 4 \
788
+ # An example config for text alignment is here: dinov3/eval/text/configs/dinov3_vitl_text.yaml \
789
+ trainer_config_file="<PATH/TO/DINOv3/TEXT/CONFIG>" \
790
+ output-dir=<PATH/TO/OUTPUT/DIR>
791
+ ```
792
+ Launching the above trains text alignment on 4 nodes with 8 gpus each (32 gpus in total).
793
+ Please note that the text alignment model in the DINOv3 paper was trained on a private dataset and here we have given an example config in ```dinov3/eval/text/configs/dinov3_vitl_text.yaml``` using ```CocoCaptions``` dataset for illustration purposes.
794
+ Please adapt the provided ```CocoCaptions``` dataset class, the dataset can be found [here](https://www.kaggle.com/datasets/nikhil7280/coco-image-caption)
795
+
796
+
797
+ ## Canopy Height Maps v2 (CHMv2)
798
+
799
+ John Brandt, Seungeun Yi, Jamie Tolan, Xinyuan Li, Peter Potapov, <br/>
800
+ Jessica Ertel, Justine Spore, Huy V. Vo, Michaël Ramamonjisoa, Patrick Labatut, <br/>
801
+ Piotr Bojanowski, Camille Couprie
802
+
803
+ [ :scroll: [`Paper`](https://arxiv.org/abs/2603.06382)] [ :newspaper: [`Blog`](http://ai.meta.com/blog/world-resources-institute-dino-canopy-height-maps-v2)]
804
+
805
+ ### CHMv2 model loading (via PyTorch [Hub](https://docs.pytorch.org/docs/stable/hub.html))
806
+
807
+ :information_source: Please follow the link provided below to get access to the CHMv2 model weights: once accepted, an e-mail will be sent with the URL pointing to the available model weights. The URL can then be used to either:
808
+ - download the model weights to a local filesystem and point `torch.hub.load()` to these local weights via the `weights` parameters, or
809
+ - directly invoke `torch.hub.load()` to download and load a backbone from its URL.
810
+
811
+ CHMv2 uses the DINOv3 ViT-L/16 satellite as the backbone, available after requesting access [here](https://ai.meta.com/resources/models-and-libraries/dinov3-downloads/).
812
+
813
+ :warning: Please use `wget` instead of a web browser to download the weights.
814
+
815
+ Download link: https://ai.meta.com/resources/models-and-libraries/chmv2-downloads/
816
+
817
+ ```python
818
+ import torch
819
+ from dinov3.hub.backbones import Weights
820
+
821
+ REPO_DIR = <PATH/TO/A/LOCAL/DIRECTORY/WHERE/THE/DINOv3/REPO/WAS/CLONED>
822
+
823
+ chmv2_model = torch.hub.load(
824
+ REPO_DIR,
825
+ 'dinov3_vitl16_chmv2',
826
+ source="local",
827
+ weights="<CHMV2_MODEL/CHECKPOINT/URL/OR/PATH>",
828
+ backbone_weights=Weights.SAT493M, # or <DINOV3_VITL_SAT/CHECKPOINT/URL/OR/PATH>
829
+ )
830
+ ```
831
+
832
+ Refer to this [notebook](notebooks/chmv2_inference.ipynb) for an example of how to use the DINOv3 + CHMv2 model.
833
+
834
+ This [notebook](notebooks/chmv2_dataset_exploration.ipynb) can be used to download inference data from the existing global dataset stored on aws.
835
+
836
+ ### CHMv2 model loading (via Hugging Face [Transformers](https://huggingface.co/docs/transformers/))
837
+
838
+ The CHMv2 model is also available on [Hugging Face Hub](https://huggingface.co/facebook/dinov3-vitl16-chmv2-dpt-head) and supported via the Hugging Face [Transformers](https://huggingface.co/docs/transformers/index) library. Please refer to the corresponding documentation for usage, but below is a short example that demonstrates how to obtain canopy height predictions on a sample image.
839
+
840
+ ```python
841
+ from PIL import Image
842
+ import torch
843
+
844
+ from transformers import AutoModelForDepthEstimation, AutoImageProcessor
845
+
846
+ processor = AutoImageProcessor.from_pretrained("facebook/dinov3-vitl16-chmv2-dpt-head")
847
+ model = AutoModelForDepthEstimation.from_pretrained("facebook/dinov3-vitl16-chmv2-dpt-head")
848
+
849
+ image = Image.open("image.tif")
850
+ inputs = processor(images=image, return_tensors="pt")
851
+
852
+ with torch.no_grad():
853
+ outputs = model(**inputs)
854
+
855
+ depth = processor.post_process_depth_estimation(
856
+ outputs, target_sizes=[(image.height, image.width)]
857
+ )[0]["predicted_depth"]
858
+ ```
859
+
860
+ ## License
861
+
862
+ DINOv3 code and model weights are released under the DINOv3 License. See [LICENSE.md](LICENSE.md) for additional details.
863
+
864
+ ## Contributing
865
+
866
+ See [contributing](CONTRIBUTING.md) and the [code of conduct](CODE_OF_CONDUCT.md).
867
+
868
+ ## Citing DINOv3
869
+
870
+ If you find this repository useful, please consider giving a star :star: and citation :t-rex::
871
+
872
+ ```
873
+ @misc{simeoni2025dinov3,
874
+ title={{DINOv3}},
875
+ author={Sim{\'e}oni, Oriane and Vo, Huy V. and Seitzer, Maximilian and Baldassarre, Federico and Oquab, Maxime and Jose, Cijo and Khalidov, Vasil and Szafraniec, Marc and Yi, Seungeun and Ramamonjisoa, Micha{\"e}l and Massa, Francisco and Haziza, Daniel and Wehrstedt, Luca and Wang, Jianyuan and Darcet, Timoth{\'e}e and Moutakanni, Th{\'e}o and Sentana, Leonel and Roberts, Claire and Vedaldi, Andrea and Tolan, Jamie and Brandt, John and Couprie, Camille and Mairal, Julien and J{\'e}gou, Herv{\'e} and Labatut, Patrick and Bojanowski, Piotr},
876
+ year={2025},
877
+ eprint={2508.10104},
878
+ archivePrefix={arXiv},
879
+ primaryClass={cs.CV},
880
+ url={https://arxiv.org/abs/2508.10104},
881
+ }
882
+ ```
src/model/encoder/blocks/torchhub/dinov3/VENDORED_FROM.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Vendored DINOv3
2
+
3
+ - Upstream repository: https://github.com/facebookresearch/dinov3.git
4
+ - Pinned commit: `6e50ab28b75133230b6cc3a846a8adc3c300b27d`
5
+ - Import date: `2026-03-27`
6
+ - Scope: `InfiniSplat image branch only`
7
+ - Notes: Imported from the official upstream repository into this workspace snapshot. The nested `.git` directory is intentionally removed.
8
+ - Local patch: `dinov3/hub/backbones.py` accepts local `dinov3_vitl16` checkpoint paths without the upstream `-<8char_hash>.pth` suffix and infers the correct `untie_global_and_local_cls_norm` mode from the filename.
9
+ - Local patch: `dinov3/hub/backbones.py` loads local checkpoint paths with `torch.load(...)` directly instead of converting them to `file://` URLs and copying them into `~/.cache/torch/hub`.