Spaces:
Running on Zero
Running on Zero
CodecSplat demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +50 -0
- LICENSE-CodecSplat +21 -0
- README.md +44 -7
- app.py +338 -0
- codecsplat_pipeline.py +336 -0
- colmap_sfm.py +177 -0
- config/dataset/dl3dv.yaml +27 -0
- config/dataset/re10k.yaml +21 -0
- config/dataset/view_sampler/all.yaml +1 -0
- config/dataset/view_sampler/arbitrary.yaml +7 -0
- config/dataset/view_sampler/bounded.yaml +12 -0
- config/dataset/view_sampler/boundedv2.yaml +15 -0
- config/dataset/view_sampler/boundedv2_360.yaml +17 -0
- config/dataset/view_sampler/evaluation.yaml +4 -0
- config/dataset/view_sampler_dataset_specific_config/bounded_re10k.yaml +11 -0
- config/dataset/view_sampler_dataset_specific_config/boundedv2_dl3dv.yaml +14 -0
- config/dataset/view_sampler_dataset_specific_config/evaluation_dl3dv.yaml +7 -0
- config/dataset/view_sampler_dataset_specific_config/evaluation_re10k.yaml +5 -0
- config/experiment/dl3dv.yaml +41 -0
- config/experiment/re10k.yaml +40 -0
- config/loss/lpips.yaml +3 -0
- config/loss/mse.yaml +2 -0
- config/main.yaml +115 -0
- config/model/decoder/splatting_cuda.yaml +1 -0
- config/model/decoder/splatting_gsplat.yaml +10 -0
- config/model/encoder/codecsplat.yaml +48 -0
- examples/scene_a/00.jpg +3 -0
- examples/scene_a/01.jpg +3 -0
- examples/scene_a/02.jpg +3 -0
- examples/scene_a/03.jpg +3 -0
- examples/scene_a/04.jpg +3 -0
- examples/scene_a/05.jpg +3 -0
- examples/scene_a/06.jpg +3 -0
- examples/scene_a/07.jpg +3 -0
- examples/scene_a/08.jpg +3 -0
- examples/scene_a/09.jpg +3 -0
- examples/scene_a/10.jpg +3 -0
- examples/scene_a/11.jpg +3 -0
- examples/scene_a/12.jpg +3 -0
- examples/scene_a/13.jpg +3 -0
- examples/scene_a/14.jpg +3 -0
- examples/scene_a/15.jpg +3 -0
- examples/scene_a/16.jpg +3 -0
- examples/scene_a/17.jpg +3 -0
- examples/scene_a/18.jpg +3 -0
- examples/scene_a/19.jpg +3 -0
- examples/scene_a/20.jpg +3 -0
- examples/scene_a/21.jpg +3 -0
- examples/scene_a/22.jpg +3 -0
- examples/scene_a/23.jpg +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,53 @@ 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 |
+
examples/scene_a/00.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/scene_a/01.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/scene_a/02.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/scene_a/03.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/scene_a/04.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/scene_a/05.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
examples/scene_a/06.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
examples/scene_a/07.jpg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
examples/scene_a/08.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
examples/scene_a/09.jpg filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
examples/scene_a/10.jpg filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
examples/scene_a/11.jpg filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
examples/scene_a/12.jpg filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
examples/scene_a/13.jpg filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
examples/scene_a/14.jpg filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
examples/scene_a/15.jpg filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
examples/scene_a/16.jpg filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
examples/scene_a/17.jpg filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
examples/scene_a/18.jpg filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
examples/scene_a/19.jpg filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
examples/scene_a/20.jpg filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
examples/scene_a/21.jpg filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
examples/scene_a/22.jpg filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
examples/scene_a/23.jpg filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
examples/scene_a/24.jpg filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
examples/scene_b/00.jpg filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
examples/scene_b/01.jpg filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
examples/scene_b/02.jpg filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
examples/scene_b/03.jpg filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
examples/scene_b/04.jpg filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
examples/scene_b/05.jpg filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
examples/scene_b/06.jpg filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
examples/scene_b/07.jpg filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
examples/scene_b/08.jpg filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
examples/scene_b/09.jpg filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
examples/scene_b/10.jpg filter=lfs diff=lfs merge=lfs -text
|
| 72 |
+
examples/scene_b/11.jpg filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
examples/scene_b/12.jpg filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
examples/scene_b/13.jpg filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
examples/scene_b/14.jpg filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
examples/scene_b/15.jpg filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
examples/scene_b/16.jpg filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
examples/scene_b/17.jpg filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
examples/scene_b/18.jpg filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
examples/scene_b/19.jpg filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
examples/scene_b/20.jpg filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
examples/scene_b/21.jpg filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
examples/scene_b/22.jpg filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
examples/scene_b/23.jpg filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
examples/scene_b/24.jpg filter=lfs diff=lfs merge=lfs -text
|
LICENSE-CodecSplat
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Pengpeng Yu
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,13 +1,50 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CodecSplat
|
| 3 |
+
emoji: 🗜️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Compress photos into a tiny 3D Gaussian splat bitstream
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
+
models:
|
| 13 |
+
- pengpeng-yu/CodecSplat
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# CodecSplat: Ultra-Compact Latent Coding for Feed-Forward 3D Gaussian Splatting
|
| 17 |
+
|
| 18 |
+
Demo of [**CodecSplat**](https://huggingface.co/papers/2605.25563) (Yu et al.). CodecSplat
|
| 19 |
+
entropy-codes the 2D features that a feed-forward 3DGS model (DepthSplat-style) uses for depth
|
| 20 |
+
and Gaussian prediction. A scene becomes a bitstream of a few tens of KB, which is then decoded
|
| 21 |
+
into renderable 3D Gaussians.
|
| 22 |
+
|
| 23 |
+
How the Space works:
|
| 24 |
+
|
| 25 |
+
1. **COLMAP** (`pycolmap`, CPU) estimates intrinsics and camera poses for the uploaded photos.
|
| 26 |
+
The reconstruction is normalised and undistorted to PINHOLE.
|
| 27 |
+
2. 8 context views are picked by farthest-point sampling over the cameras. They are resized and
|
| 28 |
+
center-cropped to 256×448 with the authors' crop shim.
|
| 29 |
+
3. CodecSplat (`codecsplat-base-dl3dv-256x448-view8`, the authors' DL3DV evaluation preset)
|
| 30 |
+
runs the **complete bitstream path** from `ModelWrapper.test_step`: encode context →
|
| 31 |
+
compress to a real bitstream at the chosen λ → decompress → predict Gaussians, using bf16
|
| 32 |
+
autocast like `trainer.precision=bf16-mixed`.
|
| 33 |
+
4. The authors' gsplat decoder (antialiased, eps2d=0.1) renders held-out views (PSNR is reported
|
| 34 |
+
against the real photos) and a fly-through video. The decoded Gaussians are shown in an
|
| 35 |
+
interactive `gr.Model3D` splat viewer, and the `.bin` bitstream is available to download.
|
| 36 |
+
|
| 37 |
+
Notes:
|
| 38 |
+
* gsplat's CUDA kernels are JIT-compiled at startup against the CUDA toolkit in the ZeroGPU
|
| 39 |
+
image, following `hugging-apps/resplat-recurrent-gs`.
|
| 40 |
+
* The viewer gets a compact copy of the Gaussians: DC colour only, near-transparent splats
|
| 41 |
+
pruned, re-centred so the scene is framed.
|
| 42 |
+
|
| 43 |
+
Code: https://github.com/pengpeng-yu/CodecSplat (MIT) · Weights: https://huggingface.co/pengpeng-yu/CodecSplat (MIT)
|
| 44 |
+
|
| 45 |
+
## Example scenes
|
| 46 |
+
|
| 47 |
+
The example frames are from the DL3DV demo scene that the DepthSplat authors redistribute in
|
| 48 |
+
[`haofeixu/depthsplat`](https://huggingface.co/datasets/haofeixu/depthsplat) (MIT-labelled).
|
| 49 |
+
The imagery itself comes from [DL3DV-10K](https://dl3dv-10k.github.io/DL3DV-10K/), credit to
|
| 50 |
+
the DL3DV authors.
|
app.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CodecSplat: Ultra-Compact Latent Coding for Feed-Forward 3D Gaussian Splatting.
|
| 2 |
+
|
| 3 |
+
Upload overlapping photos of a static scene. The Space runs COLMAP (pycolmap, CPU) to get
|
| 4 |
+
camera poses, then CodecSplat (authors' DL3DV 8-view 256x448 checkpoint):
|
| 5 |
+
context views -> 2D latent features -> learned entropy coder -> real bitstream
|
| 6 |
+
-> decoder -> 3D Gaussians -> gsplat rendering.
|
| 7 |
+
You get an interactive splat viewer, a fly-through video, held-out novel views, the
|
| 8 |
+
size of the compressed scene bitstream (and the bitstream itself).
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import ctypes
|
| 14 |
+
import glob
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import tempfile
|
| 18 |
+
import time
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
APP_DIR = Path(__file__).parent.resolve()
|
| 22 |
+
sys.path.insert(0, str(APP_DIR))
|
| 23 |
+
|
| 24 |
+
os.environ.setdefault("TORCH_CUDA_ARCH_LIST", "12.0")
|
| 25 |
+
os.environ.setdefault("MAX_JOBS", str(min(32, (os.cpu_count() or 8))))
|
| 26 |
+
|
| 27 |
+
import spaces # noqa: E402 (must precede any CUDA-touching import)
|
| 28 |
+
import torch # noqa: E402
|
| 29 |
+
|
| 30 |
+
torch.set_float32_matmul_precision("high")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# 1. gsplat CUDA backend: JIT-compile once at startup against the CUDA toolkit
|
| 35 |
+
# shipped in the ZeroGPU image (same approach as hugging-apps/resplat-recurrent-gs).
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
def _pick_cuda_home() -> str | None:
|
| 38 |
+
torch_cuda = (torch.version.cuda or "").split(".")[0]
|
| 39 |
+
candidates: list[str] = []
|
| 40 |
+
for pattern in ("/cuda-image/usr/local/cuda-*", "/usr/local/cuda-*"):
|
| 41 |
+
candidates += sorted(glob.glob(pattern), reverse=True)
|
| 42 |
+
candidates += ["/cuda-image/usr/local/cuda", "/usr/local/cuda"]
|
| 43 |
+
usable = [c for c in candidates if os.path.isfile(os.path.join(c, "bin", "nvcc"))]
|
| 44 |
+
if not usable:
|
| 45 |
+
return None
|
| 46 |
+
for c in usable:
|
| 47 |
+
if torch_cuda and f"cuda-{torch_cuda}" in c:
|
| 48 |
+
return c
|
| 49 |
+
return usable[0]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
CUDA_HOME = _pick_cuda_home()
|
| 53 |
+
print(f"[setup] torch {torch.__version__} (cuda {torch.version.cuda}) | CUDA_HOME={CUDA_HOME}", flush=True)
|
| 54 |
+
if CUDA_HOME:
|
| 55 |
+
os.environ["CUDA_HOME"] = CUDA_HOME
|
| 56 |
+
os.environ["CUDA_PATH"] = CUDA_HOME
|
| 57 |
+
os.environ["PATH"] = os.path.join(CUDA_HOME, "bin") + os.pathsep + os.environ.get("PATH", "")
|
| 58 |
+
_libdir = os.path.join(CUDA_HOME, "lib64")
|
| 59 |
+
os.environ["LD_LIBRARY_PATH"] = _libdir + os.pathsep + os.environ.get("LD_LIBRARY_PATH", "")
|
| 60 |
+
for _so in sorted(glob.glob(os.path.join(_libdir, "libcudart.so*")), reverse=True):
|
| 61 |
+
try:
|
| 62 |
+
ctypes.CDLL(_so, mode=ctypes.RTLD_GLOBAL)
|
| 63 |
+
break
|
| 64 |
+
except OSError:
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
import torch.utils.cpp_extension as _cppext # noqa: E402
|
| 68 |
+
|
| 69 |
+
_cppext._check_cuda_version = lambda *a, **k: None
|
| 70 |
+
|
| 71 |
+
print("[setup] compiling gsplat CUDA kernels (one-off, several minutes)...", flush=True)
|
| 72 |
+
_t0 = time.time()
|
| 73 |
+
from gsplat.cuda._backend import _C as _GSPLAT_C # noqa: E402
|
| 74 |
+
|
| 75 |
+
if _GSPLAT_C is None:
|
| 76 |
+
raise RuntimeError(f"gsplat CUDA backend unavailable (CUDA_HOME={CUDA_HOME}).")
|
| 77 |
+
print(f"[setup] gsplat backend ready in {time.time() - _t0:.1f}s", flush=True)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# 2. Model
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
import imageio.v2 as imageio # noqa: E402
|
| 84 |
+
import numpy as np # noqa: E402
|
| 85 |
+
import gradio as gr # noqa: E402
|
| 86 |
+
import pycolmap # noqa: E402
|
| 87 |
+
from huggingface_hub import hf_hub_download # noqa: E402
|
| 88 |
+
from PIL import Image # noqa: E402
|
| 89 |
+
|
| 90 |
+
import codecsplat_pipeline as cp # noqa: E402
|
| 91 |
+
from colmap_sfm import IMAGE_EXT, run_sfm # noqa: E402
|
| 92 |
+
|
| 93 |
+
MODEL_REPO = "pengpeng-yu/CodecSplat"
|
| 94 |
+
CHECKPOINT_FILE = "codecsplat-base-dl3dv-256x448-view8-075062d0.pth"
|
| 95 |
+
NUM_CONTEXT = 8
|
| 96 |
+
NUM_TARGET = 8
|
| 97 |
+
MAX_INPUT_IMAGES = 60
|
| 98 |
+
|
| 99 |
+
print("[setup] downloading checkpoint...", flush=True)
|
| 100 |
+
ckpt_path = hf_hub_download(MODEL_REPO, CHECKPOINT_FILE)
|
| 101 |
+
print("[setup] building CodecSplat...", flush=True)
|
| 102 |
+
ENCODER = cp.build_encoder(ckpt_path).to("cuda")
|
| 103 |
+
DECODER = cp.build_decoder().to("cuda")
|
| 104 |
+
print("[setup] model ready", flush=True)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _u8(t: torch.Tensor) -> np.ndarray:
|
| 108 |
+
return (t.clamp(0, 1).permute(1, 2, 0).float().cpu().numpy() * 255).round().astype(np.uint8)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
# 3. GPU part: encode -> bitstream -> decode -> render
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
@spaces.GPU(duration=60)
|
| 115 |
+
def codecsplat_gpu(image_paths: list[str], c2w: np.ndarray, K: np.ndarray, lmb: float = 1024.0):
|
| 116 |
+
ctx_idx, tgt_idx = cp.select_views(c2w, NUM_CONTEXT, NUM_TARGET)
|
| 117 |
+
batch = cp.to_device(cp.build_example(image_paths, c2w, K, ctx_idx, tgt_idx), "cuda")
|
| 118 |
+
h, w = batch["context"]["image"].shape[-2:]
|
| 119 |
+
|
| 120 |
+
torch.cuda.synchronize()
|
| 121 |
+
t0 = time.perf_counter()
|
| 122 |
+
out = cp.encode_decode(ENCODER, batch, lmb)
|
| 123 |
+
torch.cuda.synchronize()
|
| 124 |
+
t_codec = time.perf_counter() - t0
|
| 125 |
+
g = out["gaussians"]
|
| 126 |
+
|
| 127 |
+
# held-out novel views (+ PSNR against the real photos)
|
| 128 |
+
renders, score = None, float("nan")
|
| 129 |
+
if len(tgt_idx) > 0:
|
| 130 |
+
renders = cp.render(DECODER, g, batch["target"]["extrinsics"][0],
|
| 131 |
+
batch["target"]["intrinsics"][0], (h, w))
|
| 132 |
+
score = cp.psnr(batch["target"]["image"][0].float(), renders)
|
| 133 |
+
|
| 134 |
+
# smooth fly-through through the context cameras
|
| 135 |
+
traj_c2w, traj_K = cp.interpolate_trajectory(
|
| 136 |
+
batch["context"]["extrinsics"][0], batch["context"]["intrinsics"][0]
|
| 137 |
+
)
|
| 138 |
+
video = cp.render(DECODER, g, traj_c2w.cuda(), traj_K.cuda(), (h, w))
|
| 139 |
+
t_total = time.perf_counter() - t0
|
| 140 |
+
|
| 141 |
+
gauss = {
|
| 142 |
+
"means": g.means[0].float().cpu(),
|
| 143 |
+
"scales": g.scales[0].float().cpu(),
|
| 144 |
+
"rotations": g.rotations[0].float().cpu(),
|
| 145 |
+
"harmonics": g.harmonics[0].float().cpu(),
|
| 146 |
+
"opacities": g.opacities[0].float().cpu(),
|
| 147 |
+
}
|
| 148 |
+
return {
|
| 149 |
+
"bitstream": out["bitstream"],
|
| 150 |
+
"num_bytes": float(out["codec_num_bytes"]),
|
| 151 |
+
"camera_bytes": float(out["codec_camera_num_bytes"]),
|
| 152 |
+
"feature_bytes": float(out["codec_feature_num_bytes"]),
|
| 153 |
+
"gaussians": gauss,
|
| 154 |
+
"context_c2w": batch["context"]["extrinsics"][0].cpu(),
|
| 155 |
+
"context_images": [_u8(x) for x in batch["context"]["image"][0]],
|
| 156 |
+
"target_images": [_u8(x) for x in batch["target"]["image"][0]],
|
| 157 |
+
"renders": [] if renders is None else [_u8(x) for x in renders],
|
| 158 |
+
"video": [_u8(x) for x in video],
|
| 159 |
+
"psnr": score,
|
| 160 |
+
"t_codec": t_codec,
|
| 161 |
+
"t_total": t_total,
|
| 162 |
+
"hw": (int(h), int(w)),
|
| 163 |
+
"ctx_idx": ctx_idx.tolist(),
|
| 164 |
+
"tgt_idx": tgt_idx.tolist(),
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
# 4. Full pipeline (CPU COLMAP -> GPU CodecSplat -> CPU export)
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
def _read_colmap_scene(scene_dir: str):
|
| 172 |
+
rec = pycolmap.Reconstruction(os.path.join(scene_dir, "sparse"))
|
| 173 |
+
items = []
|
| 174 |
+
for _, img in rec.images.items():
|
| 175 |
+
cam = rec.cameras[img.camera_id]
|
| 176 |
+
p = cam.params
|
| 177 |
+
if cam.model.name == "PINHOLE":
|
| 178 |
+
fx, fy, cx, cy = p[:4]
|
| 179 |
+
elif cam.model.name == "SIMPLE_PINHOLE":
|
| 180 |
+
fx = fy = p[0]
|
| 181 |
+
cx, cy = p[1:3]
|
| 182 |
+
else:
|
| 183 |
+
continue
|
| 184 |
+
K = np.eye(3, dtype=np.float32)
|
| 185 |
+
K[0, 0], K[1, 1] = fx / cam.width, fy / cam.height
|
| 186 |
+
K[0, 2], K[1, 2] = cx / cam.width, cy / cam.height
|
| 187 |
+
w2c = np.eye(4, dtype=np.float64)
|
| 188 |
+
w2c[:3, :4] = img.cam_from_world().matrix() if callable(img.cam_from_world) else img.cam_from_world.matrix()
|
| 189 |
+
path = os.path.join(scene_dir, "images", img.name)
|
| 190 |
+
if os.path.exists(path):
|
| 191 |
+
items.append((img.name, path, np.linalg.inv(w2c).astype(np.float32), K))
|
| 192 |
+
items.sort(key=lambda x: x[0])
|
| 193 |
+
if len(items) < 2:
|
| 194 |
+
raise gr.Error("COLMAP registered fewer than 2 usable images.")
|
| 195 |
+
return [i[1] for i in items], np.stack([i[2] for i in items]), np.stack([i[3] for i in items])
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def reconstruct(images, lmb: float = 1024.0, matcher: str = "exhaustive",
|
| 199 |
+
progress=gr.Progress(track_tqdm=False)):
|
| 200 |
+
"""Reconstruct a compressed 3D Gaussian scene from multi-view photos with CodecSplat.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
images: 3-60 overlapping photos of one static scene (camera poses are estimated with COLMAP).
|
| 204 |
+
lmb: rate-distortion trade-off lambda (16 = smallest bitstream, 1024 = best quality).
|
| 205 |
+
matcher: COLMAP matching mode, "exhaustive" (unordered photos) or "sequential" (video frames).
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
Interactive splat (.ply), fly-through video, held-out novel views (render vs. photo),
|
| 209 |
+
selected input views, the scene bitstream (.bin) and a summary.
|
| 210 |
+
"""
|
| 211 |
+
if not images:
|
| 212 |
+
raise gr.Error("Please upload at least 3 overlapping photos of the same scene.")
|
| 213 |
+
if isinstance(images, (str, Path)):
|
| 214 |
+
images = [images]
|
| 215 |
+
paths = [str(im if isinstance(im, (str, Path)) else getattr(im, "name", im)) for im in images]
|
| 216 |
+
paths = sorted([p for p in paths if p.lower().endswith(IMAGE_EXT)], key=os.path.basename)
|
| 217 |
+
if len(paths) < 3:
|
| 218 |
+
raise gr.Error(f"Got {len(paths)} usable image(s); COLMAP needs at least 3 overlapping photos.")
|
| 219 |
+
note = ""
|
| 220 |
+
if len(paths) > MAX_INPUT_IMAGES:
|
| 221 |
+
paths = [paths[i] for i in np.linspace(0, len(paths) - 1, MAX_INPUT_IMAGES).astype(int)]
|
| 222 |
+
note = f" (sub-sampled to {MAX_INPUT_IMAGES} images)"
|
| 223 |
+
|
| 224 |
+
t_start = time.time()
|
| 225 |
+
workdir = Path(tempfile.mkdtemp(prefix="codecsplat_"))
|
| 226 |
+
progress(0.05, desc=f"COLMAP on {len(paths)} images")
|
| 227 |
+
try:
|
| 228 |
+
sfm = run_sfm(paths, workdir, max_image_size=1024, matcher=matcher,
|
| 229 |
+
log=lambda m: print(m, flush=True))
|
| 230 |
+
except ValueError as exc:
|
| 231 |
+
raise gr.Error(str(exc)) from exc
|
| 232 |
+
t_sfm = time.time() - t_start
|
| 233 |
+
img_paths, c2w, K = _read_colmap_scene(sfm["scene_dir"])
|
| 234 |
+
|
| 235 |
+
progress(0.55, desc="CodecSplat: encode -> bitstream -> decode -> render")
|
| 236 |
+
r = codecsplat_gpu(img_paths, c2w, K, float(lmb))
|
| 237 |
+
|
| 238 |
+
progress(0.85, desc="Exporting Gaussians")
|
| 239 |
+
out = workdir / "out"
|
| 240 |
+
out.mkdir(exist_ok=True)
|
| 241 |
+
bin_path = out / f"codecsplat_scene_lmb{int(round(lmb))}.bin"
|
| 242 |
+
bin_path.write_bytes(r["bitstream"])
|
| 243 |
+
viewer_ply = cp.export_gaussians(r["gaussians"], r["context_c2w"], out)
|
| 244 |
+
|
| 245 |
+
video_path = str(out / "flythrough.mp4")
|
| 246 |
+
imageio.mimwrite(video_path, r["video"], fps=24, quality=8, macro_block_size=16)
|
| 247 |
+
|
| 248 |
+
gallery_ctx = [(im, f"input view {i}") for im, i in zip(r["context_images"], r["ctx_idx"])]
|
| 249 |
+
gallery_nv = []
|
| 250 |
+
for rend, gt, i in zip(r["renders"], r["target_images"], r["tgt_idx"]):
|
| 251 |
+
gallery_nv.append((np.concatenate([rend, gt], axis=1), f"held-out view {i}: render | photo"))
|
| 252 |
+
|
| 253 |
+
n_g = int(r["gaussians"]["means"].shape[0])
|
| 254 |
+
raw_bytes = n_g * (3 + 3 + 4 + 1 + r["gaussians"]["harmonics"][0].numel()) * 4
|
| 255 |
+
kb = r["num_bytes"] / 1024
|
| 256 |
+
summary = (
|
| 257 |
+
f"**Compressed scene: {kb:.1f} KB** (features {r['feature_bytes']/1024:.1f} KB + cameras "
|
| 258 |
+
f"{r['camera_bytes']/1024:.2f} KB) at λ = {lmb:g} — decodes to **{n_g:,} Gaussians** "
|
| 259 |
+
f"(≈{raw_bytes/2**20:.0f} MB as raw float32, **{raw_bytes/max(r['num_bytes'],1):,.0f}× smaller**).\n\n"
|
| 260 |
+
f"Held-out novel views: **PSNR {r['psnr']:.2f} dB** · {len(r['ctx_idx'])} input views at "
|
| 261 |
+
f"{r['hw'][0]}×{r['hw'][1]} · COLMAP registered {sfm['num_registered']}/{sfm['num_input']} "
|
| 262 |
+
f"images{note} in {t_sfm:.1f}s · CodecSplat encode+decode {r['t_codec']:.2f}s · "
|
| 263 |
+
f"total {time.time() - t_start:.1f}s"
|
| 264 |
+
)
|
| 265 |
+
return viewer_ply, video_path, gallery_nv, gallery_ctx, str(bin_path), summary
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ---------------------------------------------------------------------------
|
| 269 |
+
# 5. UI
|
| 270 |
+
# ---------------------------------------------------------------------------
|
| 271 |
+
EX_DIR = APP_DIR / "examples"
|
| 272 |
+
EXAMPLES = [sorted(str(p) for p in (EX_DIR / s).glob("*.jpg")) for s in ("scene_a", "scene_b")]
|
| 273 |
+
|
| 274 |
+
DESCRIPTION = """
|
| 275 |
+
# CodecSplat: Ultra-Compact Latent Coding for Feed-Forward 3D Gaussian Splatting
|
| 276 |
+
|
| 277 |
+
CodecSplat turns a few photos into 3D Gaussians **through a real compressed bitstream**: the 2D
|
| 278 |
+
features used for depth and Gaussian prediction are entropy-coded (tens of KB per scene), then
|
| 279 |
+
decoded into a renderable 3D Gaussian scene. Upload overlapping photos of a static scene —
|
| 280 |
+
camera poses are estimated with COLMAP right here — and pick a rate (λ).
|
| 281 |
+
|
| 282 |
+
[Paper](https://huggingface.co/papers/2605.25563) · [Code](https://github.com/pengpeng-yu/CodecSplat) · [Weights](https://huggingface.co/pengpeng-yu/CodecSplat)
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
CSS = """
|
| 286 |
+
#col-container { max-width: 1280px; margin: 0 auto; }
|
| 287 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
with gr.Blocks(title="CodecSplat") as demo:
|
| 291 |
+
with gr.Column(elem_id="col-container"):
|
| 292 |
+
gr.Markdown(DESCRIPTION)
|
| 293 |
+
with gr.Row():
|
| 294 |
+
with gr.Column(scale=1):
|
| 295 |
+
image_files = gr.File(
|
| 296 |
+
label="Photos of one static scene (8-30 overlapping shots work best)",
|
| 297 |
+
file_count="multiple", file_types=["image"], type="filepath",
|
| 298 |
+
)
|
| 299 |
+
lmb = gr.Slider(16, 1024, value=1024, step=1, label="Rate λ",
|
| 300 |
+
info="16 = smallest bitstream, 1024 = highest quality")
|
| 301 |
+
run_btn = gr.Button("Compress & reconstruct", variant="primary")
|
| 302 |
+
with gr.Accordion("Advanced options", open=False):
|
| 303 |
+
matcher = gr.Radio(["exhaustive", "sequential"], value="exhaustive",
|
| 304 |
+
label="COLMAP matching",
|
| 305 |
+
info="exhaustive = unordered photos; sequential = video frames")
|
| 306 |
+
gr.Markdown(
|
| 307 |
+
"8 input views are picked by farthest-point sampling over the COLMAP cameras and "
|
| 308 |
+
"resized/cropped to 256×448 (the checkpoint's training resolution); up to 8 other "
|
| 309 |
+
"photos are held out to measure novel-view quality.\n\n"
|
| 310 |
+
"**Viewer**: drag to orbit, right-drag to pan, scroll to zoom."
|
| 311 |
+
)
|
| 312 |
+
with gr.Column(scale=2):
|
| 313 |
+
summary = gr.Markdown()
|
| 314 |
+
splat = gr.Model3D(label="Decoded 3D Gaussians (interactive)", height=440,
|
| 315 |
+
clear_color=[0.0, 0.0, 0.0, 1.0])
|
| 316 |
+
video = gr.Video(label="Fly-through rendered from the decoded Gaussians", autoplay=True, loop=True)
|
| 317 |
+
bin_out = gr.File(label="Compressed scene bitstream (.bin)")
|
| 318 |
+
novel = gr.Gallery(label="Held-out novel views: render | real photo", columns=2, height=320,
|
| 319 |
+
object_fit="contain")
|
| 320 |
+
inputs_gal = gr.Gallery(label="Selected input views (after crop)", columns=4, height=200,
|
| 321 |
+
object_fit="contain")
|
| 322 |
+
|
| 323 |
+
outputs = [splat, video, novel, inputs_gal, bin_out, summary]
|
| 324 |
+
run_btn.click(reconstruct, inputs=[image_files, lmb, matcher], outputs=outputs,
|
| 325 |
+
api_name="reconstruct")
|
| 326 |
+
|
| 327 |
+
gr.Examples(
|
| 328 |
+
examples=[[EXAMPLES[0], 1024], [EXAMPLES[1], 64]],
|
| 329 |
+
inputs=[image_files, lmb],
|
| 330 |
+
outputs=outputs,
|
| 331 |
+
fn=reconstruct,
|
| 332 |
+
cache_examples=True,
|
| 333 |
+
cache_mode="lazy",
|
| 334 |
+
label="Frames from the DL3DV demo scene (via the DepthSplat authors)",
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
if __name__ == "__main__":
|
| 338 |
+
demo.queue(max_size=12).launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True, show_error=True)
|
codecsplat_pipeline.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CodecSplat inference helpers for the Space.
|
| 2 |
+
|
| 3 |
+
Everything here mirrors the authors' evaluation path
|
| 4 |
+
(`scripts/test_dl3dv_view8_256x448.sh` -> `src/main.py` -> `ModelWrapper.test_step`):
|
| 5 |
+
the encoder runs the *complete* learned-codec path (encode context -> entropy-code the
|
| 6 |
+
2D latent features into a real bitstream -> decode the bitstream -> predict Gaussians),
|
| 7 |
+
and the gsplat decoder renders novel views, all under bf16 autocast like
|
| 8 |
+
`trainer.precision=bf16-mixed`.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from types import SimpleNamespace
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
from dacite import Config as DaciteConfig, from_dict
|
| 20 |
+
from einops import rearrange, repeat
|
| 21 |
+
from omegaconf import OmegaConf
|
| 22 |
+
from plyfile import PlyData, PlyElement
|
| 23 |
+
from scipy.spatial.transform import Rotation as R
|
| 24 |
+
from scipy.spatial.transform import Slerp
|
| 25 |
+
|
| 26 |
+
from src.dataset.shims.crop_shim import apply_crop_shim
|
| 27 |
+
from src.dataset.shims.patch_shim import apply_patch_shim
|
| 28 |
+
from src.misc.sh_rotation import rotate_sh
|
| 29 |
+
from src.model.encoder import get_encoder
|
| 30 |
+
from src.model.encoder.encoder_codecsplat import EncoderCodecSplatCfg
|
| 31 |
+
|
| 32 |
+
APP_DIR = Path(__file__).parent.resolve()
|
| 33 |
+
|
| 34 |
+
# ---- authors' DL3DV 8-view 256x448 evaluation preset ---------------------------------
|
| 35 |
+
IMAGE_SHAPE = (256, 448)
|
| 36 |
+
NEAR, FAR = 0.5, 200.0 # config/experiment/dl3dv.yaml
|
| 37 |
+
ENCODER_OVERRIDES = {
|
| 38 |
+
"num_depth_candidates": 128,
|
| 39 |
+
"shim_patch_size": 16,
|
| 40 |
+
"num_scales": 2,
|
| 41 |
+
"upsample_factor": 4,
|
| 42 |
+
"lowest_feature_resolution": 8,
|
| 43 |
+
"monodepth_vit_type": "vitb",
|
| 44 |
+
"enable_feature_codec": True,
|
| 45 |
+
"enable_depth_guided_mv_refiner": True,
|
| 46 |
+
"enable_post_vae_mv_refiner": True,
|
| 47 |
+
"codec_lmb_range": [16.0, 1024.0],
|
| 48 |
+
"supervise_intermediate_depth": False,
|
| 49 |
+
}
|
| 50 |
+
DECODER_OVERRIDES = {
|
| 51 |
+
"use_covariances": True,
|
| 52 |
+
"rasterize_mode": "antialiased",
|
| 53 |
+
"eps2d": 0.1,
|
| 54 |
+
"packed": True,
|
| 55 |
+
"scale_invariant": False,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def build_encoder(checkpoint_path: str):
|
| 60 |
+
enc_cfg = OmegaConf.load(APP_DIR / "config/model/encoder/codecsplat.yaml")
|
| 61 |
+
enc_cfg = OmegaConf.merge(enc_cfg, ENCODER_OVERRIDES)
|
| 62 |
+
enc_cfg = from_dict(
|
| 63 |
+
EncoderCodecSplatCfg,
|
| 64 |
+
OmegaConf.to_container(enc_cfg, resolve=True),
|
| 65 |
+
config=DaciteConfig(cast=[tuple], strict=False),
|
| 66 |
+
)
|
| 67 |
+
encoder = get_encoder(enc_cfg)
|
| 68 |
+
|
| 69 |
+
ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 70 |
+
state = ckpt.get("state_dict", ckpt)
|
| 71 |
+
state = {k[len("encoder."):]: v for k, v in state.items() if k.startswith("encoder.")}
|
| 72 |
+
encoder.load_state_dict(state, strict=True)
|
| 73 |
+
# eval() also runs `compress_mode` -> builds the entropy-coder CDF tables (on CPU).
|
| 74 |
+
encoder.eval()
|
| 75 |
+
return encoder
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def build_decoder():
|
| 79 |
+
from src.model.decoder.decoder_splatting_gsplat import (
|
| 80 |
+
DecoderSplattingGsplat,
|
| 81 |
+
DecoderSplattingGsplatCfg,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
dec_cfg = OmegaConf.load(APP_DIR / "config/model/decoder/splatting_gsplat.yaml")
|
| 85 |
+
dec_cfg = OmegaConf.merge(dec_cfg, DECODER_OVERRIDES)
|
| 86 |
+
dec_cfg = from_dict(
|
| 87 |
+
DecoderSplattingGsplatCfg,
|
| 88 |
+
OmegaConf.to_container(dec_cfg, resolve=True),
|
| 89 |
+
config=DaciteConfig(strict=False),
|
| 90 |
+
)
|
| 91 |
+
decoder = DecoderSplattingGsplat(dec_cfg, SimpleNamespace(background_color=[0.0, 0.0, 0.0]))
|
| 92 |
+
return decoder.eval()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ---- view selection ------------------------------------------------------------------
|
| 96 |
+
def farthest_point_sample(xyz: np.ndarray, npoint: int) -> np.ndarray:
|
| 97 |
+
n = len(xyz)
|
| 98 |
+
if npoint >= n:
|
| 99 |
+
return np.arange(n)
|
| 100 |
+
dist = np.full(n, 1e10)
|
| 101 |
+
farthest = int(np.argmax(((xyz - xyz.mean(0)) ** 2).sum(-1)))
|
| 102 |
+
out = []
|
| 103 |
+
for _ in range(npoint):
|
| 104 |
+
out.append(farthest)
|
| 105 |
+
d = ((xyz - xyz[farthest]) ** 2).sum(-1)
|
| 106 |
+
dist = np.minimum(dist, d)
|
| 107 |
+
farthest = int(np.argmax(dist))
|
| 108 |
+
return np.sort(np.array(out))
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def select_views(c2w: np.ndarray, num_context: int = 8, num_target: int = 8):
|
| 112 |
+
n = len(c2w)
|
| 113 |
+
ctx = farthest_point_sample(c2w[:, :3, 3], min(num_context, n))
|
| 114 |
+
rest = np.setdiff1d(np.arange(n), ctx)
|
| 115 |
+
if len(rest) > num_target:
|
| 116 |
+
rest = rest[np.linspace(0, len(rest) - 1, num_target).astype(int)]
|
| 117 |
+
return ctx, rest
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ---- batch construction (same conventions as the dataset loaders) --------------------
|
| 121 |
+
def _load_images(paths):
|
| 122 |
+
from PIL import Image
|
| 123 |
+
|
| 124 |
+
imgs = []
|
| 125 |
+
for p in paths:
|
| 126 |
+
im = np.asarray(Image.open(p).convert("RGB"), dtype=np.float32) / 255.0
|
| 127 |
+
imgs.append(torch.from_numpy(im).permute(2, 0, 1))
|
| 128 |
+
return torch.stack(imgs)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def image_shape_for(h: int, w: int) -> tuple[int, int]:
|
| 132 |
+
# The checkpoint is trained at 256x448 (landscape). Portrait inputs use 448x256.
|
| 133 |
+
return IMAGE_SHAPE if w >= h else (IMAGE_SHAPE[1], IMAGE_SHAPE[0])
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def build_example(image_paths, c2w, K, ctx_idx, tgt_idx):
|
| 137 |
+
"""Build a single (un-batched) example, apply the authors' crop + patch shims."""
|
| 138 |
+
images = _load_images(image_paths) # [N, 3, H, W]
|
| 139 |
+
shape = image_shape_for(images.shape[-2], images.shape[-1])
|
| 140 |
+
c2w = torch.as_tensor(c2w, dtype=torch.float32)
|
| 141 |
+
K = torch.as_tensor(K, dtype=torch.float32)
|
| 142 |
+
|
| 143 |
+
# Express all poses relative to the middle context camera (rigid change of frame).
|
| 144 |
+
pivot = c2w[ctx_idx[len(ctx_idx) // 2]]
|
| 145 |
+
c2w = torch.linalg.inv(pivot)[None] @ c2w
|
| 146 |
+
|
| 147 |
+
def views(idx):
|
| 148 |
+
idx = torch.as_tensor(np.asarray(idx), dtype=torch.long)
|
| 149 |
+
return {
|
| 150 |
+
"extrinsics": c2w[idx],
|
| 151 |
+
"intrinsics": K[idx],
|
| 152 |
+
"image": images[idx],
|
| 153 |
+
"near": torch.full((len(idx),), NEAR),
|
| 154 |
+
"far": torch.full((len(idx),), FAR),
|
| 155 |
+
"index": idx,
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
example = {"context": views(ctx_idx), "target": views(tgt_idx), "scene": "user_scene"}
|
| 159 |
+
example = apply_crop_shim(example, shape)
|
| 160 |
+
batch = {
|
| 161 |
+
"context": {k: v[None] for k, v in example["context"].items()},
|
| 162 |
+
"target": {k: v[None] for k, v in example["target"].items()},
|
| 163 |
+
"scene": ["user_scene"],
|
| 164 |
+
}
|
| 165 |
+
# shim_patch_size * downscale_factor = 16 * 4
|
| 166 |
+
return apply_patch_shim(batch, patch_size=64)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def to_device(batch, device):
|
| 170 |
+
out = {}
|
| 171 |
+
for k, v in batch.items():
|
| 172 |
+
if isinstance(v, dict):
|
| 173 |
+
out[k] = {kk: vv.to(device) for kk, vv in v.items()}
|
| 174 |
+
else:
|
| 175 |
+
out[k] = v
|
| 176 |
+
return out
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# ---- smooth fly-through trajectory through the context cameras ----------------------
|
| 180 |
+
def order_cameras(c2w: torch.Tensor) -> list[int]:
|
| 181 |
+
"""Greedy nearest-neighbour ordering of cameras (start from an extreme one)."""
|
| 182 |
+
pos = c2w[:, :3, 3].cpu().numpy()
|
| 183 |
+
n = len(pos)
|
| 184 |
+
if n <= 2:
|
| 185 |
+
return list(range(n))
|
| 186 |
+
start = int(np.argmax(((pos - pos.mean(0)) ** 2).sum(-1)))
|
| 187 |
+
order, left = [start], set(range(n)) - {start}
|
| 188 |
+
while left:
|
| 189 |
+
last = pos[order[-1]]
|
| 190 |
+
nxt = min(left, key=lambda i: float(((pos[i] - last) ** 2).sum()))
|
| 191 |
+
order.append(nxt)
|
| 192 |
+
left.remove(nxt)
|
| 193 |
+
return order
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def interpolate_trajectory(c2w: torch.Tensor, K: torch.Tensor, frames_per_segment: int = 12):
|
| 197 |
+
order = order_cameras(c2w)
|
| 198 |
+
c2w = c2w[order].cpu().double().numpy()
|
| 199 |
+
K = K[order].cpu()
|
| 200 |
+
poses, intrs = [], []
|
| 201 |
+
for i in range(len(order) - 1):
|
| 202 |
+
rots = R.from_matrix(c2w[i : i + 2, :3, :3])
|
| 203 |
+
slerp = Slerp([0.0, 1.0], rots)
|
| 204 |
+
for t in np.linspace(0, 1, frames_per_segment, endpoint=False):
|
| 205 |
+
m = np.eye(4)
|
| 206 |
+
m[:3, :3] = slerp([t]).as_matrix()[0]
|
| 207 |
+
m[:3, 3] = (1 - t) * c2w[i, :3, 3] + t * c2w[i + 1, :3, 3]
|
| 208 |
+
poses.append(m)
|
| 209 |
+
intrs.append((1 - t) * K[i] + t * K[i + 1])
|
| 210 |
+
last = np.eye(4)
|
| 211 |
+
last[:3] = c2w[-1, :3]
|
| 212 |
+
poses.append(last)
|
| 213 |
+
intrs.append(K[-1])
|
| 214 |
+
poses = poses + poses[::-1][1:] # ping-pong loop
|
| 215 |
+
intrs = intrs + intrs[::-1][1:]
|
| 216 |
+
return (
|
| 217 |
+
torch.tensor(np.stack(poses), dtype=torch.float32),
|
| 218 |
+
torch.stack([torch.as_tensor(k, dtype=torch.float32) for k in intrs]),
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
# ---- core inference ------------------------------------------------------------------
|
| 223 |
+
@torch.no_grad()
|
| 224 |
+
def encode_decode(encoder, batch, lmb: float):
|
| 225 |
+
"""Full learned-codec round trip: context -> bitstream -> Gaussians."""
|
| 226 |
+
encoder.feature_codec.cfg.lmb = float(lmb)
|
| 227 |
+
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=batch["context"]["image"].is_cuda):
|
| 228 |
+
out = encoder(batch["context"], 0, visualization_dump={}, return_bitstream=True)
|
| 229 |
+
return out
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
@torch.no_grad()
|
| 233 |
+
def render(decoder, gaussians, extrinsics, intrinsics, hw, chunk: int = 16):
|
| 234 |
+
"""Render [V] views. `extrinsics`/`intrinsics` are [V,4,4]/[V,3,3] on device."""
|
| 235 |
+
v = extrinsics.shape[0]
|
| 236 |
+
near = torch.full((1, v), NEAR, device=extrinsics.device)
|
| 237 |
+
far = torch.full((1, v), FAR, device=extrinsics.device)
|
| 238 |
+
colors = []
|
| 239 |
+
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=extrinsics.is_cuda):
|
| 240 |
+
for s in range(0, v, chunk):
|
| 241 |
+
o = decoder.forward(
|
| 242 |
+
gaussians,
|
| 243 |
+
extrinsics[None, s : s + chunk],
|
| 244 |
+
intrinsics[None, s : s + chunk],
|
| 245 |
+
near[:, s : s + chunk],
|
| 246 |
+
far[:, s : s + chunk],
|
| 247 |
+
hw,
|
| 248 |
+
depth_mode=None,
|
| 249 |
+
)
|
| 250 |
+
colors.append(o.color[0].float().clamp(0, 1))
|
| 251 |
+
return torch.cat(colors, dim=0)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def psnr(gt: torch.Tensor, pred: torch.Tensor) -> float:
|
| 255 |
+
mse = ((gt - pred) ** 2).mean(dim=(1, 2, 3)).clamp_min(1e-10)
|
| 256 |
+
return float((-10 * torch.log10(mse)).mean())
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# ---- Gaussian export -----------------------------------------------------------------
|
| 260 |
+
def world_rotations(rotations: torch.Tensor, context_c2w: torch.Tensor) -> torch.Tensor:
|
| 261 |
+
"""Per-pixel Gaussian rotations are predicted in each context camera's frame."""
|
| 262 |
+
v = context_c2w.shape[0]
|
| 263 |
+
n = rotations.shape[0]
|
| 264 |
+
c2w_rot = repeat(context_c2w[:, :3, :3], "v i j -> (v r) i j", r=n // v)
|
| 265 |
+
local = R.from_quat(rotations.cpu().double().numpy()).as_matrix()
|
| 266 |
+
world = c2w_rot.cpu().double().numpy() @ local
|
| 267 |
+
return torch.from_numpy(R.from_matrix(world).as_quat()).float()
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def write_ply(
|
| 271 |
+
path,
|
| 272 |
+
pivot_c2w: torch.Tensor,
|
| 273 |
+
means: torch.Tensor,
|
| 274 |
+
scales: torch.Tensor,
|
| 275 |
+
rotations_xyzw_world: torch.Tensor,
|
| 276 |
+
harmonics: torch.Tensor,
|
| 277 |
+
opacities: torch.Tensor,
|
| 278 |
+
):
|
| 279 |
+
"""Same output as the authors' `export_ply(..., align_to_view=True)`, vectorised."""
|
| 280 |
+
means = means.float().cpu()
|
| 281 |
+
scales = scales.float().cpu()
|
| 282 |
+
harmonics = harmonics.float().cpu()
|
| 283 |
+
opacities = opacities.float().cpu()
|
| 284 |
+
pivot = pivot_c2w.float().cpu()
|
| 285 |
+
|
| 286 |
+
view_rot = pivot[:3, :3].T
|
| 287 |
+
means = (means - pivot[:3, 3]) @ view_rot.T
|
| 288 |
+
harmonics = rotate_sh(harmonics, view_rot)
|
| 289 |
+
rot = R.from_quat(rotations_xyzw_world.double().numpy()).as_matrix()
|
| 290 |
+
rot = R.from_matrix(view_rot.double().numpy() @ rot).as_quat() # xyzw
|
| 291 |
+
rot = np.concatenate([rot[:, 3:4], rot[:, :3]], axis=1) # wxyz
|
| 292 |
+
|
| 293 |
+
dc = harmonics[..., 0].numpy()
|
| 294 |
+
rest = harmonics[..., 1:].flatten(start_dim=1).numpy()
|
| 295 |
+
logit_op = torch.logit(opacities.clamp(1e-6, 1 - 1e-6))[:, None].numpy()
|
| 296 |
+
log_scales = scales.clamp_min(1e-12).log().numpy()
|
| 297 |
+
m = means.numpy()
|
| 298 |
+
|
| 299 |
+
cols = [m, np.zeros_like(m), dc, rest, logit_op, log_scales, rot]
|
| 300 |
+
data = np.concatenate(cols, axis=1).astype(np.float32)
|
| 301 |
+
names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2"]
|
| 302 |
+
names += [f"f_rest_{i}" for i in range(rest.shape[1])]
|
| 303 |
+
names += ["opacity", "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
|
| 304 |
+
arr = np.empty(data.shape[0], dtype=[(n, "f4") for n in names])
|
| 305 |
+
for i, n in enumerate(names):
|
| 306 |
+
arr[n] = data[:, i]
|
| 307 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 308 |
+
PlyData([PlyElement.describe(arr, "vertex")]).write(str(path))
|
| 309 |
+
return str(path)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def export_gaussians(g: dict, context_c2w: torch.Tensor, outdir: Path,
|
| 313 |
+
viewer_max: int = 600_000, viewer_min_opacity: float = 0.02):
|
| 314 |
+
"""Write a compact (DC colour, pruned), re-framed .ply of the Gaussians for the web viewer."""
|
| 315 |
+
pivot = context_c2w[len(context_c2w) // 2]
|
| 316 |
+
rot_world = world_rotations(g["rotations"], context_c2w)
|
| 317 |
+
|
| 318 |
+
keep = g["opacities"] > viewer_min_opacity
|
| 319 |
+
if int(keep.sum()) < 1000:
|
| 320 |
+
keep = torch.ones_like(keep)
|
| 321 |
+
idx = torch.nonzero(keep).squeeze(1)
|
| 322 |
+
if idx.numel() > viewer_max:
|
| 323 |
+
idx = idx[torch.randperm(idx.numel())[:viewer_max]]
|
| 324 |
+
means, scales = g["means"][idx], g["scales"][idx]
|
| 325 |
+
|
| 326 |
+
# Re-centre / re-scale (in the pivot camera frame) so the viewer's orbit camera frames it.
|
| 327 |
+
view_rot = pivot[:3, :3].T
|
| 328 |
+
local = (means - pivot[:3, 3]) @ view_rot.T
|
| 329 |
+
center = local.median(dim=0).values
|
| 330 |
+
radius = torch.quantile((local - center).norm(dim=-1).float(), 0.7)
|
| 331 |
+
s = float(3.0 / max(float(radius), 1e-6))
|
| 332 |
+
# shift in world coordinates so that the pivot-frame centre maps to the origin
|
| 333 |
+
means = (means - pivot[:3, 3] - center @ view_rot) * s + pivot[:3, 3]
|
| 334 |
+
viewer_path = write_ply(outdir / "codecsplat_viewer.ply", pivot, means, scales * s,
|
| 335 |
+
rot_world[idx], g["harmonics"][idx][..., :1], g["opacities"][idx])
|
| 336 |
+
return viewer_path
|
colmap_sfm.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""COLMAP structure-from-motion preprocessing (runs inside the Space, on CPU).
|
| 2 |
+
|
| 3 |
+
Takes a set of raw multi-view images of a scene, estimates camera intrinsics /
|
| 4 |
+
poses / a sparse point cloud with COLMAP (via `pycolmap`), undistorts the images
|
| 5 |
+
to a PINHOLE camera model and writes a standard COLMAP scene layout
|
| 6 |
+
|
| 7 |
+
<out>/images/*.jpg
|
| 8 |
+
<out>/sparse/{cameras,images,points3D}.bin
|
| 9 |
+
|
| 10 |
+
which is exactly what CodecSplat's reference COLMAP loader expects.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import shutil
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import pycolmap
|
| 19 |
+
from PIL import Image, ImageOps
|
| 20 |
+
|
| 21 |
+
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def prepare_images(image_paths, dest: Path, max_image_size: int = 1280) -> list[str]:
|
| 25 |
+
"""Copy the uploaded images into `dest`, EXIF-rotated, RGB, size-capped."""
|
| 26 |
+
dest.mkdir(parents=True, exist_ok=True)
|
| 27 |
+
names: list[str] = []
|
| 28 |
+
for i, src in enumerate(str(p) for p in image_paths): # caller decides the order
|
| 29 |
+
if not src.lower().endswith(IMAGE_EXT):
|
| 30 |
+
continue
|
| 31 |
+
try:
|
| 32 |
+
img = Image.open(src)
|
| 33 |
+
img = ImageOps.exif_transpose(img).convert("RGB")
|
| 34 |
+
except Exception as exc:
|
| 35 |
+
print(f"[colmap] skipping unreadable image {src}: {exc}", flush=True)
|
| 36 |
+
continue
|
| 37 |
+
scale = max_image_size / max(img.size)
|
| 38 |
+
if scale < 1.0:
|
| 39 |
+
img = img.resize(
|
| 40 |
+
(max(1, int(round(img.width * scale))), max(1, int(round(img.height * scale)))),
|
| 41 |
+
Image.LANCZOS,
|
| 42 |
+
)
|
| 43 |
+
name = f"{i:04d}.jpg"
|
| 44 |
+
img.save(dest / name, quality=95)
|
| 45 |
+
names.append(name)
|
| 46 |
+
return names
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _largest_reconstruction(maps) -> pycolmap.Reconstruction | None:
|
| 50 |
+
recs = list(maps.values()) if isinstance(maps, dict) else list(maps)
|
| 51 |
+
if not recs:
|
| 52 |
+
return None
|
| 53 |
+
return max(recs, key=lambda r: r.num_reg_images())
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def run_sfm(
|
| 57 |
+
image_paths,
|
| 58 |
+
workdir: Path,
|
| 59 |
+
max_image_size: int = 1280,
|
| 60 |
+
matcher: str = "exhaustive",
|
| 61 |
+
max_num_features: int = 8192,
|
| 62 |
+
log=print,
|
| 63 |
+
) -> dict:
|
| 64 |
+
"""Run COLMAP SfM on `image_paths`; return info about the written scene.
|
| 65 |
+
|
| 66 |
+
Returns a dict with `scene_dir`, `sparse_sub`, `images_sub`, `num_input`,
|
| 67 |
+
`num_registered`, `num_points3D`, `camera_model`.
|
| 68 |
+
"""
|
| 69 |
+
workdir = Path(workdir)
|
| 70 |
+
image_dir = workdir / "sfm_images"
|
| 71 |
+
names = prepare_images(image_paths, image_dir, max_image_size)
|
| 72 |
+
if len(names) < 3:
|
| 73 |
+
raise ValueError(
|
| 74 |
+
f"COLMAP needs at least 3 usable images, got {len(names)}. "
|
| 75 |
+
"Upload several overlapping photos of the same scene."
|
| 76 |
+
)
|
| 77 |
+
log(f"[colmap] {len(names)} images prepared at <= {max_image_size}px")
|
| 78 |
+
|
| 79 |
+
database_path = workdir / "colmap.db"
|
| 80 |
+
if database_path.exists():
|
| 81 |
+
database_path.unlink()
|
| 82 |
+
|
| 83 |
+
extraction_options = pycolmap.FeatureExtractionOptions()
|
| 84 |
+
extraction_options.sift.max_num_features = int(max_num_features)
|
| 85 |
+
extraction_options.sift.estimate_affine_shape = False
|
| 86 |
+
extraction_options.sift.domain_size_pooling = False
|
| 87 |
+
extraction_options.use_gpu = False
|
| 88 |
+
extraction_options.max_image_size = int(max_image_size)
|
| 89 |
+
|
| 90 |
+
reader_options = pycolmap.ImageReaderOptions()
|
| 91 |
+
reader_options.camera_model = "SIMPLE_RADIAL"
|
| 92 |
+
|
| 93 |
+
pycolmap.extract_features(
|
| 94 |
+
database_path=database_path,
|
| 95 |
+
image_path=image_dir,
|
| 96 |
+
camera_mode=pycolmap.CameraMode.SINGLE,
|
| 97 |
+
reader_options=reader_options,
|
| 98 |
+
extraction_options=extraction_options,
|
| 99 |
+
device=pycolmap.Device.cpu,
|
| 100 |
+
)
|
| 101 |
+
log("[colmap] features extracted")
|
| 102 |
+
|
| 103 |
+
if matcher == "sequential":
|
| 104 |
+
pairing = pycolmap.SequentialPairingOptions()
|
| 105 |
+
pairing.overlap = 10
|
| 106 |
+
pairing.quadratic_overlap = True
|
| 107 |
+
pycolmap.match_sequential(
|
| 108 |
+
database_path=database_path,
|
| 109 |
+
pairing_options=pairing,
|
| 110 |
+
device=pycolmap.Device.cpu,
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
pycolmap.match_exhaustive(database_path=database_path, device=pycolmap.Device.cpu)
|
| 114 |
+
log(f"[colmap] {matcher} matching done")
|
| 115 |
+
|
| 116 |
+
sparse_dir = workdir / "sparse_raw"
|
| 117 |
+
sparse_dir.mkdir(parents=True, exist_ok=True)
|
| 118 |
+
options = pycolmap.IncrementalPipelineOptions()
|
| 119 |
+
options.min_num_matches = 15
|
| 120 |
+
options.multiple_models = False
|
| 121 |
+
options.extract_colors = True
|
| 122 |
+
maps = pycolmap.incremental_mapping(
|
| 123 |
+
database_path=database_path,
|
| 124 |
+
image_path=image_dir,
|
| 125 |
+
output_path=sparse_dir,
|
| 126 |
+
options=options,
|
| 127 |
+
)
|
| 128 |
+
rec = _largest_reconstruction(maps)
|
| 129 |
+
if rec is None or rec.num_reg_images() < 3:
|
| 130 |
+
raise ValueError(
|
| 131 |
+
"COLMAP could not reconstruct this image set (only "
|
| 132 |
+
f"{0 if rec is None else rec.num_reg_images()} of {len(names)} images registered). "
|
| 133 |
+
"Use photos with plenty of visual overlap between neighbours, avoid pure "
|
| 134 |
+
"rotation / blurry frames, and keep the scene static."
|
| 135 |
+
)
|
| 136 |
+
# Deterministic world scale (same convention COLMAP itself uses), so that the
|
| 137 |
+
# fixed near/far planes of the CodecSplat checkpoint stay meaningful.
|
| 138 |
+
rec.normalize(fixed_scale=False, extent=10.0)
|
| 139 |
+
log(f"[colmap] mapped {rec.num_reg_images()}/{len(names)} images, {rec.num_points3D()} points")
|
| 140 |
+
|
| 141 |
+
model_dir = workdir / "sparse_model"
|
| 142 |
+
if model_dir.exists():
|
| 143 |
+
shutil.rmtree(model_dir)
|
| 144 |
+
model_dir.mkdir(parents=True)
|
| 145 |
+
rec.write(str(model_dir))
|
| 146 |
+
|
| 147 |
+
scene_dir = workdir / "colmap_scene"
|
| 148 |
+
if scene_dir.exists():
|
| 149 |
+
shutil.rmtree(scene_dir)
|
| 150 |
+
pycolmap.undistort_images(
|
| 151 |
+
output_path=scene_dir,
|
| 152 |
+
input_path=model_dir,
|
| 153 |
+
image_path=image_dir,
|
| 154 |
+
)
|
| 155 |
+
log("[colmap] undistorted to PINHOLE")
|
| 156 |
+
|
| 157 |
+
# Keep only what CodecSplat reads (the undistorter also writes stereo/ scaffolding).
|
| 158 |
+
for extra in ("stereo",):
|
| 159 |
+
p = scene_dir / extra
|
| 160 |
+
if p.is_dir():
|
| 161 |
+
shutil.rmtree(p, ignore_errors=True)
|
| 162 |
+
|
| 163 |
+
cam_model = "PINHOLE"
|
| 164 |
+
try:
|
| 165 |
+
cam_model = list(rec.cameras.values())[0].model.name
|
| 166 |
+
except Exception:
|
| 167 |
+
pass
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"scene_dir": str(scene_dir),
|
| 171 |
+
"sparse_sub": "sparse",
|
| 172 |
+
"images_sub": "images",
|
| 173 |
+
"num_input": len(names),
|
| 174 |
+
"num_registered": int(rec.num_reg_images()),
|
| 175 |
+
"num_points3D": int(rec.num_points3D()),
|
| 176 |
+
"camera_model": cam_model,
|
| 177 |
+
}
|
config/dataset/dl3dv.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- view_sampler: boundedv2_360
|
| 3 |
+
|
| 4 |
+
name: dl3dv
|
| 5 |
+
roots: [datasets/DL3DV-ALL-480P-Processed]
|
| 6 |
+
augment: true
|
| 7 |
+
|
| 8 |
+
image_shape: [270, 480]
|
| 9 |
+
background_color: [0.0, 0.0, 0.0]
|
| 10 |
+
cameras_are_circular: false
|
| 11 |
+
|
| 12 |
+
max_fov: 100.0
|
| 13 |
+
|
| 14 |
+
skip_bad_shape: true
|
| 15 |
+
near: -1.
|
| 16 |
+
far: -1.
|
| 17 |
+
shuffle_val: true
|
| 18 |
+
test_len: -1
|
| 19 |
+
test_chunk_interval: 1
|
| 20 |
+
sort_target_index: true
|
| 21 |
+
sort_context_index: true
|
| 22 |
+
|
| 23 |
+
train_times_per_scene: 1
|
| 24 |
+
test_times_per_scene: 1
|
| 25 |
+
ori_image_shape: [270, 480]
|
| 26 |
+
overfit_max_views: 148
|
| 27 |
+
use_index_to_load_chunk: false
|
config/dataset/re10k.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- view_sampler: bounded
|
| 3 |
+
|
| 4 |
+
name: re10k
|
| 5 |
+
roots: [datasets/RE10K-Torch-360p]
|
| 6 |
+
augment: true
|
| 7 |
+
|
| 8 |
+
image_shape: [180, 320]
|
| 9 |
+
background_color: [0.0, 0.0, 0.0]
|
| 10 |
+
cameras_are_circular: false
|
| 11 |
+
|
| 12 |
+
max_fov: 100.0
|
| 13 |
+
|
| 14 |
+
skip_bad_shape: true
|
| 15 |
+
near: -1.
|
| 16 |
+
far: -1.
|
| 17 |
+
shuffle_val: true
|
| 18 |
+
test_len: -1
|
| 19 |
+
test_chunk_interval: 1
|
| 20 |
+
|
| 21 |
+
use_index_to_load_chunk: false
|
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,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: bounded
|
| 2 |
+
|
| 3 |
+
num_target_views: 1
|
| 4 |
+
num_context_views: 2
|
| 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
|
config/dataset/view_sampler/boundedv2.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: boundedv2
|
| 2 |
+
|
| 3 |
+
num_target_views: 1
|
| 4 |
+
num_context_views: 2
|
| 5 |
+
|
| 6 |
+
min_distance_between_context_views: 2
|
| 7 |
+
max_distance_between_context_views: 6
|
| 8 |
+
max_distance_to_context_views: 0
|
| 9 |
+
|
| 10 |
+
context_gap_warm_up_steps: 0
|
| 11 |
+
target_gap_warm_up_steps: 0
|
| 12 |
+
|
| 13 |
+
initial_min_distance_between_context_views: 2
|
| 14 |
+
initial_max_distance_between_context_views: 6
|
| 15 |
+
initial_max_distance_to_context_views: 0
|
config/dataset/view_sampler/boundedv2_360.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: boundedv2
|
| 2 |
+
|
| 3 |
+
num_target_views: 4
|
| 4 |
+
num_context_views: 4
|
| 5 |
+
|
| 6 |
+
min_distance_between_context_views: 20
|
| 7 |
+
max_distance_between_context_views: 50
|
| 8 |
+
max_distance_to_context_views: 0
|
| 9 |
+
|
| 10 |
+
context_gap_warm_up_steps: 10000
|
| 11 |
+
target_gap_warm_up_steps: 0
|
| 12 |
+
|
| 13 |
+
initial_min_distance_between_context_views: 15
|
| 14 |
+
initial_max_distance_between_context_views: 30
|
| 15 |
+
initial_max_distance_to_context_views: 0
|
| 16 |
+
extra_views_sampling_strategy: farthest_point
|
| 17 |
+
target_views_replace_sample: false
|
config/dataset/view_sampler/evaluation.yaml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: evaluation
|
| 2 |
+
|
| 3 |
+
index_path: assets/evaluation_index_re10k_video.json
|
| 4 |
+
num_context_views: 2
|
config/dataset/view_sampler_dataset_specific_config/bounded_re10k.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
dataset:
|
| 4 |
+
view_sampler:
|
| 5 |
+
min_distance_between_context_views: 45
|
| 6 |
+
max_distance_between_context_views: 135
|
| 7 |
+
min_distance_to_context_views: 0
|
| 8 |
+
warm_up_steps: 30000
|
| 9 |
+
initial_min_distance_between_context_views: 25
|
| 10 |
+
initial_max_distance_between_context_views: 45
|
| 11 |
+
num_target_views: 4
|
config/dataset/view_sampler_dataset_specific_config/boundedv2_dl3dv.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
dataset:
|
| 4 |
+
view_sampler:
|
| 5 |
+
min_distance_between_context_views: 20
|
| 6 |
+
max_distance_between_context_views: 50
|
| 7 |
+
max_distance_to_context_views: 0
|
| 8 |
+
context_gap_warm_up_steps: 10000
|
| 9 |
+
target_gap_warm_up_steps: 0
|
| 10 |
+
initial_min_distance_between_context_views: 15
|
| 11 |
+
initial_max_distance_between_context_views: 30
|
| 12 |
+
initial_max_distance_to_context_views: 0
|
| 13 |
+
extra_views_sampling_strategy: farthest_point
|
| 14 |
+
num_target_views: 4
|
config/dataset/view_sampler_dataset_specific_config/evaluation_dl3dv.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
dataset:
|
| 4 |
+
image_shape: [256, 448]
|
| 5 |
+
view_sampler:
|
| 6 |
+
index_path: assets/dl3dv_evaluation/dl3dv_start_0_distance_40_ctx_8v_tgt_8v.json
|
| 7 |
+
num_context_views: 8
|
config/dataset/view_sampler_dataset_specific_config/evaluation_re10k.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
dataset:
|
| 4 |
+
view_sampler:
|
| 5 |
+
index_path: assets/evaluation_index_re10k.json
|
config/experiment/dl3dv.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- override /dataset: dl3dv
|
| 5 |
+
- override /model/encoder: codecsplat
|
| 6 |
+
- override /loss: [mse, lpips]
|
| 7 |
+
- override /dataset/view_sampler: boundedv2_360
|
| 8 |
+
|
| 9 |
+
wandb:
|
| 10 |
+
name: dl3dv
|
| 11 |
+
tags: [dl3dv, 270x480]
|
| 12 |
+
|
| 13 |
+
data_loader:
|
| 14 |
+
train:
|
| 15 |
+
batch_size: 1
|
| 16 |
+
|
| 17 |
+
trainer:
|
| 18 |
+
max_steps: 300_001
|
| 19 |
+
num_nodes: 1
|
| 20 |
+
|
| 21 |
+
model:
|
| 22 |
+
encoder:
|
| 23 |
+
num_depth_candidates: 128
|
| 24 |
+
shim_patch_size: 16
|
| 25 |
+
|
| 26 |
+
# lpips loss
|
| 27 |
+
loss:
|
| 28 |
+
lpips:
|
| 29 |
+
apply_after_step: 0
|
| 30 |
+
weight: 0.05
|
| 31 |
+
|
| 32 |
+
dataset:
|
| 33 |
+
near: 0.5
|
| 34 |
+
far: 200.
|
| 35 |
+
min_views: 0
|
| 36 |
+
max_views: 0
|
| 37 |
+
highres: false
|
| 38 |
+
|
| 39 |
+
test:
|
| 40 |
+
eval_time_skip_steps: 0
|
| 41 |
+
compute_scores: true
|
config/experiment/re10k.yaml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @package _global_
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- override /dataset: re10k
|
| 5 |
+
- override /model/encoder: codecsplat
|
| 6 |
+
- override /loss: [mse, lpips]
|
| 7 |
+
|
| 8 |
+
wandb:
|
| 9 |
+
name: re10k
|
| 10 |
+
tags: [re10k, 256x256]
|
| 11 |
+
|
| 12 |
+
data_loader:
|
| 13 |
+
train:
|
| 14 |
+
batch_size: 14
|
| 15 |
+
|
| 16 |
+
trainer:
|
| 17 |
+
max_steps: 300_001
|
| 18 |
+
num_nodes: 1
|
| 19 |
+
|
| 20 |
+
model:
|
| 21 |
+
encoder:
|
| 22 |
+
num_depth_candidates: 128
|
| 23 |
+
|
| 24 |
+
# lpips loss
|
| 25 |
+
loss:
|
| 26 |
+
lpips:
|
| 27 |
+
apply_after_step: 0
|
| 28 |
+
weight: 0.05
|
| 29 |
+
|
| 30 |
+
dataset:
|
| 31 |
+
image_shape: [256, 256]
|
| 32 |
+
roots: [datasets/RE10K-Torch-360p]
|
| 33 |
+
near: 0.5
|
| 34 |
+
far: 100.
|
| 35 |
+
train_times_per_scene: 1
|
| 36 |
+
highres: false
|
| 37 |
+
|
| 38 |
+
test:
|
| 39 |
+
eval_time_skip_steps: 0
|
| 40 |
+
compute_scores: true
|
config/loss/lpips.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lpips:
|
| 2 |
+
weight: 0.05
|
| 3 |
+
apply_after_step: 150_000
|
config/loss/mse.yaml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mse:
|
| 2 |
+
weight: 1.0
|
config/main.yaml
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- dataset: re10k
|
| 3 |
+
- optional dataset/view_sampler_dataset_specific_config: ${dataset/view_sampler}_${dataset}
|
| 4 |
+
- model/encoder: codecsplat
|
| 5 |
+
- model/decoder: splatting_gsplat
|
| 6 |
+
- loss: [mse]
|
| 7 |
+
- _self_
|
| 8 |
+
|
| 9 |
+
wandb:
|
| 10 |
+
project: codecsplat
|
| 11 |
+
entity: null
|
| 12 |
+
name: placeholder
|
| 13 |
+
mode: online
|
| 14 |
+
id: null
|
| 15 |
+
|
| 16 |
+
mode: train
|
| 17 |
+
|
| 18 |
+
dataset:
|
| 19 |
+
overfit_to_scene: null
|
| 20 |
+
|
| 21 |
+
data_loader:
|
| 22 |
+
train:
|
| 23 |
+
num_workers: 10
|
| 24 |
+
persistent_workers: true
|
| 25 |
+
batch_size: 4
|
| 26 |
+
seed: 1234
|
| 27 |
+
test:
|
| 28 |
+
num_workers: 4
|
| 29 |
+
persistent_workers: false
|
| 30 |
+
batch_size: 1
|
| 31 |
+
seed: 2345
|
| 32 |
+
val:
|
| 33 |
+
num_workers: 1
|
| 34 |
+
persistent_workers: true
|
| 35 |
+
batch_size: 1
|
| 36 |
+
seed: 3456
|
| 37 |
+
|
| 38 |
+
optimizer:
|
| 39 |
+
lr: 2.e-4
|
| 40 |
+
lr_monodepth: 2.e-6
|
| 41 |
+
warm_up_steps: 2000
|
| 42 |
+
weight_decay: 0.01
|
| 43 |
+
div_factor: 25.
|
| 44 |
+
final_div_factor: 1.e4
|
| 45 |
+
|
| 46 |
+
checkpointing:
|
| 47 |
+
load: null
|
| 48 |
+
every_n_train_steps: 5000
|
| 49 |
+
save_top_k: 5
|
| 50 |
+
pretrained_model: null
|
| 51 |
+
pretrained_monodepth: null
|
| 52 |
+
pretrained_mvdepth: null
|
| 53 |
+
pretrained_depth: null
|
| 54 |
+
no_strict_load: false
|
| 55 |
+
resume: false
|
| 56 |
+
|
| 57 |
+
train:
|
| 58 |
+
depth_mode: null
|
| 59 |
+
extended_visualization: false
|
| 60 |
+
print_log_every_n_steps: 100
|
| 61 |
+
eval_model_every_n_val: 2 # quantitative evaluation every n val
|
| 62 |
+
eval_data_length: 999999
|
| 63 |
+
eval_time_skip_steps: 3
|
| 64 |
+
eval_save_model: true
|
| 65 |
+
l1_loss: false
|
| 66 |
+
intermediate_loss_weight: 0.9
|
| 67 |
+
no_viz_video: false
|
| 68 |
+
train_ignore_large_loss: 0.
|
| 69 |
+
|
| 70 |
+
test:
|
| 71 |
+
compute_scores: true
|
| 72 |
+
eval_time_skip_steps: 0
|
| 73 |
+
save_image: false
|
| 74 |
+
save_video: false
|
| 75 |
+
save_gt_image: false
|
| 76 |
+
save_input_images: false
|
| 77 |
+
save_depth: false
|
| 78 |
+
save_depth_npy: false
|
| 79 |
+
save_depth_concat_img: false
|
| 80 |
+
save_gaussian: false
|
| 81 |
+
save_bitstream: false
|
| 82 |
+
render_chunk_size: null
|
| 83 |
+
stabilize_camera: false
|
| 84 |
+
stab_camera_kernel: 50
|
| 85 |
+
gaussian_export_all_points: true
|
| 86 |
+
gaussian_export_align_to_view: false
|
| 87 |
+
|
| 88 |
+
seed: 111123
|
| 89 |
+
|
| 90 |
+
trainer:
|
| 91 |
+
max_steps: -1
|
| 92 |
+
val_check_interval: 0.5
|
| 93 |
+
gradient_clip_val: 0.5
|
| 94 |
+
accumulate_grad_batches: 1
|
| 95 |
+
precision: 32-true
|
| 96 |
+
num_sanity_val_steps: 2
|
| 97 |
+
num_nodes: 1
|
| 98 |
+
|
| 99 |
+
output_dir: checkpoints/tmp
|
| 100 |
+
|
| 101 |
+
use_plugins: false
|
| 102 |
+
|
| 103 |
+
hydra:
|
| 104 |
+
run:
|
| 105 |
+
dir: ${output_dir}/hydra/${now:%Y-%m-%d-%H-%M-%S}
|
| 106 |
+
job:
|
| 107 |
+
chdir: false
|
| 108 |
+
output_subdir: .
|
| 109 |
+
job_logging:
|
| 110 |
+
handlers:
|
| 111 |
+
file:
|
| 112 |
+
filename: /dev/null
|
| 113 |
+
root:
|
| 114 |
+
handlers:
|
| 115 |
+
- console
|
config/model/decoder/splatting_cuda.yaml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
name: splatting_cuda
|
config/model/decoder/splatting_gsplat.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: splatting_gsplat
|
| 2 |
+
render_mode: RGB
|
| 3 |
+
rasterize_mode: antialiased
|
| 4 |
+
use_covariances: true
|
| 5 |
+
packed: true
|
| 6 |
+
eps2d: 0.1
|
| 7 |
+
sparse_grad: false
|
| 8 |
+
absgrad: false
|
| 9 |
+
channel_chunk: 32
|
| 10 |
+
scale_invariant: false
|
config/model/encoder/codecsplat.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: codecsplat
|
| 2 |
+
|
| 3 |
+
num_depth_candidates: 128
|
| 4 |
+
num_surfaces: 1
|
| 5 |
+
|
| 6 |
+
gaussian_adapter:
|
| 7 |
+
gaussian_scale_min: 1e-10
|
| 8 |
+
gaussian_scale_max: 3.
|
| 9 |
+
sh_degree: 2
|
| 10 |
+
exp_scale_bias: 4.
|
| 11 |
+
no_rotate_sh: false
|
| 12 |
+
no_sh_mask: false
|
| 13 |
+
|
| 14 |
+
# Input alignment
|
| 15 |
+
downscale_factor: 4
|
| 16 |
+
shim_patch_size: 4
|
| 17 |
+
|
| 18 |
+
# monodepth
|
| 19 |
+
monodepth_vit_type: vits
|
| 20 |
+
|
| 21 |
+
# return depth
|
| 22 |
+
supervise_intermediate_depth: true
|
| 23 |
+
return_depth: true
|
| 24 |
+
|
| 25 |
+
# mv_unimatch
|
| 26 |
+
num_scales: 1
|
| 27 |
+
upsample_factor: 4
|
| 28 |
+
lowest_feature_resolution: 4
|
| 29 |
+
depth_unet_channels: 128
|
| 30 |
+
grid_sample_disable_cudnn: false
|
| 31 |
+
local_mv_match: 2
|
| 32 |
+
|
| 33 |
+
# color branch
|
| 34 |
+
init_sh_input_img: false
|
| 35 |
+
gaussian_regressor_channels: 64
|
| 36 |
+
|
| 37 |
+
# feature codec path
|
| 38 |
+
enable_feature_codec: false
|
| 39 |
+
enable_depth_guided_mv_refiner: false
|
| 40 |
+
enable_post_vae_mv_refiner: false
|
| 41 |
+
codec_loss_weight: 1.0
|
| 42 |
+
codec_depth_loss_weight: 0.1
|
| 43 |
+
codec_lmb_range: [16.0, 1024.0]
|
| 44 |
+
freeze_legacy_modules_for_codec: false
|
| 45 |
+
codec_estimated_feature_num_bytes: false
|
| 46 |
+
deflate_feature_ablation: false
|
| 47 |
+
deflate_feature_dtype: float16
|
| 48 |
+
deflate_feature_level: 9
|
examples/scene_a/00.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/01.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/02.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/03.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/04.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/05.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/06.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/07.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/08.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/09.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/10.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/11.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/12.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/13.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/14.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/15.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/16.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/17.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/18.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/19.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/20.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/21.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/22.jpg
ADDED
|
Git LFS Details
|
examples/scene_a/23.jpg
ADDED
|
Git LFS Details
|