Spaces:
Running on Zero
Running on Zero
Add OF3GS ZeroGPU demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .gitignore +6 -0
- LICENSE +25 -0
- README.md +343 -5
- app.py +328 -0
- assets/teaser.png +3 -0
- config/compute_metrics.yaml +28 -0
- config/dataset/base_dataset.yaml +7 -0
- config/dataset/dl3dv.yaml +19 -0
- config/dataset/re10k.yaml +19 -0
- config/dataset/view_sampler/all.yaml +1 -0
- config/dataset/view_sampler/arbitrary.yaml +7 -0
- config/dataset/view_sampler/bounded.yaml +16 -0
- config/dataset/view_sampler/evaluation.yaml +4 -0
- config/dataset/view_sampler/rank.yaml +14 -0
- config/experiment/dl3dv.yaml +87 -0
- config/experiment/multi-dataset.yaml +102 -0
- config/experiment/re10k.yaml +84 -0
- config/generate_evaluation_index.yaml +36 -0
- config/loss/depth.yaml +5 -0
- config/loss/lpips.yaml +3 -0
- config/loss/mse.yaml +3 -0
- config/loss/ssim.yaml +3 -0
- config/main.yaml +78 -0
- config/model/decoder/splatting_cuda.yaml +3 -0
- config/model/encoder/backbone/croco.yaml +9 -0
- config/model/encoder/of3gs.yaml +42 -0
- requirements-train.txt +34 -0
- requirements.txt +16 -0
- scripts/download_dl3dv.sh +146 -0
- scripts/download_weights.sh +127 -0
- src/__init__.py +1 -0
- src/config.py +111 -0
- src/dataset/__init__.py +94 -0
- src/dataset/data_module.py +196 -0
- src/dataset/data_sampler.py +364 -0
- src/dataset/dataset.py +13 -0
- src/dataset/dataset_dl3dv.py +452 -0
- src/dataset/dataset_re10k.py +302 -0
- src/dataset/shims/augmentation_shim.py +219 -0
- src/dataset/shims/bounds_shim.py +80 -0
- src/dataset/shims/crop_shim.py +196 -0
- src/dataset/shims/geometry_shim.py +383 -0
- src/dataset/shims/load_shim.py +12 -0
- src/dataset/shims/normalize_shim.py +27 -0
- src/dataset/shims/patch_shim.py +38 -0
- src/dataset/types.py +51 -0
- src/dataset/validation_wrapper.py +33 -0
- src/dataset/view_sampler/__init__.py +40 -0
- src/dataset/view_sampler/three_view_hack.py +10 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ 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/teaser.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
.space_outputs/
|
| 4 |
+
weights/
|
| 5 |
+
outputs/
|
| 6 |
+
output-debug/
|
LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OF3GS Research License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 OF3GS authors.
|
| 4 |
+
|
| 5 |
+
Permission is granted to use, copy, modify, and distribute this repository for
|
| 6 |
+
non-commercial research and educational purposes, subject to the conditions
|
| 7 |
+
below.
|
| 8 |
+
|
| 9 |
+
1. This repository includes third-party code and derived code. Those components
|
| 10 |
+
remain governed by their original licenses and copyright notices.
|
| 11 |
+
2. If a third-party component imposes stricter terms than this license, the
|
| 12 |
+
stricter terms apply to that component and to derivative work that depends on
|
| 13 |
+
it.
|
| 14 |
+
3. Redistribution must preserve this license and all copyright and license
|
| 15 |
+
headers present in source files.
|
| 16 |
+
4. Commercial use is not granted by this license. Contact the authors and the
|
| 17 |
+
relevant third-party rights holders for commercial licensing.
|
| 18 |
+
|
| 19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 21 |
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
| 23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
| 24 |
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 25 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,13 +1,351 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.24.0
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: OF3GS
|
| 3 |
+
emoji: "🧭"
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.24.0
|
| 8 |
+
python_version: 3.10.13
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
models:
|
| 12 |
+
- richardchencccc/OF3GS
|
| 13 |
+
preload_from_hub:
|
| 14 |
+
- richardchencccc/OF3GS model.ckpt
|
| 15 |
+
startup_duration_timeout: 1h
|
| 16 |
+
short_description: Feed-forward 3D Gaussian Splatting from unposed images
|
| 17 |
+
tags:
|
| 18 |
+
- 3d
|
| 19 |
+
- gaussian-splatting
|
| 20 |
+
- 3d-reconstruction
|
| 21 |
---
|
| 22 |
|
| 23 |
+
<h2 align="center">
|
| 24 |
+
OF<sup>3</sup>GS: <u>O</u>n-the-<u>F</u>ly <u>F</u>eed-<u>F</u>orward 3D <u>G</u>aussian <u>S</u>platting from Unposed Images
|
| 25 |
+
</h2>
|
| 26 |
+
|
| 27 |
+
<p align="center">
|
| 28 |
+
<a href="https://richardchen225.github.io/of3gs/">
|
| 29 |
+
<img alt="Website" src="https://img.shields.io/badge/OF3GS-Website-2ea44f?style=flat&logo=googlechrome&logoColor=white">
|
| 30 |
+
</a>
|
| 31 |
+
<a href="https://arxiv.org/abs/2606.03254">
|
| 32 |
+
<img alt="Paper" src="https://img.shields.io/badge/arXiv-Paper-b31b1b?style=flat&logo=arxiv&logoColor=white">
|
| 33 |
+
</a>
|
| 34 |
+
<a href="https://github.com/richardchen225/OF3GS_code">
|
| 35 |
+
<img alt="Code" src="https://img.shields.io/badge/GitHub-Code-181717?style=flat&logo=github&logoColor=white">
|
| 36 |
+
</a>
|
| 37 |
+
<a href="https://huggingface.co/richardchencccc/OF3GS/tree/main">
|
| 38 |
+
<img alt="Model" src="https://img.shields.io/badge/Hugging%20Face-Model-ffcc4d?style=flat&logo=huggingface&logoColor=black">
|
| 39 |
+
</a>
|
| 40 |
+
</p>
|
| 41 |
+
|
| 42 |
+
<p align="center">
|
| 43 |
+
Ruiyang Chen<sup>1</sup>,
|
| 44 |
+
Feiran Li<sup>2</sup>,
|
| 45 |
+
<a href="https://fourson.github.io/">Chu Zhou</a><sup>3</sup>,
|
| 46 |
+
Zonglin Li<sup>1</sup>,
|
| 47 |
+
<a href="https://zhanyuma.cn/">Zhanyu Ma</a><sup>1</sup>,
|
| 48 |
+
<a href="https://gh-home.github.io/">Heng Guo</a><sup>1,*</sup>
|
| 49 |
+
</p>
|
| 50 |
+
|
| 51 |
+
<p align="center">
|
| 52 |
+
<sup>1</sup>Beijing University of Posts and Telecommunications
|
| 53 |
+
<sup>2</sup>Independent Researcher
|
| 54 |
+
<sup>3</sup>National Institute of Informatics
|
| 55 |
+
</p>
|
| 56 |
+
|
| 57 |
+
<p align="center">
|
| 58 |
+
<sup>*</sup>Corresponding author
|
| 59 |
+
</p>
|
| 60 |
+
|
| 61 |
+
<div align="center">
|
| 62 |
+
<img src="assets/teaser.png" alt="OF3GS teaser" width="100%">
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
Official implementation of OF3GS.
|
| 66 |
+
|
| 67 |
+
## Status
|
| 68 |
+
|
| 69 |
+
- [x] Release training and testing code.
|
| 70 |
+
- [x] Release the OF3GS checkpoint.
|
| 71 |
+
- [ ] Release the evaluation code and metric scripts.
|
| 72 |
+
- [ ] Add more demo assets and usage examples.
|
| 73 |
+
|
| 74 |
+
## Hugging Face Space
|
| 75 |
+
|
| 76 |
+
The repository includes a ZeroGPU-compatible Gradio demo in `app.py`. It accepts
|
| 77 |
+
1-64 ordered, overlapping views and returns a scale-filtered Gaussian PLY with an
|
| 78 |
+
interactive `Model3D` viewer. New-view rendering is intentionally not run by the
|
| 79 |
+
demo. Uploaded images follow the OF3GS data pipeline: they are rescaled to cover
|
| 80 |
+
the trained `252 x 518` input rectangle and center-cropped, rather than forced into
|
| 81 |
+
a square. The PLY export removes Gaussians whose largest axis scale exceeds the
|
| 82 |
+
selected threshold.
|
| 83 |
+
|
| 84 |
+
To deploy it, create a public Gradio Space with ZeroGPU, then push this repository
|
| 85 |
+
to the Space. The README metadata preloads `richardchencccc/OF3GS/model.ckpt`
|
| 86 |
+
during the Space build.
|
| 87 |
+
|
| 88 |
+
For local GPU development with the Space dependency set:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
pip install -r requirements.txt
|
| 92 |
+
python app.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
The demo targets Python 3.10, PyTorch 2.8, and current ZeroGPU runtimes. The
|
| 96 |
+
original training environment remains in `requirements-train.txt`.
|
| 97 |
+
|
| 98 |
+
## Training Environment
|
| 99 |
+
|
| 100 |
+
The original training dependency set targets:
|
| 101 |
+
|
| 102 |
+
- Linux
|
| 103 |
+
- Python 3.10
|
| 104 |
+
- CUDA 12.1
|
| 105 |
+
- PyTorch 2.2.x
|
| 106 |
+
- `gsplat==1.4.0` built for `pt22cu121`
|
| 107 |
+
|
| 108 |
+
Create an environment and install dependencies:
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
conda create -n of3gs python=3.10 -y
|
| 112 |
+
conda activate of3gs
|
| 113 |
+
|
| 114 |
+
cd OF3GS
|
| 115 |
+
pip install torch==2.2.0 torchvision==0.17.0 --index-url https://download.pytorch.org/whl/cu121
|
| 116 |
+
pip install git+https://github.com/facebookresearch/pytorch3d.git --no-build-isolation
|
| 117 |
+
pip install -r requirements-train.txt
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Download Pretrained Weights
|
| 121 |
+
|
| 122 |
+
Run:
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
bash scripts/download_weights.sh weights
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
The script writes files to `weights/` and prints the environment variables expected by `train.sh` and `test.sh`.
|
| 129 |
+
|
| 130 |
+
| File | Used For | Default Source |
|
| 131 |
+
| --- | --- | --- |
|
| 132 |
+
| `weights/pre_wm.safetensors` | Training warm start / GS head initialization | `hf://tencent/HunyuanWorld-Mirror/model.safetensors` |
|
| 133 |
+
| `weights/pre_vggt.safetensors` | VGGT initialization during training | `hf://facebook/VGGT-1B/model.safetensors` |
|
| 134 |
+
| `weights/pre_svggt.safetensors` | StreamVGGT initialization during training | `hf://lch01/StreamVGGT/model.safetensors` |
|
| 135 |
+
| `weights/pre_dav3.safetensors` | Depth Anything 3 / camera decoder initialization during training | `hf://depth-anything/DA3-LARGE/model.safetensors` |
|
| 136 |
+
| `weights/of3gs.ckpt` | OF3GS checkpoint for testing | `hf://richardchencccc/OF3GS/model.ckpt` |
|
| 137 |
+
|
| 138 |
+
The OF3GS testing checkpoint is available at [richardchencccc/OF3GS](https://huggingface.co/richardchencccc/OF3GS/tree/main). You can override any source:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
PRE_OF3GS_URL=https://huggingface.co/richardchencccc/OF3GS/resolve/main/model.ckpt \
|
| 142 |
+
bash scripts/download_weights.sh weights
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
Supported source formats:
|
| 146 |
+
|
| 147 |
+
```text
|
| 148 |
+
https://... direct URL
|
| 149 |
+
hf://namespace/repo/file Hugging Face Hub file
|
| 150 |
+
TODO:message skip with a reminder
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
After downloading, the default scripts use these paths automatically:
|
| 154 |
+
|
| 155 |
+
```bash
|
| 156 |
+
export PRE_WM_PATH=weights/pre_wm.safetensors
|
| 157 |
+
export PRE_VGGT_PATH=weights/pre_vggt.safetensors
|
| 158 |
+
export PRE_SVGGT_PATH=weights/pre_svggt.safetensors
|
| 159 |
+
export PRE_DAV3_PATH=weights/pre_dav3.safetensors
|
| 160 |
+
export PRE_OF3GS_PATH=weights/of3gs.ckpt
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
## Download DL3DV
|
| 164 |
+
|
| 165 |
+
The training code supports DL3DV. The helper script downloads `DL3DV/DL3DV-ALL-480P` `images+poses` subsets from `1K` through `11K`, then generates `train_index.json` and `test_index.json`.
|
| 166 |
+
|
| 167 |
+
First request access to the dataset on Hugging Face if needed, then log in:
|
| 168 |
+
|
| 169 |
+
```bash
|
| 170 |
+
huggingface-cli login
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
Download:
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
bash scripts/download_dl3dv.sh datasets/dl3dv
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
You can download to any location:
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
bash scripts/download_dl3dv.sh /data/DL3DV-ALL-480P
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
The expected layout after download is:
|
| 186 |
+
|
| 187 |
+
```text
|
| 188 |
+
datasets/dl3dv/
|
| 189 |
+
train_index.json
|
| 190 |
+
test_index.json
|
| 191 |
+
1K/
|
| 192 |
+
<scene_name>/
|
| 193 |
+
...
|
| 194 |
+
transforms.json
|
| 195 |
+
...
|
| 196 |
+
images_8/
|
| 197 |
+
...
|
| 198 |
+
11K/
|
| 199 |
+
<scene_name>/
|
| 200 |
+
...
|
| 201 |
+
transforms.json
|
| 202 |
+
...
|
| 203 |
+
images_8/
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
`DatasetDL3DV` reads `<root>/<split>_index.json`, then recursively resolves each indexed scene's `transforms.json` and `images_8` directory. Index entries may point to a parent scene directory such as `1K/<scene_name>` even when the actual files are nested deeper, for example `1K/<scene_name>/<subdir>/<subdir>/images_8`. The download helper generates parent-level entries when possible.
|
| 207 |
+
|
| 208 |
+
## RE10K Data
|
| 209 |
+
|
| 210 |
+
Prepare RE10K under one root directory:
|
| 211 |
+
|
| 212 |
+
```text
|
| 213 |
+
re10k/
|
| 214 |
+
train.pickle.gz
|
| 215 |
+
test.pickle.gz
|
| 216 |
+
train_index.json
|
| 217 |
+
test_index.json
|
| 218 |
+
train/
|
| 219 |
+
<scene_name>/
|
| 220 |
+
000000.png
|
| 221 |
+
000001.png
|
| 222 |
+
test/
|
| 223 |
+
<scene_name>/
|
| 224 |
+
000000.png
|
| 225 |
+
000001.png
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
The index files should be JSON arrays of scene folder names, for example `["scene_000", "scene_001"]`. `Datasetre10k` reads `<root>/<split>_index.json`, metadata from `<root>/<split>.pickle.gz`, and images from `<root>/<split>/<scene_name>/`.
|
| 229 |
+
|
| 230 |
+
## Training
|
| 231 |
+
|
| 232 |
+
`train.sh` runs distributed training with `mode=train` and `model.encoder.mode=train` explicitly set. By default it uses the files under `weights/`. For multi-dataset training, DL3DV and RE10K use separate root directories.
|
| 233 |
+
|
| 234 |
+
```bash
|
| 235 |
+
GPU_NUM=4 \
|
| 236 |
+
DL3DV_ROOT=datasets/dl3dv \
|
| 237 |
+
RE10K_ROOT=/path/to/re10k \
|
| 238 |
+
bash train.sh
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
Equivalent explicit checkpoint paths:
|
| 242 |
+
|
| 243 |
+
```bash
|
| 244 |
+
GPU_NUM=4 \
|
| 245 |
+
PRE_WM_PATH=weights/pre_wm.safetensors \
|
| 246 |
+
PRE_VGGT_PATH=weights/pre_vggt.safetensors \
|
| 247 |
+
PRE_SVGGT_PATH=weights/pre_svggt.safetensors \
|
| 248 |
+
PRE_DAV3_PATH=weights/pre_dav3.safetensors \
|
| 249 |
+
DL3DV_ROOT=datasets/dl3dv \
|
| 250 |
+
RE10K_ROOT=/path/to/re10k \
|
| 251 |
+
bash train.sh
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
Single-dataset training is also supported:
|
| 255 |
+
|
| 256 |
+
```bash
|
| 257 |
+
EXPERIMENT=dl3dv DL3DV_ROOT=datasets/dl3dv bash train.sh
|
| 258 |
+
EXPERIMENT=re10k RE10K_ROOT=/path/to/re10k bash train.sh
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
You can still override the dataset roots directly with Hydra:
|
| 262 |
+
|
| 263 |
+
```bash
|
| 264 |
+
bash train.sh \
|
| 265 |
+
dataset.dl3dv.roots='[datasets/dl3dv]' \
|
| 266 |
+
dataset.re10k.roots='[/path/to/re10k]'
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
Expected output:
|
| 270 |
+
|
| 271 |
+
- Hydra run directory under `exp_${wandb.name}/...`
|
| 272 |
+
- Lightning checkpoints under the run directory's `checkpoints/`
|
| 273 |
+
- Offline W&B logs unless `wandb.mode` is changed
|
| 274 |
+
|
| 275 |
+
## Testing
|
| 276 |
+
|
| 277 |
+
`test.sh` runs inference/evaluation with `mode=test` and `model.encoder.mode=test` explicitly set. Test mode only loads `weights/of3gs.ckpt`; the OF3GS checkpoint contains the full model state.
|
| 278 |
+
|
| 279 |
+
The current code supports evaluation-style rendering on DL3DV and RE10K. Set `EXPERIMENT=dl3dv` or `EXPERIMENT=re10k` to choose the dataset configuration. NYUv2 is not wired as a built-in dataset; use the existing DL3DV and RE10K dataset classes and configs as references if you want to add a local NYUv2 adapter.
|
| 280 |
+
|
| 281 |
+
DL3DV example:
|
| 282 |
+
|
| 283 |
+
```bash
|
| 284 |
+
EXPERIMENT=dl3dv \
|
| 285 |
+
GPU_NUM=4 \
|
| 286 |
+
bash test.sh \
|
| 287 |
+
dataset.dl3dv.roots='[datasets/dl3dv]' \
|
| 288 |
+
dataset.dl3dv.mode=test \
|
| 289 |
+
dataset.dl3dv.ctx_list='[0,8,16]' \
|
| 290 |
+
dataset.dl3dv.tgt_list='[4,12,20]'
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
RE10K example:
|
| 294 |
+
|
| 295 |
+
```bash
|
| 296 |
+
EXPERIMENT=re10k \
|
| 297 |
+
GPU_NUM=4 \
|
| 298 |
+
bash test.sh \
|
| 299 |
+
dataset.re10k.roots='[/path/to/re10k]'
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
Equivalent explicit checkpoint paths:
|
| 303 |
+
|
| 304 |
+
```bash
|
| 305 |
+
EXPERIMENT=dl3dv \
|
| 306 |
+
GPU_NUM=4 \
|
| 307 |
+
PRE_OF3GS_PATH=weights/of3gs.ckpt \
|
| 308 |
+
TEST_OUTPUT_PATH=outputs/of3gs \
|
| 309 |
+
bash test.sh \
|
| 310 |
+
dataset.dl3dv.roots='[datasets/dl3dv]' \
|
| 311 |
+
dataset.dl3dv.mode=test \
|
| 312 |
+
dataset.dl3dv.ctx_list='[0,8,16]' \
|
| 313 |
+
dataset.dl3dv.tgt_list='[4,12,20]'
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
Enable PSNR/SSIM/LPIPS during testing:
|
| 317 |
+
|
| 318 |
+
```bash
|
| 319 |
+
EXPERIMENT=dl3dv COMPUTE_SCORES=true GPU_NUM=4 bash test.sh \
|
| 320 |
+
dataset.dl3dv.roots='[datasets/dl3dv]' \
|
| 321 |
+
dataset.dl3dv.mode=test \
|
| 322 |
+
dataset.dl3dv.ctx_list='[0,8,16]' \
|
| 323 |
+
dataset.dl3dv.tgt_list='[4,12,20]'
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
Expected output:
|
| 327 |
+
|
| 328 |
+
- Rendered target images under `${TEST_OUTPUT_PATH:-outputs/of3gs}/${wandb.name}/<scene>/color/`, or `<psnr>_<scene>/color/` when scores are enabled
|
| 329 |
+
- Optional PSNR/SSIM/LPIPS logging when `test.compute_scores=true`
|
| 330 |
+
- Benchmark timing summary printed at test end
|
| 331 |
+
|
| 332 |
+
## License
|
| 333 |
+
|
| 334 |
+
See `LICENSE`.
|
| 335 |
+
|
| 336 |
+
## Citation
|
| 337 |
+
|
| 338 |
+
If you find OF3GS useful, please cite:
|
| 339 |
+
|
| 340 |
+
```bibtex
|
| 341 |
+
@article{chen2026of3gs,
|
| 342 |
+
title={OF3GS: On-the-Fly Feed-Forward 3D Gaussian Splatting from Unposed Images},
|
| 343 |
+
author={Chen, Ruiyang and Li, Feiran and Zhou, Chu and Li, Zonglin and Ma, Zhanyu and Guo, Heng},
|
| 344 |
+
journal={arXiv preprint arXiv:2606.03254},
|
| 345 |
+
year={2026}
|
| 346 |
+
}
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
## Acknowledgements
|
| 350 |
+
|
| 351 |
+
We thank the authors of [VGGT](https://github.com/facebookresearch/vggt), [StreamVGGT](https://github.com/wzzheng/StreamVGGT), and [AnySplat](https://github.com/InternRobotics/AnySplat) for their excellent work and open-source contributions.
|
app.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import shutil
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import spaces
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
import numpy as np
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from torchvision.transforms.functional import to_tensor
|
| 16 |
+
|
| 17 |
+
from src.model.decoder.decoder_splatting_cuda import DecoderSplattingCUDACfg
|
| 18 |
+
from src.model.encoder.common.gaussian_adapter import GaussianAdapterCfg
|
| 19 |
+
from src.model.encoder.of3gs import EncoderOF3GSCfg, OpacityMappingCfg
|
| 20 |
+
from src.model.encoder.visualization.encoder_visualizer_epipolar_cfg import (
|
| 21 |
+
EncoderVisualizerEpipolarCfg,
|
| 22 |
+
)
|
| 23 |
+
from src.model.model.of3gs import OF3GS
|
| 24 |
+
from src.model.ply_export import export_ply
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
MODEL_REPO = os.getenv("OF3GS_MODEL_REPO", "richardchencccc/OF3GS")
|
| 28 |
+
MODEL_FILE = os.getenv("OF3GS_MODEL_FILE", "model.ckpt")
|
| 29 |
+
# These are the dimensions used by the OF3GS training experiments
|
| 30 |
+
# (config/experiment/{dl3dv,re10k}.yaml), not the standalone VGGT helpers.
|
| 31 |
+
INPUT_HEIGHT = 252
|
| 32 |
+
INPUT_WIDTH = 518
|
| 33 |
+
MIN_VIEWS = 1
|
| 34 |
+
MAX_VIEWS = 64
|
| 35 |
+
OUTPUT_ROOT = (Path.cwd() / ".space_outputs").resolve()
|
| 36 |
+
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _build_model() -> OF3GS:
|
| 40 |
+
encoder_cfg = EncoderOF3GSCfg(
|
| 41 |
+
name="of3gs",
|
| 42 |
+
anchor_feat_dim=128,
|
| 43 |
+
voxel_size=0.002,
|
| 44 |
+
n_offsets=2,
|
| 45 |
+
d_feature=32,
|
| 46 |
+
add_view=False,
|
| 47 |
+
num_monocular_samples=32,
|
| 48 |
+
backbone=None,
|
| 49 |
+
visualizer=EncoderVisualizerEpipolarCfg(
|
| 50 |
+
num_samples=8,
|
| 51 |
+
min_resolution=256,
|
| 52 |
+
export_ply=False,
|
| 53 |
+
),
|
| 54 |
+
gaussian_adapter=GaussianAdapterCfg(
|
| 55 |
+
gaussian_scale_min=0.5,
|
| 56 |
+
gaussian_scale_max=15.0,
|
| 57 |
+
sh_degree=4,
|
| 58 |
+
),
|
| 59 |
+
apply_bounds_shim=True,
|
| 60 |
+
opacity_mapping=OpacityMappingCfg(initial=0.0, final=0.0, warm_up=1),
|
| 61 |
+
gaussians_per_pixel=1,
|
| 62 |
+
num_surfaces=1,
|
| 63 |
+
gs_params_head_type="dpt_gs",
|
| 64 |
+
pose_free=True,
|
| 65 |
+
pred_pose=True,
|
| 66 |
+
gs_prune=False,
|
| 67 |
+
pred_head_type="depth",
|
| 68 |
+
freeze_backbone=True,
|
| 69 |
+
freeze_module="patch_embed",
|
| 70 |
+
distill=True,
|
| 71 |
+
render_conf=True,
|
| 72 |
+
conf_threshold=0.05,
|
| 73 |
+
voxelize=True,
|
| 74 |
+
intermediate_layer_idx=[4, 11, 17, 23],
|
| 75 |
+
mode="test",
|
| 76 |
+
)
|
| 77 |
+
decoder_cfg = DecoderSplattingCUDACfg(
|
| 78 |
+
name="splatting_cuda",
|
| 79 |
+
background_color=[1.0, 1.0, 1.0],
|
| 80 |
+
make_scale_invariant=False,
|
| 81 |
+
)
|
| 82 |
+
return OF3GS(encoder_cfg, decoder_cfg)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _checkpoint_state(path: str) -> dict[str, torch.Tensor]:
|
| 86 |
+
try:
|
| 87 |
+
checkpoint = torch.load(
|
| 88 |
+
path,
|
| 89 |
+
map_location="cpu",
|
| 90 |
+
weights_only=True,
|
| 91 |
+
mmap=True,
|
| 92 |
+
)
|
| 93 |
+
except TypeError:
|
| 94 |
+
checkpoint = torch.load(path, map_location="cpu")
|
| 95 |
+
|
| 96 |
+
if "state_dict" in checkpoint:
|
| 97 |
+
checkpoint = checkpoint["state_dict"]
|
| 98 |
+
elif "model" in checkpoint:
|
| 99 |
+
checkpoint = checkpoint["model"]
|
| 100 |
+
|
| 101 |
+
if any(key.startswith("model.") for key in checkpoint):
|
| 102 |
+
checkpoint = {
|
| 103 |
+
key.removeprefix("model."): value
|
| 104 |
+
for key, value in checkpoint.items()
|
| 105 |
+
if key.startswith("model.")
|
| 106 |
+
}
|
| 107 |
+
return {
|
| 108 |
+
key.removeprefix("module."): value for key, value in checkpoint.items()
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _load_model() -> tuple[OF3GS, torch.device]:
|
| 113 |
+
checkpoint_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
|
| 114 |
+
model = _build_model()
|
| 115 |
+
state = _checkpoint_state(checkpoint_path)
|
| 116 |
+
try:
|
| 117 |
+
incompatible = model.load_state_dict(state, strict=False, assign=True)
|
| 118 |
+
except TypeError:
|
| 119 |
+
incompatible = model.load_state_dict(state, strict=False)
|
| 120 |
+
|
| 121 |
+
if incompatible.missing_keys:
|
| 122 |
+
print(f"OF3GS checkpoint missing {len(incompatible.missing_keys)} keys")
|
| 123 |
+
if incompatible.unexpected_keys:
|
| 124 |
+
print(f"OF3GS checkpoint has {len(incompatible.unexpected_keys)} extra keys")
|
| 125 |
+
|
| 126 |
+
del state
|
| 127 |
+
gc.collect()
|
| 128 |
+
|
| 129 |
+
running_on_space = bool(os.getenv("SPACE_ID"))
|
| 130 |
+
device = torch.device(
|
| 131 |
+
"cuda" if torch.cuda.is_available() or running_on_space else "cpu"
|
| 132 |
+
)
|
| 133 |
+
model = model.to(device).eval()
|
| 134 |
+
model.requires_grad_(False)
|
| 135 |
+
return model, device
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
MODEL, DEVICE = _load_model()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _file_path(file_data: Any) -> str:
|
| 142 |
+
if isinstance(file_data, str):
|
| 143 |
+
return file_data
|
| 144 |
+
if isinstance(file_data, dict):
|
| 145 |
+
return file_data.get("path") or file_data.get("name")
|
| 146 |
+
return str(getattr(file_data, "path", getattr(file_data, "name", file_data)))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _image_tensor(image_source: str | np.ndarray | Image.Image) -> torch.Tensor:
|
| 150 |
+
if isinstance(image_source, (str, Path)):
|
| 151 |
+
# Uploaded files are temporary; load pixels before closing the file handle.
|
| 152 |
+
with Image.open(image_source) as opened_image:
|
| 153 |
+
image = opened_image.copy()
|
| 154 |
+
elif isinstance(image_source, np.ndarray):
|
| 155 |
+
image = Image.fromarray(image_source)
|
| 156 |
+
else:
|
| 157 |
+
image = image_source.copy()
|
| 158 |
+
|
| 159 |
+
image = image.convert("RGB")
|
| 160 |
+
width, height = image.size
|
| 161 |
+
# Match dataset.shims.crop_shim.rescale_and_crop(intr_aug=False): scale
|
| 162 |
+
# until the requested rectangle is covered, then center-crop to 252x518.
|
| 163 |
+
scale_factor = max(INPUT_HEIGHT / height, INPUT_WIDTH / width)
|
| 164 |
+
scaled_height = round(height * scale_factor)
|
| 165 |
+
scaled_width = round(width * scale_factor)
|
| 166 |
+
image = image.resize((scaled_width, scaled_height), Image.Resampling.LANCZOS)
|
| 167 |
+
left = (scaled_width - INPUT_WIDTH) // 2
|
| 168 |
+
top = (scaled_height - INPUT_HEIGHT) // 2
|
| 169 |
+
image = image.crop((left, top, left + INPUT_WIDTH, top + INPUT_HEIGHT))
|
| 170 |
+
# This is equivalent to the dataset's torchvision ToTensor conversion.
|
| 171 |
+
return to_tensor(image).contiguous()
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def _prepare_images(files: list[Any] | None) -> torch.Tensor:
|
| 175 |
+
if not files:
|
| 176 |
+
raise gr.Error("Upload at least one image.")
|
| 177 |
+
if isinstance(files, (str, dict)):
|
| 178 |
+
files = [files]
|
| 179 |
+
if not MIN_VIEWS <= len(files) <= MAX_VIEWS:
|
| 180 |
+
raise gr.Error(f"Select between {MIN_VIEWS} and {MAX_VIEWS} images.")
|
| 181 |
+
|
| 182 |
+
tensors = [_image_tensor(_file_path(file_data)) for file_data in files]
|
| 183 |
+
return torch.stack(tensors).unsqueeze(0)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _session_directory(request: gr.Request | None) -> Path:
|
| 187 |
+
session_hash = request.session_hash if request and request.session_hash else "local"
|
| 188 |
+
safe_hash = re.sub(r"[^a-zA-Z0-9_-]", "_", session_hash)
|
| 189 |
+
directory = (OUTPUT_ROOT / safe_hash).resolve()
|
| 190 |
+
if directory.parent != OUTPUT_ROOT:
|
| 191 |
+
raise RuntimeError("Invalid output directory")
|
| 192 |
+
if directory.exists():
|
| 193 |
+
shutil.rmtree(directory)
|
| 194 |
+
directory.mkdir(parents=True)
|
| 195 |
+
return directory
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _run_reconstruction(
|
| 199 |
+
images: torch.Tensor,
|
| 200 |
+
scale_threshold: float,
|
| 201 |
+
request: gr.Request | None,
|
| 202 |
+
progress: gr.Progress,
|
| 203 |
+
):
|
| 204 |
+
if DEVICE.type != "cuda":
|
| 205 |
+
raise gr.Error("OF3GS inference requires a CUDA GPU Space.")
|
| 206 |
+
|
| 207 |
+
output_directory = _session_directory(request)
|
| 208 |
+
progress(0.1, desc="Preparing views")
|
| 209 |
+
images = images.to(DEVICE, non_blocking=True)
|
| 210 |
+
# The encoder uses target views only for optional pose/render evaluation.
|
| 211 |
+
# Keeping this empty makes every uploaded image a context view and stops at GS output.
|
| 212 |
+
target_images = images[:, :0]
|
| 213 |
+
gaussians = scale = keep_mask = harmonics = None
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
progress(0.2, desc="Reconstructing Gaussian scene")
|
| 217 |
+
with torch.no_grad():
|
| 218 |
+
gaussians, _, _ = MODEL.encoder.inference(
|
| 219 |
+
images,
|
| 220 |
+
target_images,
|
| 221 |
+
global_step=0,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
progress(0.8, desc="Filtering and writing Gaussians")
|
| 225 |
+
ply_path = output_directory / "of3gs_gaussians.ply"
|
| 226 |
+
scale = gaussians.scales[0]
|
| 227 |
+
keep_mask = scale.max(dim=-1).values <= scale_threshold
|
| 228 |
+
kept_count = int(keep_mask.sum().item())
|
| 229 |
+
if kept_count == 0:
|
| 230 |
+
raise gr.Error("The scale threshold removed every Gaussian.")
|
| 231 |
+
harmonics = gaussians.harmonics[0]
|
| 232 |
+
export_ply(
|
| 233 |
+
gaussians.means[0],
|
| 234 |
+
gaussians.scales[0],
|
| 235 |
+
gaussians.rotations[0],
|
| 236 |
+
harmonics,
|
| 237 |
+
gaussians.opacities[0],
|
| 238 |
+
ply_path,
|
| 239 |
+
scale_threshold=float(scale_threshold),
|
| 240 |
+
save_sh_dc_only=True,
|
| 241 |
+
)
|
| 242 |
+
progress(1.0, desc="Complete")
|
| 243 |
+
|
| 244 |
+
status = f"{images.shape[1]} views | {kept_count:,} Gaussians kept"
|
| 245 |
+
return str(ply_path), str(ply_path), status
|
| 246 |
+
finally:
|
| 247 |
+
del images, target_images, gaussians, scale, keep_mask, harmonics
|
| 248 |
+
gc.collect()
|
| 249 |
+
torch.cuda.empty_cache()
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@spaces.GPU(duration=300)
|
| 253 |
+
def reconstruct_images(
|
| 254 |
+
files: list[Any] | None,
|
| 255 |
+
scale_threshold: float,
|
| 256 |
+
request: gr.Request,
|
| 257 |
+
progress: gr.Progress = gr.Progress(track_tqdm=False),
|
| 258 |
+
) -> tuple[str, str, str]:
|
| 259 |
+
"""Reconstruct an interactive Gaussian PLY from 1-64 ordered images."""
|
| 260 |
+
images = _prepare_images(files)
|
| 261 |
+
return _run_reconstruction(
|
| 262 |
+
images,
|
| 263 |
+
float(scale_threshold),
|
| 264 |
+
request,
|
| 265 |
+
progress,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def cleanup_session(request: gr.Request) -> None:
|
| 270 |
+
"""Remove temporary PLY files when a browser session is closed."""
|
| 271 |
+
if not request.session_hash:
|
| 272 |
+
return
|
| 273 |
+
safe_hash = re.sub(r"[^a-zA-Z0-9_-]", "_", request.session_hash)
|
| 274 |
+
directory = (OUTPUT_ROOT / safe_hash).resolve()
|
| 275 |
+
if directory.parent == OUTPUT_ROOT:
|
| 276 |
+
shutil.rmtree(directory, ignore_errors=True)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
CSS = """
|
| 280 |
+
.gradio-container { max-width: 1180px !important; }
|
| 281 |
+
#project-title { margin-bottom: 0.25rem; }
|
| 282 |
+
#project-links { color: var(--body-text-color-subdued); margin-bottom: 1rem; }
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
with gr.Blocks(title="OF3GS Demo", css=CSS) as demo:
|
| 286 |
+
gr.Markdown("# OF3GS", elem_id="project-title")
|
| 287 |
+
gr.Markdown(
|
| 288 |
+
"On-the-Fly Feed-Forward 3D Gaussian Splatting from Unposed Images | "
|
| 289 |
+
"[Project](https://richardchen225.github.io/of3gs/) | "
|
| 290 |
+
"[Paper](https://arxiv.org/abs/2606.03254) | "
|
| 291 |
+
"[Code](https://github.com/richardchen225/OF3GS_code)",
|
| 292 |
+
elem_id="project-links",
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
with gr.Row(equal_height=False):
|
| 296 |
+
with gr.Column(scale=5):
|
| 297 |
+
image_files = gr.File(
|
| 298 |
+
label="Ordered views (1-64 images)",
|
| 299 |
+
file_count="multiple",
|
| 300 |
+
file_types=["image"],
|
| 301 |
+
type="filepath",
|
| 302 |
+
)
|
| 303 |
+
scale_threshold = gr.Slider(
|
| 304 |
+
minimum=0.005,
|
| 305 |
+
maximum=0.30,
|
| 306 |
+
value=0.08,
|
| 307 |
+
step=0.005,
|
| 308 |
+
label="Maximum Gaussian scale",
|
| 309 |
+
)
|
| 310 |
+
image_submit = gr.Button("Reconstruct", variant="primary")
|
| 311 |
+
|
| 312 |
+
with gr.Column(scale=7):
|
| 313 |
+
reconstruction = gr.Model3D(label="Gaussian scene", height=520)
|
| 314 |
+
ply_download = gr.File(label="Download PLY")
|
| 315 |
+
status = gr.Textbox(label="Result", interactive=False)
|
| 316 |
+
|
| 317 |
+
outputs = [reconstruction, ply_download, status]
|
| 318 |
+
image_submit.click(
|
| 319 |
+
fn=reconstruct_images,
|
| 320 |
+
inputs=[image_files, scale_threshold],
|
| 321 |
+
outputs=outputs,
|
| 322 |
+
)
|
| 323 |
+
demo.unload(cleanup_session)
|
| 324 |
+
|
| 325 |
+
demo.queue()
|
| 326 |
+
|
| 327 |
+
if __name__ == "__main__":
|
| 328 |
+
demo.launch(show_error=True, mcp_server=True)
|
assets/teaser.png
ADDED
|
Git LFS Details
|
config/compute_metrics.yaml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- model/encoder: noposplat
|
| 3 |
+
- loss: []
|
| 4 |
+
- override dataset/view_sampler@dataset.re10k.view_sampler: evaluation
|
| 5 |
+
|
| 6 |
+
dataset:
|
| 7 |
+
re10k:
|
| 8 |
+
view_sampler:
|
| 9 |
+
index_path: assets/evaluation_index_re10k.json
|
| 10 |
+
|
| 11 |
+
data_loader:
|
| 12 |
+
train:
|
| 13 |
+
num_workers: 0
|
| 14 |
+
persistent_workers: true
|
| 15 |
+
batch_size: 1
|
| 16 |
+
seed: 1234
|
| 17 |
+
test:
|
| 18 |
+
num_workers: 4
|
| 19 |
+
persistent_workers: false
|
| 20 |
+
batch_size: 1
|
| 21 |
+
seed: 2345
|
| 22 |
+
val:
|
| 23 |
+
num_workers: 0
|
| 24 |
+
persistent_workers: true
|
| 25 |
+
batch_size: 1
|
| 26 |
+
seed: 3456
|
| 27 |
+
|
| 28 |
+
seed: 111123
|
config/dataset/base_dataset.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
make_baseline_1: true
|
| 2 |
+
relative_pose: true
|
| 3 |
+
augment: true
|
| 4 |
+
background_color: [1.0, 1.0, 1.0]
|
| 5 |
+
overfit_to_scene: null
|
| 6 |
+
skip_bad_shape: true
|
| 7 |
+
rescale_to_1cube: false
|
config/dataset/dl3dv.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- base_dataset
|
| 3 |
+
- view_sampler: bounded
|
| 4 |
+
|
| 5 |
+
name: dl3dv
|
| 6 |
+
roots: []
|
| 7 |
+
|
| 8 |
+
input_image_shape: [256, 256]
|
| 9 |
+
original_image_shape: [270, 480]
|
| 10 |
+
cameras_are_circular: false
|
| 11 |
+
|
| 12 |
+
baseline_min: 1e-3
|
| 13 |
+
baseline_max: 1e2
|
| 14 |
+
max_fov: 100.0
|
| 15 |
+
avg_pose: false
|
| 16 |
+
|
| 17 |
+
rescale_to_1cube: true
|
| 18 |
+
make_baseline_1: false
|
| 19 |
+
intr_augment: true
|
config/dataset/re10k.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- base_dataset
|
| 3 |
+
- view_sampler: bounded
|
| 4 |
+
|
| 5 |
+
name: re10k
|
| 6 |
+
roots: []
|
| 7 |
+
|
| 8 |
+
input_image_shape: [256, 256]
|
| 9 |
+
original_image_shape: [270, 480]
|
| 10 |
+
cameras_are_circular: false
|
| 11 |
+
|
| 12 |
+
baseline_min: 1e-3
|
| 13 |
+
baseline_max: 1e2
|
| 14 |
+
max_fov: 100.0
|
| 15 |
+
avg_pose: false
|
| 16 |
+
|
| 17 |
+
rescale_to_1cube: true
|
| 18 |
+
make_baseline_1: false
|
| 19 |
+
intr_augment: true
|
config/dataset/view_sampler/all.yaml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
name: all
|
config/dataset/view_sampler/arbitrary.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: arbitrary
|
| 2 |
+
|
| 3 |
+
num_target_views: 1
|
| 4 |
+
num_context_views: 2
|
| 5 |
+
|
| 6 |
+
# If you want to hard-code context views, do so here.
|
| 7 |
+
context_views: null
|
config/dataset/view_sampler/bounded.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: bounded
|
| 2 |
+
|
| 3 |
+
num_target_views: 2
|
| 4 |
+
num_context_views: 16
|
| 5 |
+
|
| 6 |
+
min_distance_between_context_views: 2
|
| 7 |
+
max_distance_between_context_views: 6
|
| 8 |
+
min_distance_to_context_views: 0
|
| 9 |
+
|
| 10 |
+
warm_up_steps: 0
|
| 11 |
+
initial_min_distance_between_context_views: 2
|
| 12 |
+
initial_max_distance_between_context_views: 6
|
| 13 |
+
|
| 14 |
+
max_img_per_gpu: 16
|
| 15 |
+
min_gap_multiplier: 3
|
| 16 |
+
max_gap_multiplier: 5
|
config/dataset/view_sampler/evaluation.yaml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: evaluation
|
| 2 |
+
|
| 3 |
+
index_path: assets/evaluation_index_re10k.json
|
| 4 |
+
num_context_views: 2
|
config/dataset/view_sampler/rank.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: rank
|
| 2 |
+
|
| 3 |
+
num_target_views: 4
|
| 4 |
+
num_context_views: 16
|
| 5 |
+
|
| 6 |
+
min_distance_between_context_views: 4
|
| 7 |
+
max_distance_between_context_views: 22
|
| 8 |
+
min_distance_to_context_views: 0
|
| 9 |
+
|
| 10 |
+
warm_up_steps: 0
|
| 11 |
+
initial_min_distance_between_context_views: 5
|
| 12 |
+
initial_max_distance_between_context_views: 7
|
| 13 |
+
|
| 14 |
+
max_img_per_gpu: 16
|
config/experiment/dl3dv.yaml
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- /dataset@_group_.dl3dv: dl3dv
|
| 5 |
+
- override /model/encoder: of3gs
|
| 6 |
+
- override /model/encoder/backbone: croco
|
| 7 |
+
- override /loss: [mse, lpips, ssim, depth]
|
| 8 |
+
|
| 9 |
+
wandb:
|
| 10 |
+
name: dl3dv
|
| 11 |
+
tags: [dl3dv, 448x448]
|
| 12 |
+
|
| 13 |
+
model:
|
| 14 |
+
encoder:
|
| 15 |
+
mode: train
|
| 16 |
+
pre_vggt_path: ""
|
| 17 |
+
pre_svggt_path: ""
|
| 18 |
+
pre_dav3_path: ""
|
| 19 |
+
gs_params_head_type: dpt_gs
|
| 20 |
+
pose_free: true
|
| 21 |
+
intrinsics_embed_loc: encoder
|
| 22 |
+
intrinsics_embed_type: token
|
| 23 |
+
pretrained_weights: ''
|
| 24 |
+
voxel_size: 0.002
|
| 25 |
+
pred_pose: true
|
| 26 |
+
anchor_feat_dim: 128
|
| 27 |
+
gs_prune: false
|
| 28 |
+
pred_head_type: depth
|
| 29 |
+
freeze_backbone: true
|
| 30 |
+
distill: true
|
| 31 |
+
render_conf: true
|
| 32 |
+
conf_threshold: 0.05
|
| 33 |
+
freeze_module: patch_embed
|
| 34 |
+
voxelize: true
|
| 35 |
+
intermediate_layer_idx: [4, 11, 17, 23]
|
| 36 |
+
|
| 37 |
+
dataset:
|
| 38 |
+
dl3dv:
|
| 39 |
+
mode: train
|
| 40 |
+
ctx_list: []
|
| 41 |
+
tgt_list: []
|
| 42 |
+
input_image_shape: [252, 518]
|
| 43 |
+
view_sampler:
|
| 44 |
+
num_target_views: 9
|
| 45 |
+
min_distance_between_context_views: 32
|
| 46 |
+
max_distance_between_context_views: 256
|
| 47 |
+
min_gap_multiplier: 3
|
| 48 |
+
max_gap_multiplier: 5
|
| 49 |
+
avg_pose: false
|
| 50 |
+
intr_augment: true
|
| 51 |
+
normalize_by_pts3d: false
|
| 52 |
+
rescale_to_1cube: false
|
| 53 |
+
|
| 54 |
+
optimizer:
|
| 55 |
+
lr: 2e-4
|
| 56 |
+
warm_up_steps: 1000
|
| 57 |
+
backbone_lr_multiplier: 0.1
|
| 58 |
+
|
| 59 |
+
data_loader:
|
| 60 |
+
train:
|
| 61 |
+
batch_size: 1
|
| 62 |
+
|
| 63 |
+
trainer:
|
| 64 |
+
max_steps: 10000
|
| 65 |
+
val_check_interval: 100
|
| 66 |
+
num_nodes: 1
|
| 67 |
+
accumulate_grad_batches: 1
|
| 68 |
+
precision: 16-mixed
|
| 69 |
+
|
| 70 |
+
checkpointing:
|
| 71 |
+
load: null
|
| 72 |
+
every_n_train_steps: 500
|
| 73 |
+
save_weights_only: true
|
| 74 |
+
save_top_k: 10
|
| 75 |
+
|
| 76 |
+
train:
|
| 77 |
+
pose_loss_alpha: 1.0
|
| 78 |
+
pose_loss_delta: 1.0
|
| 79 |
+
cxt_depth_weight: 1.0
|
| 80 |
+
weight_pose: 10.0
|
| 81 |
+
weight_depth: 1.0
|
| 82 |
+
weight_normal: 0.1
|
| 83 |
+
|
| 84 |
+
hydra:
|
| 85 |
+
run:
|
| 86 |
+
dir: ./exp_${wandb.name}/${now:%Y-%m-%d_%H-%M-%S}
|
| 87 |
+
|
config/experiment/multi-dataset.yaml
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- /dataset@_group_.dl3dv: dl3dv
|
| 5 |
+
- /dataset@_group_.re10k: re10k
|
| 6 |
+
- override /model/encoder: of3gs
|
| 7 |
+
- override /model/encoder/backbone: croco
|
| 8 |
+
- override /loss: [mse, lpips, ssim, depth]
|
| 9 |
+
|
| 10 |
+
wandb:
|
| 11 |
+
name: multidataset-16gpu
|
| 12 |
+
tags: [multidataset, 448x448]
|
| 13 |
+
|
| 14 |
+
model:
|
| 15 |
+
encoder:
|
| 16 |
+
mode: train
|
| 17 |
+
pre_vggt_path: ""
|
| 18 |
+
pre_svggt_path: ""
|
| 19 |
+
pre_dav3_path: ""
|
| 20 |
+
gs_params_head_type: dpt_gs
|
| 21 |
+
pose_free: true
|
| 22 |
+
intrinsics_embed_loc: encoder
|
| 23 |
+
intrinsics_embed_type: token
|
| 24 |
+
pretrained_weights: ''
|
| 25 |
+
voxel_size: 0.002
|
| 26 |
+
pred_pose: true
|
| 27 |
+
anchor_feat_dim: 128
|
| 28 |
+
gs_prune: false
|
| 29 |
+
pred_head_type: depth
|
| 30 |
+
freeze_backbone: true
|
| 31 |
+
distill: true
|
| 32 |
+
render_conf: true
|
| 33 |
+
conf_threshold: 0.05
|
| 34 |
+
freeze_module: patch_embed
|
| 35 |
+
voxelize: true
|
| 36 |
+
intermediate_layer_idx: [4, 11, 17, 23]
|
| 37 |
+
|
| 38 |
+
dataset:
|
| 39 |
+
dl3dv:
|
| 40 |
+
mode: train
|
| 41 |
+
ctx_list: []
|
| 42 |
+
tgt_list: []
|
| 43 |
+
input_image_shape: [252, 518]
|
| 44 |
+
view_sampler:
|
| 45 |
+
num_target_views: 9
|
| 46 |
+
min_distance_between_context_views: 32
|
| 47 |
+
max_distance_between_context_views: 256
|
| 48 |
+
min_gap_multiplier: 3
|
| 49 |
+
max_gap_multiplier: 5
|
| 50 |
+
avg_pose: false
|
| 51 |
+
intr_augment: true
|
| 52 |
+
normalize_by_pts3d: false
|
| 53 |
+
rescale_to_1cube: false
|
| 54 |
+
|
| 55 |
+
re10k:
|
| 56 |
+
input_image_shape: [252, 518]
|
| 57 |
+
view_sampler:
|
| 58 |
+
num_target_views: 9
|
| 59 |
+
min_distance_between_context_views: 16
|
| 60 |
+
max_distance_between_context_views: 128
|
| 61 |
+
min_gap_multiplier: 3
|
| 62 |
+
max_gap_multiplier: 5
|
| 63 |
+
max_img_per_gpu: 10
|
| 64 |
+
avg_pose: false
|
| 65 |
+
intr_augment: true
|
| 66 |
+
normalize_by_pts3d: false
|
| 67 |
+
rescale_to_1cube: true
|
| 68 |
+
|
| 69 |
+
optimizer:
|
| 70 |
+
lr: 1e-4
|
| 71 |
+
warm_up_steps: 0
|
| 72 |
+
backbone_lr_multiplier: 0.1
|
| 73 |
+
|
| 74 |
+
data_loader:
|
| 75 |
+
train:
|
| 76 |
+
batch_size: 1
|
| 77 |
+
|
| 78 |
+
trainer:
|
| 79 |
+
max_steps: 10000
|
| 80 |
+
val_check_interval: 100
|
| 81 |
+
num_nodes: 1
|
| 82 |
+
accumulate_grad_batches: 1
|
| 83 |
+
precision: 16-mixed
|
| 84 |
+
|
| 85 |
+
checkpointing:
|
| 86 |
+
load: null
|
| 87 |
+
every_n_train_steps: 500
|
| 88 |
+
save_weights_only: true
|
| 89 |
+
save_top_k: 10
|
| 90 |
+
|
| 91 |
+
train:
|
| 92 |
+
pose_loss_alpha: 1.0
|
| 93 |
+
pose_loss_delta: 1.0
|
| 94 |
+
cxt_depth_weight: 1.0
|
| 95 |
+
weight_pose: 10.0
|
| 96 |
+
weight_depth: 1.0
|
| 97 |
+
weight_normal: 0.1
|
| 98 |
+
|
| 99 |
+
hydra:
|
| 100 |
+
run:
|
| 101 |
+
dir: ./exp_${wandb.name}/${now:%Y-%m-%d_%H-%M-%S}
|
| 102 |
+
|
config/experiment/re10k.yaml
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- /dataset@_group_.re10k: re10k
|
| 5 |
+
- override /model/encoder: of3gs
|
| 6 |
+
- override /model/encoder/backbone: croco
|
| 7 |
+
- override /loss: [mse, lpips, ssim, depth]
|
| 8 |
+
|
| 9 |
+
wandb:
|
| 10 |
+
name: re10k
|
| 11 |
+
tags: [re10k, 448x448]
|
| 12 |
+
|
| 13 |
+
model:
|
| 14 |
+
encoder:
|
| 15 |
+
mode: train
|
| 16 |
+
pre_vggt_path: ""
|
| 17 |
+
pre_svggt_path: ""
|
| 18 |
+
pre_dav3_path: ""
|
| 19 |
+
gs_params_head_type: dpt_gs
|
| 20 |
+
pose_free: true
|
| 21 |
+
intrinsics_embed_loc: encoder
|
| 22 |
+
intrinsics_embed_type: token
|
| 23 |
+
pretrained_weights: ''
|
| 24 |
+
voxel_size: 0.002
|
| 25 |
+
pred_pose: true
|
| 26 |
+
anchor_feat_dim: 128
|
| 27 |
+
gs_prune: false
|
| 28 |
+
pred_head_type: depth
|
| 29 |
+
freeze_backbone: true
|
| 30 |
+
distill: true
|
| 31 |
+
render_conf: true
|
| 32 |
+
conf_threshold: 0.05
|
| 33 |
+
freeze_module: patch_embed
|
| 34 |
+
voxelize: true
|
| 35 |
+
intermediate_layer_idx: [4, 11, 17, 23]
|
| 36 |
+
|
| 37 |
+
dataset:
|
| 38 |
+
re10k:
|
| 39 |
+
input_image_shape: [252, 518]
|
| 40 |
+
view_sampler:
|
| 41 |
+
num_target_views: 9
|
| 42 |
+
min_distance_between_context_views: 16
|
| 43 |
+
max_distance_between_context_views: 128
|
| 44 |
+
min_gap_multiplier: 3
|
| 45 |
+
max_gap_multiplier: 5
|
| 46 |
+
max_img_per_gpu: 10
|
| 47 |
+
avg_pose: false
|
| 48 |
+
intr_augment: true
|
| 49 |
+
normalize_by_pts3d: false
|
| 50 |
+
rescale_to_1cube: true
|
| 51 |
+
|
| 52 |
+
optimizer:
|
| 53 |
+
lr: 2e-4
|
| 54 |
+
warm_up_steps: 1000
|
| 55 |
+
backbone_lr_multiplier: 0.1
|
| 56 |
+
|
| 57 |
+
data_loader:
|
| 58 |
+
train:
|
| 59 |
+
batch_size: 1
|
| 60 |
+
|
| 61 |
+
trainer:
|
| 62 |
+
max_steps: 10000
|
| 63 |
+
val_check_interval: 100
|
| 64 |
+
num_nodes: 1
|
| 65 |
+
accumulate_grad_batches: 1
|
| 66 |
+
precision: 16-mixed
|
| 67 |
+
|
| 68 |
+
checkpointing:
|
| 69 |
+
load: null
|
| 70 |
+
every_n_train_steps: 500
|
| 71 |
+
save_weights_only: true
|
| 72 |
+
save_top_k: 10
|
| 73 |
+
|
| 74 |
+
train:
|
| 75 |
+
pose_loss_alpha: 1.0
|
| 76 |
+
pose_loss_delta: 1.0
|
| 77 |
+
cxt_depth_weight: 1.0
|
| 78 |
+
weight_pose: 10.0
|
| 79 |
+
weight_depth: 1.0
|
| 80 |
+
weight_normal: 0.1
|
| 81 |
+
|
| 82 |
+
hydra:
|
| 83 |
+
run:
|
| 84 |
+
dir: ./exp_${wandb.name}/${now:%Y-%m-%d_%H-%M-%S}
|
config/generate_evaluation_index.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- dataset: re10k
|
| 3 |
+
- optional dataset/view_sampler_dataset_specific_config: ${dataset/view_sampler}_${dataset}
|
| 4 |
+
- override dataset/view_sampler: all
|
| 5 |
+
|
| 6 |
+
dataset:
|
| 7 |
+
overfit_to_scene: null
|
| 8 |
+
|
| 9 |
+
data_loader:
|
| 10 |
+
train:
|
| 11 |
+
num_workers: 0
|
| 12 |
+
persistent_workers: true
|
| 13 |
+
batch_size: 1
|
| 14 |
+
seed: 1234
|
| 15 |
+
test:
|
| 16 |
+
num_workers: 8
|
| 17 |
+
persistent_workers: false
|
| 18 |
+
batch_size: 1
|
| 19 |
+
seed: 2345
|
| 20 |
+
val:
|
| 21 |
+
num_workers: 0
|
| 22 |
+
persistent_workers: true
|
| 23 |
+
batch_size: 1
|
| 24 |
+
seed: 3456
|
| 25 |
+
|
| 26 |
+
index_generator:
|
| 27 |
+
num_target_views: 3
|
| 28 |
+
min_overlap: 0.6
|
| 29 |
+
max_overlap: 1.0
|
| 30 |
+
min_distance: 45
|
| 31 |
+
max_distance: 135
|
| 32 |
+
output_path: outputs/evaluation_index_re10k
|
| 33 |
+
save_previews: false
|
| 34 |
+
seed: 123
|
| 35 |
+
|
| 36 |
+
seed: 456
|
config/loss/depth.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
depth:
|
| 2 |
+
weight: 1.0
|
| 3 |
+
sigma_image: null
|
| 4 |
+
use_second_derivative: false
|
| 5 |
+
weights_path: ${model.encoder.pre_dav3_path}
|
config/loss/lpips.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lpips:
|
| 2 |
+
weight: 0.05
|
| 3 |
+
apply_after_step: 0
|
config/loss/mse.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mse:
|
| 2 |
+
weight_ctx: 1.0
|
| 3 |
+
weight_novel: 1.5
|
config/loss/ssim.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ssim:
|
| 2 |
+
weight: 0.05
|
| 3 |
+
apply_after_step: 0
|
config/main.yaml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- model/encoder: of3gs
|
| 3 |
+
- model/decoder: splatting_cuda
|
| 4 |
+
- loss: [mse]
|
| 5 |
+
|
| 6 |
+
wandb:
|
| 7 |
+
project: of3gs
|
| 8 |
+
entity: null
|
| 9 |
+
name: debug
|
| 10 |
+
mode: offline
|
| 11 |
+
mode: test
|
| 12 |
+
pre_wm_path: ""
|
| 13 |
+
pre_of3gs_path: ""
|
| 14 |
+
|
| 15 |
+
data_loader:
|
| 16 |
+
train:
|
| 17 |
+
num_workers: 8 # 16
|
| 18 |
+
persistent_workers: false
|
| 19 |
+
batch_size: 1
|
| 20 |
+
seed: 1234
|
| 21 |
+
test:
|
| 22 |
+
num_workers: 1
|
| 23 |
+
persistent_workers: false
|
| 24 |
+
batch_size: 1
|
| 25 |
+
seed: 1234
|
| 26 |
+
val:
|
| 27 |
+
num_workers: 1
|
| 28 |
+
persistent_workers: true
|
| 29 |
+
batch_size: 1
|
| 30 |
+
seed: 1234
|
| 31 |
+
|
| 32 |
+
optimizer:
|
| 33 |
+
lr: 1.5e-4
|
| 34 |
+
warm_up_steps: 2000
|
| 35 |
+
backbone_lr_multiplier: 0.1
|
| 36 |
+
|
| 37 |
+
checkpointing:
|
| 38 |
+
load: null
|
| 39 |
+
every_n_train_steps: 5000
|
| 40 |
+
save_top_k: 1
|
| 41 |
+
save_weights_only: true
|
| 42 |
+
|
| 43 |
+
train:
|
| 44 |
+
output_path: outputs/train
|
| 45 |
+
depth_mode: null
|
| 46 |
+
extended_visualization: false
|
| 47 |
+
print_log_every_n_steps: 10
|
| 48 |
+
distiller: ''
|
| 49 |
+
distill_max_steps: 1000000
|
| 50 |
+
random_context_views: false
|
| 51 |
+
|
| 52 |
+
test:
|
| 53 |
+
output_path: outputs/me_suppl_64
|
| 54 |
+
align_pose: false
|
| 55 |
+
pose_align_steps: 100
|
| 56 |
+
rot_opt_lr: 0.005
|
| 57 |
+
trans_opt_lr: 0.005
|
| 58 |
+
compute_scores: false
|
| 59 |
+
save_image: true
|
| 60 |
+
save_video: true
|
| 61 |
+
save_compare: true
|
| 62 |
+
generate_video: true
|
| 63 |
+
mode: inference
|
| 64 |
+
image_folder: examples/bungeenerf
|
| 65 |
+
|
| 66 |
+
seed: 111123
|
| 67 |
+
|
| 68 |
+
trainer:
|
| 69 |
+
max_steps: -1
|
| 70 |
+
val_check_interval: 250
|
| 71 |
+
gradient_clip_val: 0.5
|
| 72 |
+
num_nodes: 1
|
| 73 |
+
devices: 4
|
| 74 |
+
accumulate_grad_batches: 1
|
| 75 |
+
|
| 76 |
+
hydra:
|
| 77 |
+
run:
|
| 78 |
+
dir: output-debug/exp_${wandb.name}/${now:%Y-%m-%d_%H-%M-%S}
|
config/model/decoder/splatting_cuda.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: splatting_cuda
|
| 2 |
+
background_color: [1.0, 1.0, 1.0]
|
| 3 |
+
make_scale_invariant: false
|
config/model/encoder/backbone/croco.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: croco
|
| 2 |
+
|
| 3 |
+
model: ViTLarge_BaseDecoder
|
| 4 |
+
patch_embed_cls: PatchEmbedDust3R
|
| 5 |
+
asymmetry_decoder: true
|
| 6 |
+
|
| 7 |
+
intrinsics_embed_loc: 'encoder'
|
| 8 |
+
intrinsics_embed_degree: 4
|
| 9 |
+
intrinsics_embed_type: 'token'
|
config/model/encoder/of3gs.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- backbone: croco
|
| 3 |
+
|
| 4 |
+
name: of3gs
|
| 5 |
+
|
| 6 |
+
opacity_mapping:
|
| 7 |
+
initial: 0.0
|
| 8 |
+
final: 0.0
|
| 9 |
+
warm_up: 1
|
| 10 |
+
|
| 11 |
+
num_monocular_samples: 32
|
| 12 |
+
num_surfaces: 1
|
| 13 |
+
predict_opacity: false
|
| 14 |
+
|
| 15 |
+
gaussians_per_pixel: 1
|
| 16 |
+
|
| 17 |
+
gaussian_adapter:
|
| 18 |
+
gaussian_scale_min: 0.5
|
| 19 |
+
gaussian_scale_max: 15.0
|
| 20 |
+
sh_degree: 4
|
| 21 |
+
|
| 22 |
+
d_feature: 32
|
| 23 |
+
|
| 24 |
+
visualizer:
|
| 25 |
+
num_samples: 8
|
| 26 |
+
min_resolution: 256
|
| 27 |
+
export_ply: false
|
| 28 |
+
|
| 29 |
+
apply_bounds_shim: true
|
| 30 |
+
|
| 31 |
+
gs_params_head_type: dpt_gs
|
| 32 |
+
pose_free: true
|
| 33 |
+
pretrained_weights: ""
|
| 34 |
+
scale_align: false
|
| 35 |
+
|
| 36 |
+
voxel_size: 0.001
|
| 37 |
+
n_offsets: 2
|
| 38 |
+
anchor_feat_dim: 83 # 32
|
| 39 |
+
add_view: false
|
| 40 |
+
color_attr: 3D # 3D or RGB
|
| 41 |
+
mlp_type: unified
|
| 42 |
+
scaffold: true
|
requirements-train.txt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy==1.25.0
|
| 2 |
+
wheel
|
| 3 |
+
tqdm
|
| 4 |
+
lightning
|
| 5 |
+
black
|
| 6 |
+
ruff
|
| 7 |
+
hydra-core
|
| 8 |
+
jaxtyping
|
| 9 |
+
beartype
|
| 10 |
+
wandb
|
| 11 |
+
einops
|
| 12 |
+
colorama
|
| 13 |
+
scikit-image
|
| 14 |
+
colorspacious
|
| 15 |
+
matplotlib
|
| 16 |
+
moviepy==1.0.3
|
| 17 |
+
imageio
|
| 18 |
+
timm
|
| 19 |
+
dacite
|
| 20 |
+
lpips
|
| 21 |
+
e3nn
|
| 22 |
+
plyfile
|
| 23 |
+
tabulate
|
| 24 |
+
svg.py
|
| 25 |
+
scikit-video
|
| 26 |
+
opencv-python
|
| 27 |
+
Pillow
|
| 28 |
+
huggingface_hub
|
| 29 |
+
gradio
|
| 30 |
+
xformers==0.0.24
|
| 31 |
+
pydantic
|
| 32 |
+
open3d
|
| 33 |
+
safetensors
|
| 34 |
+
https://github.com/nerfstudio-project/gsplat/releases/download/v1.4.0/gsplat-1.4.0%2Bpt22cu121-cp310-cp310-linux_x86_64.whl
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cu128
|
| 2 |
+
|
| 3 |
+
torch==2.8.0
|
| 4 |
+
torchvision==0.23.0
|
| 5 |
+
transformers==4.57.6
|
| 6 |
+
gsplat==1.5.3
|
| 7 |
+
ninja
|
| 8 |
+
numpy==1.26.4
|
| 9 |
+
einops
|
| 10 |
+
jaxtyping
|
| 11 |
+
safetensors
|
| 12 |
+
e3nn
|
| 13 |
+
scipy
|
| 14 |
+
plyfile
|
| 15 |
+
Pillow
|
| 16 |
+
rich>=12
|
scripts/download_dl3dv.sh
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
usage() {
|
| 5 |
+
cat <<'USAGE'
|
| 6 |
+
Usage:
|
| 7 |
+
bash scripts/download_dl3dv.sh <output_dir>
|
| 8 |
+
|
| 9 |
+
Examples:
|
| 10 |
+
bash scripts/download_dl3dv.sh datasets/dl3dv
|
| 11 |
+
bash scripts/download_dl3dv.sh /data/DL3DV-ALL-480P
|
| 12 |
+
|
| 13 |
+
This script downloads DL3DV/DL3DV-ALL-480P images+poses from 1K through 11K.
|
| 14 |
+
You must request dataset access on Hugging Face first and run `huggingface-cli login`
|
| 15 |
+
if the dataset is gated for your account.
|
| 16 |
+
USAGE
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
| 20 |
+
usage
|
| 21 |
+
exit 0
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
if [[ $# -ne 1 ]]; then
|
| 25 |
+
echo "Missing output directory." >&2
|
| 26 |
+
usage
|
| 27 |
+
exit 1
|
| 28 |
+
fi
|
| 29 |
+
|
| 30 |
+
OUTPUT_DIR="$1"
|
| 31 |
+
RESOLUTION="${DL3DV_RESOLUTION:-480P}"
|
| 32 |
+
FILE_TYPE="${DL3DV_FILE_TYPE:-images+poses}"
|
| 33 |
+
SUBSETS=(${DL3DV_SUBSETS:-1K 2K 3K 4K 5K 6K 7K 8K 9K 10K 11K})
|
| 34 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 35 |
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
| 36 |
+
TEST_INDEX_SOURCE="${DL3DV_TEST_INDEX_PATH:-$REPO_ROOT/test_index.json}"
|
| 37 |
+
|
| 38 |
+
mkdir -p "$OUTPUT_DIR"
|
| 39 |
+
|
| 40 |
+
python -m pip install -U huggingface_hub pandas tqdm
|
| 41 |
+
|
| 42 |
+
DOWNLOAD_SCRIPT="$OUTPUT_DIR/dl3dv_download.py"
|
| 43 |
+
RAW_URL="https://raw.githubusercontent.com/DL3DV-10K/Dataset/main/scripts/download.py"
|
| 44 |
+
|
| 45 |
+
if command -v wget >/dev/null 2>&1; then
|
| 46 |
+
wget -O "$DOWNLOAD_SCRIPT" "$RAW_URL"
|
| 47 |
+
elif command -v curl >/dev/null 2>&1; then
|
| 48 |
+
curl -L "$RAW_URL" -o "$DOWNLOAD_SCRIPT"
|
| 49 |
+
else
|
| 50 |
+
echo "wget or curl is required to fetch the official DL3DV download script." >&2
|
| 51 |
+
exit 1
|
| 52 |
+
fi
|
| 53 |
+
|
| 54 |
+
for subset in "${SUBSETS[@]}"; do
|
| 55 |
+
echo "Downloading DL3DV/DL3DV-ALL-480P subset $subset to $OUTPUT_DIR"
|
| 56 |
+
python "$DOWNLOAD_SCRIPT" \
|
| 57 |
+
--odir "$OUTPUT_DIR" \
|
| 58 |
+
--subset "$subset" \
|
| 59 |
+
--resolution "$RESOLUTION" \
|
| 60 |
+
--file_type "$FILE_TYPE" \
|
| 61 |
+
--clean_cache
|
| 62 |
+
done
|
| 63 |
+
|
| 64 |
+
python - "$OUTPUT_DIR" "$TEST_INDEX_SOURCE" <<'PY'
|
| 65 |
+
import json
|
| 66 |
+
import shutil
|
| 67 |
+
import sys
|
| 68 |
+
from pathlib import Path
|
| 69 |
+
|
| 70 |
+
root = Path(sys.argv[1]).resolve()
|
| 71 |
+
test_index_source = Path(sys.argv[2]).resolve()
|
| 72 |
+
subsets = {f"{idx}K" for idx in range(1, 12)}
|
| 73 |
+
|
| 74 |
+
test_items = set()
|
| 75 |
+
if test_index_source.exists():
|
| 76 |
+
with test_index_source.open("r", encoding="utf-8") as f:
|
| 77 |
+
loaded = json.load(f)
|
| 78 |
+
if not isinstance(loaded, list):
|
| 79 |
+
raise TypeError(f"{test_index_source} must be a JSON array")
|
| 80 |
+
test_items = {str(item).strip("/") for item in loaded}
|
| 81 |
+
|
| 82 |
+
scenes = []
|
| 83 |
+
for transforms in sorted(root.rglob("transforms.json")):
|
| 84 |
+
scene_dir = transforms.parent
|
| 85 |
+
if not (scene_dir / "images_8").is_dir():
|
| 86 |
+
continue
|
| 87 |
+
|
| 88 |
+
rel = scene_dir.relative_to(root).as_posix()
|
| 89 |
+
rel_parts = rel.split("/")
|
| 90 |
+
subset_scene_key = (
|
| 91 |
+
"/".join(rel_parts[:2])
|
| 92 |
+
if len(rel_parts) >= 2 and rel_parts[0] in subsets
|
| 93 |
+
else rel
|
| 94 |
+
)
|
| 95 |
+
scenes.append((rel, subset_scene_key))
|
| 96 |
+
|
| 97 |
+
seen = set()
|
| 98 |
+
unique_scenes = []
|
| 99 |
+
for rel, subset_scene_key in sorted(scenes):
|
| 100 |
+
if rel in seen:
|
| 101 |
+
continue
|
| 102 |
+
seen.add(rel)
|
| 103 |
+
unique_scenes.append((rel, subset_scene_key))
|
| 104 |
+
|
| 105 |
+
test_index_items = sorted(
|
| 106 |
+
rel for rel, subset_scene_key in unique_scenes
|
| 107 |
+
if rel in test_items or subset_scene_key in test_items
|
| 108 |
+
)
|
| 109 |
+
test_index_set = set(test_index_items)
|
| 110 |
+
train_items = sorted(rel for rel, _ in unique_scenes if rel not in test_index_set)
|
| 111 |
+
index_path = root / "train_index.json"
|
| 112 |
+
test_index_path = root / "test_index.json"
|
| 113 |
+
|
| 114 |
+
if train_items:
|
| 115 |
+
index_path.write_text(json.dumps(train_items, indent=2) + "\n", encoding="utf-8")
|
| 116 |
+
print(f"Wrote {index_path} with {len(train_items)} scenes.")
|
| 117 |
+
else:
|
| 118 |
+
print(
|
| 119 |
+
"No DL3DV scenes were found for train_index.json. "
|
| 120 |
+
"Check that downloaded scenes contain images_8 and transforms.json.",
|
| 121 |
+
file=sys.stderr,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
if test_items:
|
| 125 |
+
test_index_path.write_text(json.dumps(test_index_items, indent=2) + "\n", encoding="utf-8")
|
| 126 |
+
print(
|
| 127 |
+
f"Wrote {test_index_path} with {len(test_index_items)} scenes "
|
| 128 |
+
f"matched from {test_index_source}."
|
| 129 |
+
)
|
| 130 |
+
missing = sorted(test_items - {rel for rel, _ in unique_scenes} - {key for _, key in unique_scenes})
|
| 131 |
+
if missing:
|
| 132 |
+
print(
|
| 133 |
+
f"Warning: {len(missing)} test-index entries were not found under {root}.",
|
| 134 |
+
file=sys.stderr,
|
| 135 |
+
)
|
| 136 |
+
elif test_index_source.exists():
|
| 137 |
+
shutil.copy2(test_index_source, test_index_path)
|
| 138 |
+
print(f"Copied empty test index {test_index_source} to {test_index_path}.")
|
| 139 |
+
else:
|
| 140 |
+
print(
|
| 141 |
+
f"No test index source found at {test_index_source}; "
|
| 142 |
+
"only train_index.json was generated."
|
| 143 |
+
)
|
| 144 |
+
PY
|
| 145 |
+
|
| 146 |
+
echo "DL3DV/DL3DV-ALL-480P download finished under $OUTPUT_DIR"
|
scripts/download_weights.sh
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
usage() {
|
| 5 |
+
cat <<'USAGE'
|
| 6 |
+
Usage:
|
| 7 |
+
bash scripts/download_weights.sh [weights_dir]
|
| 8 |
+
|
| 9 |
+
Examples:
|
| 10 |
+
bash scripts/download_weights.sh
|
| 11 |
+
bash scripts/download_weights.sh /data/of3gs_weights
|
| 12 |
+
|
| 13 |
+
Environment overrides:
|
| 14 |
+
PRE_WM_URL Source for pre_wm.safetensors
|
| 15 |
+
PRE_VGGT_URL Source for pre_vggt.safetensors
|
| 16 |
+
PRE_SVGGT_URL Source for pre_svggt.safetensors
|
| 17 |
+
PRE_DAV3_URL Source for pre_dav3.safetensors
|
| 18 |
+
PRE_OF3GS_URL Source for of3gs.ckpt
|
| 19 |
+
|
| 20 |
+
Supported source formats:
|
| 21 |
+
https://... direct URL
|
| 22 |
+
hf://namespace/repo/path/in/repo Hugging Face file
|
| 23 |
+
provide skip until a source is provided
|
| 24 |
+
USAGE
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
| 28 |
+
usage
|
| 29 |
+
exit 0
|
| 30 |
+
fi
|
| 31 |
+
|
| 32 |
+
WEIGHTS_DIR="${1:-weights}"
|
| 33 |
+
|
| 34 |
+
PRE_WM_URL="${PRE_WM_URL:-provide}"
|
| 35 |
+
PRE_VGGT_URL="${PRE_VGGT_URL:-provide}"
|
| 36 |
+
PRE_SVGGT_URL="${PRE_SVGGT_URL:-provide}"
|
| 37 |
+
PRE_DAV3_URL="${PRE_DAV3_URL:-provide}"
|
| 38 |
+
PRE_OF3GS_URL="${PRE_OF3GS_URL:-provide}"
|
| 39 |
+
|
| 40 |
+
mkdir -p "$WEIGHTS_DIR"
|
| 41 |
+
|
| 42 |
+
download_hf() {
|
| 43 |
+
local output="$1"
|
| 44 |
+
local source="$2"
|
| 45 |
+
local spec="${source#hf://}"
|
| 46 |
+
IFS='/' read -r -a parts <<< "$spec"
|
| 47 |
+
if [[ "${#parts[@]}" -lt 3 ]]; then
|
| 48 |
+
echo "Invalid Hugging Face source: $source" >&2
|
| 49 |
+
echo "Expected hf://namespace/repo/path/in/repo" >&2
|
| 50 |
+
exit 1
|
| 51 |
+
fi
|
| 52 |
+
local repo_id="${parts[0]}/${parts[1]}"
|
| 53 |
+
local filename
|
| 54 |
+
filename="$(IFS=/; echo "${parts[*]:2}")"
|
| 55 |
+
|
| 56 |
+
python - "$repo_id" "$filename" "$WEIGHTS_DIR/$output" <<'PY'
|
| 57 |
+
from pathlib import Path
|
| 58 |
+
import shutil
|
| 59 |
+
import sys
|
| 60 |
+
|
| 61 |
+
from huggingface_hub import hf_hub_download
|
| 62 |
+
|
| 63 |
+
repo_id, filename, output = sys.argv[1:4]
|
| 64 |
+
output_path = Path(output)
|
| 65 |
+
path = Path(hf_hub_download(repo_id=repo_id, filename=filename))
|
| 66 |
+
shutil.copy2(path, output_path)
|
| 67 |
+
print(f"Downloaded hf://{repo_id}/{filename} -> {output_path}")
|
| 68 |
+
PY
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
download_url() {
|
| 72 |
+
local output="$1"
|
| 73 |
+
local url="$2"
|
| 74 |
+
|
| 75 |
+
if command -v wget >/dev/null 2>&1; then
|
| 76 |
+
wget -O "$WEIGHTS_DIR/$output" "$url"
|
| 77 |
+
elif command -v curl >/dev/null 2>&1; then
|
| 78 |
+
curl -L "$url" -o "$WEIGHTS_DIR/$output"
|
| 79 |
+
else
|
| 80 |
+
echo "wget or curl is required for direct URL downloads." >&2
|
| 81 |
+
exit 1
|
| 82 |
+
fi
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
download() {
|
| 86 |
+
local output="$1"
|
| 87 |
+
local source="$2"
|
| 88 |
+
|
| 89 |
+
if [[ -z "$source" || "$source" == "provide" ]]; then
|
| 90 |
+
echo "Skip $output: provide a source via the corresponding environment variable."
|
| 91 |
+
return 0
|
| 92 |
+
fi
|
| 93 |
+
|
| 94 |
+
if [[ -f "$WEIGHTS_DIR/$output" ]]; then
|
| 95 |
+
echo "Skip $output: already exists."
|
| 96 |
+
return 0
|
| 97 |
+
fi
|
| 98 |
+
|
| 99 |
+
case "$source" in
|
| 100 |
+
hf://*) download_hf "$output" "$source" ;;
|
| 101 |
+
http://*|https://*) download_url "$output" "$source" ;;
|
| 102 |
+
*)
|
| 103 |
+
echo "Unsupported source for $output: $source" >&2
|
| 104 |
+
exit 1
|
| 105 |
+
;;
|
| 106 |
+
esac
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
python -m pip install -U huggingface_hub
|
| 110 |
+
|
| 111 |
+
download "pre_wm.safetensors" "$PRE_WM_URL"
|
| 112 |
+
download "pre_vggt.safetensors" "$PRE_VGGT_URL"
|
| 113 |
+
download "pre_svggt.safetensors" "$PRE_SVGGT_URL"
|
| 114 |
+
download "pre_dav3.safetensors" "$PRE_DAV3_URL"
|
| 115 |
+
download "of3gs.ckpt" "$PRE_OF3GS_URL"
|
| 116 |
+
|
| 117 |
+
cat <<EOF
|
| 118 |
+
|
| 119 |
+
Weights are under: $WEIGHTS_DIR
|
| 120 |
+
|
| 121 |
+
Suggested environment variables:
|
| 122 |
+
export PRE_WM_PATH="$WEIGHTS_DIR/pre_wm.safetensors"
|
| 123 |
+
export PRE_VGGT_PATH="$WEIGHTS_DIR/pre_vggt.safetensors"
|
| 124 |
+
export PRE_SVGGT_PATH="$WEIGHTS_DIR/pre_svggt.safetensors"
|
| 125 |
+
export PRE_DAV3_PATH="$WEIGHTS_DIR/pre_dav3.safetensors"
|
| 126 |
+
export PRE_OF3GS_PATH="$WEIGHTS_DIR/of3gs.ckpt"
|
| 127 |
+
EOF
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OF3GS package."""
|
src/config.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Literal, Optional, Type, TypeVar
|
| 4 |
+
|
| 5 |
+
from dacite import Config, from_dict
|
| 6 |
+
from omegaconf import DictConfig, OmegaConf
|
| 7 |
+
|
| 8 |
+
from .dataset import DatasetCfgWrapper
|
| 9 |
+
from .dataset.data_module import DataLoaderCfg
|
| 10 |
+
from .loss import LossCfgWrapper
|
| 11 |
+
from .model.decoder import DecoderCfg
|
| 12 |
+
from .model.encoder import EncoderCfg
|
| 13 |
+
from .model.model_wrapper import OptimizerCfg, TestCfg, TrainCfg
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class CheckpointingCfg:
|
| 18 |
+
load: Optional[str] # Not a path, since it could be something like wandb://...
|
| 19 |
+
every_n_train_steps: int
|
| 20 |
+
save_top_k: int
|
| 21 |
+
save_weights_only: bool
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class ModelCfg:
|
| 26 |
+
decoder: DecoderCfg
|
| 27 |
+
encoder: EncoderCfg
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class TrainerCfg:
|
| 32 |
+
max_steps: int
|
| 33 |
+
val_check_interval: int | float | None
|
| 34 |
+
gradient_clip_val: int | float | None
|
| 35 |
+
num_nodes: int = 1
|
| 36 |
+
devices: int | str = "auto"
|
| 37 |
+
accumulate_grad_batches: int = 1
|
| 38 |
+
precision: Literal["32-true", "16-mixed", "bf16-mixed"] = "32"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class RootCfg:
|
| 43 |
+
wandb: dict
|
| 44 |
+
mode: Literal["train", "test"]
|
| 45 |
+
pre_wm_path: str | None
|
| 46 |
+
pre_of3gs_path: str | None
|
| 47 |
+
dataset: list[DatasetCfgWrapper]
|
| 48 |
+
data_loader: DataLoaderCfg
|
| 49 |
+
model: ModelCfg
|
| 50 |
+
optimizer: OptimizerCfg
|
| 51 |
+
checkpointing: CheckpointingCfg
|
| 52 |
+
trainer: TrainerCfg
|
| 53 |
+
loss: list[LossCfgWrapper]
|
| 54 |
+
test: TestCfg
|
| 55 |
+
train: TrainCfg
|
| 56 |
+
seed: int
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
TYPE_HOOKS = {
|
| 60 |
+
Path: Path,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
T = TypeVar("T")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_typed_config(
|
| 68 |
+
cfg: DictConfig,
|
| 69 |
+
data_class: Type[T],
|
| 70 |
+
extra_type_hooks: dict = {},
|
| 71 |
+
) -> T:
|
| 72 |
+
return from_dict(
|
| 73 |
+
data_class,
|
| 74 |
+
OmegaConf.to_container(cfg, resolve=True),
|
| 75 |
+
config=Config(type_hooks={**TYPE_HOOKS, **extra_type_hooks}),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def separate_loss_cfg_wrappers(joined: dict) -> list[LossCfgWrapper]:
|
| 80 |
+
# The dummy allows the union to be converted.
|
| 81 |
+
@dataclass
|
| 82 |
+
class Dummy:
|
| 83 |
+
dummy: LossCfgWrapper
|
| 84 |
+
|
| 85 |
+
return [
|
| 86 |
+
load_typed_config(DictConfig({"dummy": {k: v}}), Dummy).dummy
|
| 87 |
+
for k, v in joined.items()
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def separate_dataset_cfg_wrappers(joined: dict) -> list[DatasetCfgWrapper]:
|
| 92 |
+
# The dummy allows the union to be converted.
|
| 93 |
+
@dataclass
|
| 94 |
+
class Dummy:
|
| 95 |
+
dummy: DatasetCfgWrapper
|
| 96 |
+
|
| 97 |
+
return [
|
| 98 |
+
load_typed_config(DictConfig({"dummy": {k: v}}), Dummy).dummy
|
| 99 |
+
for k, v in joined.items()
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def load_typed_root_config(cfg: DictConfig) -> RootCfg:
|
| 104 |
+
return load_typed_config(
|
| 105 |
+
cfg,
|
| 106 |
+
RootCfg,
|
| 107 |
+
{
|
| 108 |
+
list[LossCfgWrapper]: separate_loss_cfg_wrappers,
|
| 109 |
+
list[DatasetCfgWrapper]: separate_dataset_cfg_wrappers,
|
| 110 |
+
},
|
| 111 |
+
)
|
src/dataset/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import fields
|
| 2 |
+
from typing import Callable
|
| 3 |
+
from torch.utils.data import Dataset, ConcatDataset
|
| 4 |
+
import bisect
|
| 5 |
+
|
| 6 |
+
from ..misc.step_tracker import StepTracker
|
| 7 |
+
from .types import Stage
|
| 8 |
+
from .view_sampler import get_view_sampler
|
| 9 |
+
from .dataset_dl3dv import DatasetDL3DV, DatasetDL3DVCfgWrapper
|
| 10 |
+
from .dataset_re10k import Datasetre10k, Datasetre10kCfgWrapper
|
| 11 |
+
DATASETS: dict[str, Dataset] = {
|
| 12 |
+
"dl3dv": DatasetDL3DV,
|
| 13 |
+
"re10k": Datasetre10k,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
DatasetCfgWrapper = DatasetDL3DVCfgWrapper | Datasetre10kCfgWrapper
|
| 17 |
+
|
| 18 |
+
class TestDatasetWarpper(Dataset):
|
| 19 |
+
def __init__(self, dataset: Dataset):
|
| 20 |
+
self.dataset = dataset
|
| 21 |
+
|
| 22 |
+
def __getitem__(self, idx):
|
| 23 |
+
|
| 24 |
+
return self.dataset[(idx, self.dataset.view_sampler.num_context_views, self.dataset.cfg.input_image_shape[1] // 14)]
|
| 25 |
+
|
| 26 |
+
def __len__(self):
|
| 27 |
+
return len(self.dataset)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class CustomConcatDataset(ConcatDataset):
|
| 32 |
+
|
| 33 |
+
def __getitem__(self, idx_tuple):
|
| 34 |
+
|
| 35 |
+
if isinstance(idx_tuple, list):
|
| 36 |
+
idx_tuple = idx_tuple[0]
|
| 37 |
+
|
| 38 |
+
idx = idx_tuple[0]
|
| 39 |
+
if idx < 0:
|
| 40 |
+
if -idx > len(self):
|
| 41 |
+
raise ValueError("absolute value of index should not exceed dataset length")
|
| 42 |
+
idx = len(self) + idx
|
| 43 |
+
dataset_idx = bisect.bisect_right(self.cumulative_sizes, idx)
|
| 44 |
+
if dataset_idx == 0:
|
| 45 |
+
sample_idx = idx
|
| 46 |
+
else:
|
| 47 |
+
sample_idx = idx - self.cumulative_sizes[dataset_idx - 1]
|
| 48 |
+
return self.datasets[dataset_idx][(sample_idx, idx_tuple[1], idx_tuple[2])]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_dataset(
|
| 52 |
+
cfgs: list[DatasetCfgWrapper],
|
| 53 |
+
stage: Stage,
|
| 54 |
+
step_tracker: StepTracker | None,
|
| 55 |
+
dataset_shim: Callable[[Dataset, str], Dataset]
|
| 56 |
+
) -> list[Dataset]:
|
| 57 |
+
datasets = []
|
| 58 |
+
if stage != "test":
|
| 59 |
+
if stage == "val":
|
| 60 |
+
cfgs = [cfgs[0]]
|
| 61 |
+
for cfg in cfgs:
|
| 62 |
+
(field,) = fields(type(cfg))
|
| 63 |
+
cfg = getattr(cfg, field.name)
|
| 64 |
+
view_sampler = get_view_sampler(
|
| 65 |
+
cfg.view_sampler,
|
| 66 |
+
stage,
|
| 67 |
+
cfg.overfit_to_scene is not None,
|
| 68 |
+
cfg.cameras_are_circular,
|
| 69 |
+
step_tracker,
|
| 70 |
+
)
|
| 71 |
+
dataset = DATASETS[cfg.name](cfg, stage, view_sampler)
|
| 72 |
+
dataset = dataset_shim(dataset, stage)
|
| 73 |
+
datasets.append(dataset)
|
| 74 |
+
|
| 75 |
+
return CustomConcatDataset(datasets), datasets
|
| 76 |
+
elif stage == "test":
|
| 77 |
+
assert len(cfgs) == 1
|
| 78 |
+
cfg = cfgs[0]
|
| 79 |
+
(field,) = fields(type(cfg))
|
| 80 |
+
cfg = getattr(cfg, field.name)
|
| 81 |
+
|
| 82 |
+
view_sampler = get_view_sampler(
|
| 83 |
+
cfg.view_sampler,
|
| 84 |
+
stage,
|
| 85 |
+
cfg.overfit_to_scene is not None,
|
| 86 |
+
cfg.cameras_are_circular,
|
| 87 |
+
step_tracker,
|
| 88 |
+
)
|
| 89 |
+
dataset = DATASETS[cfg.name](cfg, stage, view_sampler)
|
| 90 |
+
dataset = dataset_shim(dataset, stage)
|
| 91 |
+
|
| 92 |
+
return TestDatasetWarpper(dataset)
|
| 93 |
+
else:
|
| 94 |
+
NotImplementedError(f"Stage {stage} is not supported")
|
src/dataset/data_module.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Callable
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from lightning.pytorch import LightningDataModule
|
| 8 |
+
from torch import Generator, nn
|
| 9 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 10 |
+
|
| 11 |
+
from src.dataset import *
|
| 12 |
+
from src.global_cfg import get_cfg
|
| 13 |
+
|
| 14 |
+
from ..misc.step_tracker import StepTracker
|
| 15 |
+
from ..misc.utils import get_world_size, get_rank
|
| 16 |
+
from . import DatasetCfgWrapper, get_dataset
|
| 17 |
+
from .types import DataShim, Stage
|
| 18 |
+
from .data_sampler import MixedBatchSampler
|
| 19 |
+
|
| 20 |
+
def get_data_shim(encoder: nn.Module) -> DataShim:
|
| 21 |
+
"""Get functions that modify the batch. It's sometimes necessary to modify batches
|
| 22 |
+
outside the data loader because GPU computations are required to modify the batch or
|
| 23 |
+
because the modification depends on something outside the data loader.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
shims: list[DataShim] = []
|
| 27 |
+
if hasattr(encoder, "get_data_shim"):
|
| 28 |
+
shims.append(encoder.get_data_shim())
|
| 29 |
+
|
| 30 |
+
def combined_shim(batch):
|
| 31 |
+
for shim in shims:
|
| 32 |
+
batch = shim(batch)
|
| 33 |
+
return batch
|
| 34 |
+
|
| 35 |
+
return combined_shim
|
| 36 |
+
|
| 37 |
+
# the training ratio of datasets (example)
|
| 38 |
+
prob_mapping = {DatasetDL3DV: 0.5,
|
| 39 |
+
Datasetre10k: 0.5}
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class DataLoaderStageCfg:
|
| 43 |
+
batch_size: int
|
| 44 |
+
num_workers: int
|
| 45 |
+
persistent_workers: bool
|
| 46 |
+
seed: int | None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class DataLoaderCfg:
|
| 51 |
+
train: DataLoaderStageCfg
|
| 52 |
+
test: DataLoaderStageCfg
|
| 53 |
+
val: DataLoaderStageCfg
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
DatasetShim = Callable[[Dataset, Stage], Dataset]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def worker_init_fn(worker_id: int) -> None:
|
| 60 |
+
random.seed(int(torch.utils.data.get_worker_info().seed) % (2**32 - 1))
|
| 61 |
+
np.random.seed(int(torch.utils.data.get_worker_info().seed) % (2**32 - 1))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class DataModule(LightningDataModule):
|
| 65 |
+
dataset_cfgs: list[DatasetCfgWrapper]
|
| 66 |
+
data_loader_cfg: DataLoaderCfg
|
| 67 |
+
step_tracker: StepTracker | None
|
| 68 |
+
dataset_shim: DatasetShim
|
| 69 |
+
global_rank: int
|
| 70 |
+
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
dataset_cfgs: list[DatasetCfgWrapper],
|
| 74 |
+
data_loader_cfg: DataLoaderCfg,
|
| 75 |
+
step_tracker: StepTracker | None = None,
|
| 76 |
+
dataset_shim: DatasetShim = lambda dataset, _: dataset,
|
| 77 |
+
global_rank: int = 0,
|
| 78 |
+
) -> None:
|
| 79 |
+
super().__init__()
|
| 80 |
+
self.dataset_cfgs = dataset_cfgs
|
| 81 |
+
self.data_loader_cfg = data_loader_cfg
|
| 82 |
+
self.step_tracker = step_tracker
|
| 83 |
+
self.dataset_shim = dataset_shim
|
| 84 |
+
self.global_rank = global_rank
|
| 85 |
+
|
| 86 |
+
def get_persistent(self, loader_cfg: DataLoaderStageCfg) -> bool | None:
|
| 87 |
+
return None if loader_cfg.num_workers == 0 else loader_cfg.persistent_workers
|
| 88 |
+
|
| 89 |
+
def get_generator(self, loader_cfg: DataLoaderStageCfg) -> torch.Generator | None:
|
| 90 |
+
if loader_cfg.seed is None:
|
| 91 |
+
return None
|
| 92 |
+
generator = Generator()
|
| 93 |
+
generator.manual_seed(loader_cfg.seed + self.global_rank)
|
| 94 |
+
self.generator = generator
|
| 95 |
+
return self.generator
|
| 96 |
+
|
| 97 |
+
def train_dataloader(self):
|
| 98 |
+
dataset, datasets_ls = get_dataset(self.dataset_cfgs, "train", self.step_tracker, self.dataset_shim)
|
| 99 |
+
world_size = get_world_size()
|
| 100 |
+
rank = get_rank()
|
| 101 |
+
prob_ls = [prob_mapping[type(dataset)] for dataset in datasets_ls]
|
| 102 |
+
# we assume all the dataset share the same num_context_views
|
| 103 |
+
|
| 104 |
+
if len(datasets_ls) > 1:
|
| 105 |
+
prob = prob_ls
|
| 106 |
+
context_num_views = [dataset.cfg.view_sampler.num_context_views for dataset in datasets_ls]
|
| 107 |
+
else:
|
| 108 |
+
prob = None
|
| 109 |
+
dataset_key = next(iter(get_cfg()["dataset"]))
|
| 110 |
+
dataset_cfg = get_cfg()["dataset"][dataset_key]
|
| 111 |
+
context_num_views = dataset_cfg['view_sampler']['num_context_views']
|
| 112 |
+
|
| 113 |
+
sampler = MixedBatchSampler(datasets_ls,
|
| 114 |
+
batch_size=self.data_loader_cfg.train.batch_size,
|
| 115 |
+
num_context_views=context_num_views,
|
| 116 |
+
world_size=world_size,
|
| 117 |
+
rank=rank,
|
| 118 |
+
prob=prob,
|
| 119 |
+
generator=self.get_generator(self.data_loader_cfg.train))
|
| 120 |
+
sampler.set_epoch(0)
|
| 121 |
+
self.train_loader = DataLoader(
|
| 122 |
+
dataset,
|
| 123 |
+
batch_sampler=sampler,
|
| 124 |
+
num_workers=self.data_loader_cfg.train.num_workers,
|
| 125 |
+
generator=self.generator,
|
| 126 |
+
worker_init_fn=worker_init_fn,
|
| 127 |
+
persistent_workers=self.get_persistent(self.data_loader_cfg.train),
|
| 128 |
+
)
|
| 129 |
+
# Set epoch for train and validation loaders (if applicable)
|
| 130 |
+
if hasattr(self.train_loader, "dataset") and hasattr(self.train_loader.dataset, "set_epoch"):
|
| 131 |
+
print("Training: Set Epoch in DataModule")
|
| 132 |
+
self.train_loader.dataset.set_epoch(0)
|
| 133 |
+
if hasattr(self.train_loader, "sampler") and hasattr(self.train_loader.sampler, "set_epoch"):
|
| 134 |
+
print("Training: Set Epoch in DataModule")
|
| 135 |
+
self.train_loader.sampler.set_epoch(0)
|
| 136 |
+
|
| 137 |
+
return self.train_loader
|
| 138 |
+
|
| 139 |
+
def val_dataloader(self):
|
| 140 |
+
dataset, datasets_ls = get_dataset(self.dataset_cfgs, "val", self.step_tracker, self.dataset_shim)
|
| 141 |
+
world_size = get_world_size()
|
| 142 |
+
rank = get_rank()
|
| 143 |
+
# here, we random select one dataset for val
|
| 144 |
+
dataset_key = next(iter(get_cfg()["dataset"]))
|
| 145 |
+
dataset_cfg = get_cfg()["dataset"][dataset_key]
|
| 146 |
+
if len(datasets_ls) > 1:
|
| 147 |
+
prob = [0.5] * len(datasets_ls)
|
| 148 |
+
else:
|
| 149 |
+
prob = None
|
| 150 |
+
sampler = MixedBatchSampler(datasets_ls,
|
| 151 |
+
batch_size=self.data_loader_cfg.train.batch_size,
|
| 152 |
+
num_context_views=dataset_cfg['view_sampler']['num_context_views'],
|
| 153 |
+
world_size=1,
|
| 154 |
+
rank=0,
|
| 155 |
+
prob=prob,
|
| 156 |
+
generator=self.get_generator(self.data_loader_cfg.train))
|
| 157 |
+
sampler.set_epoch(0)
|
| 158 |
+
self.val_loader = DataLoader(
|
| 159 |
+
dataset,
|
| 160 |
+
self.data_loader_cfg.val.batch_size,
|
| 161 |
+
num_workers=self.data_loader_cfg.val.num_workers,
|
| 162 |
+
sampler=sampler,
|
| 163 |
+
generator=self.get_generator(self.data_loader_cfg.val),
|
| 164 |
+
worker_init_fn=worker_init_fn,
|
| 165 |
+
persistent_workers=self.get_persistent(self.data_loader_cfg.val),
|
| 166 |
+
shuffle=False
|
| 167 |
+
)
|
| 168 |
+
if hasattr(self.val_loader, "dataset") and hasattr(self.val_loader.dataset, "set_epoch"):
|
| 169 |
+
print("Validation: Set Epoch in DataModule")
|
| 170 |
+
self.val_loader.dataset.set_epoch(0)
|
| 171 |
+
if hasattr(self.val_loader, "sampler") and hasattr(self.val_loader.sampler, "set_epoch"):
|
| 172 |
+
print("Validation: Set Epoch in DataModule")
|
| 173 |
+
self.val_loader.sampler.set_epoch(0)
|
| 174 |
+
return self.val_loader
|
| 175 |
+
|
| 176 |
+
def test_dataloader(self):
|
| 177 |
+
dataset = get_dataset(self.dataset_cfgs, "test", self.step_tracker, self.dataset_shim)
|
| 178 |
+
|
| 179 |
+
sampler = DistributedSampler(
|
| 180 |
+
dataset,
|
| 181 |
+
num_replicas=torch.distributed.get_world_size(),
|
| 182 |
+
rank=torch.distributed.get_rank(),
|
| 183 |
+
shuffle=False
|
| 184 |
+
)
|
| 185 |
+
sampler.set_epoch(0)
|
| 186 |
+
data_loader = DataLoader(
|
| 187 |
+
dataset,
|
| 188 |
+
self.data_loader_cfg.test.batch_size,
|
| 189 |
+
num_workers=self.data_loader_cfg.test.num_workers,
|
| 190 |
+
generator=self.get_generator(self.data_loader_cfg.test),
|
| 191 |
+
worker_init_fn=worker_init_fn,
|
| 192 |
+
persistent_workers=self.get_persistent(self.data_loader_cfg.test),
|
| 193 |
+
sampler=sampler,
|
| 194 |
+
shuffle=False
|
| 195 |
+
)
|
| 196 |
+
return data_loader
|
src/dataset/data_sampler.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
|
| 4 |
+
# This source code is licensed under the license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Copyright (C) 2024-present Naver Corporation. All rights reserved.
|
| 8 |
+
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
|
| 9 |
+
#
|
| 10 |
+
# --------------------------------------------------------
|
| 11 |
+
# Random sampling under a constraint
|
| 12 |
+
# --------------------------------------------------------
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
from typing import Callable, Iterable, Optional
|
| 16 |
+
from torch.utils.data import DistributedSampler, Sampler, BatchSampler
|
| 17 |
+
import random
|
| 18 |
+
|
| 19 |
+
def custom_collate_fn(batch):
|
| 20 |
+
"""
|
| 21 |
+
Custom collate function to handle variable batch sizes
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
batch: A list where each element could be either:
|
| 25 |
+
- A single tuple (idx, num_images, ...)
|
| 26 |
+
- A list of tuples [(idx1, num_images1, ...), (idx2, num_images2, ...)]
|
| 27 |
+
"""
|
| 28 |
+
# If batch contains lists (variable batch size case)
|
| 29 |
+
if isinstance(batch[0], list):
|
| 30 |
+
# Flatten the batch
|
| 31 |
+
flattened = []
|
| 32 |
+
for item in batch:
|
| 33 |
+
flattened.extend(item)
|
| 34 |
+
batch = flattened
|
| 35 |
+
|
| 36 |
+
# Now batch is a list of tuples, process normally
|
| 37 |
+
return torch.utils.data.default_collate(batch)
|
| 38 |
+
|
| 39 |
+
class BatchedRandomSampler:
|
| 40 |
+
"""Random sampling under a constraint: each sample in the batch has the same feature,
|
| 41 |
+
which is chosen randomly from a known pool of 'features' for each batch.
|
| 42 |
+
|
| 43 |
+
For instance, the 'feature' could be the image aspect-ratio.
|
| 44 |
+
|
| 45 |
+
The index returned is a tuple (sample_idx, feat_idx).
|
| 46 |
+
This sampler ensures that each series of `batch_size` indices has the same `feat_idx`.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self, dataset, batch_size, num_context_views, min_patch_num=20, max_patch_num=32, world_size=1, rank=0, drop_last=True
|
| 51 |
+
):
|
| 52 |
+
self.batch_size = batch_size
|
| 53 |
+
self.num_context_views = num_context_views
|
| 54 |
+
|
| 55 |
+
self.len_dataset = N = len(dataset)
|
| 56 |
+
self.total_size = round_by(N, batch_size * world_size) if drop_last else N
|
| 57 |
+
self.min_patch_num = min_patch_num
|
| 58 |
+
self.max_patch_num = max_patch_num
|
| 59 |
+
assert (
|
| 60 |
+
world_size == 1 or drop_last
|
| 61 |
+
), "must drop the last batch in distributed mode"
|
| 62 |
+
|
| 63 |
+
# distributed sampler
|
| 64 |
+
self.world_size = world_size
|
| 65 |
+
self.rank = rank
|
| 66 |
+
self.epoch = None
|
| 67 |
+
|
| 68 |
+
def __len__(self):
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
return self.total_size // self.world_size
|
| 72 |
+
|
| 73 |
+
def set_epoch(self, epoch):
|
| 74 |
+
self.epoch = epoch
|
| 75 |
+
|
| 76 |
+
def __iter__(self):
|
| 77 |
+
# prepare RNG
|
| 78 |
+
if self.epoch is None:
|
| 79 |
+
assert (
|
| 80 |
+
self.world_size == 1 and self.rank == 0
|
| 81 |
+
), "use set_epoch() if distributed mode is used"
|
| 82 |
+
seed = int(torch.empty((), dtype=torch.int64).random_().item())
|
| 83 |
+
else:
|
| 84 |
+
seed = self.epoch + 777
|
| 85 |
+
rng = np.random.default_rng(seed=seed)
|
| 86 |
+
|
| 87 |
+
# random indices (will restart from 0 if not drop_last)
|
| 88 |
+
sample_idxs = np.arange(self.total_size)
|
| 89 |
+
rng.shuffle(sample_idxs)
|
| 90 |
+
|
| 91 |
+
# random feat_idxs (same across each batch)
|
| 92 |
+
n_batches = (self.total_size + self.batch_size - 1) // self.batch_size
|
| 93 |
+
num_imgs = rng.integers(low=2, high=self.num_context_views, size=n_batches)
|
| 94 |
+
# num_imgs = (np.ones(n_batches) * self.num_context_views).astype(np.int64) # same number of context views for each batch
|
| 95 |
+
num_imgs = np.broadcast_to(num_imgs[:, None], (n_batches, self.batch_size))
|
| 96 |
+
num_imgs = num_imgs.ravel()[: self.total_size]
|
| 97 |
+
|
| 98 |
+
# put them together
|
| 99 |
+
idxs = np.c_[sample_idxs, num_imgs] # shape = (total_size, 2)
|
| 100 |
+
|
| 101 |
+
# Distributed sampler: we select a subset of batches
|
| 102 |
+
# make sure the slice for each node is aligned with batch_size
|
| 103 |
+
size_per_proc = self.batch_size * (
|
| 104 |
+
(self.total_size + self.world_size * self.batch_size - 1)
|
| 105 |
+
// (self.world_size * self.batch_size)
|
| 106 |
+
)
|
| 107 |
+
idxs = idxs[self.rank * size_per_proc : (self.rank + 1) * size_per_proc]
|
| 108 |
+
|
| 109 |
+
yield from (tuple(idx) for idx in idxs)
|
| 110 |
+
|
| 111 |
+
class DynamicBatchSampler(Sampler):
|
| 112 |
+
"""
|
| 113 |
+
A custom batch sampler that dynamically adjusts batch size, aspect ratio, and image number
|
| 114 |
+
for each sample. Batches within a sample share the same aspect ratio and image number.
|
| 115 |
+
"""
|
| 116 |
+
def __init__(self,
|
| 117 |
+
sampler,
|
| 118 |
+
image_num_range,
|
| 119 |
+
h_range,
|
| 120 |
+
batch_size=1,
|
| 121 |
+
epoch=0,
|
| 122 |
+
seed=42,
|
| 123 |
+
max_img_per_gpu=48):
|
| 124 |
+
"""
|
| 125 |
+
Initializes the dynamic batch sampler.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
sampler: Instance of DynamicDistributedSampler.
|
| 129 |
+
aspect_ratio_range: List containing [min_aspect_ratio, max_aspect_ratio].
|
| 130 |
+
image_num_range: List containing [min_images, max_images] per sample.
|
| 131 |
+
epoch: Current epoch number.
|
| 132 |
+
seed: Random seed for reproducibility.
|
| 133 |
+
max_img_per_gpu: Maximum number of images to fit in GPU memory.
|
| 134 |
+
"""
|
| 135 |
+
self.sampler = sampler
|
| 136 |
+
self.image_num_range = image_num_range
|
| 137 |
+
self.h_range = h_range
|
| 138 |
+
self.rng = random.Random()
|
| 139 |
+
|
| 140 |
+
# Uniformly sample from the range of possible image numbers
|
| 141 |
+
# For any image number, the weight is 1.0 (uniform sampling). You can set any different weights here.
|
| 142 |
+
self.image_num_weights = {num_images: float(num_images**2) for num_images in range(image_num_range[0], image_num_range[1]+1)}
|
| 143 |
+
|
| 144 |
+
# Possible image numbers, e.g., [2, 3, 4, ..., 24]
|
| 145 |
+
self.possible_nums = np.array([n for n in self.image_num_weights.keys()
|
| 146 |
+
if self.image_num_range[0] <= n <= self.image_num_range[1]])
|
| 147 |
+
|
| 148 |
+
# Normalize weights for sampling
|
| 149 |
+
weights = [self.image_num_weights[n] for n in self.possible_nums]
|
| 150 |
+
self.normalized_weights = np.array(weights) / sum(weights)
|
| 151 |
+
|
| 152 |
+
# Maximum image number per GPU
|
| 153 |
+
self.max_img_per_gpu = max_img_per_gpu
|
| 154 |
+
self.batch_size = batch_size
|
| 155 |
+
|
| 156 |
+
# Set the epoch for the sampler
|
| 157 |
+
self.set_epoch(epoch + seed)
|
| 158 |
+
|
| 159 |
+
def set_epoch(self, epoch):
|
| 160 |
+
"""
|
| 161 |
+
Sets the epoch for this sampler, affecting the random sequence.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
epoch: The epoch number.
|
| 165 |
+
"""
|
| 166 |
+
self.sampler.set_epoch(epoch)
|
| 167 |
+
self.epoch = epoch
|
| 168 |
+
self.rng.seed(epoch * 100)
|
| 169 |
+
|
| 170 |
+
def __iter__(self):
|
| 171 |
+
"""
|
| 172 |
+
Yields batches of samples with synchronized dynamic parameters.
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
Iterator yielding batches of indices with associated parameters.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
sampler_iterator = iter(self.sampler)
|
| 179 |
+
|
| 180 |
+
while True:
|
| 181 |
+
try:
|
| 182 |
+
# Sample random image number and aspect ratio
|
| 183 |
+
random_image_num = int(np.random.choice(self.possible_nums, p=self.normalized_weights))
|
| 184 |
+
random_ps_h = np.random.randint(low=(self.h_range[0] // 14), high=(self.h_range[1] // 14)+1)
|
| 185 |
+
|
| 186 |
+
# Update sampler parameters
|
| 187 |
+
self.sampler.update_parameters(
|
| 188 |
+
image_num=random_image_num,
|
| 189 |
+
ps_h=random_ps_h
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Calculate batch size based on max images per GPU and current image number
|
| 193 |
+
dynamic_batch_size = self.max_img_per_gpu / random_image_num
|
| 194 |
+
dynamic_batch_size = max(1, int(np.floor(dynamic_batch_size)))
|
| 195 |
+
batch_size = min(self.batch_size, dynamic_batch_size)
|
| 196 |
+
|
| 197 |
+
# Collect samples for the current batch
|
| 198 |
+
current_batch = []
|
| 199 |
+
for _ in range(batch_size):
|
| 200 |
+
try:
|
| 201 |
+
item = next(sampler_iterator) # item is (idx, aspect_ratio, image_num)
|
| 202 |
+
current_batch.append(item)
|
| 203 |
+
except StopIteration:
|
| 204 |
+
break # No more samples
|
| 205 |
+
|
| 206 |
+
if not current_batch:
|
| 207 |
+
break # No more data to yield
|
| 208 |
+
|
| 209 |
+
yield current_batch
|
| 210 |
+
|
| 211 |
+
except StopIteration:
|
| 212 |
+
break # End of sampler's iterator
|
| 213 |
+
|
| 214 |
+
def __len__(self):
|
| 215 |
+
# Return a large dummy length
|
| 216 |
+
return 1000000
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class DynamicDistributedSampler(DistributedSampler):
|
| 220 |
+
"""
|
| 221 |
+
Extends PyTorch's DistributedSampler to include dynamic aspect_ratio and image_num
|
| 222 |
+
parameters, which can be passed into the dataset's __getitem__ method.
|
| 223 |
+
"""
|
| 224 |
+
def __init__(
|
| 225 |
+
self,
|
| 226 |
+
dataset,
|
| 227 |
+
num_replicas: Optional[int] = None,
|
| 228 |
+
rank: Optional[int] = None,
|
| 229 |
+
shuffle: bool = False,
|
| 230 |
+
seed: int = 0,
|
| 231 |
+
drop_last: bool = False,
|
| 232 |
+
):
|
| 233 |
+
super().__init__(
|
| 234 |
+
dataset,
|
| 235 |
+
num_replicas=num_replicas,
|
| 236 |
+
rank=rank,
|
| 237 |
+
shuffle=shuffle,
|
| 238 |
+
seed=seed,
|
| 239 |
+
drop_last=drop_last
|
| 240 |
+
)
|
| 241 |
+
self.image_num = None
|
| 242 |
+
self.ps_h = None
|
| 243 |
+
|
| 244 |
+
def __iter__(self):
|
| 245 |
+
"""
|
| 246 |
+
Yields a sequence of (index, image_num, aspect_ratio).
|
| 247 |
+
Relies on the parent class's logic for shuffling/distributing
|
| 248 |
+
the indices across replicas, then attaches extra parameters.
|
| 249 |
+
"""
|
| 250 |
+
indices_iter = super().__iter__()
|
| 251 |
+
|
| 252 |
+
for idx in indices_iter:
|
| 253 |
+
yield (idx, self.image_num, self.ps_h, )
|
| 254 |
+
|
| 255 |
+
def update_parameters(self, image_num, ps_h):
|
| 256 |
+
"""
|
| 257 |
+
Updates dynamic parameters for each new epoch or iteration.
|
| 258 |
+
|
| 259 |
+
Args:
|
| 260 |
+
aspect_ratio: The aspect ratio to set.
|
| 261 |
+
image_num: The number of images to set.
|
| 262 |
+
"""
|
| 263 |
+
self.image_num = image_num
|
| 264 |
+
self.ps_h = ps_h
|
| 265 |
+
|
| 266 |
+
class MixedBatchSampler(BatchSampler):
|
| 267 |
+
"""Sample one batch from a selected dataset with given probability.
|
| 268 |
+
Compatible with datasets at different resolution
|
| 269 |
+
"""
|
| 270 |
+
|
| 271 |
+
def __init__(
|
| 272 |
+
self, src_dataset_ls, batch_size, num_context_views, world_size=1, rank=0, prob=None, sampler=None, generator=None
|
| 273 |
+
):
|
| 274 |
+
self.base_sampler = None
|
| 275 |
+
self.batch_size = batch_size
|
| 276 |
+
self.num_context_views = num_context_views
|
| 277 |
+
self.world_size = world_size
|
| 278 |
+
self.rank = rank
|
| 279 |
+
self.drop_last = True
|
| 280 |
+
self.generator = generator
|
| 281 |
+
|
| 282 |
+
self.src_dataset_ls = src_dataset_ls
|
| 283 |
+
self.n_dataset = len(self.src_dataset_ls)
|
| 284 |
+
|
| 285 |
+
# Dataset length
|
| 286 |
+
self.dataset_length = [len(ds) for ds in self.src_dataset_ls]
|
| 287 |
+
self.cum_dataset_length = [
|
| 288 |
+
sum(self.dataset_length[:i]) for i in range(self.n_dataset)
|
| 289 |
+
] # cumulative dataset length
|
| 290 |
+
|
| 291 |
+
# BatchSamplers for each source dataset
|
| 292 |
+
self.src_batch_samplers = []
|
| 293 |
+
for ds in self.src_dataset_ls:
|
| 294 |
+
sampler = DynamicDistributedSampler(ds, num_replicas=self.world_size, rank=self.rank, seed=42, shuffle=True)
|
| 295 |
+
sampler.set_epoch(0)
|
| 296 |
+
|
| 297 |
+
if hasattr(ds, "epoch"):
|
| 298 |
+
ds.epoch = 0
|
| 299 |
+
if hasattr(ds, "set_epoch"):
|
| 300 |
+
ds.set_epoch(0)
|
| 301 |
+
batch_sampler = DynamicBatchSampler(
|
| 302 |
+
sampler,
|
| 303 |
+
[2, ds.cfg.view_sampler.num_context_views],
|
| 304 |
+
ds.cfg.input_image_shape,
|
| 305 |
+
batch_size=self.batch_size,
|
| 306 |
+
seed=42,
|
| 307 |
+
max_img_per_gpu=ds.cfg.view_sampler.max_img_per_gpu
|
| 308 |
+
)
|
| 309 |
+
self.src_batch_samplers.append(batch_sampler)
|
| 310 |
+
# set epoch here
|
| 311 |
+
print("Setting epoch for all underlying BatchedRandomSamplers")
|
| 312 |
+
self.raw_batches = [
|
| 313 |
+
list(bs) for bs in self.src_batch_samplers
|
| 314 |
+
] # index in original dataset
|
| 315 |
+
|
| 316 |
+
self.n_batches = [len(b) for b in self.raw_batches]
|
| 317 |
+
self.n_total_batch = sum(self.n_batches)
|
| 318 |
+
print("Total batch num is ", self.n_total_batch)
|
| 319 |
+
# sampling probability
|
| 320 |
+
if prob is None:
|
| 321 |
+
# if not given, decide by dataset length
|
| 322 |
+
self.prob = torch.tensor(self.n_batches) / self.n_total_batch
|
| 323 |
+
else:
|
| 324 |
+
self.prob = torch.as_tensor(prob)
|
| 325 |
+
|
| 326 |
+
def __iter__(self):
|
| 327 |
+
"""Yields batches of indices in the format of (sample_idx, feat_idx) tuples,
|
| 328 |
+
where indices correspond to ConcatDataset of src_dataset_ls
|
| 329 |
+
"""
|
| 330 |
+
for _ in range(self.n_total_batch):
|
| 331 |
+
idx_ds = torch.multinomial(
|
| 332 |
+
self.prob, 1, replacement=True, generator=self.generator
|
| 333 |
+
).item()
|
| 334 |
+
|
| 335 |
+
if 0 == len(self.raw_batches[idx_ds]):
|
| 336 |
+
self.raw_batches[idx_ds] = list(self.src_batch_samplers[idx_ds])
|
| 337 |
+
|
| 338 |
+
# get a batch from list - this is already in (sample_idx, feat_idx) format
|
| 339 |
+
batch_raw = self.raw_batches[idx_ds].pop()
|
| 340 |
+
|
| 341 |
+
# shift only the sample_idx by cumulative dataset length, keep feat_idx unchanged
|
| 342 |
+
shift = self.cum_dataset_length[idx_ds]
|
| 343 |
+
processed_batch = []
|
| 344 |
+
|
| 345 |
+
for item in batch_raw:
|
| 346 |
+
# item[0] is the sample index, item[1] is the number of images
|
| 347 |
+
processed_item = (item[0] + shift, item[1], item[2])
|
| 348 |
+
processed_batch.append(processed_item)
|
| 349 |
+
yield processed_batch
|
| 350 |
+
|
| 351 |
+
def set_epoch(self, epoch):
|
| 352 |
+
"""Set epoch for all underlying BatchedRandomSamplers"""
|
| 353 |
+
for sampler in self.src_batch_samplers:
|
| 354 |
+
sampler.set_epoch(epoch)
|
| 355 |
+
# Reset raw_batches after setting new epoch
|
| 356 |
+
self.raw_batches = [list(bs) for bs in self.src_batch_samplers]
|
| 357 |
+
|
| 358 |
+
def __len__(self):
|
| 359 |
+
return self.n_total_batch
|
| 360 |
+
|
| 361 |
+
def round_by(total, multiple, up=False):
|
| 362 |
+
if up:
|
| 363 |
+
total = total + multiple - 1
|
| 364 |
+
return (total // multiple) * multiple
|
src/dataset/dataset.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
|
| 3 |
+
from .view_sampler import ViewSamplerCfg
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class DatasetCfgCommon:
|
| 8 |
+
original_image_shape: list[int]
|
| 9 |
+
input_image_shape: list[int]
|
| 10 |
+
background_color: list[float]
|
| 11 |
+
cameras_are_circular: bool
|
| 12 |
+
overfit_to_scene: str | None
|
| 13 |
+
view_sampler: ViewSamplerCfg
|
src/dataset/dataset_dl3dv.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 2 |
+
import json
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from functools import cached_property
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Literal
|
| 7 |
+
import os
|
| 8 |
+
import traceback
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
import torchvision.transforms as tf
|
| 12 |
+
from einops import repeat
|
| 13 |
+
from jaxtyping import Float
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from torch import Tensor
|
| 16 |
+
from torch.utils.data import Dataset
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
from ..geometry.projection import get_fov
|
| 19 |
+
from .dataset import DatasetCfgCommon
|
| 20 |
+
from .shims.augmentation_shim import apply_augmentation_shim
|
| 21 |
+
from .shims.crop_shim import apply_crop_shim
|
| 22 |
+
from .types import Stage
|
| 23 |
+
from .view_sampler import ViewSampler
|
| 24 |
+
from ..misc.cam_utils import camera_normalization
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class DL3DVScene:
|
| 29 |
+
index_item: str
|
| 30 |
+
scene_root: Path
|
| 31 |
+
transforms_path: Path
|
| 32 |
+
images_dir: Path
|
| 33 |
+
scene_id: str
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class DatasetDl3dvCfg(DatasetCfgCommon):
|
| 38 |
+
mode: str
|
| 39 |
+
name: str
|
| 40 |
+
roots: list[Path]
|
| 41 |
+
baseline_min: float
|
| 42 |
+
baseline_max: float
|
| 43 |
+
max_fov: float
|
| 44 |
+
make_baseline_1: bool
|
| 45 |
+
augment: bool
|
| 46 |
+
relative_pose: bool
|
| 47 |
+
skip_bad_shape: bool
|
| 48 |
+
avg_pose: bool
|
| 49 |
+
rescale_to_1cube: bool
|
| 50 |
+
intr_augment: bool
|
| 51 |
+
normalize_by_pts3d: bool
|
| 52 |
+
ctx_list: list[int] | None
|
| 53 |
+
tgt_list: list[int] | None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class DatasetDL3DVCfgWrapper:
|
| 58 |
+
dl3dv: DatasetDl3dvCfg
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class DatasetDL3DV(Dataset):
|
| 62 |
+
cfg: DatasetDl3dvCfg
|
| 63 |
+
stage: Stage
|
| 64 |
+
view_sampler: ViewSampler
|
| 65 |
+
|
| 66 |
+
to_tensor: tf.ToTensor
|
| 67 |
+
chunks: list[Path]
|
| 68 |
+
near: float = 0.1
|
| 69 |
+
far: float = 100.0
|
| 70 |
+
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
cfg: DatasetDl3dvCfg,
|
| 74 |
+
stage: Stage,
|
| 75 |
+
view_sampler: ViewSampler,
|
| 76 |
+
) -> None:
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.cfg = cfg
|
| 79 |
+
self.stage = stage
|
| 80 |
+
self.view_sampler = view_sampler
|
| 81 |
+
self.to_tensor = tf.ToTensor()
|
| 82 |
+
# load data
|
| 83 |
+
if not cfg.roots:
|
| 84 |
+
raise ValueError(
|
| 85 |
+
"DatasetDL3DV requires dataset.dl3dv.roots. Set "
|
| 86 |
+
"DL3DV_ROOT=/path/to/dl3dv when using train.sh or pass "
|
| 87 |
+
"dataset.dl3dv.roots='[/path/to/dl3dv]'."
|
| 88 |
+
)
|
| 89 |
+
self.data_root = Path(cfg.roots[0])
|
| 90 |
+
index_path = self.data_root / f"{self.data_stage}_index.json"
|
| 91 |
+
if not index_path.is_file():
|
| 92 |
+
raise FileNotFoundError(
|
| 93 |
+
f"DL3DV index file not found: {index_path}. Expected "
|
| 94 |
+
f"{self.data_stage}_index.json under dataset.dl3dv.roots[0]."
|
| 95 |
+
)
|
| 96 |
+
self.data_list = []
|
| 97 |
+
with index_path.open("r") as file:
|
| 98 |
+
data_index = json.load(file)
|
| 99 |
+
|
| 100 |
+
def filter_data_list(data_index):
|
| 101 |
+
data_list = []
|
| 102 |
+
|
| 103 |
+
for item in data_index:
|
| 104 |
+
scene = self.resolve_scene(str(item))
|
| 105 |
+
if scene is None:
|
| 106 |
+
print(item)
|
| 107 |
+
else:
|
| 108 |
+
data_list.append(scene)
|
| 109 |
+
|
| 110 |
+
return data_list
|
| 111 |
+
|
| 112 |
+
self.data_list = filter_data_list(data_index)
|
| 113 |
+
self.scene_ids = {}
|
| 114 |
+
self.scenes = {}
|
| 115 |
+
index = 0
|
| 116 |
+
|
| 117 |
+
if cfg.mode == "train":
|
| 118 |
+
with ThreadPoolExecutor(max_workers=64) as executor:
|
| 119 |
+
futures = [
|
| 120 |
+
executor.submit(self.load_jsons, scene_path)
|
| 121 |
+
for scene_path in self.data_list
|
| 122 |
+
]
|
| 123 |
+
for future in tqdm(as_completed(futures), total=len(futures)):
|
| 124 |
+
scene_frames, scene_id = future.result()
|
| 125 |
+
self.scenes[scene_id] = scene_frames
|
| 126 |
+
self.scene_ids[index] = scene_id
|
| 127 |
+
index += 1
|
| 128 |
+
else:
|
| 129 |
+
futures = [self.load_jsons(scene_path) for scene_path in self.data_list]
|
| 130 |
+
for future in tqdm(futures, total=len(futures)):
|
| 131 |
+
scene_frames, scene_id = future
|
| 132 |
+
self.scenes[scene_id] = scene_frames
|
| 133 |
+
self.scene_ids[index] = scene_id
|
| 134 |
+
index += 1
|
| 135 |
+
|
| 136 |
+
print(f"DL3DV: {self.stage}: loaded {len(self.scene_ids)} scenes")
|
| 137 |
+
|
| 138 |
+
def resolve_scene(self, item: str) -> DL3DVScene | None:
|
| 139 |
+
scene_root = self.data_root / item
|
| 140 |
+
if not scene_root.exists():
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
image_dirs = sorted(
|
| 144 |
+
(path for path in scene_root.rglob("images_8") if path.is_dir()),
|
| 145 |
+
key=lambda path: (-len(path.parts), path.as_posix()),
|
| 146 |
+
)
|
| 147 |
+
transforms_paths = sorted(
|
| 148 |
+
scene_root.rglob("transforms.json"),
|
| 149 |
+
key=lambda path: (-len(path.parts), path.as_posix()),
|
| 150 |
+
)
|
| 151 |
+
if not image_dirs or not transforms_paths:
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
matches = []
|
| 155 |
+
for transforms_path in transforms_paths:
|
| 156 |
+
for images_dir in image_dirs:
|
| 157 |
+
try:
|
| 158 |
+
transforms_path.relative_to(images_dir.parent)
|
| 159 |
+
matches.append((transforms_path, images_dir))
|
| 160 |
+
continue
|
| 161 |
+
except ValueError:
|
| 162 |
+
pass
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
images_dir.relative_to(transforms_path.parent)
|
| 166 |
+
matches.append((transforms_path, images_dir))
|
| 167 |
+
except ValueError:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
if not matches:
|
| 171 |
+
matches = [(transforms_paths[0], image_dirs[0])]
|
| 172 |
+
if len(matches) > 1:
|
| 173 |
+
print(
|
| 174 |
+
f"Ambiguous DL3DV scene path for {item}: found {len(matches)} "
|
| 175 |
+
"matches; using the deepest transforms/images_8 pair."
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
transforms_path, images_dir = matches[0]
|
| 179 |
+
scene_rel = transforms_path.parent.relative_to(self.data_root).as_posix()
|
| 180 |
+
scene_id = scene_rel.replace("/", "_")
|
| 181 |
+
return DL3DVScene(
|
| 182 |
+
index_item=item,
|
| 183 |
+
scene_root=scene_root,
|
| 184 |
+
transforms_path=transforms_path,
|
| 185 |
+
images_dir=images_dir,
|
| 186 |
+
scene_id=scene_id,
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
def convert_intrinsics(self, meta_data):
|
| 190 |
+
store_h, store_w = meta_data["h"], meta_data["w"]
|
| 191 |
+
fx, fy, cx, cy = (
|
| 192 |
+
meta_data["fl_x"],
|
| 193 |
+
meta_data["fl_y"],
|
| 194 |
+
meta_data["cx"],
|
| 195 |
+
meta_data["cy"],
|
| 196 |
+
)
|
| 197 |
+
intrinsics = np.eye(3, dtype=np.float32)
|
| 198 |
+
intrinsics[0, 0] = float(fx) / float(store_w)
|
| 199 |
+
intrinsics[1, 1] = float(fy) / float(store_h)
|
| 200 |
+
intrinsics[0, 2] = float(cx) / float(store_w)
|
| 201 |
+
intrinsics[1, 2] = float(cy) / float(store_h)
|
| 202 |
+
return intrinsics
|
| 203 |
+
|
| 204 |
+
def blender2opencv_c2w(self, pose):
|
| 205 |
+
blender2opencv = np.array(
|
| 206 |
+
[[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
|
| 207 |
+
)
|
| 208 |
+
opencv_c2w = np.array(pose) @ blender2opencv
|
| 209 |
+
return opencv_c2w.tolist()
|
| 210 |
+
|
| 211 |
+
def load_jsons(self, scene: DL3DVScene):
|
| 212 |
+
with scene.transforms_path.open("r") as f:
|
| 213 |
+
data = json.load(f)
|
| 214 |
+
|
| 215 |
+
scene_frames = []
|
| 216 |
+
for i, frame in enumerate(data["frames"]):
|
| 217 |
+
frame_tmp = {}
|
| 218 |
+
frame_tmp["file_path"] = self.resolve_image_path(
|
| 219 |
+
scene.images_dir,
|
| 220 |
+
frame["file_path"],
|
| 221 |
+
)
|
| 222 |
+
frame_tmp["intrinsics"] = self.convert_intrinsics(data).tolist()
|
| 223 |
+
frame_tmp["extrinsics"] = self.blender2opencv_c2w(frame["transform_matrix"])
|
| 224 |
+
scene_frames.append(frame_tmp)
|
| 225 |
+
return scene_frames, scene.scene_id
|
| 226 |
+
|
| 227 |
+
def load_frames(self, frames):
|
| 228 |
+
with ThreadPoolExecutor(max_workers=32) as executor:
|
| 229 |
+
# Create a list to store futures with their original indices
|
| 230 |
+
futures_with_idx = []
|
| 231 |
+
for idx, file_path in enumerate(frames):
|
| 232 |
+
file_path = file_path["file_path"]
|
| 233 |
+
futures_with_idx.append(
|
| 234 |
+
(
|
| 235 |
+
idx,
|
| 236 |
+
executor.submit(
|
| 237 |
+
lambda p: self.to_tensor(Image.open(p).convert("RGB")),
|
| 238 |
+
file_path,
|
| 239 |
+
),
|
| 240 |
+
)
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Pre-allocate list with correct size to maintain order
|
| 244 |
+
torch_images = [None] * len(frames)
|
| 245 |
+
for idx, future in futures_with_idx:
|
| 246 |
+
torch_images[idx] = future.result()
|
| 247 |
+
# Check if all images have the same size
|
| 248 |
+
|
| 249 |
+
sizes = set(img.shape for img in torch_images)
|
| 250 |
+
if len(sizes) == 1:
|
| 251 |
+
torch_images = torch.stack(torch_images)
|
| 252 |
+
return torch_images
|
| 253 |
+
|
| 254 |
+
def resolve_image_path(self, images_dir: Path, frame_path: str) -> str:
|
| 255 |
+
frame_rel = Path(frame_path)
|
| 256 |
+
filename = frame_rel.name
|
| 257 |
+
candidates = [
|
| 258 |
+
images_dir / filename,
|
| 259 |
+
images_dir / frame_rel,
|
| 260 |
+
]
|
| 261 |
+
if "images" in frame_rel.parts:
|
| 262 |
+
image_idx = frame_rel.parts.index("images")
|
| 263 |
+
candidates.append(images_dir.joinpath(*frame_rel.parts[image_idx + 1 :]))
|
| 264 |
+
|
| 265 |
+
for candidate in candidates:
|
| 266 |
+
if candidate.is_file():
|
| 267 |
+
return str(candidate)
|
| 268 |
+
|
| 269 |
+
matches = sorted(images_dir.rglob(filename))
|
| 270 |
+
if matches:
|
| 271 |
+
return str(matches[0])
|
| 272 |
+
return str(images_dir / filename)
|
| 273 |
+
|
| 274 |
+
def getitem(self, index: int, num_context_views: int, patchsize: tuple) -> dict:
|
| 275 |
+
|
| 276 |
+
scene = self.scene_ids[index]
|
| 277 |
+
|
| 278 |
+
example = self.scenes[scene]
|
| 279 |
+
# load poses
|
| 280 |
+
extrinsics = []
|
| 281 |
+
intrinsics = []
|
| 282 |
+
for frame in example:
|
| 283 |
+
extrinsic = frame["extrinsics"]
|
| 284 |
+
intrinsic = frame["intrinsics"]
|
| 285 |
+
extrinsics.append(extrinsic)
|
| 286 |
+
intrinsics.append(intrinsic)
|
| 287 |
+
|
| 288 |
+
extrinsics = np.array(extrinsics)
|
| 289 |
+
intrinsics = np.array(intrinsics)
|
| 290 |
+
extrinsics = torch.tensor(extrinsics, dtype=torch.float32)
|
| 291 |
+
intrinsics = torch.tensor(intrinsics, dtype=torch.float32)
|
| 292 |
+
|
| 293 |
+
if self.cfg.mode == "train":
|
| 294 |
+
try:
|
| 295 |
+
context_indices, target_indices, overlap = self.view_sampler.sample(
|
| 296 |
+
scene,
|
| 297 |
+
num_context_views,
|
| 298 |
+
extrinsics,
|
| 299 |
+
intrinsics,
|
| 300 |
+
)
|
| 301 |
+
except ValueError:
|
| 302 |
+
# Skip because the example doesn't have enough frames.
|
| 303 |
+
raise Exception("Not enough frames")
|
| 304 |
+
else:
|
| 305 |
+
if not self.cfg.ctx_list or not self.cfg.tgt_list:
|
| 306 |
+
raise ValueError(
|
| 307 |
+
"DL3DV test mode requires dataset.dl3dv.ctx_list and "
|
| 308 |
+
"dataset.dl3dv.tgt_list to be non-empty lists."
|
| 309 |
+
)
|
| 310 |
+
context_indices = self.cfg.ctx_list
|
| 311 |
+
target_indices = self.cfg.tgt_list
|
| 312 |
+
|
| 313 |
+
if (get_fov(intrinsics).rad2deg() > self.cfg.max_fov).any():
|
| 314 |
+
raise Exception("Field of view too wide")
|
| 315 |
+
|
| 316 |
+
input_frames = [example[i] for i in context_indices]
|
| 317 |
+
target_frame = [example[i] for i in target_indices]
|
| 318 |
+
|
| 319 |
+
context_images = self.load_frames(input_frames)
|
| 320 |
+
target_images = self.load_frames(target_frame)
|
| 321 |
+
resize = tf.Resize((270, 480))
|
| 322 |
+
context_images = resize(context_images)
|
| 323 |
+
target_images = resize(target_images)
|
| 324 |
+
|
| 325 |
+
# Skip the example if the images don't have the right shape.
|
| 326 |
+
context_image_invalid = context_images.shape[1:] != (
|
| 327 |
+
3,
|
| 328 |
+
*self.cfg.original_image_shape,
|
| 329 |
+
)
|
| 330 |
+
target_image_invalid = target_images.shape[1:] != (
|
| 331 |
+
3,
|
| 332 |
+
*self.cfg.original_image_shape,
|
| 333 |
+
)
|
| 334 |
+
if self.cfg.skip_bad_shape and (context_image_invalid or target_image_invalid):
|
| 335 |
+
print(
|
| 336 |
+
f"Skipped bad example {scene}. Context shape was "
|
| 337 |
+
f"{context_images.shape} and target shape was "
|
| 338 |
+
f"{target_images.shape}."
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
raise Exception("Bad example image shape")
|
| 342 |
+
|
| 343 |
+
context_extrinsics = extrinsics[context_indices]
|
| 344 |
+
if self.cfg.make_baseline_1:
|
| 345 |
+
a, b = context_extrinsics[0, :3, 3], context_extrinsics[-1, :3, 3]
|
| 346 |
+
scale = (a - b).norm()
|
| 347 |
+
if scale < self.cfg.baseline_min or scale > self.cfg.baseline_max:
|
| 348 |
+
print(
|
| 349 |
+
f"Skipped {scene} because of baseline out of range: " f"{scale:.6f}"
|
| 350 |
+
)
|
| 351 |
+
raise Exception("baseline out of range")
|
| 352 |
+
extrinsics[:, :3, 3] /= scale
|
| 353 |
+
else:
|
| 354 |
+
scale = 1
|
| 355 |
+
|
| 356 |
+
if self.cfg.relative_pose:
|
| 357 |
+
extrinsics = camera_normalization(
|
| 358 |
+
extrinsics[context_indices][0:1], extrinsics
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
if self.cfg.rescale_to_1cube:
|
| 362 |
+
scene_scale = torch.max(
|
| 363 |
+
torch.abs(extrinsics[context_indices][:, :3, 3])
|
| 364 |
+
)
|
| 365 |
+
rescale_factor = 1 * scene_scale
|
| 366 |
+
extrinsics[:, :3, 3] /= rescale_factor
|
| 367 |
+
|
| 368 |
+
if torch.isnan(extrinsics).any() or torch.isinf(extrinsics).any():
|
| 369 |
+
raise Exception("encounter nan or inf in input poses")
|
| 370 |
+
|
| 371 |
+
example = {
|
| 372 |
+
"context": {
|
| 373 |
+
"extrinsics": extrinsics[context_indices],
|
| 374 |
+
"intrinsics": intrinsics[context_indices],
|
| 375 |
+
"image": context_images,
|
| 376 |
+
"near": self.get_bound("near", len(context_indices)) / scale,
|
| 377 |
+
"far": self.get_bound("far", len(context_indices)) / scale,
|
| 378 |
+
"index": context_indices,
|
| 379 |
+
},
|
| 380 |
+
"target": {
|
| 381 |
+
"extrinsics": extrinsics[target_indices],
|
| 382 |
+
"intrinsics": intrinsics[target_indices],
|
| 383 |
+
"image": target_images,
|
| 384 |
+
"near": self.get_bound("near", len(target_indices)) / scale,
|
| 385 |
+
"far": self.get_bound("far", len(target_indices)) / scale,
|
| 386 |
+
"index": target_indices,
|
| 387 |
+
},
|
| 388 |
+
"scene": "dl3dv_" + scene,
|
| 389 |
+
}
|
| 390 |
+
if self.stage == "train" and self.cfg.augment:
|
| 391 |
+
example = apply_augmentation_shim(example)
|
| 392 |
+
|
| 393 |
+
if self.stage == "train" and self.cfg.intr_augment:
|
| 394 |
+
intr_aug = True
|
| 395 |
+
else:
|
| 396 |
+
intr_aug = False
|
| 397 |
+
|
| 398 |
+
example = apply_crop_shim(
|
| 399 |
+
example, (patchsize[0] * 14, patchsize[1] * 14), intr_aug=intr_aug
|
| 400 |
+
)
|
| 401 |
+
return example
|
| 402 |
+
|
| 403 |
+
def __getitem__(self, index_tuple: tuple) -> dict:
|
| 404 |
+
index, num_context_views, patchsize_h = index_tuple
|
| 405 |
+
patchsize_w = self.cfg.input_image_shape[1] // 14
|
| 406 |
+
|
| 407 |
+
try:
|
| 408 |
+
return self.getitem(index, num_context_views, (patchsize_h, patchsize_w))
|
| 409 |
+
except Exception as e:
|
| 410 |
+
print(f"Error: {e}")
|
| 411 |
+
traceback.print_exc()
|
| 412 |
+
index = np.random.randint(len(self))
|
| 413 |
+
return self.__getitem__((index, num_context_views, patchsize_h))
|
| 414 |
+
|
| 415 |
+
def get_bound(
|
| 416 |
+
self,
|
| 417 |
+
bound: Literal["near", "far"],
|
| 418 |
+
num_views: int,
|
| 419 |
+
) -> Float[Tensor, " view"]:
|
| 420 |
+
value = torch.tensor(getattr(self, bound), dtype=torch.float32)
|
| 421 |
+
return repeat(value, "-> v", v=num_views)
|
| 422 |
+
|
| 423 |
+
@property
|
| 424 |
+
def data_stage(self) -> Stage:
|
| 425 |
+
if self.cfg.overfit_to_scene is not None:
|
| 426 |
+
return "test"
|
| 427 |
+
if self.stage == "val":
|
| 428 |
+
return "test"
|
| 429 |
+
return self.stage
|
| 430 |
+
|
| 431 |
+
@cached_property
|
| 432 |
+
def index(self) -> dict[str, Path]:
|
| 433 |
+
merged_index = {}
|
| 434 |
+
data_stages = [self.data_stage]
|
| 435 |
+
if self.cfg.overfit_to_scene is not None:
|
| 436 |
+
data_stages = ("test", "train")
|
| 437 |
+
for data_stage in data_stages:
|
| 438 |
+
for root in self.cfg.roots:
|
| 439 |
+
# Load the root's index.
|
| 440 |
+
with (root / data_stage / "index.json").open("r") as f:
|
| 441 |
+
index = json.load(f)
|
| 442 |
+
index = {k: Path(root / data_stage / v) for k, v in index.items()}
|
| 443 |
+
|
| 444 |
+
# The constituent datasets should have unique keys.
|
| 445 |
+
assert not (set(merged_index.keys()) & set(index.keys()))
|
| 446 |
+
|
| 447 |
+
# Merge the root's index into the main index.
|
| 448 |
+
merged_index = {**merged_index, **index}
|
| 449 |
+
return merged_index
|
| 450 |
+
|
| 451 |
+
def __len__(self) -> int:
|
| 452 |
+
return len(self.scene_ids)
|
src/dataset/dataset_re10k.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 2 |
+
import json
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from functools import cached_property
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Literal
|
| 7 |
+
import os
|
| 8 |
+
import traceback
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
import torchvision.transforms as tf
|
| 12 |
+
from einops import repeat
|
| 13 |
+
from jaxtyping import Float
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from torch import Tensor
|
| 16 |
+
from torch.utils.data import Dataset
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
from .dataset import DatasetCfgCommon
|
| 19 |
+
from .shims.augmentation_shim import apply_augmentation_shim
|
| 20 |
+
from .shims.crop_shim import apply_crop_shim
|
| 21 |
+
from .types import Stage
|
| 22 |
+
from .view_sampler import ViewSampler
|
| 23 |
+
import gzip
|
| 24 |
+
import pickle
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class Datasetre10kCfg(DatasetCfgCommon):
|
| 29 |
+
name: str
|
| 30 |
+
roots: list[Path]
|
| 31 |
+
baseline_min: float
|
| 32 |
+
baseline_max: float
|
| 33 |
+
max_fov: float
|
| 34 |
+
make_baseline_1: bool
|
| 35 |
+
augment: bool
|
| 36 |
+
relative_pose: bool
|
| 37 |
+
skip_bad_shape: bool
|
| 38 |
+
avg_pose: bool
|
| 39 |
+
rescale_to_1cube: bool
|
| 40 |
+
intr_augment: bool
|
| 41 |
+
normalize_by_pts3d: bool
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class Datasetre10kCfgWrapper:
|
| 46 |
+
re10k: Datasetre10kCfg
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Datasetre10k(Dataset):
|
| 50 |
+
cfg: Datasetre10kCfg
|
| 51 |
+
stage: Stage
|
| 52 |
+
view_sampler: ViewSampler
|
| 53 |
+
|
| 54 |
+
to_tensor: tf.ToTensor
|
| 55 |
+
chunks: list[Path]
|
| 56 |
+
near: float = 0.1
|
| 57 |
+
far: float = 100.0
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
cfg: Datasetre10kCfg,
|
| 62 |
+
stage: Stage,
|
| 63 |
+
view_sampler: ViewSampler,
|
| 64 |
+
) -> None:
|
| 65 |
+
super().__init__()
|
| 66 |
+
self.cfg = cfg
|
| 67 |
+
self.stage = stage
|
| 68 |
+
self.view_sampler = view_sampler
|
| 69 |
+
self.to_tensor = tf.ToTensor()
|
| 70 |
+
# load data
|
| 71 |
+
if not cfg.roots:
|
| 72 |
+
raise ValueError(
|
| 73 |
+
"Datasetre10k requires dataset.re10k.roots. Set "
|
| 74 |
+
"RE10K_ROOT=/path/to/re10k when using train.sh or pass "
|
| 75 |
+
"dataset.re10k.roots='[/path/to/re10k]'."
|
| 76 |
+
)
|
| 77 |
+
self.root = Path(cfg.roots[0])
|
| 78 |
+
self.split = self.data_stage
|
| 79 |
+
if len(cfg.roots) == 1:
|
| 80 |
+
self.data_root = self.root / self.split
|
| 81 |
+
self.index_root = self.root / f"{self.split}_index.json"
|
| 82 |
+
self.metadata_path = self.root / f"{self.split}.pickle.gz"
|
| 83 |
+
else:
|
| 84 |
+
self.data_root = self.root
|
| 85 |
+
self.index_root = Path(cfg.roots[1])
|
| 86 |
+
self.metadata_path = self.root / "train.pickle.gz"
|
| 87 |
+
if not self.index_root.is_file():
|
| 88 |
+
raise FileNotFoundError(
|
| 89 |
+
f"RE10K index file not found: {self.index_root}. Expected "
|
| 90 |
+
f"{self.split}_index.json under dataset.re10k.roots[0], or pass "
|
| 91 |
+
"a second roots entry pointing to the index file."
|
| 92 |
+
)
|
| 93 |
+
self.data_list = []
|
| 94 |
+
with self.index_root.open("r") as file:
|
| 95 |
+
self.data_index = json.load(file)
|
| 96 |
+
|
| 97 |
+
for item in self.data_index:
|
| 98 |
+
self.data_list.append(str(self.resolve_scene_path(str(item))))
|
| 99 |
+
self.scene_ids = {}
|
| 100 |
+
self.scenes = {}
|
| 101 |
+
index = 0
|
| 102 |
+
|
| 103 |
+
if not self.metadata_path.is_file() and self.split == "test":
|
| 104 |
+
fallback = self.root / "train.pickle.gz"
|
| 105 |
+
if fallback.is_file():
|
| 106 |
+
self.metadata_path = fallback
|
| 107 |
+
if not self.metadata_path.is_file():
|
| 108 |
+
raise FileNotFoundError(
|
| 109 |
+
f"RE10K metadata file not found: {self.metadata_path}. Expected "
|
| 110 |
+
f"{self.split}.pickle.gz under dataset.re10k.roots[0]."
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
with gzip.open(self.metadata_path, "rb") as f:
|
| 114 |
+
self.seq_data = pickle.load(f)
|
| 115 |
+
|
| 116 |
+
print("re10k loading data !!!")
|
| 117 |
+
with ThreadPoolExecutor(max_workers=64) as executor:
|
| 118 |
+
futures = [
|
| 119 |
+
executor.submit(self.load_jsons, scene_path)
|
| 120 |
+
for scene_path in self.data_list
|
| 121 |
+
]
|
| 122 |
+
for future in tqdm(as_completed(futures), total=len(futures)):
|
| 123 |
+
scene_frames, scene_id = future.result()
|
| 124 |
+
self.scenes[scene_id] = scene_frames
|
| 125 |
+
self.scene_ids[index] = scene_id
|
| 126 |
+
index += 1
|
| 127 |
+
print(f"RE10k: {self.stage}: loaded {len(self.scene_ids)} scenes")
|
| 128 |
+
|
| 129 |
+
def resolve_scene_path(self, item: str) -> Path:
|
| 130 |
+
scene_path = self.data_root / item
|
| 131 |
+
if scene_path.is_dir():
|
| 132 |
+
return scene_path
|
| 133 |
+
return self.root / item
|
| 134 |
+
|
| 135 |
+
def load_jsons(self, scene_path):
|
| 136 |
+
files = os.listdir(scene_path)
|
| 137 |
+
files = [f for f in files if os.path.isfile(os.path.join(scene_path, f))]
|
| 138 |
+
files.sort()
|
| 139 |
+
scene_frames = []
|
| 140 |
+
scene_id = Path(scene_path).name
|
| 141 |
+
for i, frame in enumerate(files):
|
| 142 |
+
frame_tmp = {}
|
| 143 |
+
|
| 144 |
+
frame_tmp["file_path"] = os.path.join(scene_path, frame)
|
| 145 |
+
frame_tmp["extrinsics"] = np.array(
|
| 146 |
+
[[0.0, -0.5, 0, 0], [0.5, 0.866, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
| 147 |
+
)
|
| 148 |
+
itmp = self.seq_data[scene_id]["intrinsics"][i]
|
| 149 |
+
intrinsics = np.eye(3, dtype=np.float32)
|
| 150 |
+
intrinsics[0, 0] = float(itmp[0])
|
| 151 |
+
intrinsics[1, 1] = float(itmp[1])
|
| 152 |
+
intrinsics[0, 2] = float(itmp[2])
|
| 153 |
+
intrinsics[1, 2] = float(itmp[3])
|
| 154 |
+
frame_tmp["intrinsics"] = intrinsics
|
| 155 |
+
|
| 156 |
+
scene_frames.append(frame_tmp)
|
| 157 |
+
|
| 158 |
+
return scene_frames, scene_id
|
| 159 |
+
|
| 160 |
+
def load_frames(self, frames):
|
| 161 |
+
with ThreadPoolExecutor(max_workers=32) as executor:
|
| 162 |
+
# Create a list to store futures with their original indices
|
| 163 |
+
futures_with_idx = []
|
| 164 |
+
for idx, file_path in enumerate(frames):
|
| 165 |
+
file_path = file_path["file_path"]
|
| 166 |
+
futures_with_idx.append(
|
| 167 |
+
(
|
| 168 |
+
idx,
|
| 169 |
+
executor.submit(
|
| 170 |
+
lambda p: self.to_tensor(Image.open(p).convert("RGB")),
|
| 171 |
+
file_path,
|
| 172 |
+
),
|
| 173 |
+
)
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Pre-allocate list with correct size to maintain order
|
| 177 |
+
torch_images = [None] * len(frames)
|
| 178 |
+
for idx, future in futures_with_idx:
|
| 179 |
+
torch_images[idx] = future.result()
|
| 180 |
+
# Check if all images have the same size
|
| 181 |
+
|
| 182 |
+
sizes = set(img.shape for img in torch_images)
|
| 183 |
+
if len(sizes) == 1:
|
| 184 |
+
torch_images = torch.stack(torch_images)
|
| 185 |
+
# Return as list if images have different sizes
|
| 186 |
+
return torch_images
|
| 187 |
+
|
| 188 |
+
def getitem(self, index: int, num_context_views: int, patchsize: tuple) -> dict:
|
| 189 |
+
scene = self.scene_ids[index]
|
| 190 |
+
example = self.scenes[scene]
|
| 191 |
+
# load poses
|
| 192 |
+
extrinsics = []
|
| 193 |
+
intrinsics = []
|
| 194 |
+
for frame in example:
|
| 195 |
+
extrinsic = frame["extrinsics"]
|
| 196 |
+
intrinsic = frame["intrinsics"]
|
| 197 |
+
extrinsics.append(extrinsic)
|
| 198 |
+
intrinsics.append(intrinsic)
|
| 199 |
+
|
| 200 |
+
extrinsics = np.array(extrinsics)
|
| 201 |
+
intrinsics = np.array(intrinsics)
|
| 202 |
+
extrinsics = torch.tensor(extrinsics, dtype=torch.float32)
|
| 203 |
+
intrinsics = torch.tensor(intrinsics, dtype=torch.float32)
|
| 204 |
+
try:
|
| 205 |
+
context_indices, target_indices, overlap = self.view_sampler.sample(
|
| 206 |
+
scene,
|
| 207 |
+
num_context_views,
|
| 208 |
+
extrinsics,
|
| 209 |
+
intrinsics,
|
| 210 |
+
)
|
| 211 |
+
except ValueError:
|
| 212 |
+
# Skip because the example doesn't have enough frames.
|
| 213 |
+
raise Exception("Not enough frames")
|
| 214 |
+
|
| 215 |
+
input_frames = [example[i] for i in context_indices]
|
| 216 |
+
target_frame = [example[i] for i in target_indices]
|
| 217 |
+
|
| 218 |
+
context_images = self.load_frames(input_frames)
|
| 219 |
+
target_images = self.load_frames(target_frame)
|
| 220 |
+
|
| 221 |
+
example = {
|
| 222 |
+
"context": {
|
| 223 |
+
"extrinsics": extrinsics[context_indices],
|
| 224 |
+
"intrinsics": intrinsics[context_indices],
|
| 225 |
+
"image": context_images,
|
| 226 |
+
"near": self.get_bound("near", len(context_indices)),
|
| 227 |
+
"far": self.get_bound("far", len(context_indices)),
|
| 228 |
+
"index": context_indices,
|
| 229 |
+
},
|
| 230 |
+
"target": {
|
| 231 |
+
"extrinsics": extrinsics[target_indices],
|
| 232 |
+
"intrinsics": intrinsics[target_indices],
|
| 233 |
+
"image": target_images,
|
| 234 |
+
"near": self.get_bound("near", len(target_indices)),
|
| 235 |
+
"far": self.get_bound("far", len(target_indices)),
|
| 236 |
+
"index": target_indices,
|
| 237 |
+
},
|
| 238 |
+
"scene": "re10k_" + scene,
|
| 239 |
+
}
|
| 240 |
+
if self.stage == "train" and self.cfg.augment:
|
| 241 |
+
example = apply_augmentation_shim(example)
|
| 242 |
+
|
| 243 |
+
if self.stage == "train" and self.cfg.intr_augment:
|
| 244 |
+
intr_aug = True
|
| 245 |
+
else:
|
| 246 |
+
intr_aug = False
|
| 247 |
+
|
| 248 |
+
example = apply_crop_shim(
|
| 249 |
+
example, (patchsize[0] * 14, patchsize[1] * 14), intr_aug=intr_aug
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
return example
|
| 253 |
+
|
| 254 |
+
def __getitem__(self, index_tuple: tuple) -> dict:
|
| 255 |
+
index, num_context_views, patchsize_h = index_tuple
|
| 256 |
+
patchsize_w = self.cfg.input_image_shape[1] // 14
|
| 257 |
+
try:
|
| 258 |
+
return self.getitem(index, num_context_views, (patchsize_h, patchsize_w))
|
| 259 |
+
except Exception as e:
|
| 260 |
+
print(f"Error: {e}")
|
| 261 |
+
traceback.print_exc()
|
| 262 |
+
index = np.random.randint(len(self))
|
| 263 |
+
return self.__getitem__((index, num_context_views, patchsize_h))
|
| 264 |
+
|
| 265 |
+
def get_bound(
|
| 266 |
+
self,
|
| 267 |
+
bound: Literal["near", "far"],
|
| 268 |
+
num_views: int,
|
| 269 |
+
) -> Float[Tensor, " view"]:
|
| 270 |
+
value = torch.tensor(getattr(self, bound), dtype=torch.float32)
|
| 271 |
+
return repeat(value, "-> v", v=num_views)
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def data_stage(self) -> Stage:
|
| 275 |
+
if self.cfg.overfit_to_scene is not None:
|
| 276 |
+
return "test"
|
| 277 |
+
if self.stage == "val":
|
| 278 |
+
return "test"
|
| 279 |
+
return self.stage
|
| 280 |
+
|
| 281 |
+
@cached_property
|
| 282 |
+
def index(self) -> dict[str, Path]:
|
| 283 |
+
merged_index = {}
|
| 284 |
+
data_stages = [self.data_stage]
|
| 285 |
+
if self.cfg.overfit_to_scene is not None:
|
| 286 |
+
data_stages = ("test", "train")
|
| 287 |
+
for data_stage in data_stages:
|
| 288 |
+
for root in self.cfg.roots:
|
| 289 |
+
# Load the root's index.
|
| 290 |
+
with (root / data_stage / "index.json").open("r") as f:
|
| 291 |
+
index = json.load(f)
|
| 292 |
+
index = {k: Path(root / data_stage / v) for k, v in index.items()}
|
| 293 |
+
|
| 294 |
+
# The constituent datasets should have unique keys.
|
| 295 |
+
assert not (set(merged_index.keys()) & set(index.keys()))
|
| 296 |
+
|
| 297 |
+
# Merge the root's index into the main index.
|
| 298 |
+
merged_index = {**merged_index, **index}
|
| 299 |
+
return merged_index
|
| 300 |
+
|
| 301 |
+
def __len__(self) -> int:
|
| 302 |
+
return len(self.scene_ids)
|
src/dataset/shims/augmentation_shim.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import random
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from jaxtyping import Float
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from ..types import AnyExample, AnyViews
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def reflect_extrinsics(
|
| 12 |
+
extrinsics: Float[Tensor, "*batch 4 4"],
|
| 13 |
+
) -> Float[Tensor, "*batch 4 4"]:
|
| 14 |
+
reflect = torch.eye(4, dtype=torch.float32, device=extrinsics.device)
|
| 15 |
+
reflect[0, 0] = -1
|
| 16 |
+
return reflect @ extrinsics @ reflect
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def reflect_views(views: AnyViews) -> AnyViews:
|
| 20 |
+
if "depth" in views.keys():
|
| 21 |
+
return {
|
| 22 |
+
**views,
|
| 23 |
+
"image": views["image"].flip(-1),
|
| 24 |
+
"extrinsics": reflect_extrinsics(views["extrinsics"]),
|
| 25 |
+
"depth": views["depth"].flip(-1),
|
| 26 |
+
}
|
| 27 |
+
else:
|
| 28 |
+
return {
|
| 29 |
+
**views,
|
| 30 |
+
"image": views["image"].flip(-1),
|
| 31 |
+
"extrinsics": reflect_extrinsics(views["extrinsics"]),
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def apply_augmentation_shim(
|
| 36 |
+
example: AnyExample,
|
| 37 |
+
generator: torch.Generator | None = None,
|
| 38 |
+
) -> AnyExample:
|
| 39 |
+
"""Randomly augment the training images."""
|
| 40 |
+
# Do not augment with 50% chance.
|
| 41 |
+
if torch.rand(tuple(), generator=generator) < 0.5:
|
| 42 |
+
return example
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
**example,
|
| 46 |
+
"context": reflect_views(example["context"]),
|
| 47 |
+
"target": reflect_views(example["target"]),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
def rotate_90_degrees(
|
| 51 |
+
image: torch.Tensor, depth_map: torch.Tensor | None, extri_opencv: torch.Tensor, intri_opencv: torch.Tensor, clockwise=True
|
| 52 |
+
):
|
| 53 |
+
"""
|
| 54 |
+
Rotates the input image, depth map, and camera parameters by 90 degrees.
|
| 55 |
+
|
| 56 |
+
Applies one of two 90-degree rotations:
|
| 57 |
+
- Clockwise
|
| 58 |
+
- Counterclockwise (if clockwise=False)
|
| 59 |
+
|
| 60 |
+
The extrinsic and intrinsic matrices are adjusted accordingly to maintain
|
| 61 |
+
correct camera geometry.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
image (torch.Tensor):
|
| 65 |
+
Input image tensor of shape (C, H, W).
|
| 66 |
+
depth_map (torch.Tensor or None):
|
| 67 |
+
Depth map tensor of shape (H, W), or None if not available.
|
| 68 |
+
extri_opencv (torch.Tensor):
|
| 69 |
+
Extrinsic matrix (3x4) in OpenCV convention.
|
| 70 |
+
intri_opencv (torch.Tensor):
|
| 71 |
+
Intrinsic matrix (3x3).
|
| 72 |
+
clockwise (bool):
|
| 73 |
+
If True, rotates the image 90 degrees clockwise; else 90 degrees counterclockwise.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
tuple:
|
| 77 |
+
(
|
| 78 |
+
rotated_image,
|
| 79 |
+
rotated_depth_map,
|
| 80 |
+
new_extri_opencv,
|
| 81 |
+
new_intri_opencv
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
Where each is the updated version after the rotation.
|
| 85 |
+
"""
|
| 86 |
+
image_height, image_width = image.shape[-2:]
|
| 87 |
+
|
| 88 |
+
# Rotate the image and depth map
|
| 89 |
+
rotated_image, rotated_depth_map = rotate_image_and_depth_rot90(image, depth_map, clockwise)
|
| 90 |
+
# Adjust the intrinsic matrix
|
| 91 |
+
new_intri_opencv = adjust_intrinsic_matrix_rot90(intri_opencv, image_width, image_height, clockwise)
|
| 92 |
+
# Adjust the extrinsic matrix
|
| 93 |
+
new_extri_opencv = adjust_extrinsic_matrix_rot90(extri_opencv, clockwise)
|
| 94 |
+
|
| 95 |
+
return (
|
| 96 |
+
rotated_image,
|
| 97 |
+
rotated_depth_map,
|
| 98 |
+
new_extri_opencv,
|
| 99 |
+
new_intri_opencv,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def rotate_image_and_depth_rot90(image: torch.Tensor, depth_map: torch.Tensor | None, clockwise: bool):
|
| 104 |
+
"""
|
| 105 |
+
Rotates the given image and depth map by 90 degrees (clockwise or counterclockwise).
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
image (torch.Tensor):
|
| 109 |
+
Input image tensor of shape (C, H, W).
|
| 110 |
+
depth_map (torch.Tensor or None):
|
| 111 |
+
Depth map tensor of shape (H, W), or None if not available.
|
| 112 |
+
clockwise (bool):
|
| 113 |
+
If True, rotate 90 degrees clockwise; else 90 degrees counterclockwise.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
tuple:
|
| 117 |
+
(rotated_image, rotated_depth_map)
|
| 118 |
+
"""
|
| 119 |
+
rotated_depth_map = None
|
| 120 |
+
if clockwise:
|
| 121 |
+
rotated_image = torch.rot90(image, k=-1, dims=[-2, -1])
|
| 122 |
+
if depth_map is not None:
|
| 123 |
+
rotated_depth_map = torch.rot90(depth_map, k=-1, dims=[-2, -1])
|
| 124 |
+
else:
|
| 125 |
+
rotated_image = torch.rot90(image, k=1, dims=[-2, -1])
|
| 126 |
+
if depth_map is not None:
|
| 127 |
+
rotated_depth_map = torch.rot90(depth_map, k=1, dims=[-2, -1])
|
| 128 |
+
return rotated_image, rotated_depth_map
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def adjust_extrinsic_matrix_rot90(extri_opencv: torch.Tensor, clockwise: bool):
|
| 132 |
+
"""
|
| 133 |
+
Adjusts the extrinsic matrix (3x4) for a 90-degree rotation of the image.
|
| 134 |
+
|
| 135 |
+
The rotation is in the image plane. This modifies the camera orientation
|
| 136 |
+
accordingly. The function applies either a clockwise or counterclockwise
|
| 137 |
+
90-degree rotation.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
extri_opencv (torch.Tensor):
|
| 141 |
+
Extrinsic matrix (3x4) in OpenCV convention.
|
| 142 |
+
clockwise (bool):
|
| 143 |
+
If True, rotate extrinsic for a 90-degree clockwise image rotation;
|
| 144 |
+
otherwise, counterclockwise.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
torch.Tensor:
|
| 148 |
+
A new 3x4 extrinsic matrix after the rotation.
|
| 149 |
+
"""
|
| 150 |
+
R = extri_opencv[:3, :3]
|
| 151 |
+
t = extri_opencv[:3, 3]
|
| 152 |
+
|
| 153 |
+
if clockwise:
|
| 154 |
+
R_rotation = torch.tensor([
|
| 155 |
+
[0, -1, 0],
|
| 156 |
+
[1, 0, 0],
|
| 157 |
+
[0, 0, 1]
|
| 158 |
+
], dtype=extri_opencv.dtype, device=extri_opencv.device)
|
| 159 |
+
else:
|
| 160 |
+
R_rotation = torch.tensor([
|
| 161 |
+
[0, 1, 0],
|
| 162 |
+
[-1, 0, 0],
|
| 163 |
+
[0, 0, 1]
|
| 164 |
+
], dtype=extri_opencv.dtype, device=extri_opencv.device)
|
| 165 |
+
|
| 166 |
+
new_R = torch.matmul(R_rotation, R)
|
| 167 |
+
new_t = torch.matmul(R_rotation, t)
|
| 168 |
+
new_extri_opencv = torch.cat((new_R, new_t.reshape(-1, 1)), dim=1)
|
| 169 |
+
new_extri_opencv = torch.cat((new_extri_opencv,
|
| 170 |
+
torch.tensor([[0, 0, 0, 1]],
|
| 171 |
+
dtype=extri_opencv.dtype, device=extri_opencv.device)), dim=0)
|
| 172 |
+
return new_extri_opencv
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def adjust_intrinsic_matrix_rot90(intri_opencv: torch.Tensor, image_width: int, image_height: int, clockwise: bool):
|
| 176 |
+
"""
|
| 177 |
+
Adjusts the intrinsic matrix (3x3) for a 90-degree rotation of the image in the image plane.
|
| 178 |
+
|
| 179 |
+
Args:
|
| 180 |
+
intri_opencv (torch.Tensor):
|
| 181 |
+
Intrinsic matrix (3x3).
|
| 182 |
+
image_width (int):
|
| 183 |
+
Original width of the image.
|
| 184 |
+
image_height (int):
|
| 185 |
+
Original height of the image.
|
| 186 |
+
clockwise (bool):
|
| 187 |
+
If True, rotate 90 degrees clockwise; else 90 degrees counterclockwise.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
torch.Tensor:
|
| 191 |
+
A new 3x3 intrinsic matrix after the rotation.
|
| 192 |
+
"""
|
| 193 |
+
intri_opencv = copy.deepcopy(intri_opencv)
|
| 194 |
+
intri_opencv[0, :] *= image_width
|
| 195 |
+
intri_opencv[1, :] *= image_height
|
| 196 |
+
|
| 197 |
+
fx, fy, cx, cy = (
|
| 198 |
+
intri_opencv[0, 0],
|
| 199 |
+
intri_opencv[1, 1],
|
| 200 |
+
intri_opencv[0, 2],
|
| 201 |
+
intri_opencv[1, 2],
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
new_intri_opencv = torch.eye(3, dtype=intri_opencv.dtype, device=intri_opencv.device)
|
| 205 |
+
if clockwise:
|
| 206 |
+
new_intri_opencv[0, 0] = fy
|
| 207 |
+
new_intri_opencv[1, 1] = fx
|
| 208 |
+
new_intri_opencv[0, 2] = image_height - cy
|
| 209 |
+
new_intri_opencv[1, 2] = cx
|
| 210 |
+
else:
|
| 211 |
+
new_intri_opencv[0, 0] = fy
|
| 212 |
+
new_intri_opencv[1, 1] = fx
|
| 213 |
+
new_intri_opencv[0, 2] = cy
|
| 214 |
+
new_intri_opencv[1, 2] = image_width - cx
|
| 215 |
+
|
| 216 |
+
new_intri_opencv[0, :] /= image_height
|
| 217 |
+
new_intri_opencv[1, :] /= image_width
|
| 218 |
+
|
| 219 |
+
return new_intri_opencv
|
src/dataset/shims/bounds_shim.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from einops import einsum, reduce, repeat
|
| 3 |
+
from jaxtyping import Float
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
|
| 6 |
+
from ..types import BatchedExample
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def compute_depth_for_disparity(
|
| 10 |
+
extrinsics: Float[Tensor, "batch view 4 4"],
|
| 11 |
+
intrinsics: Float[Tensor, "batch view 3 3"],
|
| 12 |
+
image_shape: tuple[int, int],
|
| 13 |
+
disparity: float,
|
| 14 |
+
delta_min: float = 1e-6, # This prevents motionless scenes from lacking depth.
|
| 15 |
+
) -> Float[Tensor, " batch"]:
|
| 16 |
+
"""Compute the depth at which moving the maximum distance between cameras
|
| 17 |
+
corresponds to the specified disparity (in pixels).
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
# Use the furthest distance between cameras as the baseline.
|
| 21 |
+
origins = extrinsics[:, :, :3, 3]
|
| 22 |
+
deltas = (origins[:, None, :, :] - origins[:, :, None, :]).norm(dim=-1)
|
| 23 |
+
deltas = deltas.clip(min=delta_min)
|
| 24 |
+
baselines = reduce(deltas, "b v ov -> b", "max")
|
| 25 |
+
|
| 26 |
+
# Compute a single pixel's size at depth 1.
|
| 27 |
+
h, w = image_shape
|
| 28 |
+
pixel_size = 1 / torch.tensor((w, h), dtype=torch.float32, device=extrinsics.device)
|
| 29 |
+
pixel_size = einsum(
|
| 30 |
+
intrinsics[..., :2, :2].inverse(), pixel_size, "... i j, j -> ... i"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# This wouldn't make sense with non-square pixels, but then again, non-square pixels
|
| 34 |
+
# don't make much sense anyway.
|
| 35 |
+
mean_pixel_size = reduce(pixel_size, "b v xy -> b", "mean")
|
| 36 |
+
|
| 37 |
+
return baselines / (disparity * mean_pixel_size)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def apply_bounds_shim(
|
| 41 |
+
batch: BatchedExample,
|
| 42 |
+
near_disparity: float,
|
| 43 |
+
far_disparity: float,
|
| 44 |
+
) -> BatchedExample:
|
| 45 |
+
"""Compute reasonable near and far planes (lower and upper bounds on depth). This
|
| 46 |
+
assumes that all of an example's views are of roughly the same thing.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
context = batch["context"]
|
| 50 |
+
_, cv, _, h, w = context["image"].shape
|
| 51 |
+
|
| 52 |
+
# Compute near and far planes using the context views.
|
| 53 |
+
near = compute_depth_for_disparity(
|
| 54 |
+
context["extrinsics"],
|
| 55 |
+
context["intrinsics"],
|
| 56 |
+
(h, w),
|
| 57 |
+
near_disparity,
|
| 58 |
+
)
|
| 59 |
+
far = compute_depth_for_disparity(
|
| 60 |
+
context["extrinsics"],
|
| 61 |
+
context["intrinsics"],
|
| 62 |
+
(h, w),
|
| 63 |
+
far_disparity,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
target = batch["target"]
|
| 67 |
+
_, tv, _, _, _ = target["image"].shape
|
| 68 |
+
return {
|
| 69 |
+
**batch,
|
| 70 |
+
"context": {
|
| 71 |
+
**context,
|
| 72 |
+
"near": repeat(near, "b -> b v", v=cv),
|
| 73 |
+
"far": repeat(far, "b -> b v", v=cv),
|
| 74 |
+
},
|
| 75 |
+
"target": {
|
| 76 |
+
**target,
|
| 77 |
+
"near": repeat(near, "b -> b v", v=tv),
|
| 78 |
+
"far": repeat(far, "b -> b v", v=tv),
|
| 79 |
+
},
|
| 80 |
+
}
|
src/dataset/shims/crop_shim.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
from einops import rearrange
|
| 5 |
+
from jaxtyping import Float
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
import torchvision.transforms.functional as F
|
| 9 |
+
import cv2
|
| 10 |
+
|
| 11 |
+
from ..types import AnyExample, AnyViews
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def rescale(
|
| 15 |
+
image: Float[Tensor, "3 h_in w_in"],
|
| 16 |
+
shape: tuple[int, int],
|
| 17 |
+
) -> Float[Tensor, "3 h_out w_out"]:
|
| 18 |
+
h, w = shape
|
| 19 |
+
image_new = (image * 255).clip(min=0, max=255).type(torch.uint8)
|
| 20 |
+
image_new = rearrange(image_new, "c h w -> h w c").detach().cpu().numpy()
|
| 21 |
+
image_new = Image.fromarray(image_new)
|
| 22 |
+
image_new = image_new.resize((w, h), Image.LANCZOS)
|
| 23 |
+
image_new = np.array(image_new) / 255
|
| 24 |
+
image_new = torch.tensor(image_new, dtype=image.dtype, device=image.device)
|
| 25 |
+
return rearrange(image_new, "h w c -> c h w")
|
| 26 |
+
|
| 27 |
+
def rescale_depth(
|
| 28 |
+
depth: Float[Tensor, "1 h w"],
|
| 29 |
+
shape: tuple[int, int],
|
| 30 |
+
) -> Float[Tensor, "1 h_out w_out"]:
|
| 31 |
+
h, w = shape
|
| 32 |
+
depth_new = depth.detach().cpu().numpy()
|
| 33 |
+
depth_new = cv2.resize(depth_new, (w,h), interpolation=cv2.INTER_NEAREST)
|
| 34 |
+
depth_new = torch.from_numpy(depth_new).to(depth.device)
|
| 35 |
+
return depth_new
|
| 36 |
+
|
| 37 |
+
def center_crop(
|
| 38 |
+
images: Float[Tensor, "*#batch c h w"],
|
| 39 |
+
intrinsics: Float[Tensor, "*#batch 3 3"],
|
| 40 |
+
shape: tuple[int, int],
|
| 41 |
+
depths: Float[Tensor, "*#batch 1 h w"] | None = None,
|
| 42 |
+
) -> tuple[
|
| 43 |
+
Float[Tensor, "*#batch c h_out w_out"], # updated images
|
| 44 |
+
Float[Tensor, "*#batch 3 3"], # updated intrinsics
|
| 45 |
+
Float[Tensor, "*#batch 1 h_out w_out"] | None, # updated depths
|
| 46 |
+
]:
|
| 47 |
+
*_, h_in, w_in = images.shape
|
| 48 |
+
h_out, w_out = shape
|
| 49 |
+
|
| 50 |
+
# Note that odd input dimensions induce half-pixel misalignments.
|
| 51 |
+
row = (h_in - h_out) // 2
|
| 52 |
+
col = (w_in - w_out) // 2
|
| 53 |
+
|
| 54 |
+
# Center-crop the image.
|
| 55 |
+
images = images[..., :, row : row + h_out, col : col + w_out]
|
| 56 |
+
|
| 57 |
+
if depths is not None:
|
| 58 |
+
depths = depths[..., row : row + h_out, col : col + w_out]
|
| 59 |
+
|
| 60 |
+
# Adjust the intrinsics to account for the cropping.
|
| 61 |
+
intrinsics = intrinsics.clone()
|
| 62 |
+
intrinsics[..., 0, 0] *= w_in / w_out # fx
|
| 63 |
+
intrinsics[..., 1, 1] *= h_in / h_out # fy
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if depths is not None:
|
| 67 |
+
return images, intrinsics, depths
|
| 68 |
+
else:
|
| 69 |
+
return images, intrinsics
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def rescale_and_crop(
|
| 73 |
+
images: Float[Tensor, "*#batch c h w"],
|
| 74 |
+
intrinsics: Float[Tensor, "*#batch 3 3"],
|
| 75 |
+
shape: tuple[int, int],
|
| 76 |
+
intr_aug: bool = False,
|
| 77 |
+
scale_range: tuple[float, float] = (0.77, 1.0),
|
| 78 |
+
depths: Float[Tensor, "*#batch 1 h w"] | None = None,
|
| 79 |
+
) -> tuple[
|
| 80 |
+
Float[Tensor, "*#batch c h_out w_out"], # updated images
|
| 81 |
+
Float[Tensor, "*#batch 3 3"], # updated intrinsics
|
| 82 |
+
Float[Tensor, "*#batch 1 h_out w_out"] | None, # updated depths
|
| 83 |
+
]:
|
| 84 |
+
if type(images) == list:
|
| 85 |
+
images_new = []
|
| 86 |
+
intrinsics_new = []
|
| 87 |
+
for i in range(len(images)):
|
| 88 |
+
image = images[i]
|
| 89 |
+
intrinsic = intrinsics[i]
|
| 90 |
+
|
| 91 |
+
*_, h_in, w_in = image.shape
|
| 92 |
+
h_out, w_out = shape
|
| 93 |
+
|
| 94 |
+
scale_factor = max(h_out / h_in, w_out / w_in)
|
| 95 |
+
h_scaled = round(h_in * scale_factor)
|
| 96 |
+
w_scaled = round(w_in * scale_factor)
|
| 97 |
+
image = F.resize(image, (h_scaled, w_scaled))
|
| 98 |
+
image = F.center_crop(image, (h_out, w_out))
|
| 99 |
+
images_new.append(image)
|
| 100 |
+
|
| 101 |
+
intrinsic_new = intrinsic.clone()
|
| 102 |
+
intrinsic_new[..., 0, 0] *= w_scaled / w_in # fx
|
| 103 |
+
intrinsic_new[..., 1, 1] *= h_scaled / h_in # fy
|
| 104 |
+
intrinsics_new.append(intrinsic_new)
|
| 105 |
+
|
| 106 |
+
if depths is not None:
|
| 107 |
+
depths_new = []
|
| 108 |
+
for i in range(len(depths)):
|
| 109 |
+
depth = depths[i]
|
| 110 |
+
depth = rescale_depth(depth, (h_out, w_out))
|
| 111 |
+
depth = F.center_crop(depth, (h_out, w_out))
|
| 112 |
+
depths_new.append(depth)
|
| 113 |
+
return torch.stack(images_new), torch.stack(intrinsics_new), torch.stack(depths_new)
|
| 114 |
+
else:
|
| 115 |
+
return torch.stack(images_new), torch.stack(intrinsics_new)
|
| 116 |
+
|
| 117 |
+
else:
|
| 118 |
+
# we only support intr_aug for clean datasets
|
| 119 |
+
*_, h_in, w_in = images.shape
|
| 120 |
+
h_out, w_out = shape
|
| 121 |
+
# assert h_out <= h_in and w_out <= w_in # to avoid the case that the image is too small, like co3d
|
| 122 |
+
|
| 123 |
+
if intr_aug:
|
| 124 |
+
scale = random.uniform(*scale_range)
|
| 125 |
+
h_scale = round(h_out * scale)
|
| 126 |
+
w_scale = round(w_out * scale)
|
| 127 |
+
else:
|
| 128 |
+
h_scale = h_out
|
| 129 |
+
w_scale = w_out
|
| 130 |
+
|
| 131 |
+
scale_factor = max(h_scale / h_in, w_scale / w_in)
|
| 132 |
+
h_scaled = round(h_in * scale_factor)
|
| 133 |
+
w_scaled = round(w_in * scale_factor)
|
| 134 |
+
assert h_scaled == h_scale or w_scaled == w_scale
|
| 135 |
+
|
| 136 |
+
# Reshape the images to the correct size. Assume we don't have to worry about
|
| 137 |
+
# changing the intrinsics based on how the images are rounded.
|
| 138 |
+
*batch, c, h, w = images.shape
|
| 139 |
+
images = images.reshape(-1, c, h, w)
|
| 140 |
+
images = torch.stack([rescale(image, (h_scaled, w_scaled)) for image in images])
|
| 141 |
+
images = images.reshape(*batch, c, h_scaled, w_scaled)
|
| 142 |
+
|
| 143 |
+
if depths is not None:
|
| 144 |
+
if type(depths) == list:
|
| 145 |
+
depths_new = []
|
| 146 |
+
for i in range(len(depths)):
|
| 147 |
+
depth = depths[i]
|
| 148 |
+
depth = rescale_depth(depth, (h_scaled, w_scaled))
|
| 149 |
+
depths_new.append(depth)
|
| 150 |
+
depths = torch.stack(depths_new)
|
| 151 |
+
else:
|
| 152 |
+
depths = depths.reshape(-1, h, w)
|
| 153 |
+
depths = torch.stack([rescale_depth(depth, (h_scaled, w_scaled)) for depth in depths])
|
| 154 |
+
depths = depths.reshape(*batch, h_scaled, w_scaled)
|
| 155 |
+
|
| 156 |
+
images, intrinsics, depths = center_crop(images, intrinsics, (h_scale, w_scale), depths)
|
| 157 |
+
|
| 158 |
+
if intr_aug:
|
| 159 |
+
images = F.resize(images, size=(h_out, w_out), interpolation=F.InterpolationMode.BILINEAR)
|
| 160 |
+
depths = F.resize(depths, size=(h_out, w_out), interpolation=F.InterpolationMode.NEAREST)
|
| 161 |
+
|
| 162 |
+
return images, intrinsics, depths
|
| 163 |
+
else:
|
| 164 |
+
images, intrinsics = center_crop(images, intrinsics, (h_scale, w_scale))
|
| 165 |
+
|
| 166 |
+
if intr_aug:
|
| 167 |
+
images = F.resize(images, size=(h_out, w_out))
|
| 168 |
+
|
| 169 |
+
return images, intrinsics
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def apply_crop_shim_to_views(views: AnyViews, shape: tuple[int, int], intr_aug: bool = False) -> AnyViews:
|
| 173 |
+
if "depth" in views.keys():
|
| 174 |
+
images, intrinsics, depths = rescale_and_crop(views["image"], views["intrinsics"], shape, depths=views["depth"], intr_aug=intr_aug)
|
| 175 |
+
return {
|
| 176 |
+
**views,
|
| 177 |
+
"image": images,
|
| 178 |
+
"intrinsics": intrinsics,
|
| 179 |
+
"depth": depths,
|
| 180 |
+
}
|
| 181 |
+
else:
|
| 182 |
+
images, intrinsics = rescale_and_crop(views["image"], views["intrinsics"], shape, intr_aug)
|
| 183 |
+
return {
|
| 184 |
+
**views,
|
| 185 |
+
"image": images,
|
| 186 |
+
"intrinsics": intrinsics,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def apply_crop_shim(example: AnyExample, shape: tuple[int, int], intr_aug: bool = False) -> AnyExample:
|
| 191 |
+
"""Crop images in the example."""
|
| 192 |
+
return {
|
| 193 |
+
**example,
|
| 194 |
+
"context": apply_crop_shim_to_views(example["context"], shape, intr_aug),
|
| 195 |
+
"target": apply_crop_shim_to_views(example["target"], shape, intr_aug),
|
| 196 |
+
}
|
src/dataset/shims/geometry_shim.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2024-present Naver Corporation. All rights reserved.
|
| 2 |
+
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
|
| 3 |
+
#
|
| 4 |
+
# --------------------------------------------------------
|
| 5 |
+
# geometry utilitary functions
|
| 6 |
+
# --------------------------------------------------------
|
| 7 |
+
import torch
|
| 8 |
+
import numpy as np
|
| 9 |
+
from scipy.spatial import cKDTree as KDTree
|
| 10 |
+
|
| 11 |
+
def invalid_to_nans(arr, valid_mask, ndim=999):
|
| 12 |
+
if valid_mask is not None:
|
| 13 |
+
arr = arr.clone()
|
| 14 |
+
arr[~valid_mask] = float('nan')
|
| 15 |
+
if arr.ndim > ndim:
|
| 16 |
+
arr = arr.flatten(-2 - (arr.ndim - ndim), -2)
|
| 17 |
+
return arr
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def invalid_to_zeros(arr, valid_mask, ndim=999):
|
| 21 |
+
if valid_mask is not None:
|
| 22 |
+
arr = arr.clone()
|
| 23 |
+
arr[~valid_mask] = 0
|
| 24 |
+
nnz = valid_mask.view(len(valid_mask), -1).sum(1)
|
| 25 |
+
else:
|
| 26 |
+
nnz = arr.numel() // len(arr) if len(arr) else 0 # number of point per image
|
| 27 |
+
if arr.ndim > ndim:
|
| 28 |
+
arr = arr.flatten(-2 - (arr.ndim - ndim), -2)
|
| 29 |
+
return arr, nnz
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def xy_grid(W, H, device=None, origin=(0, 0), unsqueeze=None, cat_dim=-1, homogeneous=False, **arange_kw):
|
| 33 |
+
""" Output a (H,W,2) array of int32
|
| 34 |
+
with output[j,i,0] = i + origin[0]
|
| 35 |
+
output[j,i,1] = j + origin[1]
|
| 36 |
+
"""
|
| 37 |
+
if device is None:
|
| 38 |
+
# numpy
|
| 39 |
+
arange, meshgrid, stack, ones = np.arange, np.meshgrid, np.stack, np.ones
|
| 40 |
+
else:
|
| 41 |
+
# torch
|
| 42 |
+
arange = lambda *a, **kw: torch.arange(*a, device=device, **kw)
|
| 43 |
+
meshgrid, stack = torch.meshgrid, torch.stack
|
| 44 |
+
ones = lambda *a: torch.ones(*a, device=device)
|
| 45 |
+
|
| 46 |
+
tw, th = [arange(o, o + s, **arange_kw) for s, o in zip((W, H), origin)]
|
| 47 |
+
grid = meshgrid(tw, th, indexing='xy')
|
| 48 |
+
if homogeneous:
|
| 49 |
+
grid = grid + (ones((H, W)),)
|
| 50 |
+
if unsqueeze is not None:
|
| 51 |
+
grid = (grid[0].unsqueeze(unsqueeze), grid[1].unsqueeze(unsqueeze))
|
| 52 |
+
if cat_dim is not None:
|
| 53 |
+
grid = stack(grid, cat_dim)
|
| 54 |
+
return grid
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def geotrf(Trf, pts, ncol=None, norm=False):
|
| 58 |
+
""" Apply a geometric transformation to a list of 3-D points.
|
| 59 |
+
|
| 60 |
+
H: 3x3 or 4x4 projection matrix (typically a Homography)
|
| 61 |
+
p: numpy/torch/tuple of coordinates. Shape must be (...,2) or (...,3)
|
| 62 |
+
|
| 63 |
+
ncol: int. number of columns of the result (2 or 3)
|
| 64 |
+
norm: float. if != 0, the resut is projected on the z=norm plane.
|
| 65 |
+
|
| 66 |
+
Returns an array of projected 2d points.
|
| 67 |
+
"""
|
| 68 |
+
assert Trf.ndim >= 2
|
| 69 |
+
if isinstance(Trf, np.ndarray):
|
| 70 |
+
pts = np.asarray(pts)
|
| 71 |
+
elif isinstance(Trf, torch.Tensor):
|
| 72 |
+
pts = torch.as_tensor(pts, dtype=Trf.dtype)
|
| 73 |
+
|
| 74 |
+
# adapt shape if necessary
|
| 75 |
+
output_reshape = pts.shape[:-1]
|
| 76 |
+
ncol = ncol or pts.shape[-1]
|
| 77 |
+
|
| 78 |
+
# optimized code
|
| 79 |
+
if (isinstance(Trf, torch.Tensor) and isinstance(pts, torch.Tensor) and
|
| 80 |
+
Trf.ndim == 3 and pts.ndim == 4):
|
| 81 |
+
d = pts.shape[3]
|
| 82 |
+
if Trf.shape[-1] == d:
|
| 83 |
+
pts = torch.einsum("bij, bhwj -> bhwi", Trf, pts)
|
| 84 |
+
elif Trf.shape[-1] == d + 1:
|
| 85 |
+
pts = torch.einsum("bij, bhwj -> bhwi", Trf[:, :d, :d], pts) + Trf[:, None, None, :d, d]
|
| 86 |
+
else:
|
| 87 |
+
raise ValueError(f'bad shape, not ending with 3 or 4, for {pts.shape=}')
|
| 88 |
+
else:
|
| 89 |
+
if Trf.ndim >= 3:
|
| 90 |
+
n = Trf.ndim - 2
|
| 91 |
+
assert Trf.shape[:n] == pts.shape[:n], 'batch size does not match'
|
| 92 |
+
Trf = Trf.reshape(-1, Trf.shape[-2], Trf.shape[-1])
|
| 93 |
+
|
| 94 |
+
if pts.ndim > Trf.ndim:
|
| 95 |
+
# Trf == (B,d,d) & pts == (B,H,W,d) --> (B, H*W, d)
|
| 96 |
+
pts = pts.reshape(Trf.shape[0], -1, pts.shape[-1])
|
| 97 |
+
elif pts.ndim == 2:
|
| 98 |
+
# Trf == (B,d,d) & pts == (B,d) --> (B, 1, d)
|
| 99 |
+
pts = pts[:, None, :]
|
| 100 |
+
|
| 101 |
+
if pts.shape[-1] + 1 == Trf.shape[-1]:
|
| 102 |
+
Trf = Trf.swapaxes(-1, -2) # transpose Trf
|
| 103 |
+
pts = pts @ Trf[..., :-1, :] + Trf[..., -1:, :]
|
| 104 |
+
elif pts.shape[-1] == Trf.shape[-1]:
|
| 105 |
+
Trf = Trf.swapaxes(-1, -2) # transpose Trf
|
| 106 |
+
pts = pts @ Trf
|
| 107 |
+
else:
|
| 108 |
+
pts = Trf @ pts.T
|
| 109 |
+
if pts.ndim >= 2:
|
| 110 |
+
pts = pts.swapaxes(-1, -2)
|
| 111 |
+
|
| 112 |
+
if norm:
|
| 113 |
+
pts = pts / pts[..., -1:] # DONT DO /= BECAUSE OF WEIRD PYTORCH BUG
|
| 114 |
+
if norm != 1:
|
| 115 |
+
pts *= norm
|
| 116 |
+
|
| 117 |
+
res = pts[..., :ncol].reshape(*output_reshape, ncol)
|
| 118 |
+
return res
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def inv(mat):
|
| 122 |
+
""" Invert a torch or numpy matrix
|
| 123 |
+
"""
|
| 124 |
+
if isinstance(mat, torch.Tensor):
|
| 125 |
+
return torch.linalg.inv(mat)
|
| 126 |
+
if isinstance(mat, np.ndarray):
|
| 127 |
+
return np.linalg.inv(mat)
|
| 128 |
+
raise ValueError(f'bad matrix type = {type(mat)}')
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def depthmap_to_pts3d(depth, pseudo_focal, pp=None, **_):
|
| 132 |
+
"""
|
| 133 |
+
Args:
|
| 134 |
+
- depthmap (BxHxW array):
|
| 135 |
+
- pseudo_focal: [B,H,W] ; [B,2,H,W] or [B,1,H,W]
|
| 136 |
+
Returns:
|
| 137 |
+
pointmap of absolute coordinates (BxHxWx3 array)
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
if len(depth.shape) == 4:
|
| 141 |
+
B, H, W, n = depth.shape
|
| 142 |
+
else:
|
| 143 |
+
B, H, W = depth.shape
|
| 144 |
+
n = None
|
| 145 |
+
|
| 146 |
+
if len(pseudo_focal.shape) == 3: # [B,H,W]
|
| 147 |
+
pseudo_focalx = pseudo_focaly = pseudo_focal
|
| 148 |
+
elif len(pseudo_focal.shape) == 4: # [B,2,H,W] or [B,1,H,W]
|
| 149 |
+
pseudo_focalx = pseudo_focal[:, 0]
|
| 150 |
+
if pseudo_focal.shape[1] == 2:
|
| 151 |
+
pseudo_focaly = pseudo_focal[:, 1]
|
| 152 |
+
else:
|
| 153 |
+
pseudo_focaly = pseudo_focalx
|
| 154 |
+
else:
|
| 155 |
+
raise NotImplementedError("Error, unknown input focal shape format.")
|
| 156 |
+
|
| 157 |
+
assert pseudo_focalx.shape == depth.shape[:3]
|
| 158 |
+
assert pseudo_focaly.shape == depth.shape[:3]
|
| 159 |
+
grid_x, grid_y = xy_grid(W, H, cat_dim=0, device=depth.device)[:, None]
|
| 160 |
+
|
| 161 |
+
# set principal point
|
| 162 |
+
if pp is None:
|
| 163 |
+
grid_x = grid_x - (W - 1) / 2
|
| 164 |
+
grid_y = grid_y - (H - 1) / 2
|
| 165 |
+
else:
|
| 166 |
+
grid_x = grid_x.expand(B, -1, -1) - pp[:, 0, None, None]
|
| 167 |
+
grid_y = grid_y.expand(B, -1, -1) - pp[:, 1, None, None]
|
| 168 |
+
|
| 169 |
+
if n is None:
|
| 170 |
+
pts3d = torch.empty((B, H, W, 3), device=depth.device)
|
| 171 |
+
pts3d[..., 0] = depth * grid_x / pseudo_focalx
|
| 172 |
+
pts3d[..., 1] = depth * grid_y / pseudo_focaly
|
| 173 |
+
pts3d[..., 2] = depth
|
| 174 |
+
else:
|
| 175 |
+
pts3d = torch.empty((B, H, W, 3, n), device=depth.device)
|
| 176 |
+
pts3d[..., 0, :] = depth * (grid_x / pseudo_focalx)[..., None]
|
| 177 |
+
pts3d[..., 1, :] = depth * (grid_y / pseudo_focaly)[..., None]
|
| 178 |
+
pts3d[..., 2, :] = depth
|
| 179 |
+
return pts3d
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def depthmap_to_camera_coordinates(depthmap, camera_intrinsics, pseudo_focal=None):
|
| 183 |
+
"""
|
| 184 |
+
Args:
|
| 185 |
+
- depthmap (HxW array):
|
| 186 |
+
- camera_intrinsics: a 3x3 matrix
|
| 187 |
+
Returns:
|
| 188 |
+
pointmap of absolute coordinates (HxWx3 array), and a mask specifying valid pixels.
|
| 189 |
+
"""
|
| 190 |
+
camera_intrinsics = np.float32(camera_intrinsics)
|
| 191 |
+
H, W = depthmap.shape
|
| 192 |
+
|
| 193 |
+
# Compute 3D ray associated with each pixel
|
| 194 |
+
# Strong assumption: there are no skew terms
|
| 195 |
+
assert camera_intrinsics[0, 1] == 0.0
|
| 196 |
+
assert camera_intrinsics[1, 0] == 0.0
|
| 197 |
+
if pseudo_focal is None:
|
| 198 |
+
fu = camera_intrinsics[0, 0]
|
| 199 |
+
fv = camera_intrinsics[1, 1]
|
| 200 |
+
else:
|
| 201 |
+
assert pseudo_focal.shape == (H, W)
|
| 202 |
+
fu = fv = pseudo_focal
|
| 203 |
+
cu = camera_intrinsics[0, 2]
|
| 204 |
+
cv = camera_intrinsics[1, 2]
|
| 205 |
+
|
| 206 |
+
u, v = np.meshgrid(np.arange(W), np.arange(H))
|
| 207 |
+
z_cam = depthmap
|
| 208 |
+
x_cam = (u - cu) * z_cam / fu
|
| 209 |
+
y_cam = (v - cv) * z_cam / fv
|
| 210 |
+
X_cam = np.stack((x_cam, y_cam, z_cam), axis=-1).astype(np.float32)
|
| 211 |
+
|
| 212 |
+
# Mask for valid coordinates
|
| 213 |
+
valid_mask = (depthmap > 0.0)
|
| 214 |
+
return X_cam, valid_mask
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def depthmap_to_absolute_camera_coordinates(depthmap, camera_intrinsics, camera_pose=None, **kw):
|
| 218 |
+
"""
|
| 219 |
+
Args:
|
| 220 |
+
- depthmap (HxW array):
|
| 221 |
+
- camera_intrinsics: a 3x3 matrix
|
| 222 |
+
- camera_pose: a 4x3 or 4x4 cam2world matrix
|
| 223 |
+
Returns:
|
| 224 |
+
pointmap of absolute coordinates (HxWx3 array), and a mask specifying valid pixels."""
|
| 225 |
+
X_cam, valid_mask = depthmap_to_camera_coordinates(depthmap, camera_intrinsics)
|
| 226 |
+
|
| 227 |
+
X_world = X_cam # default
|
| 228 |
+
if camera_pose is not None:
|
| 229 |
+
# R_cam2world = np.float32(camera_params["R_cam2world"])
|
| 230 |
+
# t_cam2world = np.float32(camera_params["t_cam2world"]).squeeze()
|
| 231 |
+
R_cam2world = camera_pose[:3, :3]
|
| 232 |
+
t_cam2world = camera_pose[:3, 3]
|
| 233 |
+
|
| 234 |
+
# Express in absolute coordinates (invalid depth values)
|
| 235 |
+
X_world = np.einsum("ik, vuk -> vui", R_cam2world, X_cam) + t_cam2world[None, None, :]
|
| 236 |
+
|
| 237 |
+
return X_world, valid_mask
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def colmap_to_opencv_intrinsics(K):
|
| 241 |
+
"""
|
| 242 |
+
Modify camera intrinsics to follow a different convention.
|
| 243 |
+
Coordinates of the center of the top-left pixels are by default:
|
| 244 |
+
- (0.5, 0.5) in Colmap
|
| 245 |
+
- (0,0) in OpenCV
|
| 246 |
+
"""
|
| 247 |
+
K = K.copy()
|
| 248 |
+
K[0, 2] -= 0.5
|
| 249 |
+
K[1, 2] -= 0.5
|
| 250 |
+
return K
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def opencv_to_colmap_intrinsics(K):
|
| 254 |
+
"""
|
| 255 |
+
Modify camera intrinsics to follow a different convention.
|
| 256 |
+
Coordinates of the center of the top-left pixels are by default:
|
| 257 |
+
- (0.5, 0.5) in Colmap
|
| 258 |
+
- (0,0) in OpenCV
|
| 259 |
+
"""
|
| 260 |
+
K = K.copy()
|
| 261 |
+
K[0, 2] += 0.5
|
| 262 |
+
K[1, 2] += 0.5
|
| 263 |
+
return K
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def normalize_pointcloud(pts1, pts2, norm_mode='avg_dis', valid1=None, valid2=None, ret_factor=False):
|
| 267 |
+
""" renorm pointmaps pts1, pts2 with norm_mode
|
| 268 |
+
"""
|
| 269 |
+
assert pts1.ndim >= 3 and pts1.shape[-1] == 3
|
| 270 |
+
assert pts2 is None or (pts2.ndim >= 3 and pts2.shape[-1] == 3)
|
| 271 |
+
norm_mode, dis_mode = norm_mode.split('_')
|
| 272 |
+
|
| 273 |
+
if norm_mode == 'avg':
|
| 274 |
+
# gather all points together (joint normalization)
|
| 275 |
+
nan_pts1, nnz1 = invalid_to_zeros(pts1, valid1, ndim=3)
|
| 276 |
+
nan_pts2, nnz2 = invalid_to_zeros(pts2, valid2, ndim=3) if pts2 is not None else (None, 0)
|
| 277 |
+
all_pts = torch.cat((nan_pts1, nan_pts2), dim=1) if pts2 is not None else nan_pts1
|
| 278 |
+
|
| 279 |
+
# compute distance to origin
|
| 280 |
+
all_dis = all_pts.norm(dim=-1)
|
| 281 |
+
if dis_mode == 'dis':
|
| 282 |
+
pass # do nothing
|
| 283 |
+
elif dis_mode == 'log1p':
|
| 284 |
+
all_dis = torch.log1p(all_dis)
|
| 285 |
+
elif dis_mode == 'warp-log1p':
|
| 286 |
+
# actually warp input points before normalizing them
|
| 287 |
+
log_dis = torch.log1p(all_dis)
|
| 288 |
+
warp_factor = log_dis / all_dis.clip(min=1e-8)
|
| 289 |
+
H1, W1 = pts1.shape[1:-1]
|
| 290 |
+
pts1 = pts1 * warp_factor[:, :W1 * H1].view(-1, H1, W1, 1)
|
| 291 |
+
if pts2 is not None:
|
| 292 |
+
H2, W2 = pts2.shape[1:-1]
|
| 293 |
+
pts2 = pts2 * warp_factor[:, W1 * H1:].view(-1, H2, W2, 1)
|
| 294 |
+
all_dis = log_dis # this is their true distance afterwards
|
| 295 |
+
else:
|
| 296 |
+
raise ValueError(f'bad {dis_mode=}')
|
| 297 |
+
|
| 298 |
+
norm_factor = all_dis.sum(dim=1) / (nnz1 + nnz2 + 1e-8)
|
| 299 |
+
else:
|
| 300 |
+
# gather all points together (joint normalization)
|
| 301 |
+
nan_pts1 = invalid_to_nans(pts1, valid1, ndim=3)
|
| 302 |
+
nan_pts2 = invalid_to_nans(pts2, valid2, ndim=3) if pts2 is not None else None
|
| 303 |
+
all_pts = torch.cat((nan_pts1, nan_pts2), dim=1) if pts2 is not None else nan_pts1
|
| 304 |
+
|
| 305 |
+
# compute distance to origin
|
| 306 |
+
all_dis = all_pts.norm(dim=-1)
|
| 307 |
+
|
| 308 |
+
if norm_mode == 'avg':
|
| 309 |
+
norm_factor = all_dis.nanmean(dim=1)
|
| 310 |
+
elif norm_mode == 'median':
|
| 311 |
+
norm_factor = all_dis.nanmedian(dim=1).values.detach()
|
| 312 |
+
elif norm_mode == 'sqrt':
|
| 313 |
+
norm_factor = all_dis.sqrt().nanmean(dim=1)**2
|
| 314 |
+
else:
|
| 315 |
+
raise ValueError(f'bad {norm_mode=}')
|
| 316 |
+
|
| 317 |
+
norm_factor = norm_factor.clip(min=1e-8)
|
| 318 |
+
while norm_factor.ndim < pts1.ndim:
|
| 319 |
+
norm_factor.unsqueeze_(-1)
|
| 320 |
+
|
| 321 |
+
res = pts1 / norm_factor
|
| 322 |
+
if pts2 is not None:
|
| 323 |
+
res = (res, pts2 / norm_factor)
|
| 324 |
+
if ret_factor:
|
| 325 |
+
res = res + (norm_factor,)
|
| 326 |
+
return res
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
@torch.no_grad()
|
| 330 |
+
def get_joint_pointcloud_depth(z1, z2, valid_mask1, valid_mask2=None, quantile=0.5):
|
| 331 |
+
# set invalid points to NaN
|
| 332 |
+
_z1 = invalid_to_nans(z1, valid_mask1).reshape(len(z1), -1)
|
| 333 |
+
_z2 = invalid_to_nans(z2, valid_mask2).reshape(len(z2), -1) if z2 is not None else None
|
| 334 |
+
_z = torch.cat((_z1, _z2), dim=-1) if z2 is not None else _z1
|
| 335 |
+
|
| 336 |
+
# compute median depth overall (ignoring nans)
|
| 337 |
+
if quantile == 0.5:
|
| 338 |
+
shift_z = torch.nanmedian(_z, dim=-1).values
|
| 339 |
+
else:
|
| 340 |
+
shift_z = torch.nanquantile(_z, quantile, dim=-1)
|
| 341 |
+
return shift_z # (B,)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
@torch.no_grad()
|
| 345 |
+
def get_joint_pointcloud_center_scale(pts1, pts2, valid_mask1=None, valid_mask2=None, z_only=False, center=True):
|
| 346 |
+
# set invalid points to NaN
|
| 347 |
+
_pts1 = invalid_to_nans(pts1, valid_mask1).reshape(len(pts1), -1, 3)
|
| 348 |
+
_pts2 = invalid_to_nans(pts2, valid_mask2).reshape(len(pts2), -1, 3) if pts2 is not None else None
|
| 349 |
+
_pts = torch.cat((_pts1, _pts2), dim=1) if pts2 is not None else _pts1
|
| 350 |
+
|
| 351 |
+
# compute median center
|
| 352 |
+
_center = torch.nanmedian(_pts, dim=1, keepdim=True).values # (B,1,3)
|
| 353 |
+
if z_only:
|
| 354 |
+
_center[..., :2] = 0 # do not center X and Y
|
| 355 |
+
|
| 356 |
+
# compute median norm
|
| 357 |
+
_norm = ((_pts - _center) if center else _pts).norm(dim=-1)
|
| 358 |
+
scale = torch.nanmedian(_norm, dim=1).values
|
| 359 |
+
return _center[:, None, :, :], scale[:, None, None, None]
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def find_reciprocal_matches(P1, P2):
|
| 363 |
+
"""
|
| 364 |
+
returns 3 values:
|
| 365 |
+
1 - reciprocal_in_P2: a boolean array of size P2.shape[0], a "True" value indicates a match
|
| 366 |
+
2 - nn2_in_P1: a int array of size P2.shape[0], it contains the indexes of the closest points in P1
|
| 367 |
+
3 - reciprocal_in_P2.sum(): the number of matches
|
| 368 |
+
"""
|
| 369 |
+
tree1 = KDTree(P1)
|
| 370 |
+
tree2 = KDTree(P2)
|
| 371 |
+
|
| 372 |
+
_, nn1_in_P2 = tree2.query(P1, workers=8)
|
| 373 |
+
_, nn2_in_P1 = tree1.query(P2, workers=8)
|
| 374 |
+
|
| 375 |
+
reciprocal_in_P1 = (nn2_in_P1[nn1_in_P2] == np.arange(len(nn1_in_P2)))
|
| 376 |
+
reciprocal_in_P2 = (nn1_in_P2[nn2_in_P1] == np.arange(len(nn2_in_P1)))
|
| 377 |
+
assert reciprocal_in_P1.sum() == reciprocal_in_P2.sum()
|
| 378 |
+
return reciprocal_in_P2, nn2_in_P1, reciprocal_in_P2.sum()
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def get_med_dist_between_poses(poses):
|
| 382 |
+
from scipy.spatial.distance import pdist
|
| 383 |
+
return np.median(pdist([p[:3, 3].detach().cpu().numpy() for p in poses]))
|
src/dataset/shims/load_shim.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
|
| 3 |
+
def imread_cv2(path, options=cv2.IMREAD_COLOR):
|
| 4 |
+
"""Open an image or a depthmap with opencv-python."""
|
| 5 |
+
if path.endswith((".exr", "EXR")):
|
| 6 |
+
options = cv2.IMREAD_ANYDEPTH
|
| 7 |
+
img = cv2.imread(path, options)
|
| 8 |
+
if img is None:
|
| 9 |
+
raise IOError(f"Could not load image={path} with {options=}")
|
| 10 |
+
if img.ndim == 3:
|
| 11 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 12 |
+
return img
|
src/dataset/shims/normalize_shim.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from einops import einsum, reduce, repeat
|
| 3 |
+
from jaxtyping import Float
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
|
| 6 |
+
from ..types import BatchedExample
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def inverse_normalize_image(tensor, mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)):
|
| 10 |
+
mean = torch.as_tensor(mean, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)
|
| 11 |
+
std = torch.as_tensor(std, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)
|
| 12 |
+
return tensor * std + mean
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def normalize_image(tensor, mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)):
|
| 16 |
+
mean = torch.as_tensor(mean, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)
|
| 17 |
+
std = torch.as_tensor(std, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)
|
| 18 |
+
return (tensor - mean) / std
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def apply_normalize_shim(
|
| 22 |
+
batch: BatchedExample,
|
| 23 |
+
mean: tuple[float, float, float] = (0.5, 0.5, 0.5),
|
| 24 |
+
std: tuple[float, float, float] = (0.5, 0.5, 0.5),
|
| 25 |
+
) -> BatchedExample:
|
| 26 |
+
batch["context"]["image"] = normalize_image(batch["context"]["image"], mean, std)
|
| 27 |
+
return batch
|
src/dataset/shims/patch_shim.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..types import BatchedExample, BatchedViews
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def apply_patch_shim_to_views(views: BatchedViews, patch_size: int) -> BatchedViews:
|
| 5 |
+
_, _, _, h, w = views["image"].shape
|
| 6 |
+
|
| 7 |
+
# Image size must be even so that naive center-cropping does not cause misalignment.
|
| 8 |
+
assert h % 2 == 0 and w % 2 == 0
|
| 9 |
+
|
| 10 |
+
h_new = (h // patch_size) * patch_size
|
| 11 |
+
row = (h - h_new) // 2
|
| 12 |
+
w_new = (w // patch_size) * patch_size
|
| 13 |
+
col = (w - w_new) // 2
|
| 14 |
+
|
| 15 |
+
# Center-crop the image.
|
| 16 |
+
image = views["image"][:, :, :, row : row + h_new, col : col + w_new]
|
| 17 |
+
|
| 18 |
+
# Adjust the intrinsics to account for the cropping.
|
| 19 |
+
intrinsics = views["intrinsics"].clone()
|
| 20 |
+
intrinsics[:, :, 0, 0] *= w / w_new # fx
|
| 21 |
+
intrinsics[:, :, 1, 1] *= h / h_new # fy
|
| 22 |
+
|
| 23 |
+
return {
|
| 24 |
+
**views,
|
| 25 |
+
"image": image,
|
| 26 |
+
"intrinsics": intrinsics,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def apply_patch_shim(batch: BatchedExample, patch_size: int) -> BatchedExample:
|
| 31 |
+
"""Crop images in the batch so that their dimensions are cleanly divisible by the
|
| 32 |
+
specified patch size.
|
| 33 |
+
"""
|
| 34 |
+
return {
|
| 35 |
+
**batch,
|
| 36 |
+
"context": apply_patch_shim_to_views(batch["context"], patch_size),
|
| 37 |
+
"target": apply_patch_shim_to_views(batch["target"], patch_size),
|
| 38 |
+
}
|
src/dataset/types.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable, Literal, TypedDict
|
| 2 |
+
|
| 3 |
+
from jaxtyping import Float, Int64
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
|
| 6 |
+
Stage = Literal["train", "val", "test"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# The following types mainly exist to make type-hinted keys show up in VS Code. Some
|
| 10 |
+
# dimensions are annotated as "_" because either:
|
| 11 |
+
# 1. They're expected to change as part of a function call (e.g., resizing the dataset).
|
| 12 |
+
# 2. They're expected to vary within the same function call (e.g., the number of views,
|
| 13 |
+
# which differs between context and target BatchedViews).
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class BatchedViews(TypedDict, total=False):
|
| 17 |
+
extrinsics: Float[Tensor, "batch _ 4 4"] # batch view 4 4
|
| 18 |
+
intrinsics: Float[Tensor, "batch _ 3 3"] # batch view 3 3
|
| 19 |
+
image: Float[Tensor, "batch _ _ _ _"] # batch view channel height width
|
| 20 |
+
near: Float[Tensor, "batch _"] # batch view
|
| 21 |
+
far: Float[Tensor, "batch _"] # batch view
|
| 22 |
+
index: Int64[Tensor, "batch _"] # batch view
|
| 23 |
+
overlap: Float[Tensor, "batch _"] # batch view
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class BatchedExample(TypedDict, total=False):
|
| 27 |
+
target: BatchedViews
|
| 28 |
+
context: BatchedViews
|
| 29 |
+
scene: list[str]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class UnbatchedViews(TypedDict, total=False):
|
| 33 |
+
extrinsics: Float[Tensor, "_ 4 4"]
|
| 34 |
+
intrinsics: Float[Tensor, "_ 3 3"]
|
| 35 |
+
image: Float[Tensor, "_ 3 height width"]
|
| 36 |
+
near: Float[Tensor, " _"]
|
| 37 |
+
far: Float[Tensor, " _"]
|
| 38 |
+
index: Int64[Tensor, " _"]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class UnbatchedExample(TypedDict, total=False):
|
| 42 |
+
target: UnbatchedViews
|
| 43 |
+
context: UnbatchedViews
|
| 44 |
+
scene: str
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# A data shim modifies the example after it's been returned from the data loader.
|
| 48 |
+
DataShim = Callable[[BatchedExample], BatchedExample]
|
| 49 |
+
|
| 50 |
+
AnyExample = BatchedExample | UnbatchedExample
|
| 51 |
+
AnyViews = BatchedViews | UnbatchedViews
|
src/dataset/validation_wrapper.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Iterator, Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch.utils.data import Dataset, IterableDataset
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ValidationWrapper(Dataset):
|
| 8 |
+
"""Wraps a dataset so that PyTorch Lightning's validation step can be turned into a
|
| 9 |
+
visualization step.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
dataset: Dataset
|
| 13 |
+
dataset_iterator: Optional[Iterator]
|
| 14 |
+
length: int
|
| 15 |
+
|
| 16 |
+
def __init__(self, dataset: Dataset, length: int) -> None:
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.dataset = dataset
|
| 19 |
+
self.length = length
|
| 20 |
+
self.dataset_iterator = None
|
| 21 |
+
|
| 22 |
+
def __len__(self):
|
| 23 |
+
return self.length
|
| 24 |
+
|
| 25 |
+
def __getitem__(self, index: tuple):
|
| 26 |
+
if isinstance(self.dataset, IterableDataset):
|
| 27 |
+
if self.dataset_iterator is None:
|
| 28 |
+
self.dataset_iterator = iter(self.dataset)
|
| 29 |
+
return next(self.dataset_iterator)
|
| 30 |
+
|
| 31 |
+
random_index = torch.randint(0, len(self.dataset), tuple())
|
| 32 |
+
random_context_num = torch.randint(2, self.dataset.view_sampler.num_context_views + 1, tuple())
|
| 33 |
+
return self.dataset[random_index.item(), random_context_num.item()]
|
src/dataset/view_sampler/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from ...misc.step_tracker import StepTracker
|
| 4 |
+
from ..types import Stage
|
| 5 |
+
from .view_sampler import ViewSampler
|
| 6 |
+
from .view_sampler_all import ViewSamplerAll, ViewSamplerAllCfg
|
| 7 |
+
from .view_sampler_arbitrary import ViewSamplerArbitrary, ViewSamplerArbitraryCfg
|
| 8 |
+
from .view_sampler_bounded import ViewSamplerBounded, ViewSamplerBoundedCfg
|
| 9 |
+
from .view_sampler_evaluation import ViewSamplerEvaluation, ViewSamplerEvaluationCfg
|
| 10 |
+
from .view_sampler_rank import ViewSamplerRank, ViewSamplerRankCfg
|
| 11 |
+
VIEW_SAMPLERS: dict[str, ViewSampler[Any]] = {
|
| 12 |
+
"all": ViewSamplerAll,
|
| 13 |
+
"arbitrary": ViewSamplerArbitrary,
|
| 14 |
+
"bounded": ViewSamplerBounded,
|
| 15 |
+
"evaluation": ViewSamplerEvaluation,
|
| 16 |
+
"rank": ViewSamplerRank,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
ViewSamplerCfg = (
|
| 20 |
+
ViewSamplerArbitraryCfg
|
| 21 |
+
| ViewSamplerBoundedCfg
|
| 22 |
+
| ViewSamplerEvaluationCfg
|
| 23 |
+
| ViewSamplerAllCfg
|
| 24 |
+
| ViewSamplerRankCfg
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def get_view_sampler(
|
| 28 |
+
cfg: ViewSamplerCfg,
|
| 29 |
+
stage: Stage,
|
| 30 |
+
overfit: bool,
|
| 31 |
+
cameras_are_circular: bool,
|
| 32 |
+
step_tracker: StepTracker | None,
|
| 33 |
+
) -> ViewSampler[Any]:
|
| 34 |
+
return VIEW_SAMPLERS[cfg.name](
|
| 35 |
+
cfg,
|
| 36 |
+
stage,
|
| 37 |
+
overfit,
|
| 38 |
+
cameras_are_circular,
|
| 39 |
+
step_tracker,
|
| 40 |
+
)
|
src/dataset/view_sampler/three_view_hack.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from jaxtyping import Int
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def add_third_context_index(
|
| 7 |
+
indices: Int[Tensor, "*batch 2"]
|
| 8 |
+
) -> Int[Tensor, "*batch 3"]:
|
| 9 |
+
left, right = indices.unbind(dim=-1)
|
| 10 |
+
return torch.stack((left, (left + right) // 2, right), dim=-1)
|