SteEsp commited on
Commit
70cba97
·
1 Parent(s): 8407819

Switch the demo to the gradio GUI

Browse files

Run demo.py --with-gui gradio (Dockerfile CMD); add gradio + scipy to
requirements; refresh README. Sync the optgs source + demo.py to the current
upstream-merged tree (the gradio GUI: live decoder-render stream + interactive
Model3D, with the Z-up->Y-up splat reorientation and the thread-safe
optimize_iter). Prebuilt wheels and submodules are unchanged.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +6 -16
  2. README.md +5 -3
  3. demo.py +379 -6
  4. optgs/config.py +75 -88
  5. optgs/config/dataset/dl3dv.yaml +1 -28
  6. optgs/config/dataset/re10k.yaml +1 -9
  7. optgs/config/experiment/re10k_unified.yaml +0 -5
  8. optgs/config/experiment/train_dl3dv.yaml +2 -3
  9. optgs/config/experiment/train_l2s_sparse_dl3dv.yaml +15 -10
  10. optgs/config/experiment/train_l2s_sparse_dl3dv_no_delta.yaml +5 -7
  11. optgs/config/experiment/train_l2s_sparse_dl3dv_no_loss.yaml +5 -7
  12. optgs/config/loss/lpips.yaml +1 -0
  13. optgs/config/loss/mse.yaml +2 -0
  14. optgs/config/loss/sgd.yaml +3 -1
  15. optgs/config/loss/stability.yaml +2 -0
  16. optgs/config/main.yaml +21 -29
  17. optgs/config/meta_trainer/train/{replay_buffer_cfg → ckpt_buffer_cfg}/default.yaml +4 -4
  18. optgs/config/meta_trainer/train/{replay_buffer_cfg → ckpt_buffer_cfg}/none.yaml +4 -4
  19. optgs/config/scene_trainer/scene_initializer/edgs.yaml +2 -1
  20. optgs/config/scene_trainer/scene_initializer/resplat_v1.yaml +0 -10
  21. optgs/config/scene_trainer/scene_initializer/resplat_v2.yaml +0 -10
  22. optgs/config/scene_trainer/scene_optimizer/3dgs.yaml +2 -13
  23. optgs/config/scene_trainer/scene_optimizer/3dgs_star.yaml +6 -11
  24. optgs/config/scene_trainer/scene_optimizer/base.yaml +6 -6
  25. optgs/config/scene_trainer/scene_optimizer/knn_based.yaml +36 -51
  26. optgs/config/scene_trainer/scene_optimizer/learn2splat.yaml +14 -2
  27. optgs/config/scene_trainer/scene_optimizer/learn2splat_dense.yaml +8 -0
  28. optgs/config/scene_trainer/scene_optimizer/lr_scheduler/expon.yaml +28 -0
  29. optgs/config/scene_trainer/scene_optimizer/refiner/mcmc.yaml +1 -1
  30. optgs/config/scene_trainer/scene_optimizer/resplat_v2.yaml +2 -2
  31. optgs/config/scene_trainer/scene_optimizer/sgd.yaml +0 -22
  32. optgs/config_migrate.py +285 -3
  33. optgs/dataset/camera_datasets/camera.py +0 -141
  34. optgs/dataset/data_module.py +3 -3
  35. optgs/dataset/dataset_dl3dv.py +20 -212
  36. optgs/dataset/dataset_re10k.py +22 -129
  37. optgs/dataset/shims/augmentation_shim.py +17 -16
  38. optgs/dataset/shims/bounds_shim.py +0 -80
  39. optgs/dataset/view_sampler/view_sampler.py +0 -17
  40. optgs/evaluation/metric_computer.py +0 -115
  41. optgs/evaluation/metrics.py +1 -1
  42. optgs/experimental/api/api.py +34 -24
  43. optgs/experimental/api/integration/config_bridge.py +14 -25
  44. optgs/global_cfg.py +0 -19
  45. optgs/loss/loss_deltas.py +7 -2
  46. optgs/loss/loss_lpips.py +8 -2
  47. optgs/loss/loss_monodepth.py +2 -21
  48. optgs/loss/loss_mse.py +7 -5
  49. optgs/loss/loss_sgd.py +11 -10
  50. optgs/loss/loss_sh0.py +3 -0
Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
  # Learn2Splat — interactive demo for a Hugging Face Space (Docker SDK, GPU).
2
  #
3
  # Installs the optgs package + prebuilt CUDA-extension wheels, then runs
4
- # demo.py's viser GUI: SfM-initialize a COLMAP scene and refine the Gaussians
5
  # with the learned optimizer live in the browser.
6
  #
7
  # The CUDA extensions are NOT compiled here — the HF Docker builder runs out of
@@ -53,15 +53,6 @@ RUN pip install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1 \
53
  COPY --chown=user:user requirements.txt .
54
  RUN pip install -r requirements.txt
55
 
56
- # HF serves the Space over HTTP/2, and the WebSocket subprotocol viser uses to
57
- # announce its client version doesn't survive the proxy — so viser's server
58
- # reads the client version as "unknown" and rejects the connection (the GUI
59
- # then hangs on "connecting"). Client and server are the same viser build
60
- # here, so treat an undeterminable client version as a match, not a reject.
61
- RUN VISER_INFRA="$(python -c 'import viser.infra._infra as m; print(m.__file__)')" \
62
- && sed -i 's/client_version_str = "unknown"/client_version_str = viser.__version__/' "$VISER_INFRA" \
63
- && grep -q 'client_version_str = viser.__version__' "$VISER_INFRA"
64
-
65
  # Prebuilt CUDA-extension wheels — gsplat, nerfacc, pycolmap, fused-ssim,
66
  # simple-knn, pointops, fused_knn_attn. Built on a matching machine (see
67
  # DEPLOY.md) so the HF builder never compiles CUDA and never OOMs.
