richardchencccc commited on
Commit
f737f60
·
verified ·
1 Parent(s): c35e6e6

Add OF3GS ZeroGPU demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. LICENSE +25 -0
  4. README.md +343 -5
  5. app.py +328 -0
  6. assets/teaser.png +3 -0
  7. config/compute_metrics.yaml +28 -0
  8. config/dataset/base_dataset.yaml +7 -0
  9. config/dataset/dl3dv.yaml +19 -0
  10. config/dataset/re10k.yaml +19 -0
  11. config/dataset/view_sampler/all.yaml +1 -0
  12. config/dataset/view_sampler/arbitrary.yaml +7 -0
  13. config/dataset/view_sampler/bounded.yaml +16 -0
  14. config/dataset/view_sampler/evaluation.yaml +4 -0
  15. config/dataset/view_sampler/rank.yaml +14 -0
  16. config/experiment/dl3dv.yaml +87 -0
  17. config/experiment/multi-dataset.yaml +102 -0
  18. config/experiment/re10k.yaml +84 -0
  19. config/generate_evaluation_index.yaml +36 -0
  20. config/loss/depth.yaml +5 -0
  21. config/loss/lpips.yaml +3 -0
  22. config/loss/mse.yaml +3 -0
  23. config/loss/ssim.yaml +3 -0
  24. config/main.yaml +78 -0
  25. config/model/decoder/splatting_cuda.yaml +3 -0
  26. config/model/encoder/backbone/croco.yaml +9 -0
  27. config/model/encoder/of3gs.yaml +42 -0
  28. requirements-train.txt +34 -0
  29. requirements.txt +16 -0
  30. scripts/download_dl3dv.sh +146 -0
  31. scripts/download_weights.sh +127 -0
  32. src/__init__.py +1 -0
  33. src/config.py +111 -0
  34. src/dataset/__init__.py +94 -0
  35. src/dataset/data_module.py +196 -0
  36. src/dataset/data_sampler.py +364 -0
  37. src/dataset/dataset.py +13 -0
  38. src/dataset/dataset_dl3dv.py +452 -0
  39. src/dataset/dataset_re10k.py +302 -0
  40. src/dataset/shims/augmentation_shim.py +219 -0
  41. src/dataset/shims/bounds_shim.py +80 -0
  42. src/dataset/shims/crop_shim.py +196 -0
  43. src/dataset/shims/geometry_shim.py +383 -0
  44. src/dataset/shims/load_shim.py +12 -0
  45. src/dataset/shims/normalize_shim.py +27 -0
  46. src/dataset/shims/patch_shim.py +38 -0
  47. src/dataset/types.py +51 -0
  48. src/dataset/validation_wrapper.py +33 -0
  49. src/dataset/view_sampler/__init__.py +40 -0
  50. 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: Of3gs Demo
3
- emoji: 📚
4
- colorFrom: indigo
5
  colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.24.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 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 &nbsp;&nbsp;
53
+ <sup>2</sup>Independent Researcher &nbsp;&nbsp;
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

  • SHA256: 0e5451b83f38850f744717801ee955a43ca4b76ab6d1000313ff8d39f1633aec
  • Pointer size: 131 Bytes
  • Size of remote file: 306 kB
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)