Spaces:
Running on Zero
Running on Zero
Deploy InfiniSplat ZeroGPU demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +9 -0
- .gitignore +42 -0
- INSTALL.md +72 -0
- README.md +137 -6
- app.py +28 -0
- assets/github_demo.gif +3 -0
- config/experiment/infinisplat_hypersim_lidar.yaml +5 -0
- config/experiment/infinisplat_hypersim_rgb.yaml +5 -0
- config/inference.yaml +4 -0
- config/model/decoder/gsplat.yaml +10 -0
- config/model/encoder/infinisplat.yaml +28 -0
- config/model/encoder/infinisplat_infinidepth.yaml +28 -0
- config/viewer_settings.json +49 -0
- docs/inference.md +154 -0
- examples/data/lidar_demo/eth3d_kicker.npz +3 -0
- examples/data/lidar_demo/eth3d_kicker.png +3 -0
- examples/data/lidar_demo/eth3d_pipes.npz +3 -0
- examples/data/lidar_demo/eth3d_pipes.png +3 -0
- examples/data/lidar_demo/waymo_147.npz +3 -0
- examples/data/lidar_demo/waymo_147.png +3 -0
- examples/data/lidar_demo/waymo_9.npz +3 -0
- examples/data/lidar_demo/waymo_9.png +3 -0
- examples/data/rgb_demo/eth3d_courtyard.png +3 -0
- examples/data/rgb_demo/maksim-shutov-unsplash.jpg +3 -0
- examples/data/rgb_demo/pexels-masi.jpg +3 -0
- examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG +3 -0
- packages.txt +6 -0
- requirements.txt +30 -0
- scripts/download_checkpoints.sh +14 -0
- src/__init__.py +1 -0
- src/demo/__init__.py +2 -0
- src/demo/config.py +32 -0
- src/demo/hf_runtime.py +320 -0
- src/demo/hf_ui.py +658 -0
- src/demo/infer_batch_images.py +486 -0
- src/demo/infer_single_image.py +889 -0
- src/model/__init__.py +1 -0
- src/model/decoder/__init__.py +23 -0
- src/model/decoder/decoder.py +28 -0
- src/model/decoder/decoder_gsplat.py +98 -0
- src/model/encoder/__init__.py +19 -0
- src/model/encoder/blocks/torchhub/dinov3/.docstr.yaml +6 -0
- src/model/encoder/blocks/torchhub/dinov3/.gitignore +18 -0
- src/model/encoder/blocks/torchhub/dinov3/CODE_OF_CONDUCT.md +80 -0
- src/model/encoder/blocks/torchhub/dinov3/CONTRIBUTING.md +31 -0
- src/model/encoder/blocks/torchhub/dinov3/DATASETS.md +43 -0
- src/model/encoder/blocks/torchhub/dinov3/LICENSE.md +66 -0
- src/model/encoder/blocks/torchhub/dinov3/MODEL_CARD.md +432 -0
- src/model/encoder/blocks/torchhub/dinov3/README.md +882 -0
- 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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
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
|
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
|
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
|
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
|
examples/data/rgb_demo/eth3d_courtyard.png
ADDED
|
Git LFS Details
|
examples/data/rgb_demo/maksim-shutov-unsplash.jpg
ADDED
|
Git LFS Details
|
examples/data/rgb_demo/pexels-masi.jpg
ADDED
|
Git LFS Details
|
examples/data/rgb_demo/scannetpp_fe94fc30cf.JPG
ADDED
|
|
Git LFS Details
|
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`.
|