@@ -72,11 +63,10 @@ RUN pip install --no-deps ./wheels/*.whl
72
  COPY --chown=user:user . .
73
  RUN pip install --no-build-isolation --no-deps -e .
74
 
75
- # viser serves the GUI here — must equal app_port in README.md.
76
  EXPOSE 7860
77
 
78
- # server mode: the optgs decoder renders frames on the GPU and viser streams
79
- # them as images no multi-MB splat-geometry transfer to the browser and no
80
- # client-side WebGL, which is far more robust behind HF's HTTP/2 proxy.
81
- # viser binds 0.0.0.0 by default.
82
- CMD ["python", "demo.py", "--with-gui", "server", "--gui-port", "7860"]
 
1
  # Learn2Splat — interactive demo for a Hugging Face Space (Docker SDK, GPU).
2
  #
3
  # Installs the optgs package + prebuilt CUDA-extension wheels, then runs
4
+ # demo.py's gradio GUI: SfM-initialize a COLMAP scene and refine the Gaussians
5
  # with the learned optimizer live in the browser.
6
  #
7
  # The CUDA extensions are NOT compiled here — the HF Docker builder runs out of
 
53
  COPY --chown=user:user requirements.txt .
54
  RUN pip install -r requirements.txt
55
 
 
 
 
 
 
 
 
 
 
56
  # Prebuilt CUDA-extension wheels — gsplat, nerfacc, pycolmap, fused-ssim,
57
  # simple-knn, pointops, fused_knn_attn. Built on a matching machine (see
58
  # DEPLOY.md) so the HF builder never compiles CUDA and never OOMs.
 
63
  COPY --chown=user:user . .
64
  RUN pip install --no-build-isolation --no-deps -e .
65
 
66
+ # gradio serves the GUI here — must equal app_port in README.md.
67
  EXPOSE 7860
68
 
69
+ # gradio mode: the optgs decoder renders the live optimization on the GPU and
70
+ # gradio streams the frames as images (column 2); the finished splats load into
71
+ # an interactive Model3D viewer (column 3). demo.py binds 0.0.0.0 on this port.
72
+ CMD ["python", "demo.py", "--with-gui", "gradio", "--gui-port", "7860"]
 
README.md CHANGED
@@ -14,11 +14,13 @@ short_description: Interactive demo of the Learn2Splat learned 3DGS optimizer
14
  A learned optimizer for 3D Gaussian Splatting. This Space SfM-initializes a
15
  COLMAP scene and refines the Gaussians live in your browser: pick the
16
  Learn2Splat optimizer (dense or sparse checkpoint) or a 3DGS Adam baseline,
17
- press **Start**, and watch the splats converge.
 
18
 
19
- Runs `demo.py --with-gui client` from the
20
  [Learn2Splat repository](https://github.com/autonomousvision/learn2splat);
21
- the splats are drawn by viser's in-browser WebGL renderer.
 
22
 
23
  > Requires GPU hardware. The demo holds two checkpoints in VRAM at once —
24
  > an A10G (24 GB) is recommended.
 
14
  A learned optimizer for 3D Gaussian Splatting. This Space SfM-initializes a
15
  COLMAP scene and refines the Gaussians live in your browser: pick the
16
  Learn2Splat optimizer (dense or sparse checkpoint) or a 3DGS Adam baseline,
17
+ press **Start**, and watch the decoder render converge — then explore the
18
+ finished splats in an interactive 3D viewer.
19
 
20
+ Runs `demo.py --with-gui gradio` from the
21
  [Learn2Splat repository](https://github.com/autonomousvision/learn2splat);
22
+ the optimization is rendered on the GPU and streamed by gradio, and the
23
+ result loads into a `Model3D` splat viewer.
24
 
25
  > Requires GPU hardware. The demo holds two checkpoints in VRAM at once —
26
  > an A10G (24 GB) is recommended.
demo.py CHANGED
@@ -27,6 +27,7 @@ Usage (run from the repo root, with ``optgs`` importable):
27
  python demo.py # headless: dense + sparse checkpoints + an Adam baseline
28
  python demo.py --with-gui server # interactive viser GUI (frames rendered by the decoder)
29
  python demo.py --with-gui client # interactive viser GUI (viser's WebGL splat renderer)
 
30
 
31
  The demo scene and the checkpoints are fetched from the Hugging Face Hub on
32
  first run (cached under ./data and ./checkpoints). A CUDA device is required.
@@ -118,11 +119,13 @@ class Config:
118
  seed: int = 42
119
 
120
  # --- Interactive GUI ---
121
- # Launch a viser GUI instead of the headless comparison. "server" renders
122
- # frames with the optgs decoder; "client" uses viser's built-in WebGL
123
- # Gaussian-splat renderer. Unset = headless run.
124
- with_gui: Optional[Literal["client", "server"]] = None
125
- # Port for the viser GUI web server (--with-gui only).
 
 
126
  gui_port: int = 8080
127
 
128
  # --- OptGS learned optimizer ---
@@ -600,6 +603,373 @@ def run_gui(
600
  console.print("\n[yellow]GUI stopped.[/]")
601
 
602
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  def main(cfg: Config) -> None:
604
  # Fetch the demo scene on first run, before anything else touches it.
605
  ensure_data(cfg.data_dir)
@@ -663,7 +1033,10 @@ def main(cfg: Config) -> None:
663
  for inst in instances.values():
664
  inst.initialize_from_tensors(gaussians, train_bv)
665
 
666
- run_gui(instances, gaussians, train_bv, cfg, device, dtype)
 
 
 
667
  return
668
 
669
  val_c2w, val_Ks, val_images = collect_cameras(dataset, val_idx)
 
27
  python demo.py # headless: dense + sparse checkpoints + an Adam baseline
28
  python demo.py --with-gui server # interactive viser GUI (frames rendered by the decoder)
29
  python demo.py --with-gui client # interactive viser GUI (viser's WebGL splat renderer)
30
+ python demo.py --with-gui gradio # interactive gradio GUI (streamed renders + Model3D splats)
31
 
32
  The demo scene and the checkpoints are fetched from the Hugging Face Hub on
33
  first run (cached under ./data and ./checkpoints). A CUDA device is required.
 
119
  seed: int = 42
120
 
121
  # --- Interactive GUI ---
122
+ # Launch an interactive GUI instead of the headless comparison. viser:
123
+ # "server" renders frames with the optgs decoder, "client" uses viser's
124
+ # built-in WebGL Gaussian-splat renderer. "gradio" runs a browser GUI
125
+ # (decoder renders streamed live + an interactive Model3D splat viewer for
126
+ # the result). Unset = headless run.
127
+ with_gui: Optional[Literal["client", "server", "gradio"]] = None
128
+ # Port for the GUI web server (--with-gui only).
129
  gui_port: int = 8080
130
 
131
  # --- OptGS learned optimizer ---
 
603
  console.print("\n[yellow]GUI stopped.[/]")
604
 
605
 
606
+ def run_gradio_gui(
607
+ instances: dict,
608
+ gaussians: Gaussians,
609
+ train_bv: dict,
610
+ cfg: Config,
611
+ device: torch.device,
612
+ ) -> None:
613
+ """Interactive gradio GUI — a browser port of :func:`run_gui` (viser).
614
+
615
+ gradio can't stream the camera back to Python, so there is no free-camera
616
+ server rendering. Instead the optimization is *watched* as a streamed
617
+ decoder render from a chosen training view (``gr.Image``, refreshed every
618
+ step), and the finished scene is handed to an interactive ``gr.Model3D``
619
+ splat viewer (orbit / zoom in the browser). The controls mirror the viser
620
+ GUI: pick the optimizer (Learn2Splat dense/sparse or a 3DGS Adam baseline),
621
+ set the step budget / view-minibatch size / sampling strategy, Start, Reset.
622
+
623
+ ``instances`` maps "dense"/"sparse" to their initialized ``OptGS``.
624
+ """
625
+ import gc
626
+
627
+ import gradio as gr
628
+
629
+ from optgs.experimental.api.integration.config_bridge import build_adam_baseline
630
+ from optgs.misc.image_io import prep_image
631
+ from optgs.model.ply_export import save_gaussian_ply
632
+
633
+ # Optimizer dropdown label -> (instances key, swap in a 3DGS Adam baseline);
634
+ # mirrors run_gui's OPTIONS.
635
+ OPTIONS: Dict[str, Tuple[str, bool]] = {
636
+ "Learn2Splat (dense)": ("dense", False),
637
+ "Learn2Splat (sparse)": ("sparse", False),
638
+ "Adam (3DGS)": ("dense", True),
639
+ }
640
+
641
+ n_train_views = int(train_bv["image"].shape[1])
642
+ h_full, w_full = train_bv["image"].shape[3], train_bv["image"].shape[4]
643
+ init_gaussians = gaussians.clone() # pristine copy; each Start re-inits from it
644
+
645
+ # Shared state (single-GPU, single-session demo, like run_gui's globals): the
646
+ # Gaussians currently shown, the OptGS rendering them, and a counter for
647
+ # unique PLY filenames (so Model3D reloads instead of serving a stale cache).
648
+ holder = {"current": init_gaussians, "active": instances["dense"], "ply": 0}
649
+
650
+ ply_dir = os.path.join(cfg.result_dir, "gradio")
651
+ os.makedirs(ply_dir, exist_ok=True)
652
+
653
+ @torch.no_grad()
654
+ def render_decoder(inst, g: Gaussians, view_idx: float, height: float) -> np.ndarray:
655
+ """Decoder-render Gaussians ``g`` from training view ``view_idx``.
656
+
657
+ Normalized intrinsics make the render resolution-independent; the width
658
+ is derived from ``height`` at the training views' aspect ratio.
659
+ """
660
+ h = int(height)
661
+ w = max(1, round(h * w_full / h_full))
662
+ sl = slice(int(view_idx), int(view_idx) + 1)
663
+ out = inst.decoder.forward(
664
+ g,
665
+ train_bv["extrinsics"][:, sl],
666
+ train_bv["intrinsics"][:, sl],
667
+ train_bv["near"][:, sl],
668
+ train_bv["far"][:, sl],
669
+ image_shape=(h, w),
670
+ )
671
+ return prep_image(out.color[0, 0]) # [H, W, 3] uint8
672
+
673
+ def reorient_for_viewer(g: Gaussians) -> Gaussians:
674
+ """Reorient a copy of ``g`` from the COLMAP world (this scene is Z-up)
675
+ into the Y-up frame the gradio Model3D shows upright.
676
+
677
+ gradio/Babylon flips the loaded splats' Y (``scaling.y *= -1``), so
678
+ exporting through the reflection E(p)=(x,-z,-y) makes the *displayed*
679
+ scene N(p)=(x,z,-y) — the world's Z-up mapped onto Babylon's Y-up. E's
680
+ point-reflection part leaves the covariance unchanged; its proper-rotation
681
+ part R_p=-E rotates the splat orientations to match.
682
+ """
683
+ from scipy.spatial.transform import Rotation as Rsp
684
+
685
+ g = g.clone()
686
+ m = g.means[0]
687
+ g.means = torch.stack([m[:, 0], -m[:, 2], -m[:, 1]], dim=1)[None] # E
688
+ q = F.normalize(g.rotations_unnorm[0], dim=-1).detach().cpu().numpy() # xyzw
689
+ R_p = np.array([[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
690
+ q = (Rsp.from_matrix(R_p) * Rsp.from_quat(q)).as_quat() # R_p @ R, xyzw
691
+ g.rotations_unnorm = torch.from_numpy(q).to(g.means)[None]
692
+ return g
693
+
694
+ def export_ply(g: Gaussians) -> str:
695
+ """Write ``g`` (reoriented to Y-up) to a fresh PLY path for the viewer."""
696
+ from pathlib import Path
697
+
698
+ holder["ply"] += 1
699
+ path = os.path.join(ply_dir, f"result_{holder['ply']}.ply")
700
+ save_gaussian_ply(reorient_for_viewer(g), save_path=Path(path))
701
+ return path
702
+
703
+ def start(optimizer_label, max_steps, batch_size, strategy, view_idx, height):
704
+ """Generator: run the picked optimizer, streaming a decoder render each
705
+ step, then load the finished splats into the Model3D viewer."""
706
+ name, use_adam = OPTIONS[optimizer_label]
707
+ inst = instances[name]
708
+ holder["active"] = inst
709
+ # Re-init from the pristine copy so repeated Starts share one start point.
710
+ inst.initialize_from_tensors(init_gaussians.clone(), train_bv)
711
+ inst.num_refine = int(max_steps)
712
+ inst.opt_batch_size = min(int(batch_size), n_train_views)
713
+ inst.opt_batch_strategy = strategy
714
+ opt = build_adam_baseline(inst.num_refine).to(device) if use_adam else None
715
+
716
+ try:
717
+ # Disable Start, hide the viewer, keep the placeholder while running.
718
+ yield (
719
+ render_decoder(inst, init_gaussians, view_idx, height),
720
+ f"**optimizing** — step 0/{inst.num_refine} — "
721
+ f"{init_gaussians.means.shape[1]} Gaussians",
722
+ gr.update(visible=False, value=None), # hide the viewer
723
+ gr.update(interactive=False), # disable Start
724
+ gr.update(visible=True), # keep the placeholder
725
+ )
726
+
727
+ g = init_gaussians
728
+ for step, g in inst.optimize_iter(optimizer=opt):
729
+ holder["current"] = g
730
+ yield (
731
+ render_decoder(inst, g, view_idx, height),
732
+ f"**optimizing** — step {step + 1}/{inst.num_refine} — "
733
+ f"{g.means.shape[1]} Gaussians",
734
+ gr.update(), gr.update(), gr.update(), # no change mid-run
735
+ )
736
+
737
+ yield (
738
+ render_decoder(inst, g, view_idx, height),
739
+ f"**done** — {inst.num_refine} steps — {g.means.shape[1]} Gaussians",
740
+ gr.update(visible=True, value=export_ply(g)), # reveal the viewer
741
+ gr.update(interactive=True), # re-enable Start
742
+ gr.update(visible=False), # hide the placeholder
743
+ )
744
+ finally:
745
+ # Free the run's CUDA work (also runs if the user hits Stop mid-run),
746
+ # so GPU memory doesn't accumulate across repeated runs.
747
+ opt = None
748
+ gc.collect()
749
+ torch.cuda.empty_cache()
750
+
751
+ def reset(view_idx, height):
752
+ """Restore the initialization: re-render it, hide the viewer.
753
+
754
+ No CUDA cleanup here — ``start``'s ``finally`` already frees each run's
755
+ work; reset only re-renders the init.
756
+ """
757
+ holder["current"] = init_gaussians
758
+ holder["active"] = instances["dense"]
759
+ return (
760
+ render_decoder(instances["dense"], init_gaussians, view_idx, height),
761
+ "**Initialized.** Pick a method, then Start.",
762
+ gr.update(visible=False, value=None), # hide the viewer
763
+ gr.update(interactive=True), # enable Start
764
+ gr.update(visible=True), # show the placeholder
765
+ )
766
+
767
+ def rerender(view_idx, height):
768
+ """Re-render the current Gaussians (preview view / height changed)."""
769
+ return render_decoder(holder["active"], holder["current"], view_idx, height)
770
+
771
+ initial_img = render_decoder(instances["dense"], init_gaussians, 0, 540)
772
+
773
+ # Open the interactive viewer on the same vantage as the live render's
774
+ # default preview (view 0). The viewer shows splats in the reoriented frame
775
+ # N(p)=(x,z,-y) (see reorient_for_viewer); the orbit camera sits at the
776
+ # training view's position and looks at the scene centroid. babylon_camera
777
+ # maps the world camera into N and inverts Babylon's ArcRotateCamera position
778
+ # formula rel=(r·cosα·sinβ, r·cosβ, r·sinα·sinβ) into (alpha°, beta°, radius).
779
+ def babylon_camera(c2w: np.ndarray, centroid: np.ndarray) -> tuple:
780
+ p = c2w[:3, 3] - centroid
781
+ rel = np.array([p[0], p[2], -p[1]], dtype=np.float64) # N applied to (cam - centroid)
782
+ radius = float(np.linalg.norm(rel)) or 1e-3
783
+ beta = float(np.degrees(np.arccos(np.clip(rel[1] / radius, -1.0, 1.0))))
784
+ alpha = float(np.degrees(np.arctan2(rel[2], rel[0])))
785
+ return (alpha, beta, radius)
786
+
787
+ means0 = init_gaussians.means[0].detach().float().cpu().numpy()
788
+ centroid0 = (means0.min(0) + means0.max(0)) / 2.0
789
+ cam0_c2w = train_bv["extrinsics"][0, 0].detach().float().cpu().numpy()
790
+ init_camera = babylon_camera(cam0_c2w, centroid0)
791
+
792
+ # --- Visual style: lifted from the Learn2Splat project page
793
+ # (https://naamapearl.github.io/learn2splat/) — plum accent (#B04080) with an
794
+ # indigo hover, a light slate canvas with white cards, Source Serif 4
795
+ # headings / Inter body / JetBrains Mono labels. ---
796
+ plum = gr.themes.Color(
797
+ c50="#faf0f6", c100="#f5e6ef", c200="#eccadd", c300="#dda3c2",
798
+ c400="#c66ba0", c500="#b04080", c600="#9b3570", c700="#7f2a5b",
799
+ c800="#6a2550", c900="#581f43", c950="#350e26", name="plum",
800
+ )
801
+ slate = gr.themes.Color(
802
+ c50="#f7f8fc", c100="#eef0f8", c200="#e2e5ef", c300="#c8cde0",
803
+ c400="#8890b0", c500="#5a6080", c600="#454b6b", c700="#343a56",
804
+ c800="#262b42", c900="#1a1d2e", c950="#0f1120", name="slate",
805
+ )
806
+ theme = gr.themes.Soft(
807
+ primary_hue=plum,
808
+ neutral_hue=slate,
809
+ font=["Inter", "system-ui", "sans-serif"],
810
+ font_mono=["JetBrains Mono", "ui-monospace", "monospace"],
811
+ ).set(
812
+ body_background_fill="#f7f8fc",
813
+ body_text_color="#1a1d2e",
814
+ block_background_fill="#ffffff",
815
+ block_border_color="#e2e5ef",
816
+ block_radius="12px",
817
+ block_label_text_color="#5a6080",
818
+ block_title_text_color="#1a1d2e",
819
+ border_color_primary="#e2e5ef",
820
+ input_background_fill="#ffffff",
821
+ button_primary_background_fill="#b04080",
822
+ button_primary_background_fill_hover="#3d50c0",
823
+ button_primary_text_color="#ffffff",
824
+ button_secondary_background_fill="#ffffff",
825
+ button_secondary_border_color="#c8cde0",
826
+ button_secondary_text_color="#1a1d2e",
827
+ slider_color="#b04080",
828
+ link_text_color="#b04080",
829
+ )
830
+ # Source Serif 4 / Inter / JetBrains Mono, loaded into <head> like the page.
831
+ fonts_head = (
832
+ '<link rel="preconnect" href="https://fonts.googleapis.com">'
833
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
834
+ '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?'
835
+ "family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&"
836
+ "family=Inter:wght@300;400;500;600&"
837
+ 'family=JetBrains+Mono:wght@400;500&display=swap">'
838
+ )
839
+ css = """
840
+ .gradio-container { max-width: 1580px !important; margin: 0 auto !important; }
841
+ #l2s-hero { background: linear-gradient(180deg,#f3f4fb 0%,#f7f8fc 100%);
842
+ border:1px solid #e2e5ef; border-radius:14px; padding:26px 30px; margin-bottom:6px; }
843
+ #l2s-hero .eyebrow { font-family:'JetBrains Mono',monospace; font-size:12px;
844
+ letter-spacing:.16em; text-transform:uppercase; color:#b04080; font-weight:500;
845
+ display:inline-flex; align-items:center; gap:8px; }
846
+ #l2s-hero .eyebrow .dot { width:6px; height:6px; border-radius:50%; background:#b04080; }
847
+ #l2s-hero h1 { font-family:'Source Serif 4',Georgia,serif; font-weight:600; color:#1a1d2e;
848
+ font-size:clamp(1.55rem,3vw,2.25rem); line-height:1.2; margin:13px 0 10px; }
849
+ #l2s-hero p { color:#5a6080; font-size:15px; line-height:1.6; margin:0; max-width:820px; }
850
+ #l2s-hero a { color:#b04080; text-decoration:none; border-bottom:1px solid #e6cdd9;
851
+ white-space:nowrap; }
852
+ .l2s-eyebrow span { font-family:'JetBrains Mono',monospace; font-size:11px;
853
+ letter-spacing:.15em; text-transform:uppercase; color:#8890b0; font-weight:500; }
854
+ #l2s-start button, #l2s-reset button { font-family:'JetBrains Mono',monospace;
855
+ letter-spacing:.03em; font-weight:500; }
856
+ #l2s-status { background:#f7f8fc; border:1px solid #e2e5ef; border-left:3px solid #b04080;
857
+ border-radius:9px; padding:4px 14px; }
858
+ #l2s-status p { color:#5a6080; font-size:14px; margin:8px 0; }
859
+ #l2s-status strong { color:#1a1d2e; }
860
+ .l2s-ph { display:flex; align-items:center; justify-content:center; text-align:center;
861
+ height:420px; border:1px dashed #c8cde0; border-radius:12px; background:#fbfcff;
862
+ color:#8890b0; font-family:'JetBrains Mono',monospace; font-size:12.5px;
863
+ letter-spacing:.04em; line-height:1.7; }
864
+ footer { display:none !important; }
865
+ """
866
+ hero_html = (
867
+ "<div class='eyebrow'><span class='dot'></span>Learn2Splat · Interactive demo</div>"
868
+ "<h1>Extending the Horizon of Learned 3DGS Optimization</h1>"
869
+ "<p>SfM-initialize a COLMAP scene, then refine the Gaussians with the "
870
+ "meta-learned optimizer — pick a method, press <b>Start</b>, and watch the "
871
+ "decoder render converge. The finished splats load in the interactive 3D "
872
+ "viewer. <a href='https://naamapearl.github.io/learn2splat/' target='_blank' "
873
+ "rel='noopener'>Project page&nbsp;↗</a></p>"
874
+ )
875
+
876
+ with gr.Blocks(
877
+ title="Learn2Splat — Demo", theme=theme, css=css, head=fonts_head,
878
+ analytics_enabled=False,
879
+ ) as ui:
880
+ gr.HTML(hero_html, elem_id="l2s-hero")
881
+ with gr.Row(equal_height=False):
882
+ # Column 1 — controls.
883
+ with gr.Column(scale=3, min_width=300):
884
+ with gr.Group():
885
+ gr.HTML("<div class='l2s-eyebrow'><span>Optimizer</span></div>")
886
+ optimizer_dd = gr.Dropdown(
887
+ list(OPTIONS), value=next(iter(OPTIONS)), label="Method"
888
+ )
889
+ with gr.Row():
890
+ max_steps_input = gr.Number(
891
+ value=cfg.max_steps, minimum=1, maximum=1000, step=1,
892
+ precision=0, label="Max steps",
893
+ )
894
+ batch_size_input = gr.Number(
895
+ value=min(cfg.opt_batch_size, n_train_views),
896
+ minimum=1, maximum=n_train_views, step=1, precision=0,
897
+ label="Opt batch size",
898
+ )
899
+ strategy_dd = gr.Dropdown(
900
+ ["random", "sequential", "fps"],
901
+ value=cfg.opt_batch_strategy, label="Batch strategy",
902
+ )
903
+ with gr.Group():
904
+ gr.HTML("<div class='l2s-eyebrow'><span>Preview</span></div>")
905
+ view_slider = gr.Slider(
906
+ 0, n_train_views - 1, value=0, step=1, label="Preview view"
907
+ )
908
+ height_slider = gr.Slider(
909
+ 240, 1080, value=540, step=60, label="Render height"
910
+ )
911
+ with gr.Row():
912
+ start_btn = gr.Button(
913
+ "Start optimization", variant="primary",
914
+ elem_id="l2s-start", scale=2,
915
+ )
916
+ reset_btn = gr.Button(
917
+ "Reset", variant="secondary", elem_id="l2s-reset", scale=1
918
+ )
919
+ status_md = gr.Markdown(
920
+ "**Initialized.** Pick a method, then Start.", elem_id="l2s-status"
921
+ )
922
+ # Column 2 — live decoder render (streamed during optimization).
923
+ with gr.Column(scale=5, min_width=380):
924
+ image_out = gr.Image(
925
+ value=initial_img, label="Optimizer · live",
926
+ height=540, format="jpeg", interactive=False,
927
+ )
928
+ # Column 3 — interactive splats (hidden until a run finishes; a
929
+ # placeholder holds the column so the 3-up layout stays balanced).
930
+ with gr.Column(scale=5, min_width=380):
931
+ model3d_out = gr.Model3D(
932
+ label="Refined splats · interactive", height=540,
933
+ visible=False, camera_position=init_camera,
934
+ )
935
+ placeholder = gr.HTML(
936
+ "<div class='l2s-ph'>The interactive 3D splats<br>"
937
+ "appear here once a run finishes.</div>"
938
+ )
939
+
940
+ start_inputs = [
941
+ optimizer_dd, max_steps_input, batch_size_input, strategy_dd,
942
+ view_slider, height_slider,
943
+ ]
944
+ gui_outputs = [image_out, status_md, model3d_out, start_btn, placeholder]
945
+ # One shared GPU lane (concurrency_id) so Start / Reset / preview re-renders
946
+ # never run on the GPU at the same time — overlapping runs were the path to
947
+ # runaway VRAM growth.
948
+ start_btn.click(
949
+ start, inputs=start_inputs, outputs=gui_outputs, concurrency_id="gpu"
950
+ )
951
+ reset_btn.click(
952
+ reset, inputs=[view_slider, height_slider], outputs=gui_outputs,
953
+ concurrency_id="gpu",
954
+ )
955
+ # Slider release (not change) — re-render once when the user lets go.
956
+ view_slider.release(
957
+ rerender, [view_slider, height_slider], image_out, concurrency_id="gpu"
958
+ )
959
+ height_slider.release(
960
+ rerender, [view_slider, height_slider], image_out, concurrency_id="gpu"
961
+ )
962
+
963
+ console.print(
964
+ f"[green]✓[/] gradio GUI on port [cyan]{cfg.gui_port}[/]"
965
+ f" — forward the port over SSH and open the printed URL"
966
+ )
967
+ ui.queue(default_concurrency_limit=1).launch(
968
+ server_name="0.0.0.0", server_port=cfg.gui_port, share=False,
969
+ show_error=True,
970
+ )
971
+
972
+
973
  def main(cfg: Config) -> None:
974
  # Fetch the demo scene on first run, before anything else touches it.
975
  ensure_data(cfg.data_dir)
 
1033
  for inst in instances.values():
1034
  inst.initialize_from_tensors(gaussians, train_bv)
1035
 
1036
+ if cfg.with_gui == "gradio":
1037
+ run_gradio_gui(instances, gaussians, train_bv, cfg, device)
1038
+ else:
1039
+ run_gui(instances, gaussians, train_bv, cfg, device, dtype)
1040
  return
1041
 
1042
  val_c2w, val_Ks, val_images = collect_cameras(dataset, val_idx)
optgs/config.py CHANGED
@@ -1,29 +1,34 @@
1
- import importlib
 
 
 
 
 
 
 
 
2
  from copy import deepcopy
3
  from dataclasses import dataclass
4
  from pathlib import Path
5
- from typing import Literal, Optional, Type, TypeVar, Any, Callable
6
 
7
- import hydra
8
  import torch
9
  from dacite import Config, from_dict, UnionMatchError
10
- from hydra.core.global_hydra import GlobalHydra
11
  from hydra.core.hydra_config import HydraConfig
12
- from hydra.types import RunMode
13
  from omegaconf import DictConfig
14
  from omegaconf import OmegaConf
15
  from pytorch_lightning.strategies import DDPStrategy, FSDPStrategy
16
 
17
  from .config_migrate import migrate, CURRENT_CFG_VERSION
18
  from .dataset.data_module import DataLoaderCfg, DatasetCfg
19
- from .global_cfg import set_cfg
20
  from .loss import LossCfgWrapper
21
  from .misc.io import CustomPath
22
  from .misc.io import cyan, read_omega_cfg
23
  from .misc.checkpointing import find_latest_ckpt
24
- from .misc.hf_ckpt import maybe_resolve_hf_ref
25
- from .paths import CKPT_DIR, RESULTS_DIR
26
- from .scene_trainer.scene_trainer_cfg import SceneTrainerCfg, MetaOptimizerCfg, TestCfg, TrainCfg
 
27
 
28
 
29
  # In order to extract filename or dirname from a path in the config
@@ -58,15 +63,28 @@ class CheckpointingCfg:
58
  resume_update_module: str | None
59
  load_existing_cfg: bool
60
 
 
 
 
 
 
61
  def __post_init__(self):
62
  # Resolve any Hugging Face Hub references (hf://org/repo/file[@rev]) to
63
  # local cached paths so all downstream torch.load calls work unchanged.
 
 
 
 
 
64
  for attr in ("pretrained_model", "pretrained_optimizer", "pretrained_initializer",
65
  "pretrained_monodepth", "pretrained_mvdepth", "pretrained_depth",
66
  "pretrained_scale_predictor", "pretrained_depth_teacher",
67
  "resume_update_module"):
68
- resolved = maybe_resolve_hf_ref(getattr(self, attr))
69
- if resolved != getattr(self, attr):
 
 
 
70
  setattr(self, attr, resolved)
71
 
72
  for attr in ("pretrained_model", "pretrained_optimizer", "pretrained_initializer"):
@@ -90,6 +108,7 @@ class MetaTrainerCfg:
90
  eval_index: str | None
91
  limit_test_batches: int | float
92
  limit_train_batches: int | float
 
93
  test: TestCfg
94
  train: TrainCfg
95
 
@@ -109,9 +128,9 @@ class MetaTrainerCfg:
109
  return has_trainable
110
 
111
  dist_strategy = FSDPStrategy(auto_wrap_policy=only_wrap_trainable)
112
- if self.train.use_replay_buffer:
113
- # When resampling from the replay buffer,
114
- # we don't project the condition_features to state, so the update_proj is not used
115
  dist_strategy = "ddp_find_unused_parameters_true"
116
  return dist_strategy
117
 
@@ -123,14 +142,13 @@ class RootCfg:
123
  dataset: DatasetCfg
124
  data_loader: DataLoaderCfg
125
  scene_trainer: SceneTrainerCfg
126
- meta_optimizer: MetaOptimizerCfg ## TODO Naama: should we move under meta trainer config?
127
  checkpointing: CheckpointingCfg
128
  meta_trainer: MetaTrainerCfg
129
  loss: list[LossCfgWrapper]
130
  seed: int
131
  use_plugins: bool
132
  output_dir: str
133
- version: int | None
134
  debug_cfg: bool
135
 
136
  def __post_init__(self):
@@ -202,12 +220,6 @@ TYPE_HOOKS = {
202
  T = TypeVar("T")
203
 
204
 
205
- def get_class_by_path(path: str):
206
- module_path, class_name = path.rsplit('.', 1)
207
- module = importlib.import_module(module_path)
208
- return getattr(module, class_name)
209
-
210
-
211
  def _diagnose_union_error(e: UnionMatchError, data: dict, dacite_config: Config) -> str:
212
  """Try each union member individually and report per-member errors."""
213
  import dataclasses
@@ -273,32 +285,6 @@ def separate_loss_cfg_wrappers(joined: dict) -> list[LossCfgWrapper]:
273
  ]
274
 
275
 
276
- def universal_target_hook(cfg: dict, _: Type) -> Any:
277
- """Generic hook to construct config objects from `__target__`."""
278
- if not isinstance(cfg, dict):
279
- return None
280
- if "__target__" not in cfg:
281
- return None # Let decite handle it
282
-
283
- cfg_copy = deepcopy(cfg) # avoid mutating original
284
- target = cfg_copy.pop("__target__")
285
-
286
- if isinstance(target, str):
287
- target_type = get_class_by_path(target)
288
- else:
289
- target_type = target
290
-
291
- # Use recursive loading with known additional hooks
292
- return load_typed_config(
293
- DictConfig(cfg_copy),
294
- target_type,
295
- )
296
-
297
-
298
- def make_target_hook_for_type(t: Type) -> Callable:
299
- return lambda cfg: universal_target_hook(cfg, t)
300
-
301
-
302
  def load_typed_root_config(cfg: DictConfig) -> RootCfg:
303
  # scene_trainer/scene_optimizer=none loads a full dict from none.yaml;
304
  # dacite can't match that dict to the None arm of SceneOptimizerCfg | None.
@@ -316,30 +302,17 @@ def load_typed_root_config(cfg: DictConfig) -> RootCfg:
316
  )
317
 
318
 
319
- def should_run(cfg_dict):
320
- if cfg_dict.mode == "test":
321
- if cfg_dict.meta_trainer.test.skip_if_outputs_exist:
322
- output_dir = cfg_dict.output_dir
323
- if not output_dir.exists():
324
- return True
325
- metrics_path_pattern = output_dir / "metrics" / "target_*_psnr.json"
326
- metric_paths = list(metrics_path_pattern.parent.glob(metrics_path_pattern.name))
327
- if len(metric_paths) > 0:
328
- print(cyan(f"Test metrics already exist at {metric_paths}."))
329
- return False
330
- return True
331
-
332
-
333
  def setup_cfg(cfg_dict):
334
  # Get the original config from the output directory, when testing or resuming.
335
  cfg_dict = merge_config_from_file(cfg_dict)
336
  eval_cfg = get_eval_cfg(cfg_dict)
337
  cfg = load_typed_root_config(cfg_dict)
338
- # Set global cfg object.
339
- set_cfg(cfg_dict)
340
  # Set up the output directory.
341
  setup_output_dir(cfg, cfg_dict)
342
- return cfg, cfg_dict, eval_cfg # TODO Naama: why do we need both cfg and cfg_dict?
 
 
 
343
 
344
 
345
  def flatten_wandb(cfg):
@@ -375,6 +348,11 @@ def _apply_cli_overrides(merged_cfg: DictConfig, orig_cli_cfg: DictConfig, raw_o
375
  print(cyan(f"Re-applying {len(raw_overrides)} CLI overrides onto merged config."))
376
  OmegaConf.set_struct(merged_cfg, False)
377
 
 
 
 
 
 
378
  # Architecture subtrees: CLI group default fills in *new* fields only;
379
  # checkpoint values win for fields that already exist.
380
  ARCH_KEYS = {"scene_optimizer", "scene_initializer"}
@@ -390,7 +368,7 @@ def _apply_cli_overrides(merged_cfg: DictConfig, orig_cli_cfg: DictConfig, raw_o
390
  if cli_val is None:
391
  # No direct config path — e.g. +experiment=re10k is a defaults-list override
392
  # whose effect is already baked into orig_cli_cfg; nothing to apply.
393
- print(cyan(f" Skipping '{key}' (no direct config path in cli)"))
394
  continue
395
 
396
  # For architecture group overrides: fill in missing fields from CLI defaults
@@ -401,7 +379,7 @@ def _apply_cli_overrides(merged_cfg: DictConfig, orig_cli_cfg: DictConfig, raw_o
401
  dotkey_parts = set(dotkey.split("."))
402
  if dotkey_parts & CLI_WINS_SUBKEYS:
403
  OmegaConf.update(merged_cfg, dotkey, cli_val, merge=False)
404
- print(cyan(f" '{dotkey}': replace from cli (CLI wins)"))
405
  continue
406
 
407
  existing_val = OmegaConf.select(merged_cfg, dotkey, default=None)
@@ -414,15 +392,15 @@ def _apply_cli_overrides(merged_cfg: DictConfig, orig_cli_cfg: DictConfig, raw_o
414
  if cli_subval is not None:
415
  OmegaConf.set_struct(new_val, False)
416
  OmegaConf.update(new_val, subkey, cli_subval, merge=False)
417
- print(cyan(f" '{dotkey}.{subkey}': CLI override applied (CLI wins)"))
418
  OmegaConf.update(merged_cfg, dotkey, new_val, merge=False)
419
- print(cyan(f" '{dotkey}': fill-missing from cli (checkpoint values preserved)"))
420
  continue
421
 
422
  # Group overrides and complex values replace the whole subtree;
423
  # scalars are merged so sibling keys are preserved.
424
  replace = is_group_override
425
- print(cyan(f" '{dotkey}': {'replace' if replace else 'update'} from cli"))
426
  OmegaConf.update(merged_cfg, dotkey, cli_val, merge=not replace)
427
 
428
  OmegaConf.set_struct(merged_cfg, True)
@@ -548,12 +526,16 @@ def _merge_test_mode(
548
  ) -> tuple[DictConfig, DictConfig]:
549
  """
550
  Test mode: CLI config is the base for all settings (dataset, test flags, etc.).
551
- Only optimizer and initializer *architecture* are patched in from checkpoint configs.
 
 
552
 
553
  Initializer source priority:
554
  1. separate initializer_config_path (pretrained_initializer ckpt with a config file)
555
- 2. main loaded_cfg (optimizer checkpoint's bundled initializer)
556
- 3. CLI config as-is (pretrained_initializer set but has no config file)
 
 
557
 
558
  Returns (merged_cfg, orig_cli_cfg); orig_cli_cfg is the snapshot taken before any
559
  checkpoint patches so that _apply_cli_overrides can restore explicit CLI values.
@@ -572,17 +554,28 @@ def _merge_test_mode(
572
  print(cyan("Test mode: patching scene_trainer.scene_optimizer from checkpoint config."))
573
  OmegaConf.update(merged_cfg, "scene_trainer.scene_optimizer", optimizer_subcfg, merge=True)
574
 
 
 
 
 
 
 
 
 
 
575
  # Patch initializer architecture (priority order above)
576
  if initializer_config_path is not None and initializer_config_path.exists():
577
  _patch_scene_initializer(merged_cfg, initializer_config_path, context="Test mode")
578
  elif pretrained_initializer is None:
579
- pass
580
- # TODO Naama
581
- # No explicit initializer checkpoint fall back to the optimizer checkpoint's initializer
582
- # initializer_subcfg = OmegaConf.select(loaded_cfg, "scene_trainer.scene_initializer", default=None)
583
- # if initializer_subcfg is not None:
584
- # print(cyan("Test mode: patching scene_trainer.scene_initializer from checkpoint config."))
585
- # OmegaConf.update(merged_cfg, "scene_trainer.scene_initializer", initializer_subcfg, merge=True)
 
 
586
  else:
587
  print(cyan("pretrained_initializer set but has no config file; using CLI scene_initializer config."))
588
 
@@ -677,14 +670,8 @@ def setup_output_dir(cfg, cfg_dict):
677
  output_dir = CustomPath(output_dir)
678
  output_dir.mkdir(exist_ok=True, parents=True)
679
 
680
- if HydraConfig.get().mode == RunMode.MULTIRUN and output_dir == "placeholder":
681
- # Hack to overcome multirun issues
682
- # TODO Naama, need to move to post_init of cfg
683
- output_dir = CustomPath(hydra.core.hydra_config.HydraConfig.get()["run"]["dir"])
684
- print(cyan(f"Multirun detected, setting output_dir to {CustomPath(output_dir):link}"))
685
- # save checkoint path to a file for debugging
686
- ckpt_path = cfg.checkpointing.pretrained_model or cfg.checkpointing.pretrained_optimizer
687
- (output_dir / "ckpt_dir.txt").write_text(str(ckpt_path))
688
  cfg_dict.output_dir = output_dir
689
  cfg.output_dir = output_dir
690
  output_dir.mkdir(exist_ok=True, parents=True)
 
1
+ """Typed configuration schema and Hydra-to-dataclass loading.
2
+
3
+ `RootCfg` is the top-level config dataclass (dataset, scene_trainer, meta_trainer, loss, checkpointing,
4
+ ...); `load_typed_root_config` converts the raw Hydra `DictConfig` into it via dacite, after running
5
+ `config_migrate` so older checkpoints' configs load. `__post_init__` also resolves the output directory
6
+ for `mode=test`. The per-component schemas live next to their components (e.g.
7
+ `scene_trainer_cfg.py`, `meta_trainer_cfg.py`).
8
+ """
9
+
10
  from copy import deepcopy
11
  from dataclasses import dataclass
12
  from pathlib import Path
13
+ from typing import Literal, Optional, Type, TypeVar
14
 
 
15
  import torch
16
  from dacite import Config, from_dict, UnionMatchError
 
17
  from hydra.core.hydra_config import HydraConfig
 
18
  from omegaconf import DictConfig
19
  from omegaconf import OmegaConf
20
  from pytorch_lightning.strategies import DDPStrategy, FSDPStrategy
21
 
22
  from .config_migrate import migrate, CURRENT_CFG_VERSION
23
  from .dataset.data_module import DataLoaderCfg, DatasetCfg
 
24
  from .loss import LossCfgWrapper
25
  from .misc.io import CustomPath
26
  from .misc.io import cyan, read_omega_cfg
27
  from .misc.checkpointing import find_latest_ckpt
28
+ from .misc.hf_ckpt import hf_sibling_config, is_hf_ref, maybe_resolve_hf_ref
29
+ from .paths import CKPT_DIR, RESULTS_DIR, DEBUG
30
+ from .scene_trainer.scene_trainer_cfg import SceneTrainerCfg
31
+ from .meta_trainer.meta_trainer_cfg import MetaOptimizerCfg, TestCfg, TrainCfg
32
 
33
 
34
  # In order to extract filename or dirname from a path in the config
 
63
  resume_update_module: str | None
64
  load_existing_cfg: bool
65
 
66
+ # Checkpoints whose sibling config.yaml carries the model architecture that
67
+ # mode=test rebuilds from (the optimizer/initializer the Gaussians were
68
+ # trained with).
69
+ _ARCH_CHECKPOINTS = ("pretrained_model", "pretrained_optimizer", "pretrained_initializer")
70
+
71
  def __post_init__(self):
72
  # Resolve any Hugging Face Hub references (hf://org/repo/file[@rev]) to
73
  # local cached paths so all downstream torch.load calls work unchanged.
74
+ # Rebuilding the model architecture needs both the .ckpt and its sibling
75
+ # config.yaml, but hf_hub_download pulls only the .ckpt, so fetch the
76
+ # sibling config.yaml too. It lands in the same checkpoints/ layout as
77
+ # the resolved .ckpt, so _find_config_for_checkpoint later discovers it
78
+ # on the local path.
79
  for attr in ("pretrained_model", "pretrained_optimizer", "pretrained_initializer",
80
  "pretrained_monodepth", "pretrained_mvdepth", "pretrained_depth",
81
  "pretrained_scale_predictor", "pretrained_depth_teacher",
82
  "resume_update_module"):
83
+ ref = getattr(self, attr)
84
+ if attr in self._ARCH_CHECKPOINTS and is_hf_ref(ref):
85
+ hf_sibling_config(ref)
86
+ resolved = maybe_resolve_hf_ref(ref)
87
+ if resolved != ref:
88
  setattr(self, attr, resolved)
89
 
90
  for attr in ("pretrained_model", "pretrained_optimizer", "pretrained_initializer"):
 
108
  eval_index: str | None
109
  limit_test_batches: int | float
110
  limit_train_batches: int | float
111
+ meta_optimizer: MetaOptimizerCfg
112
  test: TestCfg
113
  train: TrainCfg
114
 
 
128
  return has_trainable
129
 
130
  dist_strategy = FSDPStrategy(auto_wrap_policy=only_wrap_trainable)
131
+ if self.train.use_ckpt_buffer:
132
+ # When resampling from the ckpt buffer,
133
+ # we don't project the condition_features to state, so the state_proj is not used
134
  dist_strategy = "ddp_find_unused_parameters_true"
135
  return dist_strategy
136
 
 
142
  dataset: DatasetCfg
143
  data_loader: DataLoaderCfg
144
  scene_trainer: SceneTrainerCfg
 
145
  checkpointing: CheckpointingCfg
146
  meta_trainer: MetaTrainerCfg
147
  loss: list[LossCfgWrapper]
148
  seed: int
149
  use_plugins: bool
150
  output_dir: str
151
+ version: int | float | None # point versions like 2.1 are floats
152
  debug_cfg: bool
153
 
154
  def __post_init__(self):
 
220
  T = TypeVar("T")
221
 
222
 
 
 
 
 
 
 
223
  def _diagnose_union_error(e: UnionMatchError, data: dict, dacite_config: Config) -> str:
224
  """Try each union member individually and report per-member errors."""
225
  import dataclasses
 
285
  ]
286
 
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  def load_typed_root_config(cfg: DictConfig) -> RootCfg:
289
  # scene_trainer/scene_optimizer=none loads a full dict from none.yaml;
290
  # dacite can't match that dict to the None arm of SceneOptimizerCfg | None.
 
302
  )
303
 
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  def setup_cfg(cfg_dict):
306
  # Get the original config from the output directory, when testing or resuming.
307
  cfg_dict = merge_config_from_file(cfg_dict)
308
  eval_cfg = get_eval_cfg(cfg_dict)
309
  cfg = load_typed_root_config(cfg_dict)
 
 
310
  # Set up the output directory.
311
  setup_output_dir(cfg, cfg_dict)
312
+ # cfg is the typed config, used for attribute access throughout the code. cfg_dict is the
313
+ # raw OmegaConf tree: wandb logs it in full, the eval-config and migration helpers operate
314
+ # on it as nested dicts, and it holds fields outside the typed schema (e.g. profiling).
315
+ return cfg, cfg_dict, eval_cfg
316
 
317
 
318
  def flatten_wandb(cfg):
 
348
  print(cyan(f"Re-applying {len(raw_overrides)} CLI overrides onto merged config."))
349
  OmegaConf.set_struct(merged_cfg, False)
350
 
351
+ # Per-key merge decisions are noisy on every run; show them only under a debugger.
352
+ def _trace(msg: str) -> None:
353
+ if DEBUG:
354
+ print(cyan(msg))
355
+
356
  # Architecture subtrees: CLI group default fills in *new* fields only;
357
  # checkpoint values win for fields that already exist.
358
  ARCH_KEYS = {"scene_optimizer", "scene_initializer"}
 
368
  if cli_val is None:
369
  # No direct config path — e.g. +experiment=re10k is a defaults-list override
370
  # whose effect is already baked into orig_cli_cfg; nothing to apply.
371
+ _trace(f" Skipping '{key}' (no direct config path in cli)")
372
  continue
373
 
374
  # For architecture group overrides: fill in missing fields from CLI defaults
 
379
  dotkey_parts = set(dotkey.split("."))
380
  if dotkey_parts & CLI_WINS_SUBKEYS:
381
  OmegaConf.update(merged_cfg, dotkey, cli_val, merge=False)
382
+ _trace(f" '{dotkey}': replace from cli (CLI wins)")
383
  continue
384
 
385
  existing_val = OmegaConf.select(merged_cfg, dotkey, default=None)
 
392
  if cli_subval is not None:
393
  OmegaConf.set_struct(new_val, False)
394
  OmegaConf.update(new_val, subkey, cli_subval, merge=False)
395
+ _trace(f" '{dotkey}.{subkey}': CLI override applied (CLI wins)")
396
  OmegaConf.update(merged_cfg, dotkey, new_val, merge=False)
397
+ _trace(f" '{dotkey}': fill-missing from cli (checkpoint values preserved)")
398
  continue
399
 
400
  # Group overrides and complex values replace the whole subtree;
401
  # scalars are merged so sibling keys are preserved.
402
  replace = is_group_override
403
+ _trace(f" '{dotkey}': {'replace' if replace else 'update'} from cli")
404
  OmegaConf.update(merged_cfg, dotkey, cli_val, merge=not replace)
405
 
406
  OmegaConf.set_struct(merged_cfg, True)
 
526
  ) -> tuple[DictConfig, DictConfig]:
527
  """
528
  Test mode: CLI config is the base for all settings (dataset, test flags, etc.).
529
+ The optimizer and initializer *architecture* and the decoder are patched in from the
530
+ checkpoint config, so the model is rebuilt and rendered the way it was trained. An explicit
531
+ CLI override still wins for any of these via _apply_cli_overrides.
532
 
533
  Initializer source priority:
534
  1. separate initializer_config_path (pretrained_initializer ckpt with a config file)
535
+ 2. full pretrained_model checkpoint (it bundles the initializer weights, so its
536
+ architecture must match the checkpoint config)
537
+ 3. CLI config as-is (pretrained_optimizer alone carries no initializer
538
+ weights; or pretrained_initializer has no config file)
539
 
540
  Returns (merged_cfg, orig_cli_cfg); orig_cli_cfg is the snapshot taken before any
541
  checkpoint patches so that _apply_cli_overrides can restore explicit CLI values.
 
554
  print(cyan("Test mode: patching scene_trainer.scene_optimizer from checkpoint config."))
555
  OmegaConf.update(merged_cfg, "scene_trainer.scene_optimizer", optimizer_subcfg, merge=True)
556
 
557
+ # Patch decoder from checkpoint so rendering uses the rasterizer the Gaussians were trained
558
+ # for. Merge over the CLI decoder (checkpoint values win) so any field the checkpoint lacks
559
+ # is filled from the CLI default -- old checkpoints predate some DecoderCfg fields and there
560
+ # is no decoder migration. An explicit CLI decoder override still wins via _apply_cli_overrides.
561
+ decoder_subcfg = OmegaConf.select(loaded_cfg, "scene_trainer.decoder", default=None)
562
+ if decoder_subcfg is not None:
563
+ print(cyan("Test mode: patching scene_trainer.decoder from checkpoint config (CLI fills missing fields)."))
564
+ OmegaConf.update(merged_cfg, "scene_trainer.decoder", decoder_subcfg, merge=True)
565
+
566
  # Patch initializer architecture (priority order above)
567
  if initializer_config_path is not None and initializer_config_path.exists():
568
  _patch_scene_initializer(merged_cfg, initializer_config_path, context="Test mode")
569
  elif pretrained_initializer is None:
570
+ # No separate initializer checkpoint. A full pretrained_model bundles the initializer
571
+ # weights (load_full_model), so its architecture must come from the checkpoint config.
572
+ # A pretrained_optimizer checkpoint carries no initializer weights, so the CLI
573
+ # scene_initializer config is kept as-is.
574
+ if cli_cfg.checkpointing.pretrained_model is not None:
575
+ initializer_subcfg = OmegaConf.select(loaded_cfg, "scene_trainer.scene_initializer", default=None)
576
+ if initializer_subcfg is not None:
577
+ print(cyan("Test mode: patching scene_trainer.scene_initializer from full-model checkpoint config."))
578
+ OmegaConf.update(merged_cfg, "scene_trainer.scene_initializer", initializer_subcfg, merge=True)
579
  else:
580
  print(cyan("pretrained_initializer set but has no config file; using CLI scene_initializer config."))
581
 
 
670
  output_dir = CustomPath(output_dir)
671
  output_dir.mkdir(exist_ok=True, parents=True)
672
 
673
+ # Single point where output_dir is finalized. Downstream code reads cfg.output_dir; the
674
+ # value is also written into cfg_dict so the saved config.yaml records it. Keep both in sync.
 
 
 
 
 
 
675
  cfg_dict.output_dir = output_dir
676
  cfg.output_dir = output_dir
677
  output_dir.mkdir(exist_ok=True, parents=True)
optgs/config/dataset/dl3dv.yaml CHANGED
@@ -4,19 +4,16 @@ defaults:
4
 
5
  name: dl3dv
6
  roots: [datasets/dl3dv]
7
- make_baseline_1: false
8
  augment: true
9
 
10
 
11
  image_shape: [270, 480]
12
 
13
- baseline_epsilon: 1e-3
14
  max_fov: 100.0
15
 
16
  skip_bad_shape: true
17
  near: -1.
18
  far: -1.
19
- baseline_scale_bounds: false
20
  shuffle_val: true
21
  test_len: -1
22
  test_chunk_interval: 1
@@ -27,35 +24,11 @@ train_times_per_scene: 1
27
  test_times_per_scene: 1
28
  ori_image_shape: [270, 480]
29
  overfit_max_views: 148
30
- use_index_to_load_chunk: false
31
 
32
- mix_tartanair: false
33
  no_mix_test_set: true
34
- load_depth: false
35
- center_pose: false
36
-
37
- pose_align_first_view: false
38
-
39
- scale_extrinsics: 1.
40
- metric_scale_align_dl3dv: false
41
 
42
  # view filtering
43
  min_views: 0
44
  max_views: 0
45
- highres: false
46
-
47
- # mix re10k & dl3dv
48
- mix_re10k: false
49
- re10k_min_view_dist: 40
50
- re10k_max_view_dist: 300
51
-
52
- # load remaining context views
53
- load_remain_context: false
54
- num_remain_context: 8
55
-
56
- # random crop in training
57
- random_crop: false
58
- min_size: null
59
- max_size: null
60
 
61
- index_name: index.json
 
4
 
5
  name: dl3dv
6
  roots: [datasets/dl3dv]
 
7
  augment: true
8
 
9
 
10
  image_shape: [270, 480]
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
 
24
  test_times_per_scene: 1
25
  ori_image_shape: [270, 480]
26
  overfit_max_views: 148
 
27
 
 
28
  no_mix_test_set: true
 
 
 
 
 
 
 
29
 
30
  # view filtering
31
  min_views: 0
32
  max_views: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ index_name: index.json
optgs/config/dataset/re10k.yaml CHANGED
@@ -4,24 +4,16 @@ defaults:
4
 
5
  name: re10k
6
  roots: [datasets/re10k]
7
- make_baseline_1: false
8
  augment: true
9
 
10
  image_shape: [180, 320]
11
  highres: false
12
 
13
- baseline_epsilon: 1e-3
14
  max_fov: 100.0
15
 
16
  skip_bad_shape: true
17
  near: -1.
18
  far: -1.
19
- baseline_scale_bounds: true
20
  shuffle_val: true
21
  test_len: -1
22
- test_chunk_interval: 1
23
-
24
- use_index_to_load_chunk: false
25
-
26
- average_pose: false
27
- center_pose: false
 
4
 
5
  name: re10k
6
  roots: [datasets/re10k]
 
7
  augment: true
8
 
9
  image_shape: [180, 320]
10
  highres: 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
 
 
 
 
 
optgs/config/experiment/re10k_unified.yaml CHANGED
@@ -63,16 +63,11 @@ dataset:
63
  roots: [datasets/re10k]
64
  near: 0.01
65
  far: 100.
66
- baseline_scale_bounds: false
67
- make_baseline_1: false
68
  train_times_per_scene: 1
69
  highres: false
70
  scannet: false
71
  tartanair: false
72
  load_depth: false
73
- pose_align_first_view: false
74
- scale_extrinsics: 1.
75
- load_remain_context: false
76
  pose_align_middle_view: false
77
  overfit_to_scene: null
78
  opencv_pose_format: false
 
63
  roots: [datasets/re10k]
64
  near: 0.01
65
  far: 100.
 
 
66
  train_times_per_scene: 1
67
  highres: false
68
  scannet: false
69
  tartanair: false
70
  load_depth: false
 
 
 
71
  pose_align_middle_view: false
72
  overfit_to_scene: null
73
  opencv_pose_format: false
optgs/config/experiment/train_dl3dv.yaml CHANGED
@@ -21,7 +21,6 @@ meta_trainer:
21
  max_steps: 50_000
22
  val_check_interval: 0.25
23
  train:
24
- l1_loss: true
25
  depth_smooth_loss_weight: 0.0
26
  test:
27
  eval_time_skip_steps: 0
@@ -31,6 +30,8 @@ meta_trainer:
31
 
32
  # lpips loss
33
  loss:
 
 
34
  lpips:
35
  apply_after_step: 0
36
  weight: 0.5
@@ -40,8 +41,6 @@ dataset:
40
  roots: [ datasets/dl3dv-480p-chunks ]
41
  near: 0.01
42
  far: 200.
43
- min_size: [ 384,512 ]
44
- max_size: [ 512,960 ]
45
  image_shape: [ 256, 448 ]
46
  view_sampler:
47
  num_context_views: 8
 
21
  max_steps: 50_000
22
  val_check_interval: 0.25
23
  train:
 
24
  depth_smooth_loss_weight: 0.0
25
  test:
26
  eval_time_skip_steps: 0
 
30
 
31
  # lpips loss
32
  loss:
33
+ mse:
34
+ l1_loss: true
35
  lpips:
36
  apply_after_step: 0
37
  weight: 0.5
 
41
  roots: [ datasets/dl3dv-480p-chunks ]
42
  near: 0.01
43
  far: 200.
 
 
44
  image_shape: [ 256, 448 ]
45
  view_sampler:
46
  num_context_views: 8
optgs/config/experiment/train_l2s_sparse_dl3dv.yaml CHANGED
@@ -2,8 +2,8 @@
2
 
3
  defaults:
4
  - train_dl3dv
5
- - override /meta_trainer/train/replay_buffer_cfg: default
6
- - override /loss: [ mse, lpips, deltas ]
7
 
8
  loss:
9
  mse:
@@ -23,19 +23,24 @@ meta_trainer:
23
  train:
24
  loss_on_input_views: true
25
  loss_on_input_views_num: 4
26
- use_replay_buffer: true
 
 
 
27
 
28
  scene_trainer:
29
  train_scene_opt: true
30
- num_update_steps: 4
 
31
  train_max_refine: 6
32
  train_min_refine: 1
33
-
34
- meta_optimizer:
35
- lr: 1e-4
36
- lr_monodepth: 0.0
37
-
38
 
39
  checkpointing:
40
- pretrained_initializer: checkpoints/optgs/unified-dl3dv-8views/init/checkpoints/epoch_20-step_100000.ckpt # resplat inititalizer
 
41
  no_strict_load: false
 
2
 
3
  defaults:
4
  - train_dl3dv
5
+ - override /meta_trainer/train/ckpt_buffer_cfg: default
6
+ - override /loss: [ mse, lpips, deltas, stability ]
7
 
8
  loss:
9
  mse:
 
23
  train:
24
  loss_on_input_views: true
25
  loss_on_input_views_num: 4
26
+ use_ckpt_buffer: true
27
+ meta_optimizer:
28
+ lr: 1e-4
29
+ lr_monodepth: 0.0
30
 
31
  scene_trainer:
32
  train_scene_opt: true
33
+ num_update_steps: 2000
34
+ opt_batch_size: 8
35
  train_max_refine: 6
36
  train_min_refine: 1
37
+ # Decoder settings for the learn2splat paper weights (the generic gsplat default is
38
+ # antialiased / eps2d=0.3; these weights are trained with classic rasterization).
39
+ decoder:
40
+ rasterize_mode: classic
41
+ eps2d: 1e-4
42
 
43
  checkpointing:
44
+ # ReSplat initializer
45
+ pretrained_initializer: hf://autonomousvision/learn2splat/resplat_init/checkpoints/epoch_20-step_100000.ckpt
46
  no_strict_load: false
optgs/config/experiment/train_l2s_sparse_dl3dv_no_delta.yaml CHANGED
@@ -2,7 +2,7 @@
2
 
3
  defaults:
4
  - train_dl3dv
5
- - override /meta_trainer/train/replay_buffer_cfg: default
6
  - override /loss: [ mse, lpips ]
7
 
8
  loss:
@@ -17,7 +17,10 @@ meta_trainer:
17
  train:
18
  loss_on_input_views: true
19
  loss_on_input_views_num: 4
20
- use_replay_buffer: true
 
 
 
21
 
22
  scene_trainer:
23
  train_scene_opt: true
@@ -25,11 +28,6 @@ scene_trainer:
25
  train_max_refine: 6
26
  train_min_refine: 1
27
 
28
- meta_optimizer:
29
- lr: 1e-4
30
- lr_monodepth: 0.0
31
-
32
-
33
  checkpointing:
34
  pretrained_initializer: checkpoints/optgs/unified-dl3dv-8views/init/checkpoints/epoch_20-step_100000.ckpt # resplat inititalizer
35
  no_strict_load: false
 
2
 
3
  defaults:
4
  - train_dl3dv
5
+ - override /meta_trainer/train/ckpt_buffer_cfg: default
6
  - override /loss: [ mse, lpips ]
7
 
8
  loss:
 
17
  train:
18
  loss_on_input_views: true
19
  loss_on_input_views_num: 4
20
+ use_ckpt_buffer: true
21
+ meta_optimizer:
22
+ lr: 1e-4
23
+ lr_monodepth: 0.0
24
 
25
  scene_trainer:
26
  train_scene_opt: true
 
28
  train_max_refine: 6
29
  train_min_refine: 1
30
 
 
 
 
 
 
31
  checkpointing:
32
  pretrained_initializer: checkpoints/optgs/unified-dl3dv-8views/init/checkpoints/epoch_20-step_100000.ckpt # resplat inititalizer
33
  no_strict_load: false
optgs/config/experiment/train_l2s_sparse_dl3dv_no_loss.yaml CHANGED
@@ -2,7 +2,7 @@
2
 
3
  defaults:
4
  - train_dl3dv
5
- - override /meta_trainer/train/replay_buffer_cfg: default
6
  - override /loss: [ mse, lpips ]
7
 
8
  loss:
@@ -17,7 +17,10 @@ meta_trainer:
17
  train:
18
  loss_on_input_views: true
19
  loss_on_input_views_num: 4
20
- use_replay_buffer: true
 
 
 
21
 
22
  scene_trainer:
23
  train_scene_opt: true
@@ -25,11 +28,6 @@ scene_trainer:
25
  train_max_refine: 6
26
  train_min_refine: 1
27
 
28
- meta_optimizer:
29
- lr: 1e-4
30
- lr_monodepth: 0.0
31
-
32
-
33
  checkpointing:
34
  pretrained_initializer: checkpoints/optgs/unified-dl3dv-8views/init/checkpoints/epoch_20-step_100000.ckpt # resplat inititalizer
35
  no_strict_load: false
 
2
 
3
  defaults:
4
  - train_dl3dv
5
+ - override /meta_trainer/train/ckpt_buffer_cfg: default
6
  - override /loss: [ mse, lpips ]
7
 
8
  loss:
 
17
  train:
18
  loss_on_input_views: true
19
  loss_on_input_views_num: 4
20
+ use_ckpt_buffer: true
21
+ meta_optimizer:
22
+ lr: 1e-4
23
+ lr_monodepth: 0.0
24
 
25
  scene_trainer:
26
  train_scene_opt: true
 
28
  train_max_refine: 6
29
  train_min_refine: 1
30
 
 
 
 
 
 
31
  checkpointing:
32
  pretrained_initializer: checkpoints/optgs/unified-dl3dv-8views/init/checkpoints/epoch_20-step_100000.ckpt # resplat inititalizer
33
  no_strict_load: false
optgs/config/loss/lpips.yaml CHANGED
@@ -2,3 +2,4 @@ lpips:
2
  weight: 0.05
3
  apply_after_step: 150_000
4
  perceptual_loss: false
 
 
2
  weight: 0.05
3
  apply_after_step: 150_000
4
  perceptual_loss: false
5
+ half_res: false
optgs/config/loss/mse.yaml CHANGED
@@ -1,2 +1,4 @@
1
  mse:
2
  weight: 1.0
 
 
 
1
  mse:
2
  weight: 1.0
3
+ l1_loss: false
4
+ clamp_large_error: 0.0
optgs/config/loss/sgd.yaml CHANGED
@@ -1,2 +1,4 @@
1
  sgd:
2
- weight: 1.0
 
 
 
1
  sgd:
2
+ weight: 1.0
3
+ l1_loss: false
4
+ clamp_large_error: 0.0
optgs/config/loss/stability.yaml CHANGED
@@ -1,2 +1,4 @@
1
  stability:
2
  weight: 1.0
 
 
 
1
  stability:
2
  weight: 1.0
3
+ # Also penalize inputs optimized on per-step view subsets (not fully tested in training).
4
+ subset_aware: false
optgs/config/main.yaml CHANGED
@@ -5,7 +5,7 @@ defaults:
5
  - scene_trainer/scene_optimizer: null
6
  - scene_trainer/decoder: gsplat
7
  - meta_trainer/test/postprocessing: none
8
- - meta_trainer/train/replay_buffer_cfg: none
9
 
10
  wandb:
11
  project: placeholder
@@ -34,15 +34,6 @@ data_loader:
34
  batch_size: 1
35
  seed: 3456
36
 
37
- meta_optimizer:
38
- lr: 2.e-4
39
- lr_monodepth: 2.e-6
40
- lr_depth: 0.
41
- warm_up_steps: 2000
42
- weight_decay: 0.01
43
- warm_up_ratio: 0.01
44
- adamw_8bit: false
45
-
46
  checkpointing:
47
  load: null
48
  every_n_train_steps: 1000
@@ -75,25 +66,30 @@ meta_trainer:
75
  limit_test_batches: 1.0
76
  limit_train_batches: 1.0
77
  num_nodes: 1
 
 
 
 
 
 
 
 
78
  train:
79
  depth_mode: null
80
  extended_visualization: false
81
  print_log_every_n_steps: 100
82
  eval_model_every_n_val: 2 # quantitative evaluation every n val
83
  eval_data_length: 999999
84
- eval_deterministic: false
85
  eval_time_skip_steps: 3
86
  eval_save_model: true
87
- l1_loss: false
88
  intermediate_loss_weight: 0.9
89
  no_viz_video: false
90
  eval_depth: false
91
- train_ignore_large_loss: 0.
92
  no_log_projections: true
93
  no_log_video: true
94
  depth_loss_weight: 0.
95
  log_depth_loss: true
96
- depth_smooth_loss_weight: 0.01
97
  depth_smooth_loss_nonorm: false
98
  depth_smooth_loss_weight_nvs: 0. # for novel views
99
  monodepth_loss_weight: 0. # for monocular depth loss
@@ -103,21 +99,17 @@ meta_trainer:
103
  render_depth_loss_weight: 0.
104
  viz_render_depth: false
105
  use_gt_depth_range: false
106
- depth_range_from_disparity: false
107
- max_disparity: 128.
108
- min_disparity: 4.
109
  loss_on_input_views: false
110
  loss_on_target_views: true
111
  loss_on_input_views_num: 1
112
  loss_on_target_views_num: -1
113
  train_window_size: null
114
- half_res_lpips_loss: false
115
  viz_depth_separate: false
116
  # L2 weight decay on Gaussian properties (meta-loss)
117
  scale_l2_loss_weight: 0.
118
  sh_l2_loss_weight: 0.
119
  opacity_l2_loss_weight: 0.
120
- use_replay_buffer: false
121
  test:
122
  output_path: null
123
  compute_scores: true
@@ -130,22 +122,22 @@ meta_trainer:
130
  save_gt_image: false
131
  save_render_depth: false
132
  save_gt_depth: false
 
133
  save_error_image: false
134
  save_video: false
135
- save_video_fixed_view: false
136
- save_video_fixed_view_index: 0
137
- save_video_fixed_view_duplicate: 0
138
- save_video_fixed_iteration: false
139
- save_video_fixed_iteration_indices: null
140
- save_video_fixed_iteration_render_fixed_view: false
141
- save_video_combined: false
142
- save_video_combined_iterations: null
143
- save_video_combined_fixed_iteration_length: 50
144
  save_gaussian: false
145
  save_poses: false
146
  save_cameras_json: true
147
  save_cameras_npz: true
148
- save_point_cloud: false
149
  render_chunk_size: null
150
  dec_chunk_size: null
151
  stablize_camera: false
 
5
  - scene_trainer/scene_optimizer: null
6
  - scene_trainer/decoder: gsplat
7
  - meta_trainer/test/postprocessing: none
8
+ - meta_trainer/train/ckpt_buffer_cfg: none
9
 
10
  wandb:
11
  project: placeholder
 
34
  batch_size: 1
35
  seed: 3456
36
 
 
 
 
 
 
 
 
 
 
37
  checkpointing:
38
  load: null
39
  every_n_train_steps: 1000
 
66
  limit_test_batches: 1.0
67
  limit_train_batches: 1.0
68
  num_nodes: 1
69
+ meta_optimizer:
70
+ lr: 2.e-4
71
+ lr_monodepth: 2.e-6
72
+ lr_depth: 0.
73
+ warm_up_steps: 2000
74
+ weight_decay: 0.01
75
+ warm_up_ratio: 0.01
76
+ adamw_8bit: false
77
  train:
78
  depth_mode: null
79
  extended_visualization: false
80
  print_log_every_n_steps: 100
81
  eval_model_every_n_val: 2 # quantitative evaluation every n val
82
  eval_data_length: 999999
 
83
  eval_time_skip_steps: 3
84
  eval_save_model: true
 
85
  intermediate_loss_weight: 0.9
86
  no_viz_video: false
87
  eval_depth: false
 
88
  no_log_projections: true
89
  no_log_video: true
90
  depth_loss_weight: 0.
91
  log_depth_loss: true
92
+ depth_smooth_loss_weight: 0. # only meaningful when train_scene_init=True
93
  depth_smooth_loss_nonorm: false
94
  depth_smooth_loss_weight_nvs: 0. # for novel views
95
  monodepth_loss_weight: 0. # for monocular depth loss
 
99
  render_depth_loss_weight: 0.
100
  viz_render_depth: false
101
  use_gt_depth_range: false
 
 
 
102
  loss_on_input_views: false
103
  loss_on_target_views: true
104
  loss_on_input_views_num: 1
105
  loss_on_target_views_num: -1
106
  train_window_size: null
 
107
  viz_depth_separate: false
108
  # L2 weight decay on Gaussian properties (meta-loss)
109
  scale_l2_loss_weight: 0.
110
  sh_l2_loss_weight: 0.
111
  opacity_l2_loss_weight: 0.
112
+ use_ckpt_buffer: false
113
  test:
114
  output_path: null
115
  compute_scores: true
 
122
  save_gt_image: false
123
  save_render_depth: false
124
  save_gt_depth: false
125
+ save_init_pred_depth: false
126
  save_error_image: false
127
  save_video: false
128
+ save_video_optim: false
129
+ save_video_view_index: 0
130
+ save_video_frame_repeat: 1
131
+ save_video_orbit: false
132
+ save_video_orbit_steps: null
133
+ save_video_orbit_with_optim: false
134
+ save_video_optim_orbit: false
135
+ save_video_optim_orbit_steps: null
136
+ save_video_orbit_span: 50
137
  save_gaussian: false
138
  save_poses: false
139
  save_cameras_json: true
140
  save_cameras_npz: true
 
141
  render_chunk_size: null
142
  dec_chunk_size: null
143
  stablize_camera: false
optgs/config/meta_trainer/train/{replay_buffer_cfg → ckpt_buffer_cfg}/default.yaml RENAMED
@@ -3,10 +3,10 @@ sample_batch_size: 1
3
  sample_prob: 0.7
4
  insert_prob: 0.7
5
  return_prob: 0.99
6
- simulate_ahead: true
7
- simulate_ahead_min_steps: 1
8
- simulate_ahead_max_steps: 50
9
- simulate_ahead_grow: 10000
10
  max_t: null
11
  push_only_if_not_full: false
12
  remove_strategy_when_full: oldest
 
3
  sample_prob: 0.7
4
  insert_prob: 0.7
5
  return_prob: 0.99
6
+ rollout: true
7
+ rollout_min_steps: 1
8
+ rollout_max_steps: 50
9
+ rollout_grow: 10000
10
  max_t: null
11
  push_only_if_not_full: false
12
  remove_strategy_when_full: oldest
optgs/config/meta_trainer/train/{replay_buffer_cfg → ckpt_buffer_cfg}/none.yaml RENAMED
@@ -3,10 +3,10 @@ sample_batch_size: 1
3
  sample_prob: 0.0
4
  insert_prob: 0.0
5
  return_prob: 0.0
6
- simulate_ahead: false
7
- simulate_ahead_min_steps: 0
8
- simulate_ahead_max_steps: 0
9
- simulate_ahead_grow: 0
10
  max_t: null
11
  push_only_if_not_full: false
12
  remove_strategy_when_full: oldest
 
3
  sample_prob: 0.0
4
  insert_prob: 0.0
5
  return_prob: 0.0
6
+ rollout: false
7
+ rollout_min_steps: 0
8
+ rollout_max_steps: 0
9
+ rollout_grow: 0
10
  max_t: null
11
  push_only_if_not_full: false
12
  remove_strategy_when_full: oldest
optgs/config/scene_trainer/scene_initializer/edgs.yaml CHANGED
@@ -7,4 +7,5 @@ sh_degree: 3
7
  init_opacity: 0.5
8
  scaling_factor: 0.5
9
  roma_model_type: outdoors
10
- sample_init_gaussians: -1
 
 
7
  init_opacity: 0.5
8
  scaling_factor: 0.5
9
  roma_model_type: outdoors
10
+ sample_init_gaussians: -1
11
+ cache_dir: cache/edgs
optgs/config/scene_trainer/scene_initializer/resplat_v1.yaml CHANGED
@@ -71,19 +71,13 @@ init_pt_with_mv_attn_lowres: false
71
  pt_head_channels: null
72
  pt_head_concat_img: false
73
  pt_head_conv: false
74
- multi_scale_pt: false
75
  attn_proj_channels: 64
76
- fps_num_samples: null
77
  knn_samples: 16
78
  post_norm: false
79
  no_rpe: true
80
  no_knn_attn: false
81
  num_blocks: 4
82
- pt_downsample: 0
83
- fps_agg_func: attn
84
- subsample_method: fps
85
  add_pt_residual: true
86
- pt_pred_residual_position: false
87
 
88
  # freeze depth
89
  freeze_depth: false
@@ -99,10 +93,6 @@ bilinear_upsample_depth: false
99
  no_upsample_depth: false
100
  return_lowres_depth: false
101
 
102
- # lvsm gaussian regressor
103
- lvsm_gaussian_regressor: false
104
- lvsm_layers: 6
105
-
106
  # latent gaussian instead of pixel aligned gaussians
107
  latent_gs: true
108
  latent_downsample: 4
 
71
  pt_head_channels: null
72
  pt_head_concat_img: false
73
  pt_head_conv: false
 
74
  attn_proj_channels: 64
 
75
  knn_samples: 16
76
  post_norm: false
77
  no_rpe: true
78
  no_knn_attn: false
79
  num_blocks: 4
 
 
 
80
  add_pt_residual: true
 
81
 
82
  # freeze depth
83
  freeze_depth: false
 
93
  no_upsample_depth: false
94
  return_lowres_depth: false
95
 
 
 
 
 
96
  # latent gaussian instead of pixel aligned gaussians
97
  latent_gs: true
98
  latent_downsample: 4
optgs/config/scene_trainer/scene_initializer/resplat_v2.yaml CHANGED
@@ -77,19 +77,13 @@ init_pt_with_mv_attn_lowres: true
77
  pt_head_channels: null
78
  pt_head_concat_img: false
79
  pt_head_conv: false
80
- multi_scale_pt: false
81
  attn_proj_channels: 64
82
- fps_num_samples: null
83
  knn_samples: 16
84
  post_norm: false
85
  no_rpe: true
86
  no_knn_attn: false
87
  num_blocks: 6
88
- pt_downsample: 0
89
- fps_agg_func: attn
90
- subsample_method: fps
91
  add_pt_residual: true
92
- pt_pred_residual_position: false
93
 
94
  # freeze depth
95
  freeze_depth: false
@@ -105,10 +99,6 @@ bilinear_upsample_depth: false
105
  no_upsample_depth: false
106
  return_lowres_depth: false
107
 
108
- # lvsm gaussian regressor
109
- lvsm_gaussian_regressor: false
110
- lvsm_layers: 6
111
-
112
  # latent gaussian instead of pixel aligned gaussians
113
  latent_gs: true
114
  latent_downsample: 4
 
77
  pt_head_channels: null
78
  pt_head_concat_img: false
79
  pt_head_conv: false
 
80
  attn_proj_channels: 64
 
81
  knn_samples: 16
82
  post_norm: false
83
  no_rpe: true
84
  no_knn_attn: false
85
  num_blocks: 6
 
 
 
86
  add_pt_residual: true
 
87
 
88
  # freeze depth
89
  freeze_depth: false
 
99
  no_upsample_depth: false
100
  return_lowres_depth: false
101
 
 
 
 
 
102
  # latent gaussian instead of pixel aligned gaussians
103
  latent_gs: true
104
  latent_downsample: 4
optgs/config/scene_trainer/scene_optimizer/3dgs.yaml CHANGED
@@ -1,22 +1,11 @@
1
  defaults:
2
  - base
3
  - override refiner: default
 
4
 
5
  name: adam
6
 
7
  # Adam optimizer
8
  betas: [0.9, 0.999]
9
  eps: 1e-15
10
- weight_decay: 0.0
11
-
12
- # learning rates (gsplat)
13
- base_lr: 1
14
- means_lr_init: 1.6e-4
15
- means_lr_final: 1.6e-6
16
- means_lr_delay_mult: 1.0
17
- means_lr_max_steps: 30000 # should be equal to total optimization steps
18
- scales_lr: 5e-3
19
- rotations_lr: 1e-3
20
- opacities_lr: 5e-2
21
- sh0s_lr: 2.5e-3
22
- shNs_lr: 1.25e-4
 
1
  defaults:
2
  - base
3
  - override refiner: default
4
+ - override lr_scheduler: expon
5
 
6
  name: adam
7
 
8
  # Adam optimizer
9
  betas: [0.9, 0.999]
10
  eps: 1e-15
11
+ weight_decay: 0.0
 
 
 
 
 
 
 
 
 
 
 
 
optgs/config/scene_trainer/scene_optimizer/3dgs_star.yaml CHANGED
@@ -1,6 +1,7 @@
1
  defaults:
2
  - base
3
  - override refiner: default
 
4
 
5
  name: adam
6
 
@@ -9,14 +10,8 @@ betas: [0.99, 0.999]
9
  eps: 1e-15
10
  weight_decay: 0.0
11
 
12
- base_lr: 5
13
- # 3dgs defaults
14
- means_lr_init: 1.6e-4 # Setting same as final to have constant LR
15
- means_lr_final: 1.6e-4
16
- means_lr_delay_mult: 0.0
17
- means_lr_max_steps: 30000 # should be equal to total optimization steps
18
- scales_lr: 5e-3
19
- rotations_lr: 1e-3
20
- opacities_lr: 5e-2
21
- sh0s_lr: 2.5e-3
22
- shNs_lr: 1.25e-4
 
1
  defaults:
2
  - base
3
  - override refiner: default
4
+ - override lr_scheduler: expon
5
 
6
  name: adam
7
 
 
10
  eps: 1e-15
11
  weight_decay: 0.0
12
 
13
+ # 5x base LR; means held constant (lr_final == _means, so means init 5*1.6e-4 == final 5*1.6e-4).
14
+ lr_scheduler:
15
+ lr_data:
16
+ _base: 5
17
+ lr_final: 1.6e-4
 
 
 
 
 
 
optgs/config/scene_trainer/scene_optimizer/base.yaml CHANGED
@@ -6,12 +6,12 @@ defaults:
6
  input_gradients_chunk_size: -1
7
 
8
  # iterative refine
9
- no_refine_mean: false
10
- no_refine_scale: false
11
- no_refine_rotation: false
12
- no_refine_opacity: false
13
- no_refine_sh0: false
14
- no_refine_shN: false
15
 
16
  zero_state_on_densify: false
17
 
 
6
  input_gradients_chunk_size: -1
7
 
8
  # iterative refine
9
+ freeze_mean: false
10
+ freeze_scale: false
11
+ freeze_rotation: false
12
+ freeze_opacity: false
13
+ freeze_sh0: false
14
+ freeze_shN: false
15
 
16
  zero_state_on_densify: false
17
 
optgs/config/scene_trainer/scene_optimizer/knn_based.yaml CHANGED
@@ -5,19 +5,18 @@ name: knn_based
5
 
6
  # iterative refine
7
  no_render_error: false
8
- refine_sh_only: false
9
- num_basic_refine_blocks: 4
10
- num_refine_blocks: 1
11
  concat_init_state: false
12
  replace_init_state: false
13
  state_channels: 256
14
- refine_block_rmsnorm: false
15
- refine_block_layernorm: false
16
  pt_qk_norm: false
17
  norm_pt_block: false
18
- refine_gaussian_multiple: 1
19
- refine_residual_init_state: false
20
- clamp_refine_max_scale: 3.0
21
  clamp_min_scale: 1e-6
22
  clamp_min_raw_scales: -1e10
23
  clamp_max_raw_scales: 1e10
@@ -30,15 +29,14 @@ clamp_min_shs: -1e10
30
  clamp_max_shs: 1e10
31
  clamp_shs_soft: false
32
 
33
- update_attn_proj_channels: 64
34
- update_no_knn_attn: false
35
- update_no_tran_block_norm: false
36
- update_tran_block_act: gelu
37
  multi_gaussian_scale_smaller: false
38
  init_gaussian_multiple: 1
39
- refine_condition_pt_feature: true
40
- reinit_gaussian_when_refine_multiple: false
41
- refine_same_num_points: false
42
  input_error_rgb_no_shuffle: false
43
  input_error_cache_resnet_feature: false
44
 
@@ -50,20 +48,19 @@ init_state_scale: 0
50
  pt_heads: 1
51
 
52
  # refine with mv attention
53
- refine_with_mv_attn: false
54
- refine_with_mv_attn_lowres: false
55
- refine_no_mv_attn: false
56
  mv_attn_conv_with_norm: false
57
- refine_mv_shuffle_attn: false
58
- refine_mv_attn_with_pos_enc: false
59
- refine_shuffle_attn_no_norm: false
60
- refine_mv_unimatch_attn: false
61
- refine_knn_samples: 16
62
- refine_multi_scale_pt: false
63
 
64
  # KNN
65
  use_fused_attn: true # fused KNN gather + attention CUDA kernel (faster, less memory)
66
- prune_invisible_gaussians: false
67
  knn_idx_update_every: 1
68
 
69
  # inputs
@@ -103,25 +100,17 @@ input_gradient: false
103
  input_gradient_log: false
104
  input_gradient_log_clip_deltas: 0.001
105
  input_gradient_scale: 1.
106
- gradient_update_scale: 1.
107
  input_gradient_with_ssim_loss: false
108
  input_gradient_same_loss: false
109
  input_gradient_loss_reduction: mean
110
  scale_residual_grads: false
111
 
112
- window_local_refine: false
113
- window_global_refine: false
114
- window_local_global_refine: false
115
-
116
- # sliding window update to save training memory
117
- update_window_size: 0
118
- local_gaussian_render: false
119
-
120
- train_global_update_only: false
121
 
122
  # random size refine
123
  # update more for low resolution, less for high
124
- random_update_with_size: false
125
 
126
 
127
  use_amp: true
@@ -131,9 +120,6 @@ pt_update_amp: true
131
  use_checkpointing: false
132
  recurrent_use_checkpointing: false
133
 
134
- # Debugging
135
- debug_refine_update_module: true
136
-
137
  # Normalizing input
138
  input_gradient_normalize: false
139
  input_gradient_normalize_type: layer
@@ -146,21 +132,20 @@ predict_state_scale: false
146
  predict_state_scale_norm: false
147
 
148
  # Update head
149
- update_head_concat_img: false
150
- update_head_layer_num: 2
151
- update_head_act: gelu
152
- update_head_final_act: identity
153
- update_head_hidden_dim_matches: "input" # rebuttal version. switch to "output" for submission version
154
 
155
- update_head_scale_mag: false
156
- update_head_scalar_scale: false
157
- update_head_scalar_scale_act: relu
158
 
159
  # Per-parameter-group heads (Feature A): separate heads per param group, each with own normalize+scale
160
- update_head_per_param_heads: false
161
- update_head_per_param_hidden_dim: 48 # tuned so total params ≈ baseline head (~81K)
162
- # Per-parameter scalar scales (Feature B): per-group scalar scales (requires update_head_scalar_scale=true)
163
- update_head_per_param_scales: false
164
 
165
  opt_scales_before_act: false
166
 
 
5
 
6
  # iterative refine
7
  no_render_error: false
8
+ num_basic_blocks: 4
9
+ num_blocks: 1
 
10
  concat_init_state: false
11
  replace_init_state: false
12
  state_channels: 256
13
+ block_rmsnorm: false
14
+ block_layernorm: false
15
  pt_qk_norm: false
16
  norm_pt_block: false
17
+ delta_gaussian_multiple: 1
18
+ residual_init_state: false
19
+ clamp_max_scale: 3.0
20
  clamp_min_scale: 1e-6
21
  clamp_min_raw_scales: -1e10
22
  clamp_max_raw_scales: 1e10
 
29
  clamp_max_shs: 1e10
30
  clamp_shs_soft: false
31
 
32
+ attn_proj_channels: 64
33
+ no_knn_attn: false
34
+ no_tran_block_norm: false
35
+ tran_block_act: gelu
36
  multi_gaussian_scale_smaller: false
37
  init_gaussian_multiple: 1
38
+ condition_pt_feature: true
39
+ same_num_points: false
 
40
  input_error_rgb_no_shuffle: false
41
  input_error_cache_resnet_feature: false
42
 
 
48
  pt_heads: 1
49
 
50
  # refine with mv attention
51
+ with_mv_attn: false
52
+ with_mv_attn_lowres: false
53
+ no_mv_attn: false
54
  mv_attn_conv_with_norm: false
55
+ mv_shuffle_attn: false
56
+ mv_attn_with_pos_enc: false
57
+ shuffle_attn_no_norm: false
58
+ mv_unimatch_attn: false
59
+ knn_samples: 16
60
+ max_active_gaussians: 100_000
61
 
62
  # KNN
63
  use_fused_attn: true # fused KNN gather + attention CUDA kernel (faster, less memory)
 
64
  knn_idx_update_every: 1
65
 
66
  # inputs
 
100
  input_gradient_log: false
101
  input_gradient_log_clip_deltas: 0.001
102
  input_gradient_scale: 1.
103
+ residual_grad_scale: 1.
104
  input_gradient_with_ssim_loss: false
105
  input_gradient_same_loss: false
106
  input_gradient_loss_reduction: mean
107
  scale_residual_grads: false
108
 
109
+ train_global_only: false
 
 
 
 
 
 
 
 
110
 
111
  # random size refine
112
  # update more for low resolution, less for high
113
+ random_step_with_size: false
114
 
115
 
116
  use_amp: true
 
120
  use_checkpointing: false
121
  recurrent_use_checkpointing: false
122
 
 
 
 
123
  # Normalizing input
124
  input_gradient_normalize: false
125
  input_gradient_normalize_type: layer
 
132
  predict_state_scale_norm: false
133
 
134
  # Update head
135
+ delta_head_concat_img: false
136
+ delta_head_layer_num: 2
137
+ delta_head_act: gelu
138
+ delta_head_final_act: identity
139
+ delta_head_hidden_dim_matches: "input" # rebuttal version. switch to "output" for submission version
140
 
141
+ delta_head_scalar_scale: false
142
+ delta_head_scalar_scale_act: relu
 
143
 
144
  # Per-parameter-group heads (Feature A): separate heads per param group, each with own normalize+scale
145
+ delta_head_per_param_heads: false
146
+ delta_head_per_param_hidden_dim: 48 # tuned so total params ≈ baseline head (~81K)
147
+ # Per-parameter scalar scales (Feature B): per-group scalar scales (requires delta_head_scalar_scale=true)
148
+ delta_head_per_param_scales: false
149
 
150
  opt_scales_before_act: false
151
 
optgs/config/scene_trainer/scene_optimizer/learn2splat.yaml CHANGED
@@ -5,13 +5,25 @@ name: l2s
5
 
6
  # General optimization settings
7
  opt_scales_before_act: true
 
 
 
8
  sh_d: 16 # should be the default, but just in case
9
 
 
 
 
 
 
 
 
10
  # Input gradient settings
11
  input_gradient: true
12
  input_gradient_normalize: true
13
  input_gradient_normalize_type: adam
14
  input_gradient_with_ssim_loss: true
 
 
15
 
16
  # Freeze zero-grad gaussians ❄️($G_{∇=0})
17
  update_only_nonzero_grad: true
@@ -20,8 +32,8 @@ update_only_nonzero_grad: true
20
  predict_state_scale: true
21
 
22
  # Delta scale
23
- update_head_scalar_scale: true
24
- update_head_scalar_scale_act: relu # should be the default, but just in case
25
 
26
 
27
 
 
5
 
6
  # General optimization settings
7
  opt_scales_before_act: true
8
+ # Scales are refined in log space, so clamp in log space too.
9
+ clamp_min_raw_scales: -8.0
10
+ clamp_max_raw_scales: 1.0986 # ln(3.0)
11
  sh_d: 16 # should be the default, but just in case
12
 
13
+ # SH coefficients are clamped to a fixed range during refinement.
14
+ clamp_min_shs: -2.0
15
+ clamp_max_shs: 2.0
16
+
17
+ # KNN neighborhood size for the point-transformer attention.
18
+ knn_samples: 4
19
+
20
  # Input gradient settings
21
  input_gradient: true
22
  input_gradient_normalize: true
23
  input_gradient_normalize_type: adam
24
  input_gradient_with_ssim_loss: true
25
+ # Average the input gradient over pixels, then sum over the supervised views.
26
+ input_gradient_loss_reduction: mean_pixels_sum_views
27
 
28
  # Freeze zero-grad gaussians ❄️($G_{∇=0})
29
  update_only_nonzero_grad: true
 
32
  predict_state_scale: true
33
 
34
  # Delta scale
35
+ delta_head_scalar_scale: true
36
+ delta_head_scalar_scale_act: relu # should be the default, but just in case
37
 
38
 
39
 
optgs/config/scene_trainer/scene_optimizer/learn2splat_dense.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - learn2splat
3
+
4
+ # Dense variant (64-view SfM/COLMAP init): the optimizer state is initialized as random
5
+ # features rather than the sparse model's constant zero state.
6
+ init_state_wo_features: true
7
+ init_state_type: random
8
+ init_state_scale: 1.0
optgs/config/scene_trainer/scene_optimizer/lr_scheduler/expon.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - base
3
+
4
+ name: expon
5
+
6
+ # 3DGS learning rates (gsplat); means decays, the rest are constant
7
+ lr_data:
8
+ _base: 1
9
+ _means: 1.6e-4
10
+ _scales: 5e-3
11
+ _quats: 1e-3
12
+ _opacities: 5e-2
13
+ _sh0: 2.5e-3
14
+ _shN: 1.25e-4
15
+ apply_scheduler:
16
+ _base: true # gates all params (Bool3DGSCfg.<param> = _base AND _param); must stay true
17
+ _means: true
18
+ _scales: false
19
+ _quats: false
20
+ _opacities: false
21
+ _sh0: false
22
+ _shN: false
23
+
24
+ # means decay: 1.6e-4 -> 1.6e-6 over the full step budget
25
+ lr_final: 1.6e-6
26
+ lr_delay_steps: 0
27
+ lr_delay_mult: 1.0
28
+ max_steps: 30000 # should be equal to total optimization steps
optgs/config/scene_trainer/scene_optimizer/refiner/mcmc.yaml CHANGED
@@ -39,7 +39,7 @@ fallback_means_lr: 1.6e-4
39
  relocate_copy_state: true # inherit alive Gaussian's optimizer state (better than zeroing)
40
 
41
  # Cap (in scale units) applied to the *noise-only* covariance computation. Does NOT change the
42
- # rendered Gaussian scales. Needed for knn_based whose network saturates clamp_refine_max_scale,
43
  # which produces covariances 10²-10⁴× larger than vanilla's and makes the noise overflow the
44
  # renderer's tile-binning math (silent CUDA OOB downstream). Tune to ~scene_scale / 5.
45
  noise_scale_cap: 1.0
 
39
  relocate_copy_state: true # inherit alive Gaussian's optimizer state (better than zeroing)
40
 
41
  # Cap (in scale units) applied to the *noise-only* covariance computation. Does NOT change the
42
+ # rendered Gaussian scales. Needed for knn_based whose network saturates clamp_max_scale,
43
  # which produces covariances 10²-10⁴× larger than vanilla's and makes the noise overflow the
44
  # renderer's tile-binning math (silent CUDA OOB downstream). Tune to ~scene_scale / 5.
45
  noise_scale_cap: 1.0
optgs/config/scene_trainer/scene_optimizer/resplat_v2.yaml CHANGED
@@ -5,7 +5,7 @@ name: resplat_v2
5
  input_error: true
6
  input_error_mv_attn: true
7
  input_error_add_rgb_feature: true
8
- refine_knn_samples: 8
9
  state_channels: 512
10
  residual_state: true
11
- update_head_layer_num: 4
 
5
  input_error: true
6
  input_error_mv_attn: true
7
  input_error_add_rgb_feature: true
8
+ knn_samples: 8
9
  state_channels: 512
10
  residual_state: true
11
+ delta_head_layer_num: 4
optgs/config/scene_trainer/scene_optimizer/sgd.yaml DELETED
@@ -1,22 +0,0 @@
1
- defaults:
2
- - base
3
- - override refiner: default
4
-
5
- name: sgd
6
-
7
- # Adam optimizer
8
- betas: [0.9, 0.999]
9
- eps: 1e-15
10
- weight_decay: 0.0
11
-
12
- # learning rates (gsplat)
13
- base_lr: 1
14
- means_lr_init: 1.6e-4
15
- means_lr_final: 1e-5
16
- means_lr_delay_mult: 0.01
17
- means_lr_max_steps: 30000 # should be equal to total optimization steps
18
- scales_lr: 5e-3
19
- rotations_lr: 1e-3
20
- opacities_lr: 5e-2
21
- sh0s_lr: 2.5e-3
22
- shNs_lr: 1.25e-4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/config_migrate.py CHANGED
@@ -1,7 +1,9 @@
 
 
1
  from omegaconf import OmegaConf
2
 
3
 
4
- CURRENT_CFG_VERSION = 2
5
 
6
  def migrate(cfg_dict):
7
  was_omega = not isinstance(cfg_dict, dict)
@@ -27,6 +29,31 @@ def migrate(cfg_dict):
27
  cfg_dict = migrate_v1_to_v2(cfg_dict)
28
  version = 2
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  if version != CURRENT_CFG_VERSION:
31
  raise ValueError(f"Unsupported config version: {version}")
32
 
@@ -43,16 +70,95 @@ def migrate(cfg_dict):
43
  si["name"] = "resplat_v1"
44
 
45
  # Strip stale postprocessing fields from old checkpoint configs
46
- pp = cfg_container.get("meta_trainer", {}).get("test", {}).get("postprocessing", None)
 
47
  if isinstance(pp, dict):
48
  pp.pop("__target__", None)
49
  pp.pop("enabled", None)
50
  pp.pop("lr", None)
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  # Strip stale foundationstereo fields (encoder removed)
53
  si.pop("foundationstereo", None)
54
  si.pop("fstereo_num_refine", None)
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if was_omega:
57
  return OmegaConf.create(cfg_container)
58
  return cfg_container
@@ -74,6 +180,183 @@ def migrate_v1_to_v2(cfg_dict):
74
  return cfg
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def migrate_v0_to_v1(cfg):
78
  """
79
  Migration from submission v0 (refine_*) to rebuttal v1 (input_error_*).
@@ -159,7 +442,6 @@ def migrate_v0_to_v1(cfg):
159
 
160
  # update head
161
  "final_head_act": "update_head_final_act",
162
- "refine_output_scale_mag": "update_head_scale_mag",
163
  "scalar_scale_out": "update_head_scalar_scale",
164
  "scalar_scale_out_act": "update_head_scalar_scale_act",
165
 
 
1
+ from math import log
2
+
3
  from omegaconf import OmegaConf
4
 
5
 
6
+ CURRENT_CFG_VERSION = 2.5
7
 
8
  def migrate(cfg_dict):
9
  was_omega = not isinstance(cfg_dict, dict)
 
29
  cfg_dict = migrate_v1_to_v2(cfg_dict)
30
  version = 2
31
 
32
+ if version == 2:
33
+ print("Migrating config from version 2 to version 2.1 (strip update_/refine_ prefixes)...")
34
+ cfg_dict = migrate_v2_to_v2_1(cfg_dict)
35
+ version = 2.1
36
+
37
+ if version == 2.1:
38
+ print("Migrating config from version 2.1 to version 2.2 (single-space scale clamp)...")
39
+ cfg_dict = migrate_v2_1_to_v2_2(cfg_dict)
40
+ version = 2.2
41
+
42
+ if version == 2.2:
43
+ print("Migrating config from version 2.2 to version 2.3 (replay_buffer -> ckpt_buffer, simulate_ahead -> rollout)...")
44
+ cfg_dict = migrate_v2_2_to_v2_3(cfg_dict)
45
+ version = 2.3
46
+
47
+ if version == 2.3:
48
+ print("Migrating config from version 2.3 to version 2.4 (adam flat LRs -> expon lr_scheduler)...")
49
+ cfg_dict = migrate_v2_3_to_v2_4(cfg_dict)
50
+ version = 2.4
51
+
52
+ if version == 2.4:
53
+ print("Migrating config from version 2.4 to version 2.5 (meta_optimizer moved under meta_trainer)...")
54
+ cfg_dict = migrate_v2_4_to_v2_5(cfg_dict)
55
+ version = 2.5
56
+
57
  if version != CURRENT_CFG_VERSION:
58
  raise ValueError(f"Unsupported config version: {version}")
59
 
 
70
  si["name"] = "resplat_v1"
71
 
72
  # Strip stale postprocessing fields from old checkpoint configs
73
+ te = cfg_container.get("meta_trainer", {}).get("test", {})
74
+ pp = te.get("postprocessing", None) if isinstance(te, dict) else None
75
  if isinstance(pp, dict):
76
  pp.pop("__target__", None)
77
  pp.pop("enabled", None)
78
  pp.pop("lr", None)
79
 
80
+ # save_point_cloud removed (the flag was never read by any code path).
81
+ if isinstance(te, dict):
82
+ te.pop("save_point_cloud", None)
83
+
84
+ # Video flags renamed (fixed_view->optim, fixed_iteration->orbit, combined->optim_orbit).
85
+ # Test flags come from the current run's config, so just drop the stale keys from old configs.
86
+ for k in ("save_video_fixed_view", "save_video_fixed_view_index", "save_video_fixed_view_duplicate",
87
+ "save_video_fixed_iteration", "save_video_fixed_iteration_indices",
88
+ "save_video_fixed_iteration_render_fixed_view", "save_video_combined",
89
+ "save_video_combined_iterations", "save_video_combined_fixed_iteration_length"):
90
+ if isinstance(te, dict):
91
+ te.pop(k, None)
92
+
93
  # Strip stale foundationstereo fields (encoder removed)
94
  si.pop("foundationstereo", None)
95
  si.pop("fstereo_num_refine", None)
96
 
97
+ # Strip removed optimizer sliding-window fields (feature removed)
98
+ for k in ("window_size", "update_window_size", "local_gaussian_render",
99
+ "window_local_refine", "window_global_refine", "window_local_global_refine"):
100
+ so.pop(k, None)
101
+
102
+ # delta_head_scale_mag removed (experimental). Drop it under all historical names.
103
+ for k in ("delta_head_scale_mag", "update_head_scale_mag", "refine_output_scale_mag"):
104
+ so.pop(k, None)
105
+
106
+ # reinit_gaussian_when_multiple removed (reinit path was never implemented). Drop it
107
+ # under all historical names (reinit_gaussian_when_refine_multiple is the pre-2.1 name).
108
+ for k in ("reinit_gaussian_when_multiple", "reinit_gaussian_when_refine_multiple"):
109
+ so.pop(k, None)
110
+
111
+ # The initializer has no point-downsampling path, so pt_downsample,
112
+ # fps_agg_func and subsample_method are not valid config keys; drop them.
113
+ # multi_scale_pt and fps_num_samples are likewise not config keys (the point
114
+ # transformer is always PlainPointTransformer). subsample_method also appears
115
+ # on old optimizer configs, so strip it from there too.
116
+ for k in ("multi_scale_pt", "refine_multi_scale_pt", "subsample_method"):
117
+ so.pop(k, None)
118
+ for k in ("multi_scale_pt", "fps_num_samples", "pt_downsample", "fps_agg_func", "subsample_method"):
119
+ si.pop(k, None)
120
+
121
+ # The lvsm gaussian-regressor path was removed (disabled in every config), so
122
+ # lvsm_gaussian_regressor and lvsm_layers are no longer valid initializer keys.
123
+ for k in ("lvsm_gaussian_regressor", "lvsm_layers"):
124
+ si.pop(k, None)
125
+
126
+ # pt_pred_residual_position removed (residual-position prediction was disabled in every config).
127
+ si.pop("pt_pred_residual_position", None)
128
+
129
+ # sh_only removed: it was "freeze everything but SH", now expressed via per-group freeze_*.
130
+ # (refine_sh_only is the pre-2.1 name.)
131
+ sh_only = so.pop("sh_only", None)
132
+ if sh_only is None:
133
+ sh_only = so.pop("refine_sh_only", None)
134
+ if sh_only:
135
+ for fk in ("freeze_mean", "freeze_scale", "freeze_rotation", "freeze_opacity"):
136
+ so[fk] = True
137
+
138
+ # l1_loss / train_ignore_large_loss / half_res_lpips_loss moved off train into the per-loss
139
+ # cfgs: mse/sgd take l1_loss + clamp_large_error, lpips takes half_res.
140
+ train = cfg_container.get("meta_trainer", {}).get("train", {})
141
+
142
+ # depth_range_from_disparity removed (disabled in every config); its only consumers,
143
+ # max_disparity and min_disparity, are removed with it.
144
+ for k in ("depth_range_from_disparity", "max_disparity", "min_disparity"):
145
+ train.pop(k, None)
146
+
147
+ old_l1 = train.pop("l1_loss", None)
148
+ old_clamp = train.pop("train_ignore_large_loss", None)
149
+ old_half_res = train.pop("half_res_lpips_loss", None)
150
+ for loss_wrapper in cfg_container.get("loss", []) or []:
151
+ if not isinstance(loss_wrapper, dict):
152
+ continue
153
+ for name, lcfg in loss_wrapper.items():
154
+ if not isinstance(lcfg, dict):
155
+ continue
156
+ if name in ("mse", "sgd"):
157
+ lcfg.setdefault("l1_loss", old_l1 if old_l1 is not None else False)
158
+ lcfg.setdefault("clamp_large_error", old_clamp if old_clamp is not None else 0.0)
159
+ elif name == "lpips":
160
+ lcfg.setdefault("half_res", old_half_res if old_half_res is not None else False)
161
+
162
  if was_omega:
163
  return OmegaConf.create(cfg_container)
164
  return cfg_container
 
180
  return cfg
181
 
182
 
183
+ def migrate_v2_4_to_v2_5(cfg_dict):
184
+ """Migration from v2.4 to v2.5: move top-level 'meta_optimizer' under 'meta_trainer'."""
185
+ cfg = OmegaConf.to_container(cfg_dict, resolve=False) if not isinstance(cfg_dict, dict) else dict(cfg_dict)
186
+
187
+ if "meta_optimizer" in cfg:
188
+ meta_trainer = cfg.setdefault("meta_trainer", {})
189
+ if "meta_optimizer" not in meta_trainer:
190
+ meta_trainer["meta_optimizer"] = cfg.pop("meta_optimizer")
191
+
192
+ cfg["version"] = 2.5
193
+ return cfg
194
+
195
+
196
+ def migrate_v2_to_v2_1(cfg_dict):
197
+ """
198
+ Migration from v2 to v2.1: strip redundant update_/refine_ prefixes from optimizer
199
+ config fields (the class is already named Optimizer), rename no_refine_* -> freeze_*,
200
+ and update_head_* -> delta_head_*.
201
+ """
202
+ cfg = OmegaConf.to_container(cfg_dict, resolve=False) if not isinstance(cfg_dict, dict) else dict(cfg_dict)
203
+
204
+ so = cfg.get("scene_trainer", {}).get("scene_optimizer", {})
205
+
206
+ RENAME_MAP = {
207
+ # refine_
208
+ "num_basic_refine_blocks": "num_basic_blocks",
209
+ "num_refine_blocks": "num_blocks",
210
+ "refine_block_rmsnorm": "block_rmsnorm",
211
+ "refine_block_layernorm": "block_layernorm",
212
+ "refine_gaussian_multiple": "delta_gaussian_multiple",
213
+ "refine_residual_init_state": "residual_init_state",
214
+ "clamp_refine_max_scale": "clamp_max_scale",
215
+ "refine_condition_pt_feature": "condition_pt_feature",
216
+ "refine_same_num_points": "same_num_points",
217
+ "refine_knn_samples": "knn_samples",
218
+ "refine_with_mv_attn": "with_mv_attn",
219
+ "refine_with_mv_attn_lowres": "with_mv_attn_lowres",
220
+ "refine_no_mv_attn": "no_mv_attn",
221
+ "refine_mv_shuffle_attn": "mv_shuffle_attn",
222
+ "refine_mv_attn_with_pos_enc": "mv_attn_with_pos_enc",
223
+ "refine_shuffle_attn_no_norm": "shuffle_attn_no_norm",
224
+ "refine_mv_unimatch_attn": "mv_unimatch_attn",
225
+ # no_refine_ -> freeze_
226
+ "no_refine_mean": "freeze_mean",
227
+ "no_refine_scale": "freeze_scale",
228
+ "no_refine_rotation": "freeze_rotation",
229
+ "no_refine_opacity": "freeze_opacity",
230
+ "no_refine_sh0": "freeze_sh0",
231
+ "no_refine_shN": "freeze_shN",
232
+ # other update_
233
+ "update_attn_proj_channels": "attn_proj_channels",
234
+ "update_no_knn_attn": "no_knn_attn",
235
+ "update_no_tran_block_norm": "no_tran_block_norm",
236
+ "update_tran_block_act": "tran_block_act",
237
+ "train_global_update_only": "train_global_only",
238
+ "random_update_with_size": "random_step_with_size",
239
+ "gradient_update_scale": "residual_grad_scale",
240
+ # update_head_ -> delta_head_
241
+ "update_head_layer_num": "delta_head_layer_num",
242
+ "update_head_concat_img": "delta_head_concat_img",
243
+ "update_head_act": "delta_head_act",
244
+ "update_head_final_act": "delta_head_final_act",
245
+ "update_head_hidden_dim_matches": "delta_head_hidden_dim_matches",
246
+ "update_head_scalar_scale": "delta_head_scalar_scale",
247
+ "update_head_scalar_scale_act": "delta_head_scalar_scale_act",
248
+ "update_head_per_param_heads": "delta_head_per_param_heads",
249
+ "update_head_per_param_hidden_dim": "delta_head_per_param_hidden_dim",
250
+ "update_head_per_param_scales": "delta_head_per_param_scales",
251
+ }
252
+
253
+ for old, new in RENAME_MAP.items():
254
+ if old in so:
255
+ so[new] = so.pop(old)
256
+
257
+ cfg["version"] = 2.1
258
+ return cfg
259
+
260
+
261
+ def migrate_v2_1_to_v2_2(cfg_dict):
262
+ """
263
+ Migration from v2.1 to v2.2: scales are now clamped in a single space.
264
+
265
+ When opt_scales_before_act is set, scales are refined in log space, and the clamp is now
266
+ applied there (raw limits) both when unpacking and in the per-step update. Earlier configs
267
+ instead clamped once in activation space (clamp_min/max_scale) during unpack and once in log
268
+ space (clamp_min/max_raw_scales) during the update, so the scale was effectively bounded by
269
+ the intersection of the two. Fold that intersection into the raw limits so old checkpoints
270
+ keep the same effective bounds under the new single-space clamp.
271
+ """
272
+ cfg = OmegaConf.to_container(cfg_dict, resolve=False) if not isinstance(cfg_dict, dict) else dict(cfg_dict)
273
+
274
+ so = cfg.get("scene_trainer", {}).get("scene_optimizer", {})
275
+ if so.get("opt_scales_before_act", False):
276
+ min_scale = float(so.get("clamp_min_scale", 1e-6))
277
+ max_scale = float(so.get("clamp_max_scale", 3.0))
278
+ min_raw = float(so.get("clamp_min_raw_scales", -1e10))
279
+ max_raw = float(so.get("clamp_max_raw_scales", 1e10))
280
+ so["clamp_min_raw_scales"] = max(log(min_scale), min_raw)
281
+ so["clamp_max_raw_scales"] = min(log(max_scale), max_raw)
282
+
283
+ cfg["version"] = 2.2
284
+ return cfg
285
+
286
+
287
+ def migrate_v2_2_to_v2_3(cfg_dict):
288
+ """
289
+ Migration from v2.2 to v2.3: the replay-buffer feature is renamed to ckpt_buffer and
290
+ simulate_ahead to rollout (use_replay_buffer -> use_ckpt_buffer, replay_buffer_cfg ->
291
+ ckpt_buffer_cfg, and the simulate_ahead* fields inside it -> rollout*).
292
+ """
293
+ cfg = OmegaConf.to_container(cfg_dict, resolve=False) if not isinstance(cfg_dict, dict) else dict(cfg_dict)
294
+
295
+ train = cfg.get("meta_trainer", {}).get("train", {})
296
+ if "use_replay_buffer" in train:
297
+ train["use_ckpt_buffer"] = train.pop("use_replay_buffer")
298
+ if "replay_buffer_cfg" in train:
299
+ train["ckpt_buffer_cfg"] = train.pop("replay_buffer_cfg")
300
+
301
+ buffer_cfg = train.get("ckpt_buffer_cfg", {})
302
+ if isinstance(buffer_cfg, dict):
303
+ ROLLOUT_RENAME_MAP = {
304
+ "simulate_ahead": "rollout",
305
+ "simulate_ahead_min_steps": "rollout_min_steps",
306
+ "simulate_ahead_max_steps": "rollout_max_steps",
307
+ "simulate_ahead_grow": "rollout_grow",
308
+ }
309
+ for old, new in ROLLOUT_RENAME_MAP.items():
310
+ if old in buffer_cfg:
311
+ buffer_cfg[new] = buffer_cfg.pop(old)
312
+
313
+ cfg["version"] = 2.3
314
+ return cfg
315
+
316
+
317
+ def migrate_v2_3_to_v2_4(cfg_dict):
318
+ """
319
+ Migration from v2.3 to v2.4: the Adam baseline (name adam/sgd) no longer carries flat per-param
320
+ LR fields. They move into the inherited lr_scheduler cfg (name "expon"): per-param values into
321
+ lr_data, the means decay into the expon scheduler params, with apply_scheduler enabling the
322
+ schedule on means only.
323
+
324
+ The expon scheduler scales the whole curve by lr_data._base (= old base_lr) and the means LR is
325
+ additionally scaled by scene extent at runtime, so the bounds reproduce the old behavior:
326
+ old means LR = base_lr * scene_scale * expon(means_lr_init -> means_lr_final).
327
+ """
328
+ cfg = OmegaConf.to_container(cfg_dict, resolve=False) if not isinstance(cfg_dict, dict) else dict(cfg_dict)
329
+
330
+ so = cfg.get("scene_trainer", {}).get("scene_optimizer", {})
331
+ if so.get("name") in ("adam", "sgd") and "means_lr_init" in so:
332
+ base = so.pop("base_lr", 1)
333
+ lr_scheduler = so.get("lr_scheduler", {}) or {}
334
+ lr_scheduler["name"] = "expon"
335
+ lr_scheduler["lr_data"] = {
336
+ "_base": base,
337
+ "_means": so.pop("means_lr_init"),
338
+ "_scales": so.pop("scales_lr"),
339
+ "_quats": so.pop("rotations_lr"),
340
+ "_opacities": so.pop("opacities_lr"),
341
+ "_sh0": so.pop("sh0s_lr"),
342
+ "_shN": so.pop("shNs_lr"),
343
+ }
344
+ # _base gates all params (Bool3DGSCfg.<param> = _base AND _param), so it must stay True;
345
+ # the schedule is enabled per-param via the individual flags (means only).
346
+ lr_scheduler["apply_scheduler"] = {
347
+ "_base": True, "_means": True, "_scales": False,
348
+ "_quats": False, "_opacities": False, "_sh0": False, "_shN": False,
349
+ }
350
+ lr_scheduler["lr_final"] = so.pop("means_lr_final")
351
+ lr_scheduler["lr_delay_steps"] = 0
352
+ lr_scheduler["lr_delay_mult"] = so.pop("means_lr_delay_mult", 1.0)
353
+ lr_scheduler["max_steps"] = so.pop("means_lr_max_steps", 30000)
354
+ so["lr_scheduler"] = lr_scheduler
355
+
356
+ cfg["version"] = 2.4
357
+ return cfg
358
+
359
+
360
  def migrate_v0_to_v1(cfg):
361
  """
362
  Migration from submission v0 (refine_*) to rebuttal v1 (input_error_*).
 
442
 
443
  # update head
444
  "final_head_act": "update_head_final_act",
 
445
  "scalar_scale_out": "update_head_scalar_scale",
446
  "scalar_scale_out_act": "update_head_scalar_scale_act",
447
 
optgs/dataset/camera_datasets/camera.py CHANGED
@@ -10,8 +10,6 @@ import os
10
  import json
11
 
12
  from optgs.geometry.projection import get_fov, get_projection_matrix
13
- from optgs.visualization.camera_trajectory.wobble import generate_wobble_transformation
14
- from optgs.visualization.camera_trajectory.interpolation import interpolate_extrinsics, interpolate_intrinsics
15
 
16
 
17
  def get_scene_scale(camtoworlds: Float[np.ndarray, "N 4 4"]) -> float:
@@ -143,133 +141,6 @@ class Camera(nn.Module):
143
  indent=4,
144
  )
145
 
146
- @classmethod
147
- def load_camera(cls, cam_dir: Path, data_device: torch.device):
148
- extrinsics = torch.load(cam_dir / "extrinsics.pt")
149
- intrinsics = torch.load(cam_dir / "intrinsics.pt")
150
- image = torch.load(cam_dir / "image.pt")
151
-
152
- if (cam_dir / "gt_alpha_mask.pt").exists():
153
- gt_alpha_mask = torch.load(cam_dir / "gt_alpha_mask.pt")
154
- else:
155
- gt_alpha_mask = None
156
-
157
- with open(cam_dir / "cam_info.json", "r") as f:
158
- cam_info = json.load(f)
159
-
160
- return cls(
161
- colmap_id=cam_info["colmap_id"],
162
- extrinsics=extrinsics.to(data_device),
163
- intrinsics=intrinsics.to(data_device),
164
- image=image.to(data_device),
165
- gt_alpha_mask=gt_alpha_mask.to(data_device) if gt_alpha_mask is not None else None,
166
- raw_image_shape=tuple(cam_info["raw_image_shape"]),
167
- image_name=cam_info["image_name"],
168
- uid=cam_info["uid"],
169
- near=torch.Tensor([cam_info["near"]]).to(data_device),
170
- far=torch.Tensor([cam_info["far"]]).to(data_device),
171
- data_device=data_device,
172
- ).to(data_device)
173
-
174
-
175
- def generate_cam_params_for_wobble(t: Tensor, cam_a: Camera, cam_b: Camera):
176
- origin_a = cam_a.extrinsics[:3, 3]
177
- origin_b = cam_b.extrinsics[:3, 3]
178
- cam_a_extrinsics = cam_a.extrinsics
179
- cam_b_extrinsics = cam_b.extrinsics
180
- cam_a_intrinsics = cam_a.intrinsics
181
- cam_b_intrinsics = cam_b.intrinsics
182
-
183
- delta = (origin_a - origin_b).norm(dim=-1)
184
-
185
- tf = generate_wobble_transformation(
186
- radius=delta * 0.5,
187
- t=t,
188
- num_rotations=1,
189
- scale_radius_with_t=False,
190
- )
191
-
192
- extrinsics = interpolate_extrinsics(
193
- initial=cam_a_extrinsics,
194
- final=cam_b_extrinsics,
195
- t=(t - 2),
196
- )
197
- intrinsics = interpolate_intrinsics(
198
- initial=cam_a_intrinsics,
199
- final=cam_b_intrinsics,
200
- t=(t - 2),
201
- )
202
- return extrinsics @ tf, intrinsics
203
-
204
-
205
- def generate_cam_params_for_interpolation(t: Tensor, cam_a: Camera, cam_b: Camera):
206
- cam_a_extrinsics = cam_a.extrinsics
207
- cam_a_extrinsics_render_view = cam_a.extrinsics_render_view
208
- cam_b_extrinsics = cam_b.extrinsics
209
- cam_b_extrinsics_render_view = cam_b.extrinsics_render_view
210
- cam_a_intrinsics = cam_a.intrinsics
211
- cam_a_intrinsics_render_view = cam_a.intrinsics_render_view
212
- cam_b_intrinsics = cam_b.intrinsics
213
- cam_b_intrinsics_render_view = cam_b.intrinsics_render_view
214
-
215
- extrinsics = interpolate_extrinsics(
216
- initial=cam_a_extrinsics,
217
- final=cam_b_extrinsics,
218
- t=(t - 2),
219
- )
220
- intrinsics = interpolate_intrinsics(
221
- initial=cam_a_intrinsics,
222
- final=cam_b_intrinsics,
223
- t=(t - 2),
224
- )
225
- extrinsics_render_view = interpolate_extrinsics(
226
- initial=cam_a_extrinsics_render_view,
227
- final=cam_b_extrinsics_render_view,
228
- t=(t - 2),
229
- )
230
- intrinsics_render_view = interpolate_intrinsics(
231
- initial=cam_a_intrinsics_render_view,
232
- final=cam_b_intrinsics_render_view,
233
- t=(t - 2),
234
- )
235
- return extrinsics, intrinsics, extrinsics_render_view, intrinsics_render_view
236
-
237
-
238
- def get_intermediate_cameras(cam_a: Camera, cam_b: Camera, num_frames: int = 150, smooth: bool = False):
239
- t = torch.linspace(0, 1, num_frames, dtype=torch.float32, device=cam_a.data_device)
240
- if smooth: t = (torch.cos(torch.pi * (t + 1)) + 1) / 2
241
-
242
- extrinsics, intrinsics, extrinsics_render_view, intrinsics_render_view = (
243
- generate_cam_params_for_interpolation(t, cam_a, cam_b)
244
- )
245
- extrinsics = extrinsics.squeeze(0)
246
- intrinsics = intrinsics.squeeze(0)
247
- extrinsics_render_view = extrinsics_render_view.squeeze(0)
248
- intrinsics_render_view = intrinsics_render_view.squeeze(0)
249
-
250
- cameras = [
251
- Camera(
252
- colmap_id=cam_a.colmap_id,
253
- image_name=f"{cam_a.image_name}_{index:04d}",
254
- uid=index,
255
- near=cam_a.znear,
256
- far=cam_a.zfar,
257
- data_device=cam_a.data_device,
258
- image=cam_a.original_image, # These views have no ground truth image but we should never require images for mesh views
259
- raw_image_shape=cam_a.raw_image_shape,
260
- extrinsics=extrinsics[index],
261
- intrinsics=intrinsics[index],
262
- extrinsics_render_view=extrinsics_render_view[index],
263
- intrinsics_render_view=intrinsics_render_view[index],
264
- scale_matrix=cam_a.scale_matrix,
265
- trans_matrix=cam_a.trans_matrix,
266
- gt_alpha_mask=None
267
- )
268
- for index in range(num_frames)
269
- ]
270
- return cameras
271
-
272
-
273
  def patch_shim(cams: list[Camera], patch_size: int) -> list[Camera]:
274
  new_cams = []
275
 
@@ -319,18 +190,6 @@ def patch_shim(cams: list[Camera], patch_size: int) -> list[Camera]:
319
  return new_cams
320
 
321
 
322
- def calculate_cameras_extent(cam_centers: Tensor):
323
- avg_cam_center = cam_centers.mean(dim=0, keepdim=True)
324
- dist = torch.norm(cam_centers - avg_cam_center, dim=-1, keepdim=True)
325
- diagonal = dist.max()
326
-
327
- center = avg_cam_center.flatten()
328
- radius = diagonal * 1.1
329
-
330
- translate = -center
331
- return translate, radius.item()
332
-
333
-
334
  def save_cameras(cameras: list[Camera], save_dir: Path):
335
  os.makedirs(save_dir, exist_ok=True)
336
 
 
10
  import json
11
 
12
  from optgs.geometry.projection import get_fov, get_projection_matrix
 
 
13
 
14
 
15
  def get_scene_scale(camtoworlds: Float[np.ndarray, "N 4 4"]) -> float:
 
141
  indent=4,
142
  )
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  def patch_shim(cams: list[Camera], patch_size: int) -> list[Camera]:
145
  new_cams = []
146
 
 
190
  return new_cams
191
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  def save_cameras(cameras: list[Camera], save_dir: Path):
194
  os.makedirs(save_dir, exist_ok=True)
195
 
optgs/dataset/data_module.py CHANGED
@@ -14,15 +14,15 @@ from .validation_wrapper import ValidationWrapper
14
  from ..misc.step_tracker import StepTracker
15
 
16
 
17
- def get_data_shim(encoder: nn.Module) -> DataShim:
18
  """Get functions that modify the batch. It's sometimes necessary to modify batches
19
  outside the data loader because GPU computations are required to modify the batch or
20
  because the modification depends on something outside the data loader.
21
  """
22
 
23
  shims: list[DataShim] = []
24
- if hasattr(encoder, "get_data_shim"):
25
- shims.append(encoder.get_data_shim())
26
 
27
  def combined_shim(batch):
28
  for shim in shims:
 
14
  from ..misc.step_tracker import StepTracker
15
 
16
 
17
+ def get_data_shim(initializer: nn.Module) -> DataShim:
18
  """Get functions that modify the batch. It's sometimes necessary to modify batches
19
  outside the data loader because GPU computations are required to modify the batch or
20
  because the modification depends on something outside the data loader.
21
  """
22
 
23
  shims: list[DataShim] = []
24
+ if hasattr(initializer, "get_data_shim"):
25
+ shims.append(initializer.get_data_shim())
26
 
27
  def combined_shim(batch):
28
  for shim in shims:
optgs/dataset/dataset_dl3dv.py CHANGED
@@ -12,9 +12,6 @@ from jaxtyping import Float, UInt8
12
  from PIL import Image
13
  from torch import Tensor
14
  from torch.utils.data import IterableDataset
15
- import numpy as np
16
- import os
17
- import random
18
 
19
  from ..geometry.projection import get_fov
20
  from .dataset import DatasetCfgCommon
@@ -28,47 +25,24 @@ from .view_sampler import ViewSampler
28
  class DatasetDL3DVCfg(DatasetCfgCommon):
29
  name: Literal["dl3dv"]
30
  roots: list[Path]
31
- baseline_epsilon: float
32
  max_fov: float
33
- make_baseline_1: bool
34
  augment: bool
35
  test_len: int
36
  test_chunk_interval: int
37
  train_times_per_scene: int
38
  test_times_per_scene: int
39
  ori_image_shape: list[int]
40
- # random crop training
41
- random_crop: bool
42
- max_size: list[int] | None
43
- min_size: list[int] | None
44
 
45
  skip_bad_shape: bool = True
46
  near: float = -1.0
47
  far: float = -1.0
48
- baseline_scale_bounds: bool = True
49
  shuffle_val: bool = True
50
  no_mix_test_set: bool = True
51
- load_depth: bool = False
52
  min_views: int = 0
53
  max_views: int = 0
54
- highres: bool = False
55
  sort_target_index: Optional[bool] = False
56
  overfit_max_views: Optional[int] = None
57
  sort_context_index: Optional[bool] = False
58
- use_index_to_load_chunk: Optional[bool] = False
59
- pose_align_first_view: bool = False # align the camera pose to the first view
60
- scale_extrinsics: float = 1.
61
- metric_scale_align_dl3dv: bool = False
62
- center_pose: bool = False # center and normalize the pose by the distance to the center
63
-
64
- # mix re10k & dl3dv
65
- mix_re10k: bool = False
66
- re10k_min_view_dist: int = 40
67
- re10k_max_view_dist: int = 300
68
-
69
- # load remaining context views
70
- load_remain_context: bool = False
71
- num_remain_context: int = 8
72
 
73
  index_name: str = "index.json"
74
 
@@ -104,14 +78,9 @@ class DatasetDL3DV(IterableDataset):
104
  self.chunks = []
105
  for i, root in enumerate(cfg.roots):
106
  root = root / self.data_stage
107
- if self.cfg.use_index_to_load_chunk:
108
- with open(root / self.cfg.index_name, "r") as f:
109
- json_dict = json.load(f)
110
- root_chunks = sorted(list(set(json_dict.values())))
111
- else:
112
- root_chunks = sorted(
113
- [path for path in root.iterdir() if path.suffix == ".torch"]
114
- )
115
 
116
  # mixed data training only evaluate on a single test set
117
  if cfg.no_mix_test_set and self.data_stage in ['val', 'test'] and i > 0:
@@ -133,17 +102,11 @@ class DatasetDL3DV(IterableDataset):
133
  if self.stage == "val":
134
  self.chunks = self.chunks * int(1e6 // len(self.chunks))
135
 
136
- if self.cfg.metric_scale_align_dl3dv:
137
- # read invalid scales
138
- scale_dir = '/cluster/project/cvg/haofei/datasets/depthsplat/dl3dv_metric_scale_factor'
139
- filename = os.path.join(scale_dir, 'dl3dv_invalid.txt')
140
- with open(filename, "r") as f:
141
- self.invalid_scale_scenes = [line.strip() for line in f]
142
-
143
  # Calculate actual number of scenes (keys of index file that their chunks exist).
 
144
  num_available_scenes = 0
145
  for scene, chunk_path in self.index.items():
146
- if chunk_path in self.chunks:
147
  num_available_scenes += 1
148
  self.num_available_scenes = num_available_scenes
149
 
@@ -151,24 +114,6 @@ class DatasetDL3DV(IterableDataset):
151
  indices = torch.randperm(len(lst))
152
  return [lst[x] for x in indices]
153
 
154
- def _get_scale_factor(self, scene: str) -> float:
155
- """Get the scale factor for a scene."""
156
- if self.cfg.metric_scale_align_dl3dv:
157
- scale_dir = '/cluster/project/cvg/haofei/datasets/depthsplat/dl3dv_metric_scale_factor'
158
- if self.stage == 'train':
159
- folder = scene.split('_')[1]
160
- else:
161
- folder = scene
162
- filename = os.path.join(scale_dir, folder, 'scale_factor.txt')
163
-
164
- if not os.path.exists(filename) or folder in self.invalid_scale_scenes:
165
- return self.cfg.scale_extrinsics
166
- else:
167
- with open(filename, "r") as f:
168
- return float(f.read().strip())
169
- else:
170
- return self.cfg.scale_extrinsics
171
-
172
  def _process_example_to_batch(
173
  self,
174
  example: dict,
@@ -176,7 +121,6 @@ class DatasetDL3DV(IterableDataset):
176
  intrinsics: Tensor,
177
  context_indices: Tensor,
178
  target_indices: Tensor,
179
- scale_factor: float,
180
  ) -> Optional[dict]:
181
  """
182
  Process an example into a batch dict (original behavior).
@@ -184,19 +128,6 @@ class DatasetDL3DV(IterableDataset):
184
  """
185
  scene = example["key"]
186
 
187
- # Load remaining context views if configured
188
- if self.cfg.load_remain_context:
189
- remaining_indices = get_remaining_indices(
190
- context_indices, target_indices, self.cfg.num_remain_context
191
- )
192
- remain_context_images = [
193
- example["images"][index.item()] for index in remaining_indices
194
- ]
195
- try:
196
- remain_context_images = self.convert_images(remain_context_images)
197
- except OSError:
198
- return None
199
-
200
  # Load context images
201
  context_images = [
202
  example["images"][index.item()] for index in context_indices
@@ -216,16 +147,7 @@ class DatasetDL3DV(IterableDataset):
216
  return None
217
 
218
  # Validate image shapes
219
- if self.cfg.mix_re10k and 'dl3dv' not in scene:
220
- if self.cfg.highres:
221
- expected_shape = (3, 720, 1280)
222
- else:
223
- expected_shape = (3, 360, 640)
224
- else:
225
- expected_shape = tuple([3, *self.cfg.ori_image_shape])
226
-
227
- if self.stage in ['test', 'val'] or 'dl3dv' in scene:
228
- expected_shape = tuple([3, *self.cfg.ori_image_shape])
229
 
230
  if self.cfg.skip_bad_shape:
231
  if context_images.shape[1:] != expected_shape or target_images.shape[1:] != expected_shape:
@@ -236,9 +158,6 @@ class DatasetDL3DV(IterableDataset):
236
  )
237
  return None
238
 
239
- if self.cfg.load_remain_context and remain_context_images.shape[1:] != expected_shape:
240
- return None
241
-
242
  # Apply pose transformations
243
  if self.cfg.pose_align_middle_view:
244
  mid_index = context_indices.shape[0] // 2
@@ -246,12 +165,6 @@ class DatasetDL3DV(IterableDataset):
246
  extrinsics[context_indices][mid_index:mid_index + 1], extrinsics
247
  )
248
 
249
- if self.cfg.pose_align_first_view:
250
- extrinsics = camera_normalization(extrinsics[context_indices][0:1], extrinsics)
251
-
252
- if self.cfg.center_pose:
253
- extrinsics = center_norm_pose(extrinsics)
254
-
255
  # Validate extrinsics
256
  if any(torch.isnan(torch.det(extrinsics[context_indices][:, :3, :3]))):
257
  return None
@@ -272,20 +185,6 @@ class DatasetDL3DV(IterableDataset):
272
  ):
273
  return None
274
 
275
- if self.cfg.load_remain_context:
276
- if any(torch.isnan(torch.det(extrinsics[remaining_indices][:, :3, :3]))):
277
- return None
278
- if (extrinsics[remaining_indices][:, :3, 3] > 1e3).any():
279
- return None
280
- if not torch.allclose(
281
- torch.det(extrinsics[remaining_indices][:, :3, :3]),
282
- extrinsics[remaining_indices][:, :3, :3].new_tensor(1)
283
- ):
284
- return None
285
-
286
- # Apply scale factor
287
- extrinsics[:, :3, 3] *= scale_factor
288
-
289
  # Build output
290
  example_out = {
291
  "context": {
@@ -307,22 +206,12 @@ class DatasetDL3DV(IterableDataset):
307
  "scene": scene,
308
  }
309
 
310
- if self.cfg.load_remain_context:
311
- example_out["context_remain"] = {
312
- "extrinsics": extrinsics[remaining_indices],
313
- "intrinsics": intrinsics[remaining_indices],
314
- "image": remain_context_images,
315
- "near": self.get_bound("near", len(remaining_indices)),
316
- "far": self.get_bound("far", len(remaining_indices)),
317
- "index": remaining_indices,
318
- }
319
-
320
  return example_out
321
 
322
  def __iter__(self):
323
  # Chunks must be shuffled here (not inside __init__) for validation to show
324
  # random chunks.
325
- if self.stage in (("train", "val") if self.cfg.shuffle_val else ("train")):
326
  self.chunks = self.shuffle(self.chunks)
327
 
328
  # When testing, the data loaders alternate chunks.
@@ -350,7 +239,7 @@ class DatasetDL3DV(IterableDataset):
350
  else:
351
  chunk = item * len(chunk)
352
 
353
- if self.stage in (("train", "val") if self.cfg.shuffle_val else ("train")):
354
  chunk = self.shuffle(chunk)
355
 
356
  times_per_scene = (
@@ -373,8 +262,6 @@ class DatasetDL3DV(IterableDataset):
373
  if (get_fov(intrinsics).rad2deg() > self.cfg.max_fov).any():
374
  continue
375
 
376
- scale_factor = self._get_scale_factor(scene)
377
-
378
  try:
379
  extra_kwargs = {}
380
  if self.cfg.overfit_to_scene is not None and self.stage != "test":
@@ -383,16 +270,12 @@ class DatasetDL3DV(IterableDataset):
383
  else self.cfg.overfit_max_views
384
  )
385
 
386
- is_re10k = self.cfg.mix_re10k and 'dl3dv' not in scene and self.stage == 'train'
387
-
388
  out_data = self.view_sampler.sample(
389
  scene,
390
  extrinsics,
391
  intrinsics,
392
  min_context_views=self.cfg.min_views,
393
  max_context_views=self.cfg.max_views,
394
- min_view_dist=self.cfg.re10k_min_view_dist if is_re10k else None,
395
- max_view_dist=self.cfg.re10k_max_view_dist if is_re10k else None,
396
  **extra_kwargs,
397
  )
398
 
@@ -421,7 +304,7 @@ class DatasetDL3DV(IterableDataset):
421
  for context_indices, target_indices in zip(c_list, t_list):
422
  example_out = self._process_example_to_batch(
423
  example, extrinsics.clone(), intrinsics,
424
- context_indices, target_indices, scale_factor
425
  )
426
 
427
  if example_out is None:
@@ -440,13 +323,7 @@ class DatasetDL3DV(IterableDataset):
440
  if self.cfg.image_shape == list(context_images.shape[2:]):
441
  yield example_out
442
  else:
443
- if self.stage == "train" and self.cfg.random_crop:
444
- crop_h = random.randint(self.cfg.min_size[0], self.cfg.max_size[0] + 1) // 64 * 64
445
- crop_w = random.randint(self.cfg.min_size[1], self.cfg.max_size[1] + 1) // 64 * 64
446
- crop_size = (crop_h, crop_w)
447
- yield apply_crop_shim(example_out, crop_size)
448
- else:
449
- yield apply_crop_shim(example_out, tuple(self.cfg.image_shape))
450
 
451
  def convert_poses(
452
  self,
@@ -472,22 +349,16 @@ class DatasetDL3DV(IterableDataset):
472
  w2c[:, :3] = rearrange(poses[:, 6:], "b (h w) -> b h w", h=3, w=4)
473
 
474
  if self.cfg.opencv_pose_format:
475
- return self.opengl_to_opencv(w2c.inverse()), intrinsics
476
- else:
477
- return w2c.inverse(), intrinsics
478
-
479
- def opengl_to_opencv(self, c2w):
480
- # https://github.com/DL3DV-10K/Dataset/issues/4#issuecomment-2019441741
481
- blender2opencv = np.array(
482
- [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]
483
- )
484
- blender2opencv = torch.tensor(blender2opencv, dtype=c2w.dtype, device=c2w.device).unsqueeze(0)
485
- c2w = torch.matmul(c2w, blender2opencv)
486
- c2w[:, 2, :] *= -1
487
- c2w = c2w[:, torch.tensor(np.array([1, 0, 2, 3])), :]
488
- c2w[:, 0:3, 1:3] *= -1
489
-
490
- return c2w
491
 
492
  def convert_images(
493
  self,
@@ -575,66 +446,3 @@ def camera_normalization(pivotal_pose: torch.Tensor, poses: torch.Tensor):
575
  normalized_poses = camera_norm_matrix_manuall @ poses # [N, 4, 4]
576
 
577
  return normalized_poses
578
-
579
-
580
- def center_norm_pose(extrinsics):
581
- # extrinsics: [V, 4, 4]
582
- cam_centers = extrinsics[:, :3, 3] # [V, 3]
583
- avg_center = cam_centers.mean(dim=0, keepdim=True) # [1, 3]
584
- dist = (cam_centers - avg_center).norm(dim=1, keepdim=True) # [V, 1]
585
- scale = dist.max()
586
-
587
- # translate
588
- extrinsics = extrinsics.clone()
589
- extrinsics[:, :3, 3] -= avg_center
590
- extrinsics[:, :3, 3] /= scale
591
-
592
- return extrinsics
593
-
594
-
595
- def get_remaining_indices(context_indices: torch.Tensor,
596
- target_indices: torch.Tensor,
597
- num_remain_context: int) -> torch.Tensor:
598
- """
599
- Randomly selects a fixed number of remaining indices in the range [min(context), max(context)],
600
- excluding those in context or target. Pads by repeating if not enough remain.
601
-
602
- Args:
603
- context_indices (torch.Tensor): 1D tensor of context indices.
604
- target_indices (torch.Tensor): 1D tensor of target indices.
605
- num_remain_context (int): Number of remaining indices to return.
606
-
607
- Returns:
608
- torch.Tensor: 1D tensor of length `num_remain_context`.
609
- """
610
- if context_indices.numel() == 0:
611
- raise ValueError("context_indices must not be empty.")
612
-
613
- min_idx = torch.min(context_indices).item()
614
- max_idx = torch.max(context_indices).item()
615
-
616
- full_range = torch.arange(min_idx, max_idx + 1, dtype=torch.long)
617
- exclude_indices = torch.cat([context_indices, target_indices])
618
- mask = ~torch.isin(full_range, exclude_indices)
619
-
620
- remaining = full_range[mask]
621
-
622
- if remaining.numel() == 0:
623
- # Nothing to sample from; repeat the first context index (or any fallback)
624
- return context_indices[0].repeat(num_remain_context)
625
-
626
- # return all
627
- selected = remaining
628
-
629
- # Randomly sample with or without replacement
630
- # if remaining.numel() >= num_remain_context:
631
- # selected = remaining[torch.randperm(remaining.numel())[:num_remain_context]]
632
- # else:
633
- # # return all
634
- # selected = remaining
635
- # # # Repeat with wrap-around to pad
636
- # # num_repeat = (num_remain_context + remaining.numel() - 1) // remaining.numel()
637
- # # padded = remaining.repeat(num_repeat)[:num_remain_context]
638
- # # selected = padded[torch.randperm(num_remain_context)] # Shuffle for randomness
639
-
640
- return selected.sort().values
 
12
  from PIL import Image
13
  from torch import Tensor
14
  from torch.utils.data import IterableDataset
 
 
 
15
 
16
  from ..geometry.projection import get_fov
17
  from .dataset import DatasetCfgCommon
 
25
  class DatasetDL3DVCfg(DatasetCfgCommon):
26
  name: Literal["dl3dv"]
27
  roots: list[Path]
 
28
  max_fov: float
 
29
  augment: bool
30
  test_len: int
31
  test_chunk_interval: int
32
  train_times_per_scene: int
33
  test_times_per_scene: int
34
  ori_image_shape: list[int]
 
 
 
 
35
 
36
  skip_bad_shape: bool = True
37
  near: float = -1.0
38
  far: float = -1.0
 
39
  shuffle_val: bool = True
40
  no_mix_test_set: bool = True
 
41
  min_views: int = 0
42
  max_views: int = 0
 
43
  sort_target_index: Optional[bool] = False
44
  overfit_max_views: Optional[int] = None
45
  sort_context_index: Optional[bool] = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  index_name: str = "index.json"
48
 
 
78
  self.chunks = []
79
  for i, root in enumerate(cfg.roots):
80
  root = root / self.data_stage
81
+ root_chunks = sorted(
82
+ [path for path in root.iterdir() if path.suffix == ".torch"]
83
+ )
 
 
 
 
 
84
 
85
  # mixed data training only evaluate on a single test set
86
  if cfg.no_mix_test_set and self.data_stage in ['val', 'test'] and i > 0:
 
102
  if self.stage == "val":
103
  self.chunks = self.chunks * int(1e6 // len(self.chunks))
104
 
 
 
 
 
 
 
 
105
  # Calculate actual number of scenes (keys of index file that their chunks exist).
106
+ chunk_set = set(self.chunks) # self.chunks can be huge (val multiplies it) -> set lookup
107
  num_available_scenes = 0
108
  for scene, chunk_path in self.index.items():
109
+ if chunk_path in chunk_set:
110
  num_available_scenes += 1
111
  self.num_available_scenes = num_available_scenes
112
 
 
114
  indices = torch.randperm(len(lst))
115
  return [lst[x] for x in indices]
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  def _process_example_to_batch(
118
  self,
119
  example: dict,
 
121
  intrinsics: Tensor,
122
  context_indices: Tensor,
123
  target_indices: Tensor,
 
124
  ) -> Optional[dict]:
125
  """
126
  Process an example into a batch dict (original behavior).
 
128
  """
129
  scene = example["key"]
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  # Load context images
132
  context_images = [
133
  example["images"][index.item()] for index in context_indices
 
147
  return None
148
 
149
  # Validate image shapes
150
+ expected_shape = tuple([3, *self.cfg.ori_image_shape])
 
 
 
 
 
 
 
 
 
151
 
152
  if self.cfg.skip_bad_shape:
153
  if context_images.shape[1:] != expected_shape or target_images.shape[1:] != expected_shape:
 
158
  )
159
  return None
160
 
 
 
 
161
  # Apply pose transformations
162
  if self.cfg.pose_align_middle_view:
163
  mid_index = context_indices.shape[0] // 2
 
165
  extrinsics[context_indices][mid_index:mid_index + 1], extrinsics
166
  )
167
 
 
 
 
 
 
 
168
  # Validate extrinsics
169
  if any(torch.isnan(torch.det(extrinsics[context_indices][:, :3, :3]))):
170
  return None
 
185
  ):
186
  return None
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  # Build output
189
  example_out = {
190
  "context": {
 
206
  "scene": scene,
207
  }
208
 
 
 
 
 
 
 
 
 
 
 
209
  return example_out
210
 
211
  def __iter__(self):
212
  # Chunks must be shuffled here (not inside __init__) for validation to show
213
  # random chunks.
214
+ if self.stage == "train" or (self.cfg.shuffle_val and self.stage == "val"):
215
  self.chunks = self.shuffle(self.chunks)
216
 
217
  # When testing, the data loaders alternate chunks.
 
239
  else:
240
  chunk = item * len(chunk)
241
 
242
+ if self.stage == "train" or (self.cfg.shuffle_val and self.stage == "val"):
243
  chunk = self.shuffle(chunk)
244
 
245
  times_per_scene = (
 
262
  if (get_fov(intrinsics).rad2deg() > self.cfg.max_fov).any():
263
  continue
264
 
 
 
265
  try:
266
  extra_kwargs = {}
267
  if self.cfg.overfit_to_scene is not None and self.stage != "test":
 
270
  else self.cfg.overfit_max_views
271
  )
272
 
 
 
273
  out_data = self.view_sampler.sample(
274
  scene,
275
  extrinsics,
276
  intrinsics,
277
  min_context_views=self.cfg.min_views,
278
  max_context_views=self.cfg.max_views,
 
 
279
  **extra_kwargs,
280
  )
281
 
 
304
  for context_indices, target_indices in zip(c_list, t_list):
305
  example_out = self._process_example_to_batch(
306
  example, extrinsics.clone(), intrinsics,
307
+ context_indices, target_indices,
308
  )
309
 
310
  if example_out is None:
 
323
  if self.cfg.image_shape == list(context_images.shape[2:]):
324
  yield example_out
325
  else:
326
+ yield apply_crop_shim(example_out, tuple(self.cfg.image_shape))
 
 
 
 
 
 
327
 
328
  def convert_poses(
329
  self,
 
349
  w2c[:, :3] = rearrange(poses[:, 6:], "b (h w) -> b h w", h=3, w=4)
350
 
351
  if self.cfg.opencv_pose_format:
352
+ # DL3DV chunk poses are already in OpenCV convention (verified: they match the
353
+ # COLMAP/OpenCV poses up to a world rotation), so opengl_to_opencv would corrupt
354
+ # them. Fail loudly instead of silently converting. NOTE: the resplat_v2 baseline
355
+ # in scripts/testing/_common/run_experiments.sh sets opencv_pose_format=true and
356
+ # will now hit this — update that path if resplat_v2 is still needed.
357
+ raise ValueError(
358
+ "opencv_pose_format=true is not supported for DL3DV: chunk poses are already "
359
+ "in OpenCV convention and need no conversion."
360
+ )
361
+ return w2c.inverse(), intrinsics
 
 
 
 
 
 
362
 
363
  def convert_images(
364
  self,
 
446
  normalized_poses = camera_norm_matrix_manuall @ poses # [N, 4, 4]
447
 
448
  return normalized_poses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/dataset/dataset_re10k.py CHANGED
@@ -8,13 +8,11 @@ from typing import Literal, Optional
8
  import numpy as np
9
  import torch
10
  import torchvision.transforms as tf
11
- import torch.nn.functional as F
12
  from einops import rearrange, repeat
13
  from jaxtyping import Float, UInt8
14
  from PIL import Image
15
  from torch import Tensor
16
  from torch.utils.data import IterableDataset
17
- import cv2
18
 
19
  from ..geometry.projection import get_fov
20
  from .dataset import DatasetCfgCommon
@@ -22,38 +20,25 @@ from .shims.augmentation_shim import apply_augmentation_shim
22
  from .shims.crop_shim import apply_crop_shim
23
  from .data_types import Stage
24
  from .view_sampler import ViewSampler
25
- from .dataset_dl3dv import get_remaining_indices
26
 
27
 
28
  @dataclass
29
  class DatasetRE10kCfg(DatasetCfgCommon):
30
  name: Literal["re10k"]
31
  roots: list[Path]
32
- baseline_epsilon: float
33
  max_fov: float
34
- make_baseline_1: bool
35
  augment: bool
36
  test_len: int
37
  test_chunk_interval: int
38
- average_pose: bool
39
  skip_bad_shape: bool = True
40
  near: float = -1.0
41
  far: float = -1.0
42
- baseline_scale_bounds: bool = True
43
  shuffle_val: bool = True
44
  train_times_per_scene: int = 1
45
  highres: bool = False
46
  scannet: bool = False
47
  tartanair: bool = False
48
- use_index_to_load_chunk: Optional[bool] = False
49
  load_depth: bool = False
50
- pose_align_first_view: bool = False # align the camera pose to the first view
51
- center_pose: bool = False # center and normalize the pose by the distance to the center
52
-
53
- scale_extrinsics: float = 1.
54
-
55
- # load remaining context views
56
- load_remain_context: bool = False
57
 
58
 
59
  class DatasetRE10k(IterableDataset):
@@ -86,14 +71,9 @@ class DatasetRE10k(IterableDataset):
86
  self.chunks = []
87
  for i, root in enumerate(cfg.roots):
88
  root = root / self.data_stage
89
- if self.cfg.use_index_to_load_chunk:
90
- with open(root / "index.json", "r") as f:
91
- json_dict = json.load(f)
92
- root_chunks = sorted(list(set(json_dict.values())))
93
- else:
94
- root_chunks = sorted(
95
- [path for path in root.iterdir() if path.suffix == ".torch"]
96
- )
97
 
98
  self.chunks.extend(root_chunks)
99
  if self.cfg.overfit_to_scene is not None:
@@ -110,7 +90,7 @@ class DatasetRE10k(IterableDataset):
110
  def __iter__(self):
111
  # Chunks must be shuffled here (not inside __init__) for validation to show
112
  # random chunks.
113
- if self.stage in (("train", "val") if self.cfg.shuffle_val else ("train")):
114
  self.chunks = self.shuffle(self.chunks)
115
 
116
  # When testing, the data loaders alternate chunks.
@@ -134,7 +114,7 @@ class DatasetRE10k(IterableDataset):
134
  assert len(item) == 1
135
  chunk = item * len(chunk)
136
 
137
- if self.stage in (("train", "val") if self.cfg.shuffle_val else ("train")):
138
  chunk = self.shuffle(chunk)
139
 
140
  times_per_scene = (
@@ -163,23 +143,6 @@ class DatasetRE10k(IterableDataset):
163
  if (get_fov(intrinsics).rad2deg() > self.cfg.max_fov).any():
164
  continue
165
 
166
- # load remaining context views
167
- if self.cfg.load_remain_context:
168
- # randomly select fixed number of remaining views such that they can be batched
169
- remaining_indices = get_remaining_indices(context_indices, target_indices,
170
- 0)
171
-
172
- # Load the images.
173
- remain_context_images = [
174
- example["images"][index.item()] for index in remaining_indices
175
- ]
176
-
177
- try:
178
- remain_context_images = self.convert_images(remain_context_images)
179
- except OSError:
180
- # some data might be corrupted
181
- continue
182
-
183
  # Load the images.
184
  context_images = [
185
  example["images"][index.item()] for index in context_indices
@@ -207,12 +170,6 @@ class DatasetRE10k(IterableDataset):
207
  )
208
  continue
209
 
210
- if self.cfg.load_remain_context:
211
- remain_context_invalid = remain_context_images.shape[1:] != expected_shape
212
-
213
- if self.cfg.skip_bad_shape and remain_context_invalid:
214
- continue
215
-
216
  # check the extrinsics
217
  if any(torch.isnan(torch.det(extrinsics[context_indices][:, :3, :3]))):
218
  continue
@@ -220,9 +177,6 @@ class DatasetRE10k(IterableDataset):
220
  if any(torch.isnan(torch.det(extrinsics[target_indices][:, :3, :3]))):
221
  continue
222
 
223
- if self.cfg.average_pose:
224
- extrinsics = self.preprocess_poses(extrinsics)
225
-
226
  # load depth
227
  if self.cfg.load_depth:
228
  context_depths = [
@@ -245,15 +199,12 @@ class DatasetRE10k(IterableDataset):
245
  else:
246
  raise NotImplementedError
247
 
248
- # align pose to the first view
249
- if self.cfg.pose_align_first_view:
250
- extrinsics = camera_normalization(extrinsics[context_indices][0:1], extrinsics)
251
-
252
- if self.cfg.center_pose:
253
- extrinsics = center_norm_pose(extrinsics)
254
-
255
- # scale the scene when necessary: only scale the extrinsics
256
- extrinsics[:, :3, 3] *= self.cfg.scale_extrinsics
257
 
258
  example = {
259
  "context": {
@@ -275,19 +226,6 @@ class DatasetRE10k(IterableDataset):
275
  "scene": scene,
276
  }
277
 
278
- if self.cfg.load_remain_context:
279
- example.update({
280
- "context_remain": {
281
- "extrinsics": extrinsics[remaining_indices],
282
- "intrinsics": intrinsics[remaining_indices],
283
- "image": remain_context_images,
284
- "near": self.get_bound("near", len(remaining_indices)),
285
- "far": self.get_bound("far", len(remaining_indices)),
286
- "index": remaining_indices,
287
- }
288
- }
289
- )
290
-
291
  if self.cfg.load_depth:
292
  example['context']['depth'] = context_depths
293
  example['target']['depth'] = target_depths
@@ -400,65 +338,20 @@ class DatasetRE10k(IterableDataset):
400
  else len(self.index.keys()) * self.cfg.train_times_per_scene
401
  )
402
 
403
- def preprocess_poses(
404
- self,
405
- in_c2ws: torch.Tensor,
406
- scene_scale_factor=1.35,
407
- ):
408
- """
409
- Ref: https://github.com/Haian-Jin/LVSM/blob/main/data/dataset_scene.py
410
- Preprocess the poses to:
411
- 1. translate and rotate the scene to align the average camera direction and position
412
- 2. rescale the whole scene to a fixed scale
413
- """
414
-
415
- # Translation and Rotation
416
- # align coordinate system (OpenCV coordinate) to the mean camera
417
- # center is the average of all camera centers
418
- # average direction vectors are computed from all camera direction vectors (average down and forward)
419
- center = in_c2ws[:, :3, 3].mean(0)
420
- avg_forward = F.normalize(in_c2ws[:, :3, 2].mean(0), dim=-1) # average forward direction (z of opencv camera)
421
- avg_down = in_c2ws[:, :3, 1].mean(0) # average down direction (y of opencv camera)
422
- avg_right = F.normalize(torch.cross(avg_down, avg_forward, dim=-1), dim=-1) # (x of opencv camera)
423
- avg_down = F.normalize(torch.cross(avg_forward, avg_right, dim=-1), dim=-1) # (y of opencv camera)
424
-
425
- avg_pose = torch.eye(4, device=in_c2ws.device) # average c2w matrix
426
- avg_pose[:3, :3] = torch.stack([avg_right, avg_down, avg_forward], dim=-1)
427
- avg_pose[:3, 3] = center
428
- avg_pose = torch.linalg.inv(avg_pose) # average w2c matrix
429
- in_c2ws = avg_pose @ in_c2ws
430
-
431
-
432
- # Rescale the whole scene to a fixed scale
433
- scene_scale = torch.max(torch.abs(in_c2ws[:, :3, 3]))
434
- scene_scale = scene_scale_factor * scene_scale
435
-
436
- in_c2ws[:, :3, 3] /= scene_scale
437
-
438
- return in_c2ws
439
-
440
 
441
  def camera_normalization(pivotal_pose: torch.Tensor, poses: torch.Tensor):
442
  # [1, 4, 4], [N, 4, 4]
443
 
444
- camera_norm_matrix = torch.inverse(pivotal_pose)
445
-
446
- # normalize all views
447
- poses = torch.bmm(camera_norm_matrix.repeat(poses.shape[0], 1, 1), poses)
448
-
449
- return poses
450
-
 
451
 
452
- def center_norm_pose(extrinsics):
453
- # extrinsics: [V, 4, 4]
454
- cam_centers = extrinsics[:, :3, 3] # [V, 3]
455
- avg_center = cam_centers.mean(dim=0, keepdim=True) # [1, 3]
456
- dist = (cam_centers - avg_center).norm(dim=1, keepdim=True) # [V, 1]
457
- scale = dist.max()
458
-
459
- # translate
460
- extrinsics = extrinsics.clone()
461
- extrinsics[:, :3, 3] -= avg_center
462
- extrinsics[:, :3, 3] /= scale
463
 
464
- return extrinsics
 
8
  import numpy as np
9
  import torch
10
  import torchvision.transforms as tf
 
11
  from einops import rearrange, repeat
12
  from jaxtyping import Float, UInt8
13
  from PIL import Image
14
  from torch import Tensor
15
  from torch.utils.data import IterableDataset
 
16
 
17
  from ..geometry.projection import get_fov
18
  from .dataset import DatasetCfgCommon
 
20
  from .shims.crop_shim import apply_crop_shim
21
  from .data_types import Stage
22
  from .view_sampler import ViewSampler
 
23
 
24
 
25
  @dataclass
26
  class DatasetRE10kCfg(DatasetCfgCommon):
27
  name: Literal["re10k"]
28
  roots: list[Path]
 
29
  max_fov: float
 
30
  augment: bool
31
  test_len: int
32
  test_chunk_interval: int
 
33
  skip_bad_shape: bool = True
34
  near: float = -1.0
35
  far: float = -1.0
 
36
  shuffle_val: bool = True
37
  train_times_per_scene: int = 1
38
  highres: bool = False
39
  scannet: bool = False
40
  tartanair: bool = False
 
41
  load_depth: bool = False
 
 
 
 
 
 
 
42
 
43
 
44
  class DatasetRE10k(IterableDataset):
 
71
  self.chunks = []
72
  for i, root in enumerate(cfg.roots):
73
  root = root / self.data_stage
74
+ root_chunks = sorted(
75
+ [path for path in root.iterdir() if path.suffix == ".torch"]
76
+ )
 
 
 
 
 
77
 
78
  self.chunks.extend(root_chunks)
79
  if self.cfg.overfit_to_scene is not None:
 
90
  def __iter__(self):
91
  # Chunks must be shuffled here (not inside __init__) for validation to show
92
  # random chunks.
93
+ if self.stage == "train" or (self.cfg.shuffle_val and self.stage == "val"):
94
  self.chunks = self.shuffle(self.chunks)
95
 
96
  # When testing, the data loaders alternate chunks.
 
114
  assert len(item) == 1
115
  chunk = item * len(chunk)
116
 
117
+ if self.stage == "train" or (self.cfg.shuffle_val and self.stage == "val"):
118
  chunk = self.shuffle(chunk)
119
 
120
  times_per_scene = (
 
143
  if (get_fov(intrinsics).rad2deg() > self.cfg.max_fov).any():
144
  continue
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  # Load the images.
147
  context_images = [
148
  example["images"][index.item()] for index in context_indices
 
170
  )
171
  continue
172
 
 
 
 
 
 
 
173
  # check the extrinsics
174
  if any(torch.isnan(torch.det(extrinsics[context_indices][:, :3, :3]))):
175
  continue
 
177
  if any(torch.isnan(torch.det(extrinsics[target_indices][:, :3, :3]))):
178
  continue
179
 
 
 
 
180
  # load depth
181
  if self.cfg.load_depth:
182
  context_depths = [
 
199
  else:
200
  raise NotImplementedError
201
 
202
+ # align pose to the middle view
203
+ if self.cfg.pose_align_middle_view:
204
+ mid_index = context_indices.shape[0] // 2
205
+ extrinsics = camera_normalization(
206
+ extrinsics[context_indices][mid_index:mid_index + 1], extrinsics
207
+ )
 
 
 
208
 
209
  example = {
210
  "context": {
 
226
  "scene": scene,
227
  }
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  if self.cfg.load_depth:
230
  example['context']['depth'] = context_depths
231
  example['target']['depth'] = target_depths
 
338
  else len(self.index.keys()) * self.cfg.train_times_per_scene
339
  )
340
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
  def camera_normalization(pivotal_pose: torch.Tensor, poses: torch.Tensor):
343
  # [1, 4, 4], [N, 4, 4]
344
 
345
+ # Manually calculate the inverse of SE(3) to avoid numerical issues
346
+ R = pivotal_pose[:, :3, :3] # [1, 3, 3]
347
+ t = pivotal_pose[:, :3, 3:] # [1, 3, 1]
348
+ R_inv = R.transpose(-1, -2) # [1, 3, 3]
349
+ t_inv = -R_inv @ t # [1, 3, 1]
350
+ camera_norm_matrix_manuall = torch.eye(4, dtype=poses.dtype, device=poses.device).unsqueeze(0) # [1, 4, 4]
351
+ camera_norm_matrix_manuall[:, :3, :3] = R_inv
352
+ camera_norm_matrix_manuall[:, :3, 3:] = t_inv
353
 
354
+ # normalize all views
355
+ normalized_poses = camera_norm_matrix_manuall @ poses # [N, 4, 4]
 
 
 
 
 
 
 
 
 
356
 
357
+ return normalized_poses
optgs/dataset/shims/augmentation_shim.py CHANGED
@@ -22,25 +22,26 @@ def reflect_views(views: AnyViews) -> AnyViews:
22
  }
23
 
24
 
 
 
 
 
 
 
 
25
  def apply_augmentation_shim(
26
  example: AnyExample,
27
  generator: torch.Generator | None = None,
28
  ) -> AnyExample:
29
- """Randomly augment the training images."""
30
- # Do not augment with 50% chance.
31
- if torch.rand(tuple(), generator=generator) < 0.5:
32
- return example
33
-
34
- if "context_remain" in example:
35
- return {
36
- **example,
37
- "context": reflect_views(example["context"]),
38
- "target": reflect_views(example["target"]),
39
- "context_remain": reflect_views(example["context_remain"]),
40
- }
41
-
42
- return {
43
  **example,
44
- "context": reflect_views(example["context"]),
45
- "target": reflect_views(example["target"]),
46
  }
 
 
 
 
22
  }
23
 
24
 
25
+ def mark_unflipped(views: AnyViews) -> AnyViews:
26
+ """No-op augmentation that still records x_flipped=False, so the key is
27
+ always present and downstream code (e.g. BatchedViews.from_dict) need not
28
+ fall back to a default."""
29
+ return {**views, "x_flipped": False}
30
+
31
+
32
  def apply_augmentation_shim(
33
  example: AnyExample,
34
  generator: torch.Generator | None = None,
35
  ) -> AnyExample:
36
+ """Randomly horizontally-flip the scene (50% chance). Either way, x_flipped
37
+ is recorded on every view set so the key is always present."""
38
+ flip = torch.rand(tuple(), generator=generator) >= 0.5
39
+ transform = reflect_views if flip else mark_unflipped
40
+ out = {
 
 
 
 
 
 
 
 
 
41
  **example,
42
+ "context": transform(example["context"]),
43
+ "target": transform(example["target"]),
44
  }
45
+ if "context_remain" in example:
46
+ out["context_remain"] = transform(example["context_remain"])
47
+ return out
optgs/dataset/shims/bounds_shim.py DELETED
@@ -1,80 +0,0 @@
1
- import torch
2
- from einops import einsum, reduce, repeat
3
- from jaxtyping import Float
4
- from torch import Tensor
5
-
6
- from ..data_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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/dataset/view_sampler/view_sampler.py CHANGED
@@ -44,10 +44,6 @@ class ViewSampler(ABC, Generic[T]):
44
  self._all_context_indices = None
45
  self._all_target_indices = None
46
 
47
- @property
48
- def all_context_indices(self) -> Int64[Tensor, " context_view"]:
49
- return self._all_context_indices
50
-
51
  @property
52
  def context_indices(self) -> Int64[Tensor, " target_view"]:
53
  return self._all_context_indices
@@ -70,9 +66,6 @@ class ViewSampler(ABC, Generic[T]):
70
  else:
71
  raise RuntimeError("Target indices have already been set.")
72
 
73
- def sample_subset(self, extrinsics, intrinsics, device):
74
- pass
75
-
76
  @abstractmethod
77
  def _sample_impl(
78
  self,
@@ -123,13 +116,3 @@ class ViewSampler(ABC, Generic[T]):
123
  @property
124
  def global_step(self) -> int:
125
  return 0 if self.step_tracker is None else self.step_tracker.get_step()
126
-
127
- def new_instance(self) -> "ViewSampler":
128
- """Create a new instance of the same ViewSampler class with the same configuration."""
129
- return value(self.__class__)(
130
- cfg=self.cfg,
131
- stage=self.stage,
132
- is_overfitting=self.is_overfitting,
133
- cameras_are_circular=self.cameras_are_circular,
134
- step_tracker=self.step_tracker,
135
- )
 
44
  self._all_context_indices = None
45
  self._all_target_indices = None
46
 
 
 
 
 
47
  @property
48
  def context_indices(self) -> Int64[Tensor, " target_view"]:
49
  return self._all_context_indices
 
66
  else:
67
  raise RuntimeError("Target indices have already been set.")
68
 
 
 
 
69
  @abstractmethod
70
  def _sample_impl(
71
  self,
 
116
  @property
117
  def global_step(self) -> int:
118
  return 0 if self.step_tracker is None else self.step_tracker.get_step()
 
 
 
 
 
 
 
 
 
 
optgs/evaluation/metric_computer.py DELETED
@@ -1,115 +0,0 @@
1
- import os
2
- from pathlib import Path
3
-
4
- import torch
5
- from pytorch_lightning import LightningModule
6
- from ..misc.console import metrics_table
7
- from ..misc.image_io import load_image, save_image
8
- from ..visualization.annotation import add_label
9
- from ..visualization.layout import add_border, hcat
10
- from .evaluation_cfg import EvaluationCfg
11
- from .metrics import compute_lpips, compute_psnr, compute_ssim
12
-
13
-
14
- class MetricComputer(LightningModule):
15
- cfg: EvaluationCfg
16
-
17
- def __init__(self, cfg: EvaluationCfg) -> None:
18
- super().__init__()
19
- self.cfg = cfg
20
-
21
- def test_step(self, batch, batch_idx):
22
- scene = batch["scene"][0]
23
- b, cv, _, _, _ = batch["context"]["image"].shape
24
- assert b == 1 and cv == 2
25
- _, v, _, _, _ = batch["target"]["image"].shape
26
-
27
- # Skip scenes.
28
- for method in self.cfg.methods:
29
- if not (method.path / scene).exists():
30
- print(f'Skipping "{scene}".')
31
- return
32
-
33
- # Load the images.
34
- all_images = {}
35
- try:
36
- for method in self.cfg.methods:
37
- images = [
38
- load_image(method.path / scene / f"color/{index.item():0>6}.png")
39
- for index in batch["target"]["index"][0]
40
- ]
41
- all_images[method.key] = torch.stack(images).to(self.device)
42
- except FileNotFoundError:
43
- print(f'Skipping "{scene}".')
44
- return
45
-
46
- # Compute metrics.
47
- all_metrics = {}
48
- rgb_gt = batch["target"]["image"][0]
49
- for key, images in all_images.items():
50
- alex_lpips, vgg_lpips = compute_lpips(rgb_gt, images)
51
- all_metrics = {
52
- **all_metrics,
53
- f"alex_lpips_{key}": alex_lpips,
54
- f"vgg_lpips_{key}": vgg_lpips,
55
- f"ssim_{key}": compute_ssim(rgb_gt, images),
56
- f"psnr_{key}": compute_psnr(rgb_gt, images),
57
- }
58
- self.log_dict(all_metrics)
59
- self.print_preview_metrics(all_metrics)
60
-
61
- # Skip the rest if no side-by-side is needed.
62
- if self.cfg.side_by_side_path is None:
63
- return
64
-
65
- # Create side-by-side.
66
- scene_key = f"{batch_idx:0>6}_{scene}"
67
- for i in range(v):
68
- true_index = batch["target"]["index"][0, i]
69
- row = [add_label(batch["target"]["image"][0, i], "Ground Truth")]
70
- for method in self.cfg.methods:
71
- image = all_images[method.key][i]
72
- image = add_label(image, method.name)
73
- row.append(image)
74
- start_frame = batch["target"]["index"][0, 0]
75
- end_frame = batch["target"]["index"][0, -1]
76
- label = f"Scene {batch['scene'][0]} (frames {start_frame} to {end_frame})"
77
- row = add_border(add_label(hcat(*row), label, font_size=16))
78
- save_image(
79
- row,
80
- self.cfg.side_by_side_path / scene_key / f"{true_index:0>6}.png",
81
- )
82
-
83
- # Create an animation.
84
- if self.cfg.animate_side_by_side:
85
- (self.cfg.side_by_side_path / "videos").mkdir(exist_ok=True, parents=True)
86
- command = (
87
- 'ffmpeg -y -framerate 30 -pattern_type glob -i "*.png" -c:v libx264 '
88
- '-pix_fmt yuv420p -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2"'
89
- )
90
- os.system(
91
- f"cd {self.cfg.side_by_side_path / scene_key} && {command} "
92
- f"{Path.cwd()}/{self.cfg.side_by_side_path}/videos/{scene_key}.mp4"
93
- )
94
-
95
- def print_preview_metrics(self, metrics: dict[str, float]) -> None:
96
- if getattr(self, "running_metrics", None) is None:
97
- self.running_metrics = metrics
98
- self.running_metric_steps = 1
99
- else:
100
- s = self.running_metric_steps
101
- self.running_metrics = {
102
- k: ((s * v) + metrics[k]) / (s + 1)
103
- for k, v in self.running_metrics.items()
104
- }
105
- self.running_metric_steps += 1
106
-
107
- rows = []
108
- for method in self.cfg.methods:
109
- row = [
110
- f"{self.running_metrics[f'{metric}_{method.key}']:.3f}"
111
- for metric in ("psnr", "lpips", "ssim")
112
- ]
113
- rows.append((method.key, *row))
114
-
115
- metrics_table(rows, ["Method", "PSNR (dB)", "LPIPS", "SSIM"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/evaluation/metrics.py CHANGED
@@ -6,7 +6,7 @@ from jaxtyping import Float
6
  # from lpips import LPIPS
7
  # from skimage.metrics import structural_similarity
8
  from torch import Tensor
9
- from torchmetrics.image import PeakSignalNoiseRatio, StructuralSimilarityIndexMeasure
10
  from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity
11
  from tqdm import tqdm
12
 
 
6
  # from lpips import LPIPS
7
  # from skimage.metrics import structural_similarity
8
  from torch import Tensor
9
+ from torchmetrics.image import StructuralSimilarityIndexMeasure
10
  from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity
11
  from tqdm import tqdm
12
 
optgs/experimental/api/api.py CHANGED
@@ -21,7 +21,7 @@ For non-inria codebases use :meth:`OptGS.initialize_from_ply` /
21
 
22
  External SfM scenes carry no optgs encoder features, so checkpoints trained
23
  with ``init_state_wo_features=False`` are coerced at construction (with a
24
- warning): the feature-conditioned ``update_proj`` weights are dropped and the
25
  optimizer state is initialized standard-normal.
26
  """
27
 
@@ -105,7 +105,7 @@ class OptGS:
105
  "(scene_trainer.scene_optimizer.init_state_wo_features=False). "
106
  "External SfM/inria scenes carry no optgs encoder features; "
107
  "proceeding with init_state_wo_features=True — the "
108
- "feature-conditioned update_proj weights are dropped and the "
109
  "initial optimizer state is set to a standard-normal random "
110
  "vector (init_state_type='random', init_state_scale=1.0)."
111
  )
@@ -348,7 +348,6 @@ class OptGS:
348
  context=self._context,
349
  renderer=self.decoder,
350
  prev_output=self._init_output,
351
- num_refine=self.num_refine,
352
  iter_batch_size=self.iter_batch_size,
353
  target=self._context,
354
  )
@@ -416,27 +415,36 @@ class OptGS:
416
  OptimizerPreviousOutput,
417
  )
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  with torch.no_grad():
420
- inp = OptimizerInput(
421
- context=self._context,
422
- renderer=self.decoder,
423
- prev_output=self._init_output,
424
- num_refine=self.num_refine,
425
- iter_batch_size=self.iter_batch_size,
426
- target=self._context,
427
- )
428
  opt.validate_input(inp)
429
  opt.on_scene_start(inp) # InitializerOutput -> OptimizerPreviousOutput
430
- if not isinstance(inp.prev_output, OptimizerPreviousOutput):
431
- raise OptGSError(
432
- "optimizer.on_scene_start did not produce an "
433
- f"OptimizerPreviousOutput (got {type(inp.prev_output)})."
434
- )
435
-
436
- out = OptimizerOutput.empty(t=0)
437
- out.T = self.num_refine
438
- try:
439
- for step in range(self.num_refine):
 
440
  # Fresh view minibatch each step (the regime the optimizer
441
  # was trained with); full_context/target stay the whole scene.
442
  batch = self._view_minibatch(self._context)
@@ -447,12 +455,14 @@ class OptGS:
447
  full_context=self._context, full_target=self._context,
448
  )
449
  out.t = (out.t or 0) + 1
450
- yield step, inp.prev_output.gaussians
451
- finally:
 
 
452
  if torch.cuda.is_available():
453
  torch.cuda.synchronize()
454
  opt.on_scene_end()
455
- self._refined = inp.prev_output.gaussians
456
 
457
  def export_ply(self, path: str) -> None:
458
  """Write the most recently refined Gaussians to a 3DGS PLY."""
 
21
 
22
  External SfM scenes carry no optgs encoder features, so checkpoints trained
23
  with ``init_state_wo_features=False`` are coerced at construction (with a
24
+ warning): the feature-conditioned ``state_proj`` weights are dropped and the
25
  optimizer state is initialized standard-normal.
26
  """
27
 
 
105
  "(scene_trainer.scene_optimizer.init_state_wo_features=False). "
106
  "External SfM/inria scenes carry no optgs encoder features; "
107
  "proceeding with init_state_wo_features=True — the "
108
+ "feature-conditioned state_proj weights are dropped and the "
109
  "initial optimizer state is set to a standard-normal random "
110
  "vector (init_state_type='random', init_state_scale=1.0)."
111
  )
 
348
  context=self._context,
349
  renderer=self.decoder,
350
  prev_output=self._init_output,
 
351
  iter_batch_size=self.iter_batch_size,
352
  target=self._context,
353
  )
 
415
  OptimizerPreviousOutput,
416
  )
417
 
418
+ # torch's grad mode is thread-local, and a generator consumer (e.g.
419
+ # gradio) may resume this generator on a *different* worker thread each
420
+ # step — so a single ``with torch.no_grad()`` spanning the ``yield`` does
421
+ # not reliably hold across steps. Enter no_grad *per step* and yield
422
+ # outside it: the Adam baseline's in-place parameter updates require
423
+ # no_grad ambient (calc_input_gradients re-enables grad internally just
424
+ # for the gradient computation), so a step that resumes on a grad-enabled
425
+ # thread would otherwise raise "a leaf Variable that requires grad is
426
+ # being used in an in-place operation" and leak the autograd graph.
427
+ inp = OptimizerInput(
428
+ context=self._context,
429
+ renderer=self.decoder,
430
+ prev_output=self._init_output,
431
+ iter_batch_size=self.iter_batch_size,
432
+ target=self._context,
433
+ )
434
  with torch.no_grad():
 
 
 
 
 
 
 
 
435
  opt.validate_input(inp)
436
  opt.on_scene_start(inp) # InitializerOutput -> OptimizerPreviousOutput
437
+ if not isinstance(inp.prev_output, OptimizerPreviousOutput):
438
+ raise OptGSError(
439
+ "optimizer.on_scene_start did not produce an "
440
+ f"OptimizerPreviousOutput (got {type(inp.prev_output)})."
441
+ )
442
+
443
+ out = OptimizerOutput.empty(t=0)
444
+ out.T = self.num_refine
445
+ try:
446
+ for step in range(self.num_refine):
447
+ with torch.no_grad():
448
  # Fresh view minibatch each step (the regime the optimizer
449
  # was trained with); full_context/target stay the whole scene.
450
  batch = self._view_minibatch(self._context)
 
455
  full_context=self._context, full_target=self._context,
456
  )
457
  out.t = (out.t or 0) + 1
458
+ g = inp.prev_output.gaussians
459
+ yield step, g
460
+ finally:
461
+ with torch.no_grad():
462
  if torch.cuda.is_available():
463
  torch.cuda.synchronize()
464
  opt.on_scene_end()
465
+ self._refined = inp.prev_output.gaussians
466
 
467
  def export_ply(self, path: str) -> None:
468
  """Write the most recently refined Gaussians to a 3DGS PLY."""
optgs/experimental/api/integration/config_bridge.py CHANGED
@@ -190,22 +190,21 @@ def build_optimizer_cfg(cfg_path: Path) -> tuple["KnnBasedOptimizerCfg", int | N
190
 
191
  # Mirror SceneTrainerCfg (scene_trainer_cfg.py: scene_optimizer.update(
192
  # scene_initializer)): wire the checkpoint's initializer cfg into the
193
- # optimizer cfg so the runtime-only fields init_gaussian_param_num /
194
- # init_sh_d / sh_d — absent from every config file — are populated before
195
- # the optimizer nn.Module is built.
196
  si = OmegaConf.select(cfg, "scene_trainer.scene_initializer")
197
  si_name = OmegaConf.select(cfg, "scene_trainer.scene_initializer.name")
198
  if si is None or si_name in (None, "none"):
199
  raise OptGSError(
200
  f"checkpoint config at {cfg_path} has no scene_initializer "
201
- f"(name={si_name!r}); cannot derive init_gaussian_param_num "
202
- f"required to build the optimizer."
203
  )
204
  init_cls = _initializer_cfg_class(str(si_name))
205
  if init_cls is None:
206
  raise OptGSError(
207
  f"unsupported scene_initializer.name={si_name!r} in {cfg_path}; "
208
- f"cannot derive init_gaussian_param_num for the optimizer."
209
  )
210
  default_si = _compose_default_group("scene_initializer", str(si_name))
211
  if default_si is not None:
@@ -215,7 +214,7 @@ def build_optimizer_cfg(cfg_path: Path) -> tuple["KnnBasedOptimizerCfg", int | N
215
  merged_si = si
216
  try:
217
  init_cfg = load_typed_config(merged_si, init_cls)
218
- opt_cfg.update(init_cfg) # sets init_gaussian_param_num/init_sh_d/sh_d
219
  except Exception as e:
220
  raise OptGSError(
221
  f"failed to wire scene_initializer ({si_name!r}) into the "
@@ -335,7 +334,7 @@ def build_adam_baseline(num_refine: int) -> "nn.Module":
335
  )
336
  OmegaConf.set_struct(composed, False)
337
  # gsplat decays the means LR over the full step budget.
338
- composed.means_lr_max_steps = int(num_refine)
339
  # Disable densification — the baseline refines the same fixed Gaussian set
340
  # as the learned optimizer (a like-for-like comparison of the update rule).
341
  for flag in ("do_densify", "do_prune", "do_opacity_reset"):
@@ -357,14 +356,6 @@ def build_adam_baseline(num_refine: int) -> "nn.Module":
357
  return optimizer
358
 
359
 
360
- # Module-attribute renames applied when the legacy Resplat encoder was split
361
- # into separate initializer/optimizer modules (transcribed from
362
- # optgs/main.py:load_optimizer).
363
- _ORIG_OPTIMIZER_ATTR_RENAMES = {
364
- "render_error_mv_attn": "update_error_attn",
365
- }
366
-
367
-
368
  def load_optimizer_state(
369
  optimizer: "nn.Module",
370
  ckpt_path: str,
@@ -379,6 +370,8 @@ def load_optimizer_state(
379
  """
380
  import torch
381
 
 
 
382
  state = torch.load(ckpt_path, map_location="cpu")
383
  if isinstance(state, dict) and "state_dict" in state:
384
  state = state["state_dict"]
@@ -399,14 +392,10 @@ def load_optimizer_state(
399
  for k, v in state.items()
400
  if k.startswith("encoder.")
401
  }
402
- renamed = {}
403
- for k, v in osd.items():
404
- for old, new in _ORIG_OPTIMIZER_ATTR_RENAMES.items():
405
- if k == old or k.startswith(old + "."):
406
- k = new + k[len(old):]
407
- break
408
- renamed[k] = v
409
- osd = renamed
410
 
411
  if not osd:
412
  raise OptGSError(
@@ -415,6 +404,6 @@ def load_optimizer_state(
415
  )
416
 
417
  if init_state_wo_features:
418
- osd = {k: v for k, v in osd.items() if "update_proj" not in k}
419
 
420
  optimizer.load_state_dict(osd, strict=strict)
 
190
 
191
  # Mirror SceneTrainerCfg (scene_trainer_cfg.py: scene_optimizer.update(
192
  # scene_initializer)): wire the checkpoint's initializer cfg into the
193
+ # optimizer cfg so the runtime-only fields init_sh_d / sh_d — absent from
194
+ # every config file — are populated before the optimizer nn.Module is built.
 
195
  si = OmegaConf.select(cfg, "scene_trainer.scene_initializer")
196
  si_name = OmegaConf.select(cfg, "scene_trainer.scene_initializer.name")
197
  if si is None or si_name in (None, "none"):
198
  raise OptGSError(
199
  f"checkpoint config at {cfg_path} has no scene_initializer "
200
+ f"(name={si_name!r}); cannot derive the optimizer's initializer "
201
+ f"settings required to build it."
202
  )
203
  init_cls = _initializer_cfg_class(str(si_name))
204
  if init_cls is None:
205
  raise OptGSError(
206
  f"unsupported scene_initializer.name={si_name!r} in {cfg_path}; "
207
+ f"cannot derive the optimizer's initializer settings."
208
  )
209
  default_si = _compose_default_group("scene_initializer", str(si_name))
210
  if default_si is not None:
 
214
  merged_si = si
215
  try:
216
  init_cfg = load_typed_config(merged_si, init_cls)
217
+ opt_cfg.update(init_cfg) # sets init_sh_d/sh_d
218
  except Exception as e:
219
  raise OptGSError(
220
  f"failed to wire scene_initializer ({si_name!r}) into the "
 
334
  )
335
  OmegaConf.set_struct(composed, False)
336
  # gsplat decays the means LR over the full step budget.
337
+ composed.lr_scheduler.max_steps = int(num_refine)
338
  # Disable densification — the baseline refines the same fixed Gaussian set
339
  # as the learned optimizer (a like-for-like comparison of the update rule).
340
  for flag in ("do_densify", "do_prune", "do_opacity_reset"):
 
356
  return optimizer
357
 
358
 
 
 
 
 
 
 
 
 
359
  def load_optimizer_state(
360
  optimizer: "nn.Module",
361
  ckpt_path: str,
 
370
  """
371
  import torch
372
 
373
+ from optgs.misc.checkpointing import _rename_optimizer_attrs
374
+
375
  state = torch.load(ckpt_path, map_location="cpu")
376
  if isinstance(state, dict) and "state_dict" in state:
377
  state = state["state_dict"]
 
392
  for k, v in state.items()
393
  if k.startswith("encoder.")
394
  }
395
+
396
+ # Rename module attributes renamed in the update_/refine_ cleanup pass
397
+ # (and the resplat-era render_error_mv_attn).
398
+ osd = _rename_optimizer_attrs(osd)
 
 
 
 
399
 
400
  if not osd:
401
  raise OptGSError(
 
404
  )
405
 
406
  if init_state_wo_features:
407
+ osd = {k: v for k, v in osd.items() if "state_proj" not in k}
408
 
409
  optimizer.load_state_dict(osd, strict=strict)
optgs/global_cfg.py DELETED
@@ -1,19 +0,0 @@
1
- from typing import Optional
2
-
3
- from omegaconf import DictConfig
4
-
5
- cfg: Optional[DictConfig] = None
6
-
7
-
8
- def get_cfg() -> DictConfig:
9
- global cfg
10
- return cfg
11
-
12
-
13
- def set_cfg(new_cfg: DictConfig) -> None:
14
- global cfg
15
- cfg = new_cfg
16
-
17
-
18
- def get_seed() -> int:
19
- return cfg.seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/loss/loss_deltas.py CHANGED
@@ -24,6 +24,13 @@ class LossDeltasCfgWrapper:
24
 
25
 
26
  class LossDeltas(Loss[LossDeltasCfg, LossDeltasCfgWrapper]):
 
 
 
 
 
 
 
27
  def forward(
28
  self,
29
  prediction: DecoderOutput,
@@ -32,8 +39,6 @@ class LossDeltas(Loss[LossDeltasCfg, LossDeltasCfgWrapper]):
32
  gt_rgb: Tensor,
33
  pred_rgb: Tensor,
34
  valid_depth_mask: Tensor | None,
35
- l1_loss: bool,
36
- clamp_large_error: float,
37
  **kwargs,
38
  ) -> Float[Tensor, ""]:
39
 
 
24
 
25
 
26
  class LossDeltas(Loss[LossDeltasCfg, LossDeltasCfgWrapper]):
27
+ """L1 regularization on the optimizer's predicted per-Gaussian deltas, keeping the updates small.
28
+
29
+ With cfg.exclude_by_norm_grad the penalty is restricted to Gaussians with small gradients (and,
30
+ with exclude_by_norm_grad_opposite, those whose delta already agrees in sign with the gradient).
31
+ Returns 0 until global_step >= cfg.apply_after_step.
32
+ """
33
+
34
  def forward(
35
  self,
36
  prediction: DecoderOutput,
 
39
  gt_rgb: Tensor,
40
  pred_rgb: Tensor,
41
  valid_depth_mask: Tensor | None,
 
 
42
  **kwargs,
43
  ) -> Float[Tensor, ""]:
44
 
optgs/loss/loss_lpips.py CHANGED
@@ -20,6 +20,7 @@ class LossLpipsCfg:
20
  weight: float
21
  apply_after_step: int
22
  perceptual_loss: bool
 
23
 
24
 
25
  @dataclass
@@ -28,6 +29,12 @@ class LossLpipsCfgWrapper:
28
 
29
 
30
  class LossLpips(Loss[LossLpipsCfg, LossLpipsCfgWrapper]):
 
 
 
 
 
 
31
  lpips: LPIPS
32
 
33
  def __init__(self, cfg: LossLpipsCfgWrapper) -> None:
@@ -48,7 +55,6 @@ class LossLpips(Loss[LossLpipsCfg, LossLpipsCfgWrapper]):
48
  gt_rgb: Tensor,
49
  pred_rgb: Tensor,
50
  valid_depth_mask: Tensor | None,
51
- half_res_lpips: bool = False,
52
  **kwargs,
53
  ) -> Float[Tensor, ""]:
54
 
@@ -64,7 +70,7 @@ class LossLpips(Loss[LossLpipsCfg, LossLpipsCfgWrapper]):
64
  pred = rearrange(pred_rgb, "b v c h w -> (b v) c h w")
65
  gt = rearrange(gt_rgb, "b v c h w -> (b v) c h w")
66
 
67
- if half_res_lpips:
68
  pred = F.interpolate(pred, scale_factor=0.5, mode="bilinear", align_corners=True)
69
  gt = F.interpolate(gt, scale_factor=0.5, mode="bilinear", align_corners=True)
70
 
 
20
  weight: float
21
  apply_after_step: int
22
  perceptual_loss: bool
23
+ half_res: bool # downsample to half resolution before LPIPS to save memory
24
 
25
 
26
  @dataclass
 
29
 
30
 
31
  class LossLpips(Loss[LossLpipsCfg, LossLpipsCfgWrapper]):
32
+ """LPIPS perceptual distance between the rendered and ground-truth views.
33
+
34
+ Returns 0 until global_step >= cfg.apply_after_step. Uses VGG-LPIPS, or the custom
35
+ PerceptualLoss when cfg.perceptual_loss.
36
+ """
37
+
38
  lpips: LPIPS
39
 
40
  def __init__(self, cfg: LossLpipsCfgWrapper) -> None:
 
55
  gt_rgb: Tensor,
56
  pred_rgb: Tensor,
57
  valid_depth_mask: Tensor | None,
 
58
  **kwargs,
59
  ) -> Float[Tensor, ""]:
60
 
 
70
  pred = rearrange(pred_rgb, "b v c h w -> (b v) c h w")
71
  gt = rearrange(gt_rgb, "b v c h w -> (b v) c h w")
72
 
73
+ if self.cfg.half_res:
74
  pred = F.interpolate(pred, scale_factor=0.5, mode="bilinear", align_corners=True)
75
  gt = F.interpolate(gt, scale_factor=0.5, mode="bilinear", align_corners=True)
76
 
optgs/loss/loss_monodepth.py CHANGED
@@ -1,11 +1,6 @@
1
  import torch
2
 
3
-
4
- import cv2
5
- import torch
6
-
7
- from optgs.model.encoder.depth_anything_v2.dpt import DepthAnythingV2
8
-
9
 
10
 
11
  def get_monodepth_model():
@@ -16,7 +11,7 @@ def get_monodepth_model():
16
  'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}
17
  }
18
 
19
- encoder = 'vitl' # or 'vits', 'vitb', 'vitg'
20
 
21
  model = DepthAnythingV2(**model_configs[encoder])
22
  model.load_state_dict(torch.load(f'pretrained/depth_anything_v2_{encoder}.pth', map_location='cpu'))
@@ -26,17 +21,3 @@ def get_monodepth_model():
26
  param.requires_grad = False
27
 
28
  return model
29
-
30
-
31
-
32
- def get_monodepth_pred(img, model):
33
-
34
- with torch.no_grad():
35
- pass
36
-
37
-
38
- def get_monodepth_loss(pred_depth, img):
39
- pass
40
-
41
-
42
-
 
1
  import torch
2
 
3
+ from optgs.model.backbones.depth_anything_v2.dpt import DepthAnythingV2
 
 
 
 
 
4
 
5
 
6
  def get_monodepth_model():
 
11
  'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}
12
  }
13
 
14
+ encoder = 'vitl' # or 'vits', 'vitb', 'vitg'
15
 
16
  model = DepthAnythingV2(**model_configs[encoder])
17
  model.load_state_dict(torch.load(f'pretrained/depth_anything_v2_{encoder}.pth', map_location='cpu'))
 
21
  param.requires_grad = False
22
 
23
  return model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
optgs/loss/loss_mse.py CHANGED
@@ -12,6 +12,8 @@ from .loss import Loss
12
  @dataclass
13
  class LossMseCfg:
14
  weight: float
 
 
15
 
16
 
17
  @dataclass
@@ -20,6 +22,8 @@ class LossMseCfgWrapper:
20
 
21
 
22
  class LossMse(Loss[LossMseCfg, LossMseCfgWrapper]):
 
 
23
  def forward(
24
  self,
25
  prediction: DecoderOutput,
@@ -28,8 +32,6 @@ class LossMse(Loss[LossMseCfg, LossMseCfgWrapper]):
28
  gt_rgb: Tensor,
29
  pred_rgb: Tensor,
30
  valid_depth_mask: Tensor | None,
31
- l1_loss: bool,
32
- clamp_large_error: float,
33
  **kwargs,
34
  ) -> Float[Tensor, ""]:
35
 
@@ -38,15 +40,15 @@ class LossMse(Loss[LossMseCfg, LossMseCfgWrapper]):
38
  if valid_depth_mask is not None and valid_depth_mask.max() > 0.5 and valid_depth_mask.min() < 0.5:
39
  error = error[~valid_depth_mask]
40
 
41
- if l1_loss:
42
  # l1 loss
43
  error = error.abs()
44
  else:
45
  # l2 loss
46
  error = error ** 2
47
 
48
- if clamp_large_error > 0:
49
- valid_mask = error < clamp_large_error
50
  error = error[valid_mask]
51
 
52
  error = error.mean()
 
12
  @dataclass
13
  class LossMseCfg:
14
  weight: float
15
+ l1_loss: bool # use L1 instead of L2 on the RGB error
16
+ clamp_large_error: float # if > 0, drop per-pixel errors above this threshold
17
 
18
 
19
  @dataclass
 
22
 
23
 
24
  class LossMse(Loss[LossMseCfg, LossMseCfgWrapper]):
25
+ """RGB reconstruction loss between the rendered and ground-truth views (L2, or L1 if cfg.l1_loss)."""
26
+
27
  def forward(
28
  self,
29
  prediction: DecoderOutput,
 
32
  gt_rgb: Tensor,
33
  pred_rgb: Tensor,
34
  valid_depth_mask: Tensor | None,
 
 
35
  **kwargs,
36
  ) -> Float[Tensor, ""]:
37
 
 
40
  if valid_depth_mask is not None and valid_depth_mask.max() > 0.5 and valid_depth_mask.min() < 0.5:
41
  error = error[~valid_depth_mask]
42
 
43
+ if self.cfg.l1_loss:
44
  # l1 loss
45
  error = error.abs()
46
  else:
47
  # l2 loss
48
  error = error ** 2
49
 
50
+ if self.cfg.clamp_large_error > 0:
51
+ valid_mask = error < self.cfg.clamp_large_error
52
  error = error[valid_mask]
53
 
54
  error = error.mean()
optgs/loss/loss_sgd.py CHANGED
@@ -3,7 +3,6 @@ from dataclasses import dataclass
3
  from jaxtyping import Float
4
  from torch import Tensor
5
 
6
- from optgs.dataset.data_types import BatchedExample
7
  from optgs.loss import Loss
8
  from optgs.model.decoder.decoder import DecoderOutput
9
  from optgs.model.types import Gaussians
@@ -12,7 +11,8 @@ from optgs.scene_trainer.gaussian_module import GaussiansModule
12
 
13
  @dataclass
14
  class LossSGDCfg:
15
- pass
 
16
 
17
  @dataclass
18
  class LossSGDCfgWrapper:
@@ -20,20 +20,21 @@ class LossSGDCfgWrapper:
20
 
21
 
22
  class LossSGD(Loss[LossSGDCfg, LossSGDCfgWrapper]):
 
 
 
 
23
  def forward(
24
  self,
25
  prediction: DecoderOutput,
26
- batch: BatchedExample,
27
  gaussians: Gaussians | GaussiansModule | None,
28
  global_step: int,
29
- l1_loss: bool,
30
- clamp_large_error: float,
31
  valid_depth_mask: Tensor | None,
32
  **kwargs,
33
  ) -> Float[Tensor, ""]:
34
-
35
  if gaussians is None:
36
- raise ValueError("Gaussians must be provided for LossDeltas.")
37
 
38
  predicted_deltas = gaussians.deltas
39
  gt_gradients = gaussians.gradients
@@ -42,10 +43,10 @@ class LossSGD(Loss[LossSGDCfg, LossSGDCfgWrapper]):
42
  if predicted_deltas.dtype != gt_gradients.dtype:
43
  gt_gradients = gt_gradients.to(predicted_deltas.dtype)
44
 
45
- if l1_loss:
46
  loss = (predicted_deltas - gt_gradients).abs().mean()
47
  else:
48
  loss = ((predicted_deltas - gt_gradients) ** 2).mean()
49
- if clamp_large_error > 0:
50
- loss = loss.clamp(max=clamp_large_error)
51
  return loss
 
3
  from jaxtyping import Float
4
  from torch import Tensor
5
 
 
6
  from optgs.loss import Loss
7
  from optgs.model.decoder.decoder import DecoderOutput
8
  from optgs.model.types import Gaussians
 
11
 
12
  @dataclass
13
  class LossSGDCfg:
14
+ l1_loss: bool # use L1 instead of L2 on the delta-vs-gradient error
15
+ clamp_large_error: float # if > 0, clamp the loss to this maximum
16
 
17
  @dataclass
18
  class LossSGDCfgWrapper:
 
20
 
21
 
22
  class LossSGD(Loss[LossSGDCfg, LossSGDCfgWrapper]):
23
+ """Debugging / sanity-check loss: supervises the optimizer's predicted per-Gaussian deltas to match
24
+ the true gradients, i.e. checks the learned optimizer can reproduce a plain gradient-descent step
25
+ (L2, or L1 if cfg.l1_loss)."""
26
+
27
  def forward(
28
  self,
29
  prediction: DecoderOutput,
 
30
  gaussians: Gaussians | GaussiansModule | None,
31
  global_step: int,
 
 
32
  valid_depth_mask: Tensor | None,
33
  **kwargs,
34
  ) -> Float[Tensor, ""]:
35
+
36
  if gaussians is None:
37
+ raise ValueError("Gaussians must be provided for LossSGD.")
38
 
39
  predicted_deltas = gaussians.deltas
40
  gt_gradients = gaussians.gradients
 
43
  if predicted_deltas.dtype != gt_gradients.dtype:
44
  gt_gradients = gt_gradients.to(predicted_deltas.dtype)
45
 
46
+ if self.cfg.l1_loss:
47
  loss = (predicted_deltas - gt_gradients).abs().mean()
48
  else:
49
  loss = ((predicted_deltas - gt_gradients) ** 2).mean()
50
+ if self.cfg.clamp_large_error > 0:
51
+ loss = loss.clamp(max=self.cfg.clamp_large_error)
52
  return loss
optgs/loss/loss_sh0.py CHANGED
@@ -18,6 +18,9 @@ class LossSh0CfgWrapper:
18
  sh0: LossSh0Cfg
19
 
20
  class LossSh0(Loss[LossSh0Cfg, LossSh0CfgWrapper]):
 
 
 
21
  def forward(
22
  self,
23
  prediction: DecoderOutput,
 
18
  sh0: LossSh0Cfg
19
 
20
  class LossSh0(Loss[LossSh0Cfg, LossSh0CfgWrapper]):
21
+ """Supervises each Gaussian's SH degree-0 (base) color against the ground-truth image colour
22
+ sampled at the Gaussian's projected 2D location, averaged over the views where it is visible."""
23
+
24
  def forward(
25
  self,
26
  prediction: DecoderOutput,