sysu111 commited on
Commit
87608ea
·
1 Parent(s): a27f11e

Add PXDepth demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. README.md +11 -7
  3. app.py +323 -0
  4. example_images/KITTI_01.jpg +3 -0
  5. example_images/KITTI_02.jpg +3 -0
  6. example_images/KITTI_03.jpg +3 -0
  7. example_images/Libary_01.jpg +3 -0
  8. example_images/NYUv2_00175.jpg +3 -0
  9. example_images/bird_01.jpg +3 -0
  10. example_images/chair_01.jpg +3 -0
  11. example_images/courtyard_01.jpg +3 -0
  12. example_images/courtyard_02.jpg +3 -0
  13. example_images/pipe_01.jpg +3 -0
  14. example_images/room_01.jpg +3 -0
  15. example_images/room_02.jpg +3 -0
  16. example_images/room_03.jpg +3 -0
  17. example_images/stair_01.jpg +3 -0
  18. example_images/stand_01.jpg +3 -0
  19. example_images/street_01.jpg +3 -0
  20. example_images/street_02.png +3 -0
  21. example_images/street_03.jpg +3 -0
  22. example_images/umic_building.jpg +3 -0
  23. example_images/volterra.jpg +3 -0
  24. pxdepth/__init__.py +18 -0
  25. pxdepth/build.py +26 -0
  26. pxdepth/config.py +137 -0
  27. pxdepth/evaluation/__init__.py +12 -0
  28. pxdepth/evaluation/dataloader.py +675 -0
  29. pxdepth/evaluation/metrics.py +616 -0
  30. pxdepth/inference/__init__.py +20 -0
  31. pxdepth/inference/resize.py +158 -0
  32. pxdepth/inference/runner.py +84 -0
  33. pxdepth/model/CM_PiT.py +260 -0
  34. pxdepth/model/Gated_Attention.py +105 -0
  35. pxdepth/model/Global_Context_Encoder.py +185 -0
  36. pxdepth/model/PXDepth.py +276 -0
  37. pxdepth/model/Pixel_Space_Depth_Predictor.py +274 -0
  38. pxdepth/model/RoPE.py +208 -0
  39. pxdepth/model/__init__.py +12 -0
  40. pxdepth/model/checkpoint.py +83 -0
  41. pxdepth/model/dinov2/__init__.py +6 -0
  42. pxdepth/model/dinov2/hub/__init__.py +4 -0
  43. pxdepth/model/dinov2/hub/backbones.py +156 -0
  44. pxdepth/model/dinov2/hub/utils.py +39 -0
  45. pxdepth/model/dinov2/layers/__init__.py +10 -0
  46. pxdepth/model/dinov2/layers/attention.py +151 -0
  47. pxdepth/model/dinov2/layers/block.py +259 -0
  48. pxdepth/model/dinov2/layers/drop_path.py +34 -0
  49. pxdepth/model/dinov2/layers/layer_scale.py +27 -0
  50. pxdepth/model/dinov2/layers/mlp.py +40 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.jpg filter=lfs diff=lfs merge=lfs -text
37
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
38
+ *.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,17 @@
1
  ---
2
- title: PXDepth Demo
3
- emoji: 👀
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.25.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
+ title: PXDepth
3
+ emoji: 🌐
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.22.0
8
+ python_version: 3.10.13
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
+ # PXDepth Demo
14
+
15
+ Interactive monocular depth and point-cloud demo for [PXDepth](https://github.com/yuanzhy29/PXDepth).
16
+
17
+ The Space downloads the released PXDepth and MoGe-2 checkpoints automatically on first startup.
app.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Gradio Space for PXDepth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ # ZeroGPU patches torch during import, so spaces must be imported first.
12
+ try:
13
+ import spaces
14
+
15
+ gpu = spaces.GPU(duration=90)
16
+ except ImportError:
17
+ gpu = lambda fn: fn
18
+
19
+ import gradio as gr
20
+ import numpy as np
21
+ import torch
22
+ import torch.nn.functional as F
23
+ import utils3d
24
+ from PIL import Image
25
+
26
+ from pxdepth.inference import area_size_from_area, resize_image, resize_map
27
+ from pxdepth.model import PXDepth
28
+ from pxdepth.utils.ply import write_point_cloud_ply
29
+ from pxdepth.utils.vis import colorize_depth
30
+
31
+
32
+ PXDEPTH_REPO = "yuanzhy29/PXDepth"
33
+ MOGE2_REPO = "Ruicheng/moge-2-vitl-normal"
34
+ PXDEPTH_SIZE = (1022, 770)
35
+ MOGE2_TOKEN_AREA = 1200
36
+ MOGE2_PATCH_SIZE = 14
37
+ MAX_INPUT_PIXELS = 12_000_000
38
+ OUTPUT_MAX_AGE = 60 * 60
39
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
40
+
41
+ CSS = """
42
+ #pxdepth-demo { max-width: 1280px; margin: 0 auto; }
43
+ #img-display-input, #img-display-output { max-height: 72vh; }
44
+ #img-display-output img { object-fit: contain !important; }
45
+ #model-3d { min-height: 60vh; }
46
+ """
47
+
48
+
49
+ def load_model() -> PXDepth:
50
+ """Load PXDepth and its MoGe-2 metric-scale reference once at startup."""
51
+ print("Loading PXDepth...")
52
+ model = PXDepth.from_pretrained(PXDEPTH_REPO, strict=True).eval()
53
+
54
+ try:
55
+ from moge.model.v2 import MoGeModel
56
+ except ImportError as exc:
57
+ raise RuntimeError(
58
+ "MoGe-2 is required by this demo. Check the Space requirements."
59
+ ) from exc
60
+
61
+ print("Loading MoGe-2...")
62
+ model._reference_model = MoGeModel.from_pretrained(MOGE2_REPO).eval()
63
+ model = model.to(DEVICE).eval()
64
+ print(f"Models loaded on {DEVICE}.")
65
+ return model
66
+
67
+
68
+ MODEL = load_model()
69
+
70
+
71
+ def resize_for_tokens(image: torch.Tensor, tokens: int, patch: int) -> torch.Tensor:
72
+ """Preserve aspect ratio and resize an RGB tensor to a patch-token area."""
73
+ height, width = area_size_from_area(
74
+ image.shape[-2],
75
+ image.shape[-1],
76
+ tokens * patch * patch,
77
+ patch,
78
+ )
79
+ if (height, width) == tuple(image.shape[-2:]):
80
+ return image
81
+ return F.interpolate(
82
+ image.unsqueeze(0),
83
+ (height, width),
84
+ mode="bilinear",
85
+ align_corners=False,
86
+ )[0]
87
+
88
+
89
+ def cleanup_outputs(root: Path) -> None:
90
+ """Remove stale per-session files from the Space's ephemeral storage."""
91
+ if not root.exists():
92
+ return
93
+ cutoff = time.time() - OUTPUT_MAX_AGE
94
+ for path in root.iterdir():
95
+ try:
96
+ if path.is_dir() and path.stat().st_mtime < cutoff:
97
+ shutil.rmtree(path, ignore_errors=True)
98
+ except OSError:
99
+ continue
100
+
101
+
102
+ def session_dir(request: Optional[gr.Request]) -> Path:
103
+ """Create a clean output directory for the current browser session."""
104
+ session = getattr(request, "session_hash", None) or "local"
105
+ session = "".join(char for char in session if char.isalnum() or char in "-_")
106
+ root = Path(tempfile.gettempdir()) / "pxdepth-demo"
107
+ root.mkdir(parents=True, exist_ok=True)
108
+ cleanup_outputs(root)
109
+
110
+ output = root / (session or "local")
111
+ shutil.rmtree(output, ignore_errors=True)
112
+ output.mkdir(parents=True, exist_ok=True)
113
+ return output
114
+
115
+
116
+ def sample_points(
117
+ points: np.ndarray,
118
+ colors: np.ndarray,
119
+ max_points: int,
120
+ ) -> tuple[np.ndarray, np.ndarray]:
121
+ """Deterministically subsample a point cloud for browser rendering."""
122
+ if points.shape[0] <= max_points:
123
+ return points, colors
124
+ indices = np.linspace(0, points.shape[0] - 1, max_points, dtype=np.int64)
125
+ return points[indices], colors[indices]
126
+
127
+
128
+ @gpu
129
+ @torch.inference_mode()
130
+ def predict_gpu(image: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
131
+ """Run only model inference while holding the ZeroGPU allocation."""
132
+ tensor = (
133
+ torch.from_numpy(image.copy())
134
+ .to(device=DEVICE, dtype=torch.float32)
135
+ .permute(2, 0, 1)
136
+ / 255.0
137
+ )
138
+ model_image, _ = resize_image(
139
+ tensor,
140
+ PXDEPTH_SIZE,
141
+ True,
142
+ MODEL.patch_size,
143
+ )
144
+ reference_image = resize_for_tokens(
145
+ tensor,
146
+ MOGE2_TOKEN_AREA,
147
+ MOGE2_PATCH_SIZE,
148
+ )
149
+
150
+ result = MODEL.infer(
151
+ model_image,
152
+ ref_image=reference_image,
153
+ apply_mask=False,
154
+ use_fp16=DEVICE.type == "cuda",
155
+ use_fp32=DEVICE.type != "cuda",
156
+ )
157
+ return (
158
+ result["depth"].float().cpu().numpy(),
159
+ result["mask"].cpu().numpy(),
160
+ result["intrinsics"].float().cpu().numpy(),
161
+ )
162
+
163
+
164
+ def on_submit(
165
+ image: Optional[np.ndarray],
166
+ max_points: int,
167
+ apply_mask: bool,
168
+ request: gr.Request,
169
+ ):
170
+ """Run inference, build visualizations, and export downloadable files."""
171
+ if image is None:
172
+ raise gr.Error("Please upload an image first.")
173
+ if image.ndim != 3 or image.shape[-1] < 3:
174
+ raise gr.Error("The input must be an RGB image.")
175
+ if image.shape[0] * image.shape[1] > MAX_INPUT_PIXELS:
176
+ raise gr.Error(
177
+ "The uploaded image is too large. Please use an image below 12 megapixels."
178
+ )
179
+
180
+ image = np.ascontiguousarray(image[..., :3].astype(np.uint8))
181
+ original_size = image.shape[:2]
182
+ depth_raw, mask_raw, intrinsics_np = predict_gpu(image)
183
+
184
+ # Restore outputs and reconstruct the point map on CPU so ZeroGPU is held
185
+ # only for neural-network inference.
186
+ depth = resize_map(torch.from_numpy(depth_raw), original_size).float()
187
+ mask = resize_map(torch.from_numpy(mask_raw), original_size, is_mask=True)
188
+ intrinsics = torch.from_numpy(intrinsics_np).float()
189
+ finite = torch.isfinite(depth) & (depth > 0)
190
+ valid = finite & mask if apply_mask else finite
191
+ points = utils3d.pt.depth_map_to_point_map(
192
+ torch.where(finite, depth, torch.zeros_like(depth)),
193
+ intrinsics=intrinsics,
194
+ )
195
+
196
+ depth_np = depth.numpy().astype(np.float32)
197
+ mask_np = mask.numpy().astype(bool)
198
+ valid_np = valid.numpy().astype(bool)
199
+ depth_vis = colorize_depth(np.where(mask_np, depth_np, np.inf), mask=None)
200
+
201
+ output = session_dir(request)
202
+ depth_npy = output / "metric_depth.npy"
203
+ depth_png = output / "depth.png"
204
+ mask_png = output / "mask.png"
205
+ ply_path = output / "pointcloud.ply"
206
+ glb_path = output / "pointcloud_viewer.glb"
207
+
208
+ np.save(depth_npy, depth_np)
209
+ Image.fromarray(depth_vis).save(depth_png)
210
+ Image.fromarray(mask_np.astype(np.uint8) * 255, mode="L").save(mask_png)
211
+
212
+ points_np = points.numpy().reshape(-1, 3)
213
+ colors_np = image.reshape(-1, 3).astype(np.float32) / 255.0
214
+ keep = valid_np.reshape(-1) & np.isfinite(points_np).all(axis=1)
215
+ points_full, colors_full = points_np[keep], colors_np[keep]
216
+ if points_full.shape[0] == 0:
217
+ raise gr.Error("No valid 3D points were produced for this image.")
218
+ write_point_cloud_ply(ply_path, points_full, colors_full)
219
+
220
+ import trimesh
221
+
222
+ viewer_points, viewer_colors = sample_points(
223
+ points_full,
224
+ colors_full,
225
+ int(max_points),
226
+ )
227
+ viewer_points = viewer_points * np.array([1.0, -1.0, -1.0], np.float32)
228
+ viewer_colors = np.clip(viewer_colors * 255.0, 0, 255).astype(np.uint8)
229
+ trimesh.PointCloud(viewer_points, colors=viewer_colors).export(glb_path)
230
+
231
+ files = [str(depth_png), str(depth_npy), str(mask_png), str(ply_path)]
232
+ return (image, depth_vis), str(glb_path), files
233
+
234
+
235
+ def build_demo() -> gr.Blocks:
236
+ """Construct the public Gradio interface."""
237
+ description = """
238
+ Official demo for **PXDepth: Pixel-Space Modeling for Structure Preserving Monocular Depth Estimation**.
239
+ See the [paper](https://arxiv.org/abs/2608.16984),
240
+ [project page](https://yuanzhy29.github.io/PXDepth-Page/), and
241
+ [GitHub repository](https://github.com/yuanzhy29/PXDepth).
242
+ """
243
+ with gr.Blocks(theme=gr.themes.Soft(), css=CSS) as demo:
244
+ with gr.Column(elem_id="pxdepth-demo"):
245
+ gr.Markdown("# PXDepth")
246
+ gr.Markdown(description)
247
+ gr.Markdown("### Point Cloud & Depth Prediction Demo")
248
+
249
+ with gr.Row():
250
+ with gr.Column():
251
+ input_image = gr.Image(
252
+ label="Input Image",
253
+ image_mode="RGB",
254
+ type="numpy",
255
+ elem_id="img-display-input",
256
+ )
257
+ with gr.Accordion(label="Settings", open=False):
258
+ max_points = gr.Slider(
259
+ 50_000,
260
+ 500_000,
261
+ value=200_000,
262
+ step=50_000,
263
+ label="3D Viewer Max Points",
264
+ info="The downloaded PLY retains all valid points.",
265
+ )
266
+ apply_mask = gr.Checkbox(
267
+ label="Apply valid-depth mask to point cloud",
268
+ value=True,
269
+ )
270
+ submit = gr.Button("Predict", variant="primary")
271
+
272
+ with gr.Column():
273
+ with gr.Tabs():
274
+ with gr.Tab("3D View"):
275
+ model_3d = gr.Model3D(
276
+ label="3D Point Map",
277
+ clear_color=(1.0, 1.0, 1.0, 1.0),
278
+ height="60vh",
279
+ elem_id="model-3d",
280
+ )
281
+ with gr.Tab("Depth"):
282
+ depth_map = gr.ImageSlider(
283
+ label="RGB / Metric Depth",
284
+ image_mode="RGB",
285
+ type="numpy",
286
+ slider_position=50,
287
+ elem_id="img-display-output",
288
+ )
289
+ with gr.Tab("Download"):
290
+ downloads = gr.File(
291
+ label="Download Files",
292
+ file_count="multiple",
293
+ type="filepath",
294
+ )
295
+
296
+ examples = Path("example_images")
297
+ example_files = (
298
+ sorted(
299
+ str(path)
300
+ for path in examples.iterdir()
301
+ if path.suffix.lower() in {".jpg", ".jpeg", ".png", ".webp"}
302
+ )
303
+ if examples.exists()
304
+ else []
305
+ )
306
+ if example_files:
307
+ gr.Examples(example_files, input_image, cache_examples=False)
308
+
309
+ submit.click(
310
+ on_submit,
311
+ [input_image, max_points, apply_mask],
312
+ [depth_map, model_3d, downloads],
313
+ show_progress="full",
314
+ concurrency_limit=1,
315
+ )
316
+ return demo
317
+
318
+
319
+ demo = build_demo()
320
+
321
+
322
+ if __name__ == "__main__":
323
+ demo.queue(default_concurrency_limit=1).launch()
example_images/KITTI_01.jpg ADDED

Git LFS Details

  • SHA256: f6a3bc776a4f4b0e20f9e9d0ec77912db738d3b2b17a461425dc2fb9686e2d73
  • Pointer size: 131 Bytes
  • Size of remote file: 144 kB
example_images/KITTI_02.jpg ADDED

Git LFS Details

  • SHA256: 738344a8690fa43b21cb6c98bcdbe9ffdbdfeb65629e344b3d6aa48245b742e8
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
example_images/KITTI_03.jpg ADDED

Git LFS Details

  • SHA256: 5a5eac2ac8f8cc1f90cf9e5426984611358baa066a1c3794854eea0752d81c8f
  • Pointer size: 131 Bytes
  • Size of remote file: 215 kB
example_images/Libary_01.jpg ADDED

Git LFS Details

  • SHA256: ab19a901bb333ccd85b9f5cd17508d4dd83a9f1f7fabda42053c30f6b2a2acf0
  • Pointer size: 130 Bytes
  • Size of remote file: 47.3 kB
example_images/NYUv2_00175.jpg ADDED

Git LFS Details

  • SHA256: 01c654b2864893d77e46c73463f196c9046098577832d0cca382edaf33369387
  • Pointer size: 130 Bytes
  • Size of remote file: 85.4 kB
example_images/bird_01.jpg ADDED

Git LFS Details

  • SHA256: c0f44133d753a851044d3ab5d135fab07314858ec8277df76c22b044c6556cdb
  • Pointer size: 131 Bytes
  • Size of remote file: 408 kB
example_images/chair_01.jpg ADDED

Git LFS Details

  • SHA256: 845be5c970de293f7da8b9399b94eaec9ce30f9eba9af185c77f1c58099647a2
  • Pointer size: 131 Bytes
  • Size of remote file: 139 kB
example_images/courtyard_01.jpg ADDED

Git LFS Details

  • SHA256: fd404a448717b76eff4b1f9ffffd3a1c09b9a7115ad40dd01716e2f55743d12f
  • Pointer size: 130 Bytes
  • Size of remote file: 56 kB
example_images/courtyard_02.jpg ADDED

Git LFS Details

  • SHA256: d12f12f53dcd68860cd35d04e4c881673256fb05dae5942da0a991d89bc3f9f0
  • Pointer size: 130 Bytes
  • Size of remote file: 59.5 kB
example_images/pipe_01.jpg ADDED

Git LFS Details

  • SHA256: 820c7504377a971b2541d373d261d645b98f50b61a7b5b2d4a7eba20296e8603
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
example_images/room_01.jpg ADDED

Git LFS Details

  • SHA256: 523549a80a417a5543d185471cd8373d67605498e8a72f397e77208638184374
  • Pointer size: 131 Bytes
  • Size of remote file: 100 kB
example_images/room_02.jpg ADDED

Git LFS Details

  • SHA256: f2ac3b7dabd4699c2899c997852cf81676df10aa1256e9d107a54a641d4f3d2d
  • Pointer size: 130 Bytes
  • Size of remote file: 85.3 kB
example_images/room_03.jpg ADDED

Git LFS Details

  • SHA256: 0b3ea8c8cbdd30edb300af35535a307247f1b1b8a632afb37423cf256c331bae
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
example_images/stair_01.jpg ADDED

Git LFS Details

  • SHA256: 7838f9ab682c71717612462fe256bda572ff556e0f2ab90aa64eb0f46495fa5e
  • Pointer size: 130 Bytes
  • Size of remote file: 16 kB
example_images/stand_01.jpg ADDED

Git LFS Details

  • SHA256: 39921cfa3a7a48a82c4ca442704dd654903b44a782e44e1bc42d6bf05c3d49af
  • Pointer size: 131 Bytes
  • Size of remote file: 227 kB
example_images/street_01.jpg ADDED

Git LFS Details

  • SHA256: 7d70e2bdeeb71218e1dd66a06f919a244c26178b97d09b593c6a5cc7f1c63569
  • Pointer size: 131 Bytes
  • Size of remote file: 339 kB
example_images/street_02.png ADDED

Git LFS Details

  • SHA256: 6f00094173568cf0b9295735668cc7a7aee6d3cc75f61897e12ce0cff24922e6
  • Pointer size: 132 Bytes
  • Size of remote file: 2.93 MB
example_images/street_03.jpg ADDED

Git LFS Details

  • SHA256: e6029ec36beb9779ae99449ed6d0fe327e6d14ceac82c08aa53849e81cd5cee6
  • Pointer size: 130 Bytes
  • Size of remote file: 54.1 kB
example_images/umic_building.jpg ADDED

Git LFS Details

  • SHA256: e375c42219bd9a4b63d710f52cb5c8dd247f6c05fb186f33bb7c042bf4f4106d
  • Pointer size: 131 Bytes
  • Size of remote file: 686 kB
example_images/volterra.jpg ADDED

Git LFS Details

  • SHA256: 4e4aea6dad1c424deaa6c03a61ea58d4787f57cafb3b1178ec59061120d4478d
  • Pointer size: 131 Bytes
  • Size of remote file: 332 kB
pxdepth/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Top-level public API for PXDepth monocular depth estimation.
2
+
3
+ Importing this package exposes the :class:`PXDepth` model without pulling
4
+ evaluation entry points into user code. The model accepts RGB
5
+ tensors and returns normalized depth plus a finite-depth probability map.
6
+ """
7
+
8
+ from .build import build_model
9
+ from .model import PXDepth
10
+ from .registry import ENCODERS, MODELS, PREDICTORS
11
+
12
+ __all__ = [
13
+ "PXDepth",
14
+ "build_model",
15
+ "MODELS",
16
+ "ENCODERS",
17
+ "PREDICTORS",
18
+ ]
pxdepth/build.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public construction helpers for config-driven PXDepth components."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ import torch.nn as nn
6
+
7
+ from .registry import MODELS
8
+
9
+ # Import built-in modules once so their registration decorators run. External
10
+ # components are imported by ``load_config`` before these builders are called.
11
+ from . import model as _model # noqa: F401,E402
12
+
13
+
14
+ def build_model(config: Dict[str, Any]) -> nn.Module:
15
+ """Build a model from its JSON-compatible configuration.
16
+
17
+ Args:
18
+ config: Model dictionary. ``type`` defaults to ``PXDepth`` for old
19
+ public configs and checkpoints.
20
+
21
+ Returns:
22
+ Constructed ``torch.nn.Module`` registered in :data:`MODELS`.
23
+ """
24
+ config = dict(config)
25
+ config.setdefault("type", "PXDepth")
26
+ return MODELS.build(config)
pxdepth/config.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load, compose, expand, and validate PXDepth JSON configurations.
2
+
3
+ The loader deliberately stays JSON based. It adds only three conveniences that
4
+ are useful to external users: optional ``_base_`` composition, environment
5
+ variable expansion in strings, and optional module imports for custom registry
6
+ entries. There is no framework-specific config object or runtime magic.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from copy import deepcopy
12
+ from importlib import import_module
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Iterable
15
+
16
+
17
+ def _merge(base: Any, update: Any) -> Any:
18
+ """Recursively merge one configuration value into another.
19
+
20
+ Args:
21
+ base: Existing value inherited from a base config.
22
+ update: Value from the child config. Dictionaries merge recursively,
23
+ while lists and scalar values replace ``base`` completely.
24
+
25
+ Returns:
26
+ A deep-copied merged value. Neither input object is mutated.
27
+ """
28
+ if not isinstance(base, dict) or not isinstance(update, dict):
29
+ return deepcopy(update)
30
+ result = deepcopy(base)
31
+ for key, value in update.items():
32
+ result[key] = _merge(result[key], value) if key in result else deepcopy(value)
33
+ return result
34
+
35
+
36
+ def _expand(value: Any) -> Any:
37
+ """Expand filesystem shorthand throughout a nested config structure.
38
+
39
+ Args:
40
+ value: Arbitrarily nested dictionaries, lists, strings, and scalar
41
+ values loaded from JSON.
42
+
43
+ Returns:
44
+ A matching nested structure where every string has environment
45
+ variables and a leading ``~`` expanded. Non-string values are retained.
46
+ """
47
+ if isinstance(value, dict):
48
+ return {key: _expand(item) for key, item in value.items()}
49
+ if isinstance(value, list):
50
+ return [_expand(item) for item in value]
51
+ if isinstance(value, str):
52
+ return os.path.expanduser(os.path.expandvars(value))
53
+ return value
54
+
55
+
56
+ def _read(path: Path, stack: tuple[Path, ...] = ()) -> Dict[str, Any]:
57
+ """Read one JSON file and recursively compose optional base files.
58
+
59
+ Args:
60
+ path: JSON file to load. Relative ``_base_`` entries resolve beside it.
61
+ stack: Internal chain of resolved paths used to detect inheritance
62
+ cycles. Callers normally leave this empty.
63
+
64
+ Returns:
65
+ Merged plain dictionary before string expansion and validation.
66
+
67
+ Raises:
68
+ ValueError: If configs form a circular ``_base_`` dependency.
69
+ """
70
+ path = path.resolve()
71
+ if path in stack:
72
+ chain = " -> ".join(str(item) for item in (*stack, path))
73
+ raise ValueError(f"Circular config inheritance: {chain}")
74
+ config = json.loads(path.read_text())
75
+ bases = config.pop("_base_", [])
76
+ if isinstance(bases, str):
77
+ bases = [bases]
78
+ merged: Dict[str, Any] = {}
79
+ for base in bases:
80
+ base_path = Path(os.path.expanduser(os.path.expandvars(str(base))))
81
+ if not base_path.is_absolute():
82
+ base_path = path.parent / base_path
83
+ merged = _merge(merged, _read(base_path, (*stack, path)))
84
+ return _merge(merged, config)
85
+
86
+
87
+ def import_modules(names: Iterable[str]) -> None:
88
+ """Import extension modules so their registry decorators execute.
89
+
90
+ Args:
91
+ names: Iterable of importable Python module names, such as
92
+ ``my_project.components``.
93
+
94
+ Returns:
95
+ ``None``. Imports are performed for their registration side effects.
96
+ """
97
+ for name in names:
98
+ import_module(str(name))
99
+
100
+
101
+ def validate_config(config: Dict[str, Any], kind: str | None = None) -> None:
102
+ """Fail early for missing or structurally invalid public configuration fields.
103
+
104
+ Args:
105
+ config: Fully composed configuration dictionary.
106
+ kind: Optional ``'eval'`` validation profile.
107
+
108
+ Returns:
109
+ ``None``. A descriptive ``ValueError`` is raised for invalid structure.
110
+ """
111
+ if not isinstance(config, dict):
112
+ raise ValueError("The config root must be a JSON object.")
113
+ if kind not in {None, "eval"}:
114
+ raise ValueError(f"Unsupported config kind: {kind!r}")
115
+ if kind == "eval" and not config:
116
+ raise ValueError("Evaluation config must contain at least one benchmark.")
117
+
118
+
119
+ def load_config(path: str | Path, kind: str | None = None) -> Dict[str, Any]:
120
+ """Load a resolved config and import optional external extension modules.
121
+
122
+ Args:
123
+ path: JSON config path. Relative ``_base_`` paths resolve beside this file.
124
+ kind: Optional validation profile passed to :func:`validate_config`.
125
+
126
+ Returns:
127
+ Plain nested dictionaries/lists suitable for JSON serialization. The
128
+ optional top-level ``imports`` list is retained for external registry
129
+ extensions.
130
+ """
131
+ config = _expand(_read(Path(path)))
132
+ imports = config.get("imports", [])
133
+ if isinstance(imports, str):
134
+ imports = [imports]
135
+ import_modules(imports)
136
+ validate_config(config, kind=kind)
137
+ return config
pxdepth/evaluation/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public benchmark-evaluation API for PXDepth.
2
+
3
+ The exported loader produces geometry-aware evaluation samples and the metric
4
+ function aligns raw predictions before computing depth, point-cloud, camera,
5
+ local-structure, and optional boundary measurements. Command-line orchestration
6
+ is kept in ``scripts/eval.py``.
7
+ """
8
+
9
+ from .dataloader import EvalDataLoaderPipeline
10
+ from .metrics import compute_metrics
11
+
12
+ __all__ = ["EvalDataLoaderPipeline", "compute_metrics"]
pxdepth/evaluation/dataloader.py ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Asynchronous RGB-depth benchmark loader with geometry-aware resizing.
2
+
3
+ Evaluation samples follow the processed benchmark directory contract. This
4
+ module loads RGB, depth, normalized intrinsics, and optional segmentation,
5
+ applies the benchmark-configured view transformation, and returns aligned
6
+ PyTorch tensors plus an organized ground-truth point map.
7
+ """
8
+
9
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ import torch
14
+ from PIL import Image
15
+ import cv2
16
+ import utils3d
17
+ import pipeline
18
+
19
+ from ..utils.io import read_depth, read_image, read_json, read_segmentation
20
+ from ..utils.data_augmentation import sample_perspective, warp_perspective, resize_to_cover_center_crop_view
21
+
22
+
23
+ def _resolve_image_path(instance_path: Union[str, Path]) -> Path:
24
+ """Resolve a processed sample's RGB path with PNG precedence.
25
+
26
+ Args:
27
+ instance_path: Directory containing one processed benchmark sample.
28
+
29
+ Returns:
30
+ ``image.png`` when present, otherwise ``image.jpg``.
31
+ """
32
+ instance_path = Path(instance_path)
33
+ image_png = instance_path / 'image.png'
34
+ if image_png.exists():
35
+ return image_png
36
+ return instance_path / 'image.jpg'
37
+
38
+
39
+ def _resize_to_cover_center_crop_mask(mask: np.ndarray, raw_width: int, raw_height: int, tgt_width: int, tgt_height: int) -> np.ndarray:
40
+ """Apply resize-to-cover and center crop to a discrete label mask.
41
+
42
+ Args:
43
+ mask: Integer/boolean source mask ``[H_raw,W_raw]``.
44
+ raw_width: Source image width.
45
+ raw_height: Source image height.
46
+ tgt_width: Output width.
47
+ tgt_height: Output height.
48
+
49
+ Returns:
50
+ Nearest-resized and center-cropped mask ``[H_tgt,W_tgt]``.
51
+ """
52
+ scale = max(tgt_width / raw_width, tgt_height / raw_height)
53
+ resized_width = max(tgt_width, int(round(raw_width * scale)))
54
+ resized_height = max(tgt_height, int(round(raw_height * scale)))
55
+ resized_mask = cv2.resize(
56
+ mask.astype(np.uint8),
57
+ (resized_width, resized_height),
58
+ interpolation=cv2.INTER_NEAREST,
59
+ )
60
+ x0 = max(0, (resized_width - tgt_width) // 2)
61
+ y0 = max(0, (resized_height - tgt_height) // 2)
62
+ return resized_mask[y0:y0 + tgt_height, x0:x0 + tgt_width].copy()
63
+
64
+
65
+ def _intrinsics_normalized_to_pixel(intrinsics: np.ndarray, width: int, height: int) -> np.ndarray:
66
+ """Convert normalized camera intrinsics to pixel coordinates.
67
+
68
+ Args:
69
+ intrinsics: Floating camera matrix ``[3,3]`` normalized by image size.
70
+ width: Image width used to scale the first matrix row.
71
+ height: Image height used to scale the second matrix row.
72
+
73
+ Returns:
74
+ Pixel-space ``float32 [3,3]`` camera matrix.
75
+ """
76
+ intrinsics_px = intrinsics.astype(np.float32).copy()
77
+ intrinsics_px[0, :] *= float(width)
78
+ intrinsics_px[1, :] *= float(height)
79
+ intrinsics_px[2, 0] = 0.0
80
+ intrinsics_px[2, 1] = 0.0
81
+ intrinsics_px[2, 2] = 1.0
82
+ return intrinsics_px
83
+
84
+
85
+ def _intrinsics_pixel_to_normalized(intrinsics_px: np.ndarray, width: int, height: int) -> np.ndarray:
86
+ """Convert pixel camera intrinsics to normalized coordinates.
87
+
88
+ Args:
89
+ intrinsics_px: Pixel-space camera matrix ``[3,3]``.
90
+ width: Image width used to normalize the first matrix row.
91
+ height: Image height used to normalize the second matrix row.
92
+
93
+ Returns:
94
+ Normalized ``float32 [3,3]`` camera matrix.
95
+ """
96
+ intrinsics = intrinsics_px.astype(np.float32).copy()
97
+ intrinsics[0, :] /= float(width)
98
+ intrinsics[1, :] /= float(height)
99
+ intrinsics[2, 0] = 0.0
100
+ intrinsics[2, 1] = 0.0
101
+ intrinsics[2, 2] = 1.0
102
+ return intrinsics
103
+
104
+
105
+ def _resize_depth_nearest_preserve_nan(depth: np.ndarray, size: Tuple[int, int]) -> np.ndarray:
106
+ """Nearest-resize positive finite depth while preserving invalid support.
107
+
108
+ Args:
109
+ depth: Source depth ``float [H,W]`` with NaN/Inf invalid values.
110
+ size: OpenCV target tuple ``(width,height)``.
111
+
112
+ Returns:
113
+ ``float32 [height,width]`` depth. Pixels whose nearest source was invalid
114
+ are represented by NaN.
115
+ """
116
+ width, height = size
117
+ valid = np.isfinite(depth) & (depth > 0)
118
+ resized_depth = cv2.resize(
119
+ np.where(valid, depth, 0.0).astype(np.float32),
120
+ (width, height),
121
+ interpolation=cv2.INTER_NEAREST,
122
+ )
123
+ resized_valid = cv2.resize(
124
+ valid.astype(np.uint8),
125
+ (width, height),
126
+ interpolation=cv2.INTER_NEAREST,
127
+ ).astype(bool)
128
+ return np.where(resized_valid, resized_depth, np.nan).astype(np.float32)
129
+
130
+
131
+ def _mda_boundary_view(
132
+ image: np.ndarray,
133
+ depth: np.ndarray,
134
+ intrinsics: np.ndarray,
135
+ target_size: Tuple[int, int],
136
+ segmentation_mask: Optional[np.ndarray] = None,
137
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Optional[np.ndarray]]:
138
+ """Create the principal-point-centered view used by boundary benchmarks.
139
+
140
+ The image is first cropped symmetrically around the principal point, then
141
+ resized to cover the target and center-cropped. RGB uses Lanczos for
142
+ downsampling and bicubic for upsampling; depth and segmentation use nearest
143
+ interpolation. Pixel intrinsics are updated after every crop and resize.
144
+
145
+ Args:
146
+ image: RGB uint8 array ``[H,W,3]``.
147
+ depth: Depth array ``[H,W]`` with NaN invalid values.
148
+ intrinsics: Normalized camera matrix ``[3,3]``.
149
+ target_size: Output ``(height,width)``.
150
+ segmentation_mask: Optional integer labels ``[H,W]``.
151
+
152
+ Returns:
153
+ image: Transformed RGB ``[H_t,W_t,3]``.
154
+ depth: Transformed depth ``float32 [H_t,W_t]``.
155
+ intrinsics: Updated normalized matrix ``float32 [3,3]``.
156
+ segmentation_mask: Transformed labels ``[H_t,W_t]`` or ``None``.
157
+ """
158
+ tgt_height, tgt_width = target_size
159
+ raw_height, raw_width = image.shape[:2]
160
+ intrinsics_px = _intrinsics_normalized_to_pixel(intrinsics, raw_width, raw_height)
161
+
162
+ cx = float(intrinsics_px[0, 2])
163
+ cy = float(intrinsics_px[1, 2])
164
+ margin_x = max(1.0, min(cx, raw_width - cx))
165
+ margin_y = max(1.0, min(cy, raw_height - cy))
166
+ crop_left = max(0, int(round(cx - margin_x)))
167
+ crop_right = min(raw_width, int(round(cx + margin_x)))
168
+ crop_top = max(0, int(round(cy - margin_y)))
169
+ crop_bottom = min(raw_height, int(round(cy + margin_y)))
170
+
171
+ if crop_right - crop_left < 2 or crop_bottom - crop_top < 2:
172
+ crop_left, crop_top = 0, 0
173
+ crop_right, crop_bottom = raw_width, raw_height
174
+
175
+ image = image[crop_top:crop_bottom, crop_left:crop_right].copy()
176
+ depth = depth[crop_top:crop_bottom, crop_left:crop_right].copy()
177
+ if segmentation_mask is not None:
178
+ segmentation_mask = segmentation_mask[crop_top:crop_bottom, crop_left:crop_right].copy()
179
+ intrinsics_px[0, 2] -= float(crop_left)
180
+ intrinsics_px[1, 2] -= float(crop_top)
181
+
182
+ crop_height, crop_width = image.shape[:2]
183
+ scale = max(tgt_width / crop_width, tgt_height / crop_height)
184
+ resized_width = max(tgt_width, int(np.floor(crop_width * scale)))
185
+ resized_height = max(tgt_height, int(np.floor(crop_height * scale)))
186
+ if resized_width < tgt_width or resized_height < tgt_height:
187
+ resized_width = max(tgt_width, int(np.ceil(crop_width * scale)))
188
+ resized_height = max(tgt_height, int(np.ceil(crop_height * scale)))
189
+
190
+ image_resample = Image.Resampling.LANCZOS if scale < 1.0 else Image.Resampling.BICUBIC
191
+ resized_image = np.array(Image.fromarray(image).resize((resized_width, resized_height), image_resample))
192
+ resized_depth = _resize_depth_nearest_preserve_nan(depth, (resized_width, resized_height))
193
+ resized_segmentation_mask = None
194
+ if segmentation_mask is not None:
195
+ resized_segmentation_mask = cv2.resize(
196
+ segmentation_mask,
197
+ (resized_width, resized_height),
198
+ interpolation=cv2.INTER_NEAREST,
199
+ )
200
+ intrinsics_px[:2, :] *= float(scale)
201
+
202
+ x0 = int(round((resized_width - tgt_width) * 0.5))
203
+ y0 = int(round((resized_height - tgt_height) * 0.5))
204
+ x0 = min(max(x0, 0), resized_width - tgt_width)
205
+ y0 = min(max(y0, 0), resized_height - tgt_height)
206
+ x1, y1 = x0 + tgt_width, y0 + tgt_height
207
+
208
+ tgt_image = resized_image[y0:y1, x0:x1].copy()
209
+ tgt_depth = resized_depth[y0:y1, x0:x1].copy().astype(np.float32)
210
+ tgt_segmentation_mask = None
211
+ if resized_segmentation_mask is not None:
212
+ tgt_segmentation_mask = resized_segmentation_mask[y0:y1, x0:x1].copy()
213
+ intrinsics_px[0, 2] -= float(x0)
214
+ intrinsics_px[1, 2] -= float(y0)
215
+ tgt_intrinsics = _intrinsics_pixel_to_normalized(intrinsics_px, tgt_width, tgt_height)
216
+
217
+ return tgt_image, tgt_depth, tgt_intrinsics, tgt_segmentation_mask
218
+
219
+
220
+ class EvalDataLoaderPipeline:
221
+ """Asynchronously load and geometrically standardize one benchmark dataset.
222
+
223
+ The pipeline emits one sample at a time. It supports exact resolutions,
224
+ center-crop sizes, or aspect-preserving token budgets, and can optionally
225
+ include segmentation or normal annotations for local and boundary metrics.
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ path: str,
231
+ width: Optional[int] = None,
232
+ height: Optional[int] = None,
233
+ center_crop_size: Optional[int] = None,
234
+ split: int = '.index.txt',
235
+ drop_max_depth: float = 1000.,
236
+ num_load_workers: int = 4,
237
+ num_process_workers: int = 8,
238
+ include_segmentation: bool = False,
239
+ include_normal: bool = False,
240
+ depth_to_normal: bool = False,
241
+ max_segments: int = 100,
242
+ min_seg_area: int = 1000,
243
+ depth_unit: str = None,
244
+ min_depth: Optional[float] = None,
245
+ max_depth: Optional[float] = None,
246
+ has_sharp_boundary = False,
247
+ subset: int = None,
248
+ filenames: Optional[List[str]] = None,
249
+ num_tokens: Optional[int] = None,
250
+ patch_size: Optional[int] = None,
251
+ disable_augmentations: bool = True,
252
+ disable_perspective: bool = True,
253
+ resize_to_cover_center_crop: bool = False,
254
+ mda_boundary_transform: bool = False,
255
+ ):
256
+ """Configure benchmark indexing, transforms, and worker stages.
257
+
258
+ Args:
259
+ path: Processed benchmark root containing the split index.
260
+ width: Exact output width when token/crop modes are disabled.
261
+ height: Exact output height when token/crop modes are disabled.
262
+ center_crop_size: Optional square output side length.
263
+ split: Relative index filename under ``path``.
264
+ drop_max_depth: Relative dynamic-range multiplier used to suppress
265
+ extreme finite values after transformation.
266
+ num_load_workers: Number of parallel disk readers.
267
+ num_process_workers: Number of parallel geometry workers.
268
+ include_segmentation: Load ``segmentation.png`` and label metadata.
269
+ include_normal: Derive a normal map from depth.
270
+ depth_to_normal: Retained benchmark compatibility flag.
271
+ max_segments: Maximum segmentation labels retained by area.
272
+ min_seg_area: Minimum number of pixels for a retained label.
273
+ depth_unit: Optional scalar converting stored depth to metric units.
274
+ min_depth: Optional lower valid-depth bound in converted units.
275
+ max_depth: Optional upper valid-depth bound in converted units.
276
+ has_sharp_boundary: Mark samples for boundary metric computation.
277
+ subset: Optional number of leading index entries to evaluate.
278
+ filenames: Optional explicit relative paths replacing the split file.
279
+ num_tokens: Optional approximate image-token count.
280
+ patch_size: Patch divisibility used with token sampling.
281
+ disable_augmentations: Disable random flip/color transforms.
282
+ disable_perspective: Use identity perspective mapping.
283
+ resize_to_cover_center_crop: Resize to cover then center crop.
284
+ mda_boundary_transform: Use the principal-point-centered boundary
285
+ benchmark transformation.
286
+
287
+ Returns:
288
+ ``None``. Workers start when the context manager is entered.
289
+ """
290
+ if filenames is None:
291
+ filenames = Path(path).joinpath(split).read_text(encoding='utf-8').splitlines()
292
+ else:
293
+ filenames = list(filenames)
294
+ if subset is not None:
295
+ subset = int(subset)
296
+ if subset > 0:
297
+ filenames = filenames[:subset]
298
+ self.width = int(width) if width is not None else None
299
+ self.height = int(height) if height is not None else None
300
+ self.center_crop_size = int(center_crop_size) if center_crop_size is not None else None
301
+ self.drop_max_depth = drop_max_depth
302
+ self.path = Path(path)
303
+ self.filenames = filenames
304
+ self.include_segmentation = include_segmentation
305
+ self.include_normal = include_normal
306
+ self.max_segments = max_segments
307
+ self.min_seg_area = min_seg_area
308
+ self.depth_to_normal = depth_to_normal
309
+ self.depth_unit = depth_unit
310
+ self.min_depth = float(min_depth) if min_depth is not None else None
311
+ self.max_depth = float(max_depth) if max_depth is not None else None
312
+ self.has_sharp_boundary = has_sharp_boundary
313
+ self.num_tokens = int(num_tokens) if num_tokens is not None else None
314
+ self.patch_size = int(patch_size) if patch_size is not None else None
315
+ self.disable_augmentations = bool(disable_augmentations)
316
+ self.disable_perspective = bool(disable_perspective)
317
+ self.resize_to_cover_center_crop = bool(resize_to_cover_center_crop)
318
+ self.mda_boundary_transform = bool(mda_boundary_transform)
319
+
320
+ self.rng = np.random.default_rng(seed=0)
321
+
322
+ self.pipeline = pipeline.Sequential([
323
+ self._generator,
324
+ pipeline.Parallel([self._load_instance] * num_load_workers),
325
+ pipeline.Parallel([self._process_instance] * num_process_workers),
326
+ pipeline.Buffer(4)
327
+ ])
328
+
329
+ def __len__(self):
330
+ """Return the number of configured benchmark samples.
331
+
332
+ The value reflects explicit filenames and optional subset truncation.
333
+
334
+ Returns:
335
+ Integer length of the selected filename list.
336
+ """
337
+ return len(self.filenames)
338
+
339
+ def _resolve_target_size(self, raw_width: int, raw_height: int) -> Tuple[int, int]:
340
+ """Resolve output dimensions from exact, crop, or token settings.
341
+
342
+ Args:
343
+ raw_width: Source image width.
344
+ raw_height: Source image height.
345
+
346
+ Returns:
347
+ Integer tuple ``(target_width,target_height)``, optionally rounded to
348
+ patch multiples.
349
+ """
350
+ if self.num_tokens is not None:
351
+ if self.patch_size is None:
352
+ raise ValueError("patch_size must be set when using num_tokens.")
353
+ aspect_ratio = raw_width / raw_height
354
+ target_area = self.num_tokens * (self.patch_size ** 2)
355
+ tgt_width = int(round((target_area * aspect_ratio) ** 0.5))
356
+ tgt_height = int(round(tgt_width / aspect_ratio))
357
+ elif self.center_crop_size is not None:
358
+ tgt_width = self.center_crop_size
359
+ tgt_height = self.center_crop_size
360
+ else:
361
+ if self.width is None or self.height is None:
362
+ raise ValueError("width/height or center_crop_size must be set when num_tokens is not provided.")
363
+ tgt_width, tgt_height = self.width, self.height
364
+ if self.patch_size is not None:
365
+ tgt_width = max(self.patch_size, int(round(tgt_width / self.patch_size)) * self.patch_size)
366
+ tgt_height = max(self.patch_size, int(round(tgt_height / self.patch_size)) * self.patch_size)
367
+ return tgt_width, tgt_height
368
+
369
+ def _generator(self):
370
+ """Yield sequential sample indices to the asynchronous pipeline.
371
+
372
+ Disk loading and processing are parallelized after this ordered stage.
373
+
374
+ Yields:
375
+ Integer indices from zero through ``len(self)-1``.
376
+ """
377
+ for idx in range(len(self)):
378
+ yield idx
379
+
380
+ def _load_instance(self, idx):
381
+ """Read one indexed RGB-depth sample and optional segmentation.
382
+
383
+ Args:
384
+ idx: Integer index into ``self.filenames``.
385
+
386
+ Returns:
387
+ Dictionary containing RGB ``uint8 [H,W,3]``, depth ``float [H,W]``,
388
+ normalized intrinsics ``float32 [3,3]``, masks, and optional
389
+ segmentation; ``None`` for an out-of-range index.
390
+ """
391
+ if idx >= len(self.filenames):
392
+ return None
393
+
394
+ path = self.path.joinpath(self.filenames[idx])
395
+
396
+ instance = {
397
+ 'filename': self.filenames[idx],
398
+ }
399
+ instance['image'] = read_image(_resolve_image_path(path))
400
+
401
+ depth = read_depth(Path(path, 'depth.png')) # ignore depth unit from depth file, use config instead
402
+ instance.update({
403
+ 'depth': depth,
404
+ 'depth_mask': np.isfinite(depth) & (depth > 0),
405
+ 'depth_mask_inf': np.isinf(depth),
406
+ })
407
+
408
+ if self.include_segmentation:
409
+ segmentation_mask, segmentation_labels = read_segmentation(Path(path,'segmentation.png'))
410
+ instance.update({
411
+ 'segmentation_mask': segmentation_mask,
412
+ 'segmentation_labels': segmentation_labels,
413
+ })
414
+
415
+ meta = read_json(Path(path, 'meta.json'))
416
+ instance['intrinsics'] = np.array(meta['intrinsics'], dtype=np.float32)
417
+
418
+ return instance
419
+
420
+ def _process_instance(self, instance: dict):
421
+ """Transform one loaded instance and build its ground-truth point map.
422
+
423
+ Args:
424
+ instance: Raw dictionary returned by :meth:`_load_instance`, or
425
+ ``None`` propagated from a failed/out-of-range stage.
426
+
427
+ Returns:
428
+ Processed dictionary with image ``float32 [3,H,W]``, depth and masks
429
+ ``[H,W]``, normalized intrinsics ``[3,3]``, point map ``[H,W,3]``,
430
+ metadata flags, and optional normals/segmentation tensors. Returns
431
+ ``None`` when the input is ``None``.
432
+ """
433
+ if instance is None:
434
+ return None
435
+
436
+ image = instance['image']
437
+ depth = instance['depth']
438
+ intrinsics = instance['intrinsics']
439
+ segmentation_mask = instance.get('segmentation_mask', None)
440
+ segmentation_labels = instance.get('segmentation_labels', None)
441
+
442
+ raw_height, raw_width = image.shape[:2]
443
+ tgt_width, tgt_height = self._resolve_target_size(raw_width, raw_height)
444
+ tgt_aspect = tgt_width / tgt_height
445
+
446
+ raw_depth_mask = np.isfinite(depth) & (depth > 0)
447
+ raw_depth_ratio = raw_depth_mask.mean()
448
+ if raw_depth_ratio < 0.001:
449
+ depth = np.ones_like(depth, dtype=np.float32)
450
+ raw_depth_mask = np.isfinite(depth)
451
+ else:
452
+ depth = np.where(raw_depth_mask, depth, np.nan)
453
+
454
+ if self.include_normal:
455
+ raw_normal, raw_normal_mask = utils3d.np.depth_map_to_normal_map(
456
+ depth, intrinsics=intrinsics, mask=raw_depth_mask, edge_threshold=88
457
+ )
458
+ raw_normal = np.where(raw_normal_mask[..., None], raw_normal, np.nan)
459
+ else:
460
+ raw_normal = None
461
+
462
+ if self.mda_boundary_transform:
463
+ tgt_image, tgt_depth, tgt_intrinsics, tgt_segmentation_mask = _mda_boundary_view(
464
+ image,
465
+ depth,
466
+ intrinsics,
467
+ (tgt_height, tgt_width),
468
+ segmentation_mask=segmentation_mask,
469
+ )
470
+ if self.include_normal:
471
+ tgt_normal, tgt_normal_mask = utils3d.np.depth_map_to_normal_map(
472
+ tgt_depth, intrinsics=tgt_intrinsics, mask=np.isfinite(tgt_depth) & (tgt_depth > 0), edge_threshold=88
473
+ )
474
+ tgt_normal = np.where(tgt_normal_mask[..., None], tgt_normal, np.nan)
475
+ else:
476
+ tgt_normal = None
477
+ elif self.resize_to_cover_center_crop:
478
+ tgt_image, tgt_depth, tgt_intrinsics = resize_to_cover_center_crop_view(
479
+ image,
480
+ depth,
481
+ intrinsics,
482
+ (tgt_height, tgt_width),
483
+ image_interpolation='lanczos',
484
+ depth_interpolation='mixed',
485
+ )
486
+ if self.include_normal:
487
+ tgt_normal, tgt_normal_mask = utils3d.np.depth_map_to_normal_map(
488
+ tgt_depth, intrinsics=tgt_intrinsics, mask=np.isfinite(tgt_depth) & (tgt_depth > 0), edge_threshold=88
489
+ )
490
+ tgt_normal = np.where(tgt_normal_mask[..., None], tgt_normal, np.nan)
491
+ else:
492
+ tgt_normal = None
493
+ tgt_segmentation_mask = None
494
+ if segmentation_mask is not None:
495
+ tgt_segmentation_mask = _resize_to_cover_center_crop_mask(
496
+ segmentation_mask,
497
+ raw_width,
498
+ raw_height,
499
+ tgt_width,
500
+ tgt_height,
501
+ )
502
+ elif self.disable_perspective:
503
+ tgt_intrinsics = intrinsics.copy()
504
+ R = np.eye(3, dtype=np.float32)
505
+ transform = np.eye(3, dtype=np.float32)
506
+ else:
507
+ tgt_intrinsics, R = sample_perspective(
508
+ intrinsics,
509
+ tgt_aspect=tgt_aspect,
510
+ center_augmentation=0.0,
511
+ fov_range_absolute=(1, 179),
512
+ fov_range_relative=(1.0, 1.0),
513
+ rng=self.rng,
514
+ )
515
+ transform = tgt_intrinsics @ R @ np.linalg.inv(intrinsics)
516
+
517
+ if not self.resize_to_cover_center_crop and not self.mda_boundary_transform:
518
+ tgt_image = warp_perspective(image, transform, (tgt_height, tgt_width), interpolation='lanczos')
519
+
520
+ depth_edge_mask = utils3d.np.depth_map_edge(depth, mask=raw_depth_mask, kernel_size=5, ltol=0.01)
521
+ depth_bilinear_mask = raw_depth_mask & ~depth_edge_mask
522
+ warped_depth_bilinear_mask = warp_perspective(
523
+ depth_bilinear_mask.astype(np.float32),
524
+ transform,
525
+ (tgt_height, tgt_width),
526
+ interpolation='bilinear',
527
+ )
528
+ warped_depth_nearest = warp_perspective(
529
+ depth,
530
+ transform,
531
+ (tgt_height, tgt_width),
532
+ interpolation='nearest',
533
+ sparse_mask=~np.isnan(depth),
534
+ )
535
+ warped_depth_bilinear = 1 / warp_perspective(
536
+ 1 / depth,
537
+ transform,
538
+ (tgt_height, tgt_width),
539
+ interpolation='bilinear',
540
+ )
541
+ warped_depth = np.where(warped_depth_bilinear_mask == 1.0, warped_depth_bilinear, warped_depth_nearest)
542
+ tgt_uvhomo = np.concatenate(
543
+ [utils3d.np.uv_map((tgt_height, tgt_width)), np.ones((tgt_height, tgt_width, 1), dtype=np.float32)],
544
+ axis=-1,
545
+ )
546
+ tgt_depth = warped_depth / np.dot(tgt_uvhomo, np.linalg.inv(transform)[2, :])
547
+
548
+ if raw_normal is not None:
549
+ warped_normal = warp_perspective(raw_normal, transform, (tgt_height, tgt_width), interpolation='bilinear')
550
+ tgt_normal = warped_normal @ R.T
551
+ else:
552
+ tgt_normal = None
553
+
554
+ if segmentation_mask is not None:
555
+ tgt_segmentation_mask = warp_perspective(
556
+ segmentation_mask, transform, (tgt_height, tgt_width), interpolation='nearest'
557
+ )
558
+ else:
559
+ tgt_segmentation_mask = None
560
+
561
+ if not self.disable_augmentations:
562
+ if self.rng.choice([True, False]):
563
+ tgt_image = np.flip(tgt_image, axis=1).copy()
564
+ tgt_depth = np.flip(tgt_depth, axis=1).copy()
565
+ if tgt_normal is not None:
566
+ tgt_normal = np.flip(tgt_normal, axis=1).copy() * [-1, 1, 1]
567
+
568
+ if self.depth_unit is not None:
569
+ tgt_depth *= self.depth_unit
570
+ is_metric = True
571
+ else:
572
+ is_metric = False
573
+
574
+ depth_range_mask = np.isfinite(tgt_depth) & (tgt_depth > 0)
575
+ if self.min_depth is not None:
576
+ depth_range_mask &= tgt_depth >= self.min_depth
577
+ if self.max_depth is not None:
578
+ depth_range_mask &= tgt_depth <= self.max_depth
579
+ tgt_depth = np.where(depth_range_mask, tgt_depth, np.nan)
580
+
581
+ drop_max_depth = np.nanquantile(np.where(np.isfinite(tgt_depth), tgt_depth, np.nan), 0.01) * self.drop_max_depth
582
+ tgt_depth = np.where(np.isfinite(tgt_depth), np.clip(tgt_depth, 0, drop_max_depth), tgt_depth)
583
+
584
+ tgt_depth_mask_inf = np.isinf(tgt_depth)
585
+ tgt_depth_mask = np.isfinite(tgt_depth) & (tgt_depth > 0)
586
+ if not np.any(tgt_depth_mask):
587
+ tgt_depth_mask = np.ones_like(tgt_depth_mask)
588
+ tgt_depth = np.ones_like(tgt_depth)
589
+
590
+ tgt_points = utils3d.np.depth_map_to_point_map(tgt_depth, intrinsics=tgt_intrinsics)
591
+
592
+ if self.include_segmentation and tgt_segmentation_mask is not None:
593
+ for k in ['undefined', 'unannotated', 'background', 'sky']:
594
+ if k in segmentation_labels:
595
+ del segmentation_labels[k]
596
+ seg_id2count = dict(zip(*np.unique(tgt_segmentation_mask, return_counts=True)))
597
+ sorted_labels = sorted(segmentation_labels.keys(), key=lambda x: seg_id2count.get(segmentation_labels[x], 0), reverse=True)
598
+ segmentation_labels = {
599
+ k: segmentation_labels[k]
600
+ for k in sorted_labels[:self.max_segments]
601
+ if seg_id2count.get(segmentation_labels[k], 0) >= self.min_seg_area
602
+ }
603
+
604
+ instance.update({
605
+ 'image': torch.from_numpy(tgt_image.astype(np.float32) / 255.0).permute(2, 0, 1),
606
+ 'depth': torch.from_numpy(tgt_depth).float(),
607
+ 'depth_mask': torch.from_numpy(tgt_depth_mask).bool(),
608
+ 'depth_mask_inf': torch.from_numpy(tgt_depth_mask_inf).bool(),
609
+ 'intrinsics': torch.from_numpy(tgt_intrinsics).float(),
610
+ 'points': torch.from_numpy(tgt_points).float(),
611
+ 'segmentation_mask': torch.from_numpy(tgt_segmentation_mask).long() if tgt_segmentation_mask is not None else None,
612
+ 'segmentation_labels': segmentation_labels,
613
+ 'is_metric': is_metric,
614
+ 'has_sharp_boundary': self.has_sharp_boundary,
615
+ })
616
+ if tgt_normal is not None:
617
+ instance['normal'] = torch.from_numpy(tgt_normal).float()
618
+
619
+ instance = {k: v for k, v in instance.items() if v is not None}
620
+
621
+ return instance
622
+
623
+ def start(self):
624
+ """Start asynchronous loader workers.
625
+
626
+ Call this before :meth:`get` when not using the context manager.
627
+
628
+ Returns:
629
+ ``None``.
630
+ """
631
+ self.pipeline.start()
632
+
633
+ def stop(self):
634
+ """Stop asynchronous loader workers and release resources.
635
+
636
+ Any prefetched samples are discarded by the pipeline implementation.
637
+
638
+ Returns:
639
+ ``None``.
640
+ """
641
+ self.pipeline.stop()
642
+
643
+ def __enter__(self):
644
+ """Start the pipeline and return it as a context-manager value.
645
+
646
+ This is equivalent to an explicit :meth:`start` call.
647
+
648
+ Returns:
649
+ This :class:`EvalDataLoaderPipeline` instance.
650
+ """
651
+ self.start()
652
+ return self
653
+
654
+ def __exit__(self, exc_type, exc_value, traceback):
655
+ """Stop the pipeline when leaving its context.
656
+
657
+ Args:
658
+ exc_type: Exception class raised inside the context, if any.
659
+ exc_value: Exception instance raised inside the context, if any.
660
+ traceback: Associated traceback object, if any.
661
+
662
+ Returns:
663
+ ``None``; exceptions are not suppressed.
664
+ """
665
+ self.stop()
666
+
667
+ def get(self):
668
+ """Block until the next processed evaluation sample is available.
669
+
670
+ Worker-side exceptions are surfaced by the underlying pipeline call.
671
+
672
+ Returns:
673
+ Processed sample dictionary documented by :meth:`_process_instance`.
674
+ """
675
+ return self.pipeline.get()
pxdepth/evaluation/metrics.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Depth, point-cloud, local-structure, and boundary metrics.
2
+
3
+ Raw model outputs are aligned with the same low-resolution robust affine
4
+ procedures used by the MoGe evaluation protocol. Depth-space, log-depth-space,
5
+ and disparity-space predictions are converted to positive depth before common
6
+ metrics and point-cloud reconstruction are evaluated.
7
+ """
8
+
9
+ from typing import Dict, Literal, Tuple, Union
10
+ from numbers import Number
11
+
12
+ import cv2
13
+ import torch
14
+ import numpy as np
15
+ import utils3d
16
+
17
+ from ..utils.alignment import (
18
+ align_affine_lstsq,
19
+ align_depth_affine,
20
+ align_points_scale_xyz_shift,
21
+ )
22
+ from ..utils.tools import key_average
23
+
24
+
25
+ ALIGN_MIN_VALID_PIXELS = 16
26
+
27
+
28
+ def rel_depth(pred: torch.Tensor, gt: torch.Tensor, eps: float = 1e-6):
29
+ """Compute mean absolute relative depth error.
30
+
31
+ Args:
32
+ pred: Positive predicted depths at selected pixels, tensor ``[N]``.
33
+ gt: Positive ground-truth depths at the same pixels, tensor ``[N]``.
34
+ eps: Denominator stabilizer for near-zero GT values.
35
+
36
+ Returns:
37
+ Python float containing ``mean(abs(pred-gt)/(gt+eps))``.
38
+ """
39
+ rel = (torch.abs(pred - gt) / (gt + eps)).mean()
40
+ return rel.item()
41
+
42
+
43
+ def delta1_depth(pred: torch.Tensor, gt: torch.Tensor, eps: float = 1e-6):
44
+ """Compute the fraction of depth ratios below ``1.25``.
45
+
46
+ Args:
47
+ pred: Positive predicted depth tensor ``[N]``.
48
+ gt: Positive ground-truth depth tensor ``[N]``.
49
+ eps: Compatibility argument retained by the public metric API.
50
+
51
+ Returns:
52
+ Python float in ``[0,1]``; larger is better.
53
+ """
54
+ delta1 = (torch.maximum(gt / pred, pred / gt) < 1.25).float().mean()
55
+ return delta1.item()
56
+
57
+
58
+ def rel_point(pred: torch.Tensor, gt: torch.Tensor, eps: float = 1e-6):
59
+ """Compute 3D endpoint error relative to GT camera-space radius.
60
+
61
+ Args:
62
+ pred: Predicted camera-space points ``[N,3]``.
63
+ gt: Corresponding ground-truth points ``[N,3]``.
64
+ eps: Stabilizer added to each GT point radius.
65
+
66
+ Returns:
67
+ Python float mean relative Euclidean point error.
68
+ """
69
+ dist_gt = torch.norm(gt, dim=-1)
70
+ dist_err = torch.norm(pred - gt, dim=-1)
71
+ rel = (dist_err / (dist_gt + eps)).mean()
72
+ return rel.item()
73
+
74
+
75
+ def delta1_point(pred: torch.Tensor, gt: torch.Tensor, eps: float = 1e-6):
76
+ """Compute the MoGe point accuracy under a 25% radial tolerance.
77
+
78
+ Args:
79
+ pred: Predicted camera-space points ``[N,3]``.
80
+ gt: Corresponding ground-truth points ``[N,3]``.
81
+ eps: Compatibility argument retained by the metric API.
82
+
83
+ Returns:
84
+ Python float fraction whose 3D error is below 25% of the smaller
85
+ predicted/GT camera-space radius.
86
+ """
87
+ dist_pred = torch.norm(pred, dim=-1)
88
+ dist_gt = torch.norm(gt, dim=-1)
89
+ dist_err = torch.norm(pred - gt, dim=-1)
90
+
91
+ delta1 = (dist_err < 0.25 * torch.minimum(dist_gt, dist_pred)).float().mean()
92
+ return delta1.item()
93
+
94
+
95
+ def rel_point_local(pred: torch.Tensor, gt: torch.Tensor, diameter: torch.Tensor):
96
+ """Normalize local 3D endpoint error by an object's GT diameter.
97
+
98
+ Args:
99
+ pred: Locally aligned predicted points ``[N,3]``.
100
+ gt: Ground-truth points ``[N,3]`` for the same region.
101
+ diameter: Scalar tensor containing the largest GT bounding-box extent.
102
+
103
+ Returns:
104
+ Python float mean error divided by ``diameter``.
105
+ """
106
+ dist_err = torch.norm(pred - gt, dim=-1)
107
+ rel = (dist_err / diameter).mean()
108
+ return rel.item()
109
+
110
+
111
+ def delta1_point_local(pred: torch.Tensor, gt: torch.Tensor, diameter: torch.Tensor):
112
+ """Compute local point accuracy at one quarter of object diameter.
113
+
114
+ Args:
115
+ pred: Locally aligned predicted points ``[N,3]``.
116
+ gt: Ground-truth points ``[N,3]``.
117
+ diameter: Scalar GT region diameter.
118
+
119
+ Returns:
120
+ Python float fraction with Euclidean error below ``0.25*diameter``.
121
+ """
122
+ dist_err = torch.norm(pred - gt, dim=-1)
123
+ delta1 = (dist_err < 0.25 * diameter).float().mean()
124
+ return delta1.item()
125
+
126
+
127
+ def _nan_boundary_metrics() -> Dict[str, float]:
128
+ """Create a complete boundary metric record for invalid edge samples.
129
+
130
+ A stable key set keeps aggregation and JSON schemas consistent.
131
+
132
+ Returns:
133
+ Dictionary whose boundary accuracy and Chamfer distance are both NaN.
134
+ """
135
+ return {
136
+ 'acc': float('nan'),
137
+ 'cd': float('nan'),
138
+ }
139
+
140
+
141
+ def _mda_boundary_mask(gt_depth: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
142
+ """Extract the Canny GT depth edge mask used for boundary evaluation.
143
+
144
+ Args:
145
+ gt_depth: Ground-truth depth map ``[H,W]`` in meters.
146
+ mask: Boolean valid-depth mask ``[H,W]``.
147
+
148
+ Returns:
149
+ Boolean edge tensor ``[H,W]`` on ``gt_depth.device``. Depth is clipped
150
+ to ``[0.1,65]`` meters and invalid support is dilated by a 2x2 kernel
151
+ before Canny thresholds 100/200 are applied.
152
+ """
153
+ depth_np = gt_depth.detach().float().cpu().numpy()
154
+ valid_np = mask.detach().cpu().numpy().astype(bool)
155
+ depth_np = np.nan_to_num(depth_np, nan=0.0, posinf=65.0, neginf=0.0)
156
+
157
+ depth_gt_clamp = np.clip(depth_np, 0.1, 65.0)
158
+ min_val = depth_gt_clamp.min()
159
+ max_val = depth_gt_clamp.max()
160
+ norm_depth = (depth_gt_clamp - min_val) / (max_val - min_val + 1e-5)
161
+ norm_depth = np.clip(norm_depth, 0.0, 1.0)
162
+ depth_uint8 = (norm_depth * 255).astype(np.uint8)
163
+
164
+ edge = cv2.Canny(depth_uint8, 100, 200) > 0.5
165
+ kernel = np.ones((2, 2), np.uint8)
166
+ valid_np = cv2.dilate(1 - valid_np.astype(np.uint8), kernel, iterations=1) < 0.5
167
+ edge = edge & valid_np
168
+ return torch.from_numpy(edge).to(device=gt_depth.device, dtype=torch.bool)
169
+
170
+
171
+ def _as_o3d_point_cloud(points: np.ndarray):
172
+ """Convert an XYZ NumPy array to an Open3D point cloud.
173
+
174
+ Args:
175
+ points: Finite point array ``float [N,3]``.
176
+
177
+ Returns:
178
+ ``open3d.geometry.PointCloud`` containing the supplied XYZ positions.
179
+ """
180
+ import open3d as o3d
181
+
182
+ pcd = o3d.geometry.PointCloud()
183
+ pcd.points = o3d.utility.Vector3dVector(points)
184
+ return pcd
185
+
186
+
187
+ def boundary_edge_metrics(
188
+ pred_depth: torch.Tensor,
189
+ gt_depth: torch.Tensor,
190
+ mask: torch.Tensor,
191
+ intrinsics: torch.Tensor,
192
+ return_misc: bool = False,
193
+ edge_mode: Literal['mda'] = 'mda',
194
+ ) -> Union[Dict[str, float], Tuple[Dict[str, float], Dict[str, torch.Tensor]]]:
195
+ """Evaluate edge depth and 3D boundary point-cloud quality.
196
+
197
+ GT Canny edges select the boundary point clouds. The predicted cloud is
198
+ rigidly refined to GT with point-to-point ICP, then bidirectional
199
+ nearest-neighbor distances produce accuracy and symmetric Chamfer distance
200
+ in millimeters.
201
+
202
+ Args:
203
+ pred_depth: Globally aligned predicted depth ``[H,W]`` in meters.
204
+ gt_depth: Ground-truth depth ``[H,W]`` in meters.
205
+ mask: Boolean GT valid-depth mask ``[H,W]``.
206
+ intrinsics: Normalized camera matrix ``[3,3]``.
207
+ return_misc: Also return edge masks, aligned clouds, and ICP transform.
208
+ edge_mode: Boundary extraction protocol. The release supports ``'mda'``.
209
+
210
+ Returns:
211
+ metrics: Dictionary containing ``acc`` and ``cd`` in millimeters.
212
+ misc: Returned only when requested. Contains ``edge_mask`` ``[H,W]``,
213
+ edge point arrays ``[N,3]``, and ``icp_transform`` ``[4,4]``.
214
+ """
215
+ from scipy.spatial import cKDTree as KDTree
216
+ import open3d as o3d
217
+
218
+ metrics = _nan_boundary_metrics()
219
+ misc: Dict[str, torch.Tensor] = {}
220
+
221
+ def _finish():
222
+ """Package the current metric state according to ``return_misc``.
223
+
224
+ The closure captures the partially populated dictionaries by reference.
225
+
226
+ Returns:
227
+ Metrics dictionary alone, or ``(metrics,misc)`` when requested.
228
+ """
229
+ return (metrics, misc) if return_misc else metrics
230
+
231
+ valid = mask & torch.isfinite(gt_depth) & (gt_depth > 0)
232
+ pred_valid = torch.isfinite(pred_depth) & (pred_depth > 0)
233
+ if edge_mode == 'mda':
234
+ edge = _mda_boundary_mask(gt_depth, valid)
235
+ else:
236
+ raise ValueError(f"Unknown boundary edge mode: {edge_mode}")
237
+ gt_edge_mask = edge & valid
238
+ pred_edge_mask = gt_edge_mask & pred_valid
239
+ if return_misc:
240
+ misc['edge_mask'] = edge
241
+ if gt_edge_mask.sum().item() < 10 or pred_edge_mask.sum().item() < 10:
242
+ return _finish()
243
+
244
+ pred_depth_clean = pred_depth.float().clone()
245
+ pred_depth_clean[~pred_valid] = 1.0
246
+ gt_depth_clean = gt_depth.float().clone()
247
+ gt_depth_clean[~valid] = 1.0
248
+
249
+ pred_points_full = utils3d.pt.depth_map_to_point_map(pred_depth_clean, intrinsics=intrinsics)
250
+ gt_points_full = utils3d.pt.depth_map_to_point_map(gt_depth_clean, intrinsics=intrinsics)
251
+
252
+ pred_points = pred_points_full[pred_edge_mask].detach().float().cpu().numpy()
253
+ gt_points = gt_points_full[gt_edge_mask].detach().float().cpu().numpy()
254
+ pred_points = pred_points[np.isfinite(pred_points).all(axis=1)]
255
+ gt_points = gt_points[np.isfinite(gt_points).all(axis=1)]
256
+ if pred_points.shape[0] < 10 or gt_points.shape[0] < 10:
257
+ return _finish()
258
+
259
+ pcd = _as_o3d_point_cloud(pred_points)
260
+ pcd_gt = _as_o3d_point_cloud(gt_points)
261
+ reg_p2p = o3d.pipelines.registration.registration_icp(
262
+ pcd,
263
+ pcd_gt,
264
+ 0.1,
265
+ np.eye(4),
266
+ o3d.pipelines.registration.TransformationEstimationPointToPoint(),
267
+ )
268
+ transform = reg_p2p.transformation
269
+ pcd.transform(transform)
270
+ pred_points_aligned = np.asarray(pcd.points)
271
+
272
+ gt_tree = KDTree(gt_points)
273
+ acc_distances, _ = gt_tree.query(pred_points_aligned, workers=-1)
274
+ pred_tree = KDTree(pred_points_aligned)
275
+ comp_distances, _ = pred_tree.query(gt_points, workers=-1)
276
+
277
+ acc = float(np.mean(acc_distances))
278
+ comp = float(np.mean(comp_distances))
279
+ cd = (acc + comp) / 2.0
280
+ if np.isfinite(acc) and np.isfinite(cd):
281
+ metrics['acc'] = acc * 1000.0
282
+ metrics['cd'] = cd * 1000.0
283
+
284
+ if return_misc:
285
+ misc['pred_edge_points'] = torch.from_numpy(pred_points_aligned).to(device=gt_depth.device, dtype=torch.float32)
286
+ misc['gt_edge_points'] = torch.from_numpy(gt_points).to(device=gt_depth.device, dtype=torch.float32)
287
+ misc['icp_transform'] = torch.from_numpy(transform.copy()).to(device=gt_depth.device, dtype=torch.float32)
288
+ return _finish()
289
+
290
+
291
+ def _moge_lowres_affine(
292
+ pred: torch.Tensor,
293
+ target: torch.Tensor,
294
+ mask: torch.Tensor,
295
+ weight_depth: torch.Tensor,
296
+ ) -> Tuple[torch.Tensor, bool]:
297
+ """Fit MoGe-style weighted affine alignment on a 64x64 valid subset.
298
+
299
+ Args:
300
+ pred: Raw prediction map ``[H,W]`` in depth or log-depth space.
301
+ target: GT target map ``[H,W]`` in the same affine space.
302
+ mask: Boolean candidate-fit mask ``[H,W]``.
303
+ weight_depth: Positive GT depth ``[H,W]`` used for inverse-depth weights.
304
+
305
+ Returns:
306
+ aligned: Full-resolution floating prediction ``[H,W]``.
307
+ success: Boolean indicating whether finite affine parameters were found.
308
+ """
309
+ valid = (
310
+ mask
311
+ & torch.isfinite(pred)
312
+ & torch.isfinite(target)
313
+ & torch.isfinite(weight_depth)
314
+ & (weight_depth > 0)
315
+ )
316
+ if valid.sum().item() < ALIGN_MIN_VALID_PIXELS:
317
+ return pred.float(), False
318
+
319
+ pred_clean = torch.where(valid, pred.float(), torch.zeros_like(pred, dtype=torch.float32))
320
+ target_clean = torch.where(valid, target.float(), torch.zeros_like(target, dtype=torch.float32))
321
+ weight_depth_clean = torch.where(valid, weight_depth.float(), torch.ones_like(weight_depth, dtype=torch.float32))
322
+ try:
323
+ pred_lr, target_lr, weight_depth_lr, mask_lr = utils3d.pt.masked_nearest_resize(
324
+ pred_clean,
325
+ target_clean,
326
+ weight_depth_clean,
327
+ mask=valid,
328
+ size=(64, 64),
329
+ )
330
+ weight = mask_lr.flatten(-2, -1).float() / weight_depth_lr.flatten(-2, -1).clamp_min(1e-3)
331
+ if (weight > 0).sum().item() < ALIGN_MIN_VALID_PIXELS:
332
+ return pred.float(), False
333
+ scale, shift = align_depth_affine(
334
+ pred_lr.flatten(-2, -1),
335
+ target_lr.flatten(-2, -1),
336
+ weight,
337
+ )
338
+ scale = scale.squeeze()
339
+ shift = shift.squeeze()
340
+ ok = torch.isfinite(scale) & torch.isfinite(shift)
341
+ if not bool(ok.item() if ok.ndim == 0 else ok.all().item()):
342
+ return pred.float(), False
343
+ return pred.float() * scale + shift, True
344
+ except Exception:
345
+ return pred.float(), False
346
+
347
+
348
+ def _moge_disparity_affine(
349
+ pred_disparity: torch.Tensor,
350
+ gt_disparity: torch.Tensor,
351
+ mask: torch.Tensor,
352
+ ) -> Tuple[torch.Tensor, bool]:
353
+ """Fit least-squares scale and shift in disparity space.
354
+
355
+ Args:
356
+ pred_disparity: Raw predicted disparity ``[H,W]``.
357
+ gt_disparity: Ground-truth reciprocal depth ``[H,W]``.
358
+ mask: Boolean fit mask ``[H,W]``.
359
+
360
+ Returns:
361
+ aligned: Full-resolution disparity ``[H,W]``.
362
+ success: Boolean indicating a finite affine fit.
363
+ """
364
+ valid = mask & torch.isfinite(pred_disparity) & torch.isfinite(gt_disparity) & (gt_disparity > 0)
365
+ if valid.sum().item() < ALIGN_MIN_VALID_PIXELS:
366
+ return pred_disparity.float(), False
367
+ try:
368
+ scale, shift = align_affine_lstsq(pred_disparity[valid].float(), gt_disparity[valid].float())
369
+ ok = torch.isfinite(scale) & torch.isfinite(shift)
370
+ if not bool(ok.item() if ok.ndim == 0 else ok.all().item()):
371
+ return pred_disparity.float(), False
372
+ return pred_disparity.float() * scale + shift, True
373
+ except Exception:
374
+ return pred_disparity.float(), False
375
+
376
+
377
+ def _moge_points_affine(
378
+ pred_points: torch.Tensor,
379
+ gt_points: torch.Tensor,
380
+ mask: torch.Tensor,
381
+ ) -> Tuple[torch.Tensor, bool]:
382
+ """Fit one global scale and XYZ translation to a predicted point map.
383
+
384
+ Args:
385
+ pred_points: Predicted camera-space point map ``[H,W,3]``.
386
+ gt_points: Ground-truth point map ``[H,W,3]``.
387
+ mask: Boolean valid correspondence mask ``[H,W]``.
388
+
389
+ Returns:
390
+ aligned: Full-resolution point map ``[H,W,3]``.
391
+ success: Boolean indicating whether robust alignment succeeded.
392
+ """
393
+ valid = mask & torch.isfinite(pred_points).all(dim=-1) & torch.isfinite(gt_points).all(dim=-1)
394
+ if valid.sum().item() < ALIGN_MIN_VALID_PIXELS:
395
+ return pred_points.float(), False
396
+
397
+ pred_clean = torch.where(valid[..., None], pred_points.float(), torch.zeros_like(pred_points, dtype=torch.float32))
398
+ gt_clean = torch.where(valid[..., None], gt_points.float(), torch.zeros_like(gt_points, dtype=torch.float32))
399
+ try:
400
+ pred_lr, gt_lr, mask_lr = utils3d.pt.masked_nearest_resize(
401
+ pred_clean,
402
+ gt_clean,
403
+ mask=valid,
404
+ size=(64, 64),
405
+ )
406
+ weight = mask_lr.flatten(-2, -1).float() / gt_lr.norm(dim=-1).flatten(-2, -1).clamp_min(1e-6)
407
+ if (weight > 0).sum().item() < ALIGN_MIN_VALID_PIXELS:
408
+ return pred_points.float(), False
409
+ scale, shift = align_points_scale_xyz_shift(
410
+ pred_lr.flatten(-3, -2),
411
+ gt_lr.flatten(-3, -2),
412
+ weight,
413
+ )
414
+ scale = scale.squeeze()
415
+ shift = shift.squeeze()
416
+ ok = torch.isfinite(scale) & torch.isfinite(shift).all()
417
+ if not bool(ok.item() if ok.ndim == 0 else ok.all().item()):
418
+ return pred_points.float(), False
419
+ return pred_points.float() * scale + shift, True
420
+ except Exception:
421
+ return pred_points.float(), False
422
+
423
+
424
+ def compute_metrics(
425
+ pred: Dict[str, torch.Tensor],
426
+ gt: Dict[str, torch.Tensor],
427
+ vis: bool = False,
428
+ compute_boundary: bool = True,
429
+ ) -> Tuple[Dict[str, Dict[str, Number]], Dict[str, torch.Tensor]]:
430
+ """Align one prediction and compute all applicable benchmark metrics.
431
+
432
+ Args:
433
+ pred: Prediction dictionary. It may contain raw ``depth_affine_invariant``
434
+ ``[H,W]`` plus ``depth_affine_space`` (``'depth'``/``'log'``), raw
435
+ ``disparity_affine_invariant`` ``[H,W]``, optional point map
436
+ ``points_affine_invariant`` ``[H,W,3]``, and predicted ``mask``
437
+ ``[H,W]``.
438
+ gt: Ground-truth sample containing depth/mask ``[H,W]``, point map
439
+ ``[H,W,3]``, normalized intrinsics ``[3,3]``, metric/boundary flags,
440
+ and optional segmentation annotations.
441
+ vis: Include aligned depth/points and boundary visualization tensors in the
442
+ auxiliary output.
443
+ compute_boundary: Evaluate boundary metrics when the dataset is marked
444
+ ``has_sharp_boundary``.
445
+
446
+ Returns:
447
+ metrics: Nested Python-number dictionary for depth, points, local points,
448
+ and optional boundary quality.
449
+ misc: Tensor dictionary containing aligned maps and optional boundary
450
+ visualization data when ``vis=True``.
451
+ """
452
+ metrics = {}
453
+ misc = {}
454
+
455
+ mask = gt['depth_mask']
456
+ gt_depth = gt['depth']
457
+ gt_points = gt['points']
458
+
459
+ valid_depth = mask & torch.isfinite(gt_depth) & (gt_depth > 0)
460
+ pred_depth_aligned = None
461
+ pred_points_aligned = None
462
+
463
+ if 'depth_affine_invariant' in pred:
464
+ raw_depth = pred['depth_affine_invariant'].float()
465
+ fit_mask = valid_depth & torch.isfinite(raw_depth)
466
+ affine_space = str(pred.get('depth_affine_space', 'depth')).lower()
467
+ if affine_space == 'log':
468
+ target_log = torch.log1p(gt_depth)
469
+ aligned_log, ok = _moge_lowres_affine(raw_depth, target_log, fit_mask, gt_depth)
470
+ pred_depth_aligned = torch.expm1(aligned_log if ok else raw_depth)
471
+ elif affine_space == 'depth':
472
+ aligned_depth, ok = _moge_lowres_affine(raw_depth, gt_depth, fit_mask, gt_depth)
473
+ pred_depth_aligned = aligned_depth if ok else raw_depth
474
+ else:
475
+ raise ValueError(f"Unsupported depth_affine_space={affine_space!r}")
476
+
477
+ metric_mask = fit_mask
478
+ if metric_mask.any():
479
+ metrics['depth_affine_invariant'] = {
480
+ 'rel': rel_depth(pred_depth_aligned[metric_mask], gt_depth[metric_mask]),
481
+ 'delta1': delta1_depth(pred_depth_aligned[metric_mask], gt_depth[metric_mask]),
482
+ }
483
+
484
+ elif 'disparity_affine_invariant' in pred:
485
+ raw_disparity = pred['disparity_affine_invariant'].float()
486
+ fit_mask = valid_depth & torch.isfinite(raw_disparity)
487
+ gt_disparity = torch.where(valid_depth, gt_depth.reciprocal(), torch.zeros_like(gt_depth))
488
+ aligned_disparity, ok = _moge_disparity_affine(raw_disparity, gt_disparity, fit_mask)
489
+ aligned_disparity = aligned_disparity if ok else raw_disparity
490
+ if fit_mask.any():
491
+ max_depth = gt_depth[fit_mask].max()
492
+ pred_depth_metric = aligned_disparity.clamp_min(max_depth.reciprocal()).reciprocal()
493
+ else:
494
+ pred_depth_metric = aligned_disparity.clamp_min(1e-6).reciprocal()
495
+ pred_depth_aligned = pred_depth_metric
496
+ metric_mask = fit_mask & torch.isfinite(pred_depth_metric)
497
+ if metric_mask.any():
498
+ metrics['depth_affine_invariant'] = {
499
+ 'rel': rel_depth(pred_depth_metric[metric_mask], gt_depth[metric_mask]),
500
+ 'delta1': delta1_depth(pred_depth_metric[metric_mask], gt_depth[metric_mask]),
501
+ }
502
+
503
+ pred_points_affine_invariant = pred.get('points_affine_invariant', None)
504
+ if pred_points_affine_invariant is None and pred_depth_aligned is not None:
505
+ point_intrinsics = gt['intrinsics'].to(
506
+ device=pred_depth_aligned.device,
507
+ dtype=pred_depth_aligned.dtype,
508
+ )
509
+ pred_points_affine_invariant = utils3d.pt.depth_map_to_point_map(
510
+ pred_depth_aligned,
511
+ intrinsics=point_intrinsics,
512
+ )
513
+
514
+ if pred_points_affine_invariant is not None:
515
+ point_mask = (
516
+ valid_depth
517
+ & torch.isfinite(pred_points_affine_invariant).all(dim=-1)
518
+ & torch.isfinite(gt_points).all(dim=-1)
519
+ )
520
+ if point_mask.any():
521
+ aligned_points, ok = _moge_points_affine(pred_points_affine_invariant, gt_points, point_mask)
522
+ pred_points_aligned = aligned_points if ok else pred_points_affine_invariant
523
+ metrics['points_affine_invariant'] = {
524
+ 'rel': rel_point(pred_points_aligned[point_mask], gt_points[point_mask]),
525
+ 'delta1': delta1_point(pred_points_aligned[point_mask], gt_points[point_mask]),
526
+ }
527
+
528
+ # Local points
529
+ if 'segmentation_mask' in gt and 'points' in gt and pred_points_affine_invariant is not None:
530
+ pred_points = pred_points_affine_invariant
531
+ gt_points = gt['points']
532
+ segmentation_mask = gt['segmentation_mask']
533
+ segmentation_labels = gt['segmentation_labels']
534
+ local_points_metrics = []
535
+ for _, seg_id in segmentation_labels.items():
536
+ valid_mask = (
537
+ (segmentation_mask == seg_id)
538
+ & valid_depth
539
+ & torch.isfinite(pred_points).all(dim=-1)
540
+ & torch.isfinite(gt_points).all(dim=-1)
541
+ )
542
+ if valid_mask.sum().item() < 10:
543
+ continue
544
+
545
+ try:
546
+ pred_lr, gt_lr, mask_lr = utils3d.pt.masked_nearest_resize(
547
+ torch.where(valid_mask[..., None], pred_points.float(), torch.zeros_like(pred_points, dtype=torch.float32)),
548
+ torch.where(valid_mask[..., None], gt_points.float(), torch.zeros_like(gt_points, dtype=torch.float32)),
549
+ mask=valid_mask,
550
+ size=(64, 64),
551
+ )
552
+ pred_points_masked = pred_lr[mask_lr]
553
+ gt_points_masked = gt_lr[mask_lr]
554
+ if pred_points_masked.shape[0] < 10:
555
+ continue
556
+ diameter = (gt_points_masked.max(dim=0).values - gt_points_masked.min(dim=0).values).max()
557
+ scale, shift = align_points_scale_xyz_shift(
558
+ pred_points_masked.unsqueeze(0),
559
+ gt_points_masked.unsqueeze(0),
560
+ diameter.clamp_min(1e-6).reciprocal().expand(1, gt_points_masked.shape[0]),
561
+ )
562
+ pred_points_masked = pred_points[valid_mask] * scale.squeeze() + shift.squeeze()
563
+ gt_points_masked = gt_points[valid_mask]
564
+ except Exception:
565
+ pred_points_masked = pred_points[valid_mask]
566
+ gt_points_masked = gt_points[valid_mask]
567
+ diameter = (gt_points_masked.max(dim=0).values - gt_points_masked.min(dim=0).values).max()
568
+
569
+ local_points_metrics.append({
570
+ 'rel': rel_point_local(pred_points_masked, gt_points_masked, diameter),
571
+ 'delta1': delta1_point_local(pred_points_masked, gt_points_masked, diameter),
572
+ })
573
+
574
+ metrics['local_points'] = key_average(local_points_metrics)
575
+
576
+ # Boundary Acc/CD with the MDA/Canny edge.
577
+ boundary_depth = pred_depth_aligned
578
+ if compute_boundary and boundary_depth is not None and gt['has_sharp_boundary']:
579
+ if vis:
580
+ boundary_metrics, boundary_misc = boundary_edge_metrics(
581
+ boundary_depth,
582
+ gt_depth,
583
+ mask,
584
+ gt['intrinsics'],
585
+ return_misc=True,
586
+ edge_mode='mda',
587
+ )
588
+ else:
589
+ boundary_metrics = boundary_edge_metrics(
590
+ boundary_depth,
591
+ gt_depth,
592
+ mask,
593
+ gt['intrinsics'],
594
+ edge_mode='mda',
595
+ )
596
+ boundary_misc = {}
597
+ metrics['boundary'] = boundary_metrics
598
+ if vis:
599
+ if 'edge_mask' in boundary_misc:
600
+ misc['boundary_edge_mask'] = boundary_misc['edge_mask']
601
+ if 'pred_edge_points' in boundary_misc:
602
+ misc['boundary_pred_edge_points'] = boundary_misc['pred_edge_points']
603
+ if 'gt_edge_points' in boundary_misc:
604
+ misc['boundary_gt_edge_points'] = boundary_misc['gt_edge_points']
605
+ if 'icp_transform' in boundary_misc:
606
+ misc['boundary_icp_transform'] = boundary_misc['icp_transform']
607
+
608
+ if vis:
609
+ if pred_points_aligned is not None:
610
+ misc['pred_points'] = pred_points_aligned
611
+ elif pred_depth_aligned is not None:
612
+ misc['pred_points'] = utils3d.pt.depth_map_to_point_map(pred_depth_aligned, intrinsics=gt['intrinsics'])
613
+ if pred_depth_aligned is not None:
614
+ misc['pred_depth'] = pred_depth_aligned
615
+
616
+ return metrics, misc
pxdepth/inference/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public preprocessing and raw-forward helpers for PXDepth inference.
2
+
3
+ The package centralizes fixed-size and equal-area image resizing, patch-grid
4
+ alignment, output restoration, and timed model execution. These helpers return
5
+ raw normalized predictions and avoid embedding benchmark-specific alignment in
6
+ the model forward path.
7
+ """
8
+
9
+ from .runner import predict_raw
10
+ from .resize import area_size, area_size_from_area, parse_size, patch_size, resize_image, resize_map
11
+
12
+ __all__ = [
13
+ "predict_raw",
14
+ "area_size",
15
+ "area_size_from_area",
16
+ "parse_size",
17
+ "patch_size",
18
+ "resize_image",
19
+ "resize_map",
20
+ ]
pxdepth/inference/resize.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image and prediction resizing shared by evaluation and inference.
2
+
3
+ The functions parse user-facing sizes, derive patch-compatible equal-area
4
+ shapes, resize RGB tensors for a model, and restore depth or mask maps to the
5
+ source resolution. Their return values retain the original image dimensions so
6
+ camera-normalized geometry remains consistent after restoration.
7
+ """
8
+
9
+ from typing import Optional, Tuple
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+
15
+ def parse_size(value: Optional[str]) -> Optional[Tuple[int, int]]:
16
+ """Parse a CLI image-size string in ``WIDTHxHEIGHT`` notation.
17
+
18
+ Args:
19
+ value: Size string, or ``None``/empty text when no target is requested.
20
+
21
+ Returns:
22
+ Positive integer tuple ``(width, height)``, or ``None``.
23
+ """
24
+ if value is None or not str(value).strip():
25
+ return None
26
+ parts = str(value).lower().replace(",", "x").split("x")
27
+ if len(parts) != 2:
28
+ raise ValueError("Image size must be WIDTHxHEIGHT, for example 1022x770.")
29
+ width, height = map(int, parts)
30
+ if width <= 0 or height <= 0:
31
+ raise ValueError("Image width and height must be positive.")
32
+ return width, height
33
+
34
+
35
+ def area_size(
36
+ height: int,
37
+ width: int,
38
+ target_width: int,
39
+ target_height: int,
40
+ patch_size: int,
41
+ ) -> Tuple[int, int]:
42
+ """Preserve aspect ratio while matching a reference width-height area.
43
+
44
+ Args:
45
+ height: Original image height ``H``.
46
+ width: Original image width ``W``.
47
+ target_width: Width defining the desired reference area.
48
+ target_height: Height defining the desired reference area.
49
+ patch_size: Required divisibility of both output dimensions.
50
+
51
+ Returns:
52
+ Integer ``(new_height, new_width)`` with approximately
53
+ ``target_width*target_height`` pixels and the original aspect ratio.
54
+ """
55
+ area = int(target_width) * int(target_height)
56
+ return area_size_from_area(height, width, area, patch_size)
57
+
58
+
59
+ def area_size_from_area(
60
+ height: int,
61
+ width: int,
62
+ target_area: int,
63
+ patch_size: int,
64
+ ) -> Tuple[int, int]:
65
+ """Preserve aspect ratio while matching an explicit target pixel area.
66
+
67
+ Args:
68
+ height: Original image height ``H``.
69
+ width: Original image width ``W``.
70
+ target_area: Desired number of input pixels before patch rounding.
71
+ patch_size: Required divisibility of both output dimensions.
72
+
73
+ Returns:
74
+ Integer ``(new_height, new_width)`` rounded to patch multiples.
75
+ """
76
+ area = int(target_area)
77
+ if height <= 0 or width <= 0 or area <= 0:
78
+ raise ValueError("Image dimensions and target area must be positive.")
79
+ aspect = width / height
80
+ new_width = int(round((area * aspect) ** 0.5))
81
+ new_height = int(round(new_width / aspect))
82
+ new_width = max(patch_size, int(round(new_width / patch_size)) * patch_size)
83
+ new_height = max(patch_size, int(round(new_height / patch_size)) * patch_size)
84
+ return new_height, new_width
85
+
86
+
87
+ def patch_size(height: int, width: int, patch: int) -> Tuple[int, int]:
88
+ """Round spatial dimensions down to valid patch multiples.
89
+
90
+ Args:
91
+ height: Original image height.
92
+ width: Original image width.
93
+ patch: Positive encoder patch side length.
94
+
95
+ Returns:
96
+ Integer ``(new_height, new_width)``. Already divisible dimensions are
97
+ unchanged; smaller results are clamped to one patch.
98
+ """
99
+ new_height = height if height % patch == 0 else max(patch, height // patch * patch)
100
+ new_width = width if width % patch == 0 else max(patch, width // patch * patch)
101
+ return new_height, new_width
102
+
103
+
104
+ def resize_image(
105
+ image: torch.Tensor,
106
+ target: Optional[Tuple[int, int]],
107
+ resize_by_area: bool,
108
+ patch: int,
109
+ ) -> Tuple[torch.Tensor, Tuple[int, int]]:
110
+ """Resize a CHW/BCHW image to the model input resolution.
111
+
112
+ Args:
113
+ image: RGB tensor ``[3,H,W]`` or ``[B,3,H,W]``.
114
+ target: Optional ``(width,height)`` reference size.
115
+ resize_by_area: Preserve aspect ratio and use only ``target`` area when
116
+ ``True``; otherwise force the exact target dimensions.
117
+ patch: Required encoder patch divisibility.
118
+
119
+ Returns:
120
+ resized: Bilinearly resized tensor preserving the input batch layout.
121
+ original_size: Original integer tuple ``(H,W)``.
122
+ """
123
+ original = tuple(image.shape[-2:])
124
+ if target is None:
125
+ height, width = patch_size(*original, patch)
126
+ elif resize_by_area:
127
+ height, width = area_size(*original, target[0], target[1], patch)
128
+ else:
129
+ width, height = target
130
+ if height % patch or width % patch:
131
+ raise ValueError(f"Fixed input size {width}x{height} must be divisible by patch size {patch}.")
132
+ if (height, width) == original:
133
+ return image, original
134
+ batched = image.ndim == 4
135
+ source = image if batched else image.unsqueeze(0)
136
+ resized = F.interpolate(source, (height, width), mode="bilinear", align_corners=False)
137
+ return (resized if batched else resized[0]), original
138
+
139
+
140
+ def resize_map(value: torch.Tensor, size: Tuple[int, int], is_mask: bool = False) -> torch.Tensor:
141
+ """Nearest-resize a depth/probability map while preserving batch layout.
142
+
143
+ Args:
144
+ value: Map tensor ``[H,W]`` or batch ``[B,H,W]``.
145
+ size: Target ``(height,width)``.
146
+ is_mask: Threshold resized values at ``0.5`` and return boolean output.
147
+
148
+ Returns:
149
+ Tensor ``[H_t,W_t]`` or ``[B,H_t,W_t]``. Non-mask output is floating;
150
+ mask output is boolean.
151
+ """
152
+ if tuple(value.shape[-2:]) == tuple(size):
153
+ return value
154
+ batched = value.ndim == 3
155
+ source = value.float().unsqueeze(1) if batched else value.float()[None, None]
156
+ output = F.interpolate(source, size=size, mode="nearest")
157
+ output = output[:, 0] if batched else output[0, 0]
158
+ return output > 0.5 if is_mask else output
pxdepth/inference/runner.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Timed raw PXDepth forward path used by evaluation tools.
2
+
3
+ This module applies the selected fixed-size or equal-area preprocessing,
4
+ measures only model-forward latency, and restores predictions to input
5
+ resolution. It intentionally returns only genuine network outputs rather than
6
+ performing metric alignment or estimating camera intrinsics.
7
+ """
8
+
9
+ import time
10
+ from typing import Dict, Optional, Tuple
11
+
12
+ import torch
13
+
14
+ from ..model import PXDepth
15
+ from .resize import resize_image, resize_map
16
+
17
+
18
+ def synchronize(device: torch.device) -> None:
19
+ """Synchronize pending CUDA work before or after timing model forward.
20
+
21
+ Args:
22
+ device: Model device. CPU and other devices require no action.
23
+
24
+ Returns:
25
+ ``None``.
26
+ """
27
+ if device.type == "cuda":
28
+ torch.cuda.synchronize(device)
29
+
30
+
31
+ @torch.inference_mode()
32
+ def predict_raw(
33
+ model: PXDepth,
34
+ image: torch.Tensor,
35
+ input_size: Optional[Tuple[int, int]] = (1022, 770),
36
+ resize_by_area: bool = True,
37
+ use_fp16: bool = False,
38
+ use_fp32: bool = False,
39
+ ) -> Dict[str, torch.Tensor]:
40
+ """Run raw model forward and return outputs at the original image size.
41
+
42
+ Only ``model.forward`` is included in ``inference_time``. Resizing,
43
+ synchronization overhead, and output packaging are excluded. No GT depth
44
+ alignment or camera-intrinsics prediction is performed.
45
+
46
+ Args:
47
+ model: Evaluation-mode :class:`PXDepth` model.
48
+ image: RGB tensor ``[3,H,W]`` or ``[B,3,H,W]`` in ``[0,1]``.
49
+ input_size: Exact/reference tuple ``(width,height)``. Defaults to
50
+ ``(1022,770)``.
51
+ resize_by_area: Preserve aspect ratio at ``input_size`` area. Enabled
52
+ by default.
53
+ use_fp16: Use FP16 for attention-heavy model regions.
54
+ use_fp32: Force full-precision model execution.
55
+
56
+ Returns:
57
+ Dictionary with raw normalized log-depth ``depth_affine_invariant``
58
+ ``[B,H,W]``, ``depth_affine_space='log'``, boolean ``mask`` ``[B,H,W]``,
59
+ and scalar forward time. The leading batch dimension is removed for
60
+ unbatched input.
61
+ """
62
+ image, original_size = resize_image(image, input_size, resize_by_area, model.patch_size)
63
+ batched = image.ndim == 4
64
+ model_input = image if batched else image.unsqueeze(0)
65
+ model_input = model_input.to(device=model.device, dtype=torch.float32)
66
+
67
+ synchronize(model.device)
68
+ start = time.perf_counter()
69
+ output = model.forward(model_input, use_fp16=use_fp16, use_fp32=use_fp32)
70
+ synchronize(model.device)
71
+ elapsed = time.perf_counter() - start
72
+
73
+ depth = resize_map(output["depth"], original_size)
74
+ mask = resize_map(output["mask"], original_size, is_mask=True)
75
+
76
+ pred = {
77
+ "depth_affine_invariant": depth,
78
+ "depth_affine_space": "log",
79
+ "mask": mask,
80
+ "inference_time": elapsed,
81
+ }
82
+ if not batched:
83
+ pred = {key: value[0] if isinstance(value, torch.Tensor) and value.ndim > 0 else value for key, value in pred.items()}
84
+ return pred
pxdepth/model/CM_PiT.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Context-Modulated Pixel Transformer (CM-PiT) building blocks.
2
+
3
+ CM-PiT compresses local dense pixel features into attention tokens, processes
4
+ them with gated self-attention and SwiGLU, and expands them back without losing
5
+ the original pixel lattice. Global encoder tokens generate adaptive shift,
6
+ scale, and residual gates that condition both transformer sublayers.
7
+ """
8
+
9
+ from typing import Optional
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ from einops import rearrange
15
+
16
+ from .Gated_Attention import GatedAttention
17
+ from .RoPE import RotaryPositionEmbedding2D
18
+ from .precision import full_precision
19
+
20
+
21
+ def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
22
+ """Apply affine context modulation without changing the tensor layout.
23
+
24
+ Args:
25
+ x: Normalized pixel tokens with shape ``[B, N, P, C]``.
26
+ shift: Context-predicted additive offsets with shape ``[B, N, P, C]``.
27
+ scale: Context-predicted residual scales with shape ``[B, N, P, C]``.
28
+
29
+ Returns:
30
+ Modulated tokens ``x * (1 + scale) + shift`` with shape
31
+ ``[B, N, P, C]``.
32
+ """
33
+ return x * (1.0 + scale) + shift
34
+
35
+
36
+ class SwiGLU(nn.Module):
37
+ """SwiGLU feed-forward layer operating independently on every pixel token.
38
+
39
+ The first projection creates value and gate branches, SiLU activates the
40
+ gate, and the second projection returns to the pixel-channel dimension.
41
+ Spatial and patch axes are preserved throughout the module.
42
+ """
43
+
44
+ def __init__(self, dim: int, hidden_dim: int) -> None:
45
+ """Construct the gated feed-forward projections.
46
+
47
+ Args:
48
+ dim: Input and output channel count ``C``.
49
+ hidden_dim: Width of each hidden value/gate branch.
50
+
51
+ Returns:
52
+ ``None``. Learnable linear layers are registered on the module.
53
+ """
54
+ super().__init__()
55
+ self.fc1 = nn.Linear(dim, hidden_dim * 2)
56
+ self.fc2 = nn.Linear(hidden_dim, dim)
57
+
58
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
59
+ """Transform pixel tokens with a SiLU-gated hidden representation.
60
+
61
+ Args:
62
+ x: Floating tensor with arbitrary leading dimensions and final
63
+ channel dimension ``C=dim``. CM-PiT supplies ``[B,N,P,C]``.
64
+
65
+ Returns:
66
+ Tensor with the same shape and dtype as ``x``.
67
+ """
68
+ value, gate = self.fc1(x).chunk(2, dim=-1)
69
+ return self.fc2(value * F.silu(gate))
70
+
71
+
72
+ class ContextAdaNorm(nn.Module):
73
+ """Predict Context-Guided Adaptive Normalization parameters.
74
+
75
+ Each global context token produces shift, scale, and residual-gate values
76
+ for both the attention and MLP sublayers over every pixel represented by
77
+ that encoder token. The six parameter groups are unpacked by
78
+ :class:`CMPiTBlock`.
79
+ """
80
+
81
+ def __init__(self, dim_ctx: int, patch_size: int, dim_pix: int) -> None:
82
+ """Create the context-to-modulation projection.
83
+
84
+ Args:
85
+ dim_ctx: Channel count of each Global Context Encoder token.
86
+ patch_size: Encoder patch side length ``P_ctx`` in image pixels.
87
+ dim_pix: Pixel-feature channel count ``C_pix``.
88
+
89
+ Returns:
90
+ ``None``. The projection outputs ``6 * P_ctx^2 * C_pix`` values per
91
+ context token.
92
+ """
93
+ super().__init__()
94
+ self.proj = nn.Sequential(
95
+ nn.SiLU(),
96
+ nn.Linear(dim_ctx, 6 * patch_size * patch_size * dim_pix),
97
+ )
98
+
99
+ def forward(self, ctx: torch.Tensor) -> torch.Tensor:
100
+ """Project context tokens into six dense pixel-wise parameter fields.
101
+
102
+ Args:
103
+ ctx: Context token tensor ``[B, N_ctx, C_ctx]``.
104
+
105
+ Returns:
106
+ Modulation tensor ``[B, N_ctx, 6 * P_ctx^2 * C_pix]``.
107
+ """
108
+ return self.proj(ctx)
109
+
110
+
111
+ class CMPiTBlock(nn.Module):
112
+ """Context-Modulated Pixel Transformer block.
113
+
114
+ The block groups a dense pixel feature map into local patches, linearly
115
+ compresses every patch to an attention token, applies gated global
116
+ self-attention, expands the token back to pixel features, and follows it
117
+ with a per-pixel SwiGLU MLP. Both residual branches use Context-Guided
118
+ Adaptive Normalization generated from DINO context tokens.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ dim_ctx: int,
124
+ ctx_patch_size: int,
125
+ dim_pix: int,
126
+ patch_size: int,
127
+ attn_dim: int,
128
+ num_heads: int,
129
+ mlp_ratio: float = 4.0,
130
+ qk_norm: bool = True,
131
+ rope: Optional[RotaryPositionEmbedding2D] = None,
132
+ eps: float = 1e-6,
133
+ ) -> None:
134
+ """Configure one CM-PiT block.
135
+
136
+ Args:
137
+ dim_ctx: Context-token channel count ``C_ctx``.
138
+ ctx_patch_size: Image patch size ``P_ctx`` represented by one
139
+ context token.
140
+ dim_pix: Dense pixel-feature channel count ``C_pix``.
141
+ patch_size: Side length ``P`` grouped into one attention token.
142
+ It must divide ``ctx_patch_size``.
143
+ attn_dim: Compressed attention-token channel count ``D``.
144
+ num_heads: Number of attention heads. ``D`` must be divisible by it.
145
+ mlp_ratio: Expansion ratio controlling the SwiGLU hidden width.
146
+ qk_norm: Whether to apply FP32 RMSNorm to each query/key head.
147
+ rope: Optional 2D rotary position embedding shared by decoder blocks.
148
+ eps: Numerical epsilon used by RMSNorm layers.
149
+
150
+ Returns:
151
+ ``None``. Attention, modulation, MLP, and projection layers are
152
+ registered on the block.
153
+ """
154
+ super().__init__()
155
+ if ctx_patch_size % patch_size != 0:
156
+ raise ValueError(
157
+ f"ctx_patch_size ({ctx_patch_size}) must be divisible by patch_size ({patch_size})"
158
+ )
159
+
160
+ self.dim_ctx = dim_ctx
161
+ self.dim_pix = dim_pix
162
+ self.ctx_patch_size = ctx_patch_size
163
+ self.patch_size = patch_size
164
+ patch_dim = patch_size * patch_size * dim_pix
165
+
166
+ self.norm1 = nn.RMSNorm(dim_pix, eps=eps)
167
+ self.linear_compress = nn.Linear(patch_dim, attn_dim)
168
+ self.attn = GatedAttention(attn_dim, num_heads, qk_norm=qk_norm, rope=rope, eps=eps)
169
+ self.linear_expand = nn.Linear(attn_dim, patch_dim)
170
+ self.norm2 = nn.RMSNorm(dim_pix, eps=eps)
171
+ hidden_dim = max(1, int(round(dim_pix * mlp_ratio * 2.0 / 3.0)))
172
+ self.mlp = SwiGLU(dim_pix, hidden_dim)
173
+ self.ada_norm = ContextAdaNorm(dim_ctx, ctx_patch_size, dim_pix)
174
+
175
+ @staticmethod
176
+ def _norm(norm: nn.Module, x: torch.Tensor) -> torch.Tensor:
177
+ """Evaluate a normalization layer in FP32 and restore input dtype.
178
+
179
+ Args:
180
+ norm: Normalization module acting on the final channel dimension.
181
+ x: Pixel tokens ``[B, N, P^2, C_pix]`` in the active model dtype.
182
+
183
+ Returns:
184
+ Normalized tensor with the same shape and dtype as ``x``.
185
+ """
186
+ dtype = x.dtype
187
+ with full_precision(x.device):
188
+ out = norm(x.float())
189
+ return out.to(dtype)
190
+
191
+ def _modulation(self, ctx: torch.Tensor, height: int, width: int) -> torch.Tensor:
192
+ """Align context modulation fields with the block's pixel patches.
193
+
194
+ Args:
195
+ ctx: Global context tokens ``[B, H_ctx*W_ctx, C_ctx]``.
196
+ height: Dense pixel-map height ``H``.
197
+ width: Dense pixel-map width ``W``.
198
+
199
+ Returns:
200
+ Six modulation groups with shape
201
+ ``[B, (H/P)*(W/P), 6, P^2, C_pix]``. Rearrangement is exact and
202
+ contains no interpolation.
203
+ """
204
+ batch = ctx.shape[0]
205
+ p_ctx, p = self.ctx_patch_size, self.patch_size
206
+ ctx_h, ctx_w = height // p_ctx, width // p_ctx
207
+ if ctx.shape[1] != ctx_h * ctx_w:
208
+ raise ValueError(
209
+ f"Context token count ({ctx.shape[1]}) does not match grid ({ctx_h}x{ctx_w})"
210
+ )
211
+
212
+ mod = self.ada_norm(ctx).view(batch, ctx_h, ctx_w, 6, p_ctx, p_ctx, self.dim_pix)
213
+ if p_ctx == p:
214
+ return rearrange(mod, "b h w m ph pw c -> b (h w) m (ph pw) c")
215
+
216
+ ratio = p_ctx // p
217
+ return rearrange(
218
+ mod,
219
+ "b h w m (rh ph) (rw pw) c -> b (h rh w rw) m (ph pw) c",
220
+ rh=ratio,
221
+ rw=ratio,
222
+ ph=p,
223
+ pw=p,
224
+ )
225
+
226
+ def forward(self, x: torch.Tensor, ctx: torch.Tensor, pos: torch.Tensor) -> torch.Tensor:
227
+ """Apply context-modulated attention and MLP residual updates.
228
+
229
+ Args:
230
+ x: Dense pixel features ``[B, C_pix, H, W]``.
231
+ ctx: Global context tokens ``[B, (H/P_ctx)*(W/P_ctx), C_ctx]``.
232
+ pos: Integer 2D token positions ``[B, (H/P)*(W/P), 2]`` used by
233
+ rotary position embedding in self-attention.
234
+
235
+ Returns:
236
+ Updated dense pixel features ``[B, C_pix, H, W]``.
237
+ """
238
+ batch, _, height, width = x.shape
239
+ p = self.patch_size
240
+ pix = rearrange(x, "b c (h ph) (w pw) -> b (h w) (ph pw) c", ph=p, pw=p)
241
+ shift_attn, scale_attn, gate_attn, shift_mlp, scale_mlp, gate_mlp = self._modulation(
242
+ ctx, height, width
243
+ ).unbind(dim=2)
244
+
245
+ out = modulate(self._norm(self.norm1, pix), shift_attn, scale_attn)
246
+ out = self.linear_compress(out.flatten(2))
247
+ out = self.attn(out, pos=pos)
248
+ out = self.linear_expand(out).view(batch, -1, p * p, self.dim_pix)
249
+ pix = pix + gate_attn * out
250
+
251
+ out = modulate(self._norm(self.norm2, pix), shift_mlp, scale_mlp)
252
+ pix = pix + gate_mlp * self.mlp(out)
253
+ return rearrange(
254
+ pix,
255
+ "b (h w) (ph pw) c -> b c (h ph) (w pw)",
256
+ h=height // p,
257
+ w=width // p,
258
+ ph=p,
259
+ pw=p,
260
+ )
pxdepth/model/Gated_Attention.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gated multi-head self-attention used inside CM-PiT blocks.
2
+
3
+ The implementation performs optional FP32 query/key normalization, applies
4
+ two-dimensional rotary position embeddings, and delegates attention to PyTorch
5
+ SDPA. A learned token-channel sigmoid gate modulates the attended features
6
+ before the output projection.
7
+ """
8
+
9
+ from typing import Optional
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+ from .RoPE import RotaryPositionEmbedding2D
16
+ from .precision import full_precision
17
+
18
+
19
+ class GatedAttention(nn.Module):
20
+ """Multi-head self-attention followed by a learned token-channel gate.
21
+
22
+ The Q/K normalization and 2D RoPE calculations follow the numerical path
23
+ used for the released model. Q/K normalization and RoPE are evaluated in
24
+ FP32, while SDPA follows the active decoder autocast dtype.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ dim: int,
30
+ num_heads: int,
31
+ qk_norm: bool = True,
32
+ rope: Optional[RotaryPositionEmbedding2D] = None,
33
+ eps: float = 1e-6,
34
+ attn_drop: float = 0.0,
35
+ proj_drop: float = 0.0,
36
+ ) -> None:
37
+ """Construct gated multi-head self-attention.
38
+
39
+ Args:
40
+ dim: Token channel count ``D``.
41
+ num_heads: Number of attention heads. ``D`` must be divisible by it.
42
+ qk_norm: Enable per-head RMSNorm for queries and keys.
43
+ rope: Optional 2D rotary position embedding module.
44
+ eps: Epsilon used by query/key RMSNorm.
45
+ attn_drop: Attention-probability dropout used during training.
46
+ proj_drop: Dropout applied after the output projection.
47
+
48
+ Returns:
49
+ ``None``. QKV, gate, output, and optional normalization layers are
50
+ registered on the module.
51
+ """
52
+ super().__init__()
53
+ if dim % num_heads != 0:
54
+ raise ValueError(f"dim ({dim}) must be divisible by num_heads ({num_heads})")
55
+
56
+ self.num_heads = num_heads
57
+ self.head_dim = dim // num_heads
58
+ self.qkv = nn.Linear(dim, dim * 3, bias=True)
59
+ self.q_norm = nn.RMSNorm(self.head_dim, eps=eps) if qk_norm else nn.Identity()
60
+ self.k_norm = nn.RMSNorm(self.head_dim, eps=eps) if qk_norm else nn.Identity()
61
+ self.rope = rope
62
+ self.attn_drop = float(attn_drop)
63
+ self.gate = nn.Linear(dim, dim)
64
+ self.proj = nn.Linear(dim, dim)
65
+ self.proj_drop = nn.Dropout(proj_drop)
66
+
67
+ def forward(self, x: torch.Tensor, pos: Optional[torch.Tensor] = None) -> torch.Tensor:
68
+ """Apply self-attention and token-channel gating.
69
+
70
+ Args:
71
+ x: Compressed patch tokens ``[B, N, D]``.
72
+ pos: Optional integer grid coordinates ``[B, N, 2]``. They are
73
+ required when a rotary position embedding is configured.
74
+
75
+ Returns:
76
+ Gated and projected attention output ``[B, N, D]``.
77
+ """
78
+ batch, length, dim = x.shape
79
+ gate = torch.sigmoid(self.gate(x))
80
+ qkv = self.qkv(x).reshape(batch, length, 3, self.num_heads, self.head_dim)
81
+ q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
82
+
83
+ if not isinstance(self.q_norm, nn.Identity):
84
+ dtype = q.dtype
85
+ with full_precision(q.device):
86
+ q = self.q_norm(q.float())
87
+ k = self.k_norm(k.float())
88
+ q, k = q.to(dtype), k.to(dtype)
89
+
90
+ if self.rope is not None:
91
+ dtype = q.dtype
92
+ with full_precision(q.device):
93
+ q = self.rope(q.float(), pos)
94
+ k = self.rope(k.float(), pos)
95
+ q, k = q.to(dtype), k.to(dtype)
96
+
97
+ out = F.scaled_dot_product_attention(
98
+ q,
99
+ k,
100
+ v,
101
+ dropout_p=self.attn_drop if self.training else 0.0,
102
+ )
103
+ out = out.transpose(1, 2).reshape(batch, length, dim)
104
+ out = out * gate.to(out.dtype)
105
+ return self.proj_drop(self.proj(out))
pxdepth/model/Global_Context_Encoder.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Global Context Encoder used to condition pixel-space depth prediction.
2
+
3
+ A DINOv2 vision transformer extracts selected intermediate patch-token maps.
4
+ Each map is normalized, reshaped to its image grid, projected to a common
5
+ channel width, and summed into the context feature consumed by CM-PiT adaptive
6
+ normalization layers.
7
+ """
8
+
9
+ from typing import List, Sequence, Union
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+ from ..registry import ENCODERS
16
+ from .dinov2.hub import backbones
17
+ from .utils import wrap_dinov2_attention_with_sdpa, wrap_module_with_gradient_checkpointing
18
+
19
+
20
+ @ENCODERS.register()
21
+ class GlobalContextEncoder(nn.Module):
22
+ """Global Context Encoder based on intermediate DINOv2 features.
23
+
24
+ The encoder extracts several normalized patch-token maps from a ViT,
25
+ projects each map to a shared channel width with a 1x1 convolution, and
26
+ sums the projected maps. The resulting grid provides global semantic
27
+ context for Context-Guided Adaptive Normalization in the pixel predictor.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ backbone: str = "dinov2_vitl14",
33
+ intermediate_layers: Union[int, Sequence[int]] = (5, 11, 17, 23),
34
+ dim_out: int = 1024,
35
+ ) -> None:
36
+ """Construct the DINOv2 backbone and intermediate projections.
37
+
38
+ Args:
39
+ backbone: Name of a constructor exposed by ``dinov2.hub.backbones``.
40
+ intermediate_layers: Explicit zero-based block indices or an
41
+ integer requesting the last ``n`` intermediate layers.
42
+ dim_out: Channel count ``C_ctx`` of every projected context map.
43
+
44
+ Returns:
45
+ ``None``. The backbone, output projections, and ImageNet
46
+ normalization buffers are registered on the module.
47
+ """
48
+ super().__init__()
49
+ if not hasattr(backbones, backbone):
50
+ raise ValueError(f"Unsupported DINOv2 backbone: {backbone}")
51
+
52
+ self.backbone_name = backbone
53
+ self.intermediate_layers = list(intermediate_layers) if not isinstance(intermediate_layers, int) else intermediate_layers
54
+ self.backbone = getattr(backbones, backbone)(pretrained=False)
55
+ if hasattr(self.backbone, "mask_token"):
56
+ self.backbone.mask_token.requires_grad_(False)
57
+
58
+ patch_size = getattr(self.backbone, "patch_size", 14)
59
+ if isinstance(patch_size, (tuple, list)):
60
+ patch_size = patch_size[0]
61
+ self.patch_size = int(patch_size)
62
+ self.dim_features = int(getattr(self.backbone, "embed_dim"))
63
+ self.dim_out = int(dim_out)
64
+ count = self.intermediate_layers if isinstance(self.intermediate_layers, int) else len(self.intermediate_layers)
65
+ self.output_projections = nn.ModuleList(
66
+ nn.Conv2d(self.dim_features, dim_out, kernel_size=1) for _ in range(count)
67
+ )
68
+
69
+ self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
70
+ self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
71
+ self._onnx_compatible_mode = False
72
+
73
+ @property
74
+ def onnx_compatible_mode(self) -> bool:
75
+ """Report whether ONNX-compatible resize behavior is enabled.
76
+
77
+ Returns:
78
+ Boolean flag controlling antialiasing and the vendored backbone's
79
+ ONNX compatibility path.
80
+ """
81
+ return self._onnx_compatible_mode
82
+
83
+ @onnx_compatible_mode.setter
84
+ def onnx_compatible_mode(self, enabled: bool) -> None:
85
+ """Enable or disable ONNX-compatible encoder operators.
86
+
87
+ Args:
88
+ enabled: Boolean state propagated to the DINOv2 backbone.
89
+
90
+ Returns:
91
+ ``None``. Runtime flags are updated in place.
92
+ """
93
+ self._onnx_compatible_mode = bool(enabled)
94
+ self.backbone.onnx_compatible_mode = bool(enabled)
95
+
96
+ def init_weights(self) -> None:
97
+ """Load official pretrained weights for the configured DINOv2 backbone.
98
+
99
+ Returns:
100
+ ``None``. Backbone parameters are replaced in place while the
101
+ PXDepth-specific 1x1 projections keep their initialization.
102
+ """
103
+ state = getattr(backbones, self.backbone_name)(pretrained=True).state_dict()
104
+ self.backbone.load_state_dict(state, strict=True)
105
+
106
+ def enable_gradient_checkpointing(self) -> None:
107
+ """Wrap every DINO transformer block with activation checkpointing.
108
+
109
+ Parameter names and numerical block behavior remain unchanged; only
110
+ activation storage during training is affected.
111
+
112
+ Returns:
113
+ ``None``. Each backbone block is modified in place.
114
+ """
115
+ for block in self.backbone.blocks:
116
+ wrap_module_with_gradient_checkpointing(block)
117
+
118
+ def enable_pytorch_native_sdpa(self) -> None:
119
+ """Replace DINO attention forward methods with SDPA-compatible paths.
120
+
121
+ Returns:
122
+ ``None``. Attention modules are wrapped in place and use
123
+ Flash-Attention when the installed runtime supports it.
124
+ """
125
+ for block in self.backbone.blocks:
126
+ wrap_dinov2_attention_with_sdpa(block.attn)
127
+
128
+ def forward(
129
+ self,
130
+ image: torch.Tensor,
131
+ token_rows: int,
132
+ token_cols: int,
133
+ return_feature_maps: bool = False,
134
+ return_class_token: bool = False,
135
+ ):
136
+ """Encode RGB images into a summed global context feature map.
137
+
138
+ Args:
139
+ image: RGB tensor ``[B, 3, H_in, W_in]`` with values in ``[0, 1]``.
140
+ token_rows: Requested context-grid height ``H_ctx``.
141
+ token_cols: Requested context-grid width ``W_ctx``.
142
+ return_feature_maps: Also return the list of individually projected
143
+ feature maps when ``True``.
144
+ return_class_token: Also return the final selected DINO class token
145
+ ``[B, C_vit]`` when ``True``.
146
+
147
+ Returns:
148
+ By default, a context map ``[B, C_ctx, H_ctx, W_ctx]``. Optional
149
+ outputs are appended as a tuple in the order ``feature_maps`` then
150
+ ``class_token``. Each feature map has shape
151
+ ``[B, C_ctx, H_ctx, W_ctx]``.
152
+ """
153
+ target_size = (token_rows * self.patch_size, token_cols * self.patch_size)
154
+ if image.shape[-2:] != target_size:
155
+ image = F.interpolate(
156
+ image,
157
+ size=target_size,
158
+ mode="bilinear",
159
+ align_corners=False,
160
+ antialias=not self.onnx_compatible_mode,
161
+ )
162
+ image = (image - self.image_mean) / self.image_std
163
+ features = self.backbone.get_intermediate_layers(
164
+ image,
165
+ n=self.intermediate_layers,
166
+ return_class_token=True,
167
+ norm=True,
168
+ )
169
+ maps = []
170
+ context = None
171
+ for projection, (tokens, _) in zip(self.output_projections, features):
172
+ feature = tokens.permute(0, 2, 1).unflatten(2, (token_rows, token_cols)).contiguous()
173
+ projected = projection(feature)
174
+ context = projected if context is None else context + projected
175
+ if return_feature_maps:
176
+ maps.append(projected)
177
+ if context is None:
178
+ raise RuntimeError("Global Context Encoder did not receive any intermediate features.")
179
+
180
+ outputs: List[object] = [context]
181
+ if return_feature_maps:
182
+ outputs.append(maps)
183
+ if return_class_token:
184
+ outputs.append(features[-1][1])
185
+ return outputs[0] if len(outputs) == 1 else tuple(outputs)
pxdepth/model/PXDepth.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core PXDepth architecture and stable public model API.
2
+
3
+ The module connects the Global Context Encoder to the Pixel-Space Depth
4
+ Predictor and defines raw forward computation. Checkpoint translation and
5
+ metric-scale inference live in focused helper modules, while their familiar
6
+ ``from_pretrained`` and ``infer`` entry points remain methods on this class.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Dict, IO, Optional, Union
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+ from .Global_Context_Encoder import GlobalContextEncoder
16
+ from .Pixel_Space_Depth_Predictor import PixelSpaceDepthPredictor
17
+ from .checkpoint import load_pretrained
18
+ from .inference import infer as infer_model
19
+ from .precision import full_precision, inference_dtype, reduced_precision
20
+ from ..registry import ENCODERS, MODELS, PREDICTORS
21
+
22
+
23
+ @MODELS.register()
24
+ class PXDepth(nn.Module):
25
+ """Complete PXDepth monocular depth model.
26
+
27
+ A Global Context Encoder extracts semantic patch features and a Pixel-Space
28
+ Depth Predictor estimates full-resolution normalized log-depth together
29
+ with a finite-depth probability. ``forward`` exposes raw network outputs,
30
+ while ``infer`` aligns them to a GT or MoGe-2 reference for metric-scale
31
+ visualization and point-cloud reconstruction.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ encoder: Union[nn.Module, Dict[str, Any]],
37
+ predictor: Union[nn.Module, Dict[str, Any]],
38
+ remap_output: str = "linear",
39
+ mask_threshold: float = 0.5,
40
+ ) -> None:
41
+ """Construct the encoder and CM-PiT pixel predictor.
42
+
43
+ Args:
44
+ encoder: Encoder module or registry config.
45
+ predictor: Pixel predictor module or registry config. Its context
46
+ patch size and channel width default to the encoder contract.
47
+ remap_output: Output remapping applied to normalized log-depth.
48
+ The released model uses ``'linear'``.
49
+ mask_threshold: Probability threshold used by :meth:`infer`.
50
+
51
+ Returns:
52
+ ``None``. Model modules and ImageNet normalization buffers are
53
+ registered on the instance.
54
+ """
55
+ super().__init__()
56
+ if remap_output not in {"linear", "elu"}:
57
+ raise ValueError(f"Unsupported remap_output: {remap_output}")
58
+
59
+ self.remap_output = remap_output
60
+ self.mask_threshold = float(mask_threshold)
61
+ if isinstance(encoder, nn.Module):
62
+ self.encoder = encoder
63
+ else:
64
+ encoder_config = dict(encoder)
65
+ encoder_config.setdefault("type", "GlobalContextEncoder")
66
+ self.encoder = ENCODERS.build(encoder_config)
67
+ if not hasattr(self.encoder, "patch_size"):
68
+ raise TypeError("The encoder must expose an integer patch_size attribute.")
69
+ self.patch_size = self.encoder.patch_size
70
+ self.p_enc = self.patch_size
71
+ dim_ctx = getattr(self.encoder, "dim_out", None)
72
+ if isinstance(predictor, nn.Module):
73
+ self.predictor = predictor
74
+ else:
75
+ predictor_config = dict(predictor)
76
+ predictor_config.setdefault("type", "PixelSpaceDepthPredictor")
77
+ predictor_config.setdefault("in_channels", 3)
78
+ predictor_config.setdefault("ctx_patch_size", self.patch_size)
79
+ if dim_ctx is not None:
80
+ predictor_config.setdefault("dim_ctx", int(dim_ctx))
81
+ self.predictor = PREDICTORS.build(predictor_config)
82
+ self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
83
+ self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
84
+ self._reference_model: Optional[nn.Module] = None
85
+
86
+ @property
87
+ def device(self) -> torch.device:
88
+ """Return the device hosting PXDepth learnable parameters.
89
+
90
+ No inputs are required. The value is inferred from the model's first
91
+ parameter and is used when moving inference inputs or reference models.
92
+
93
+ Returns:
94
+ ``torch.device`` for the current model placement.
95
+ """
96
+ return next(self.parameters()).device
97
+
98
+ @property
99
+ def dtype(self) -> torch.dtype:
100
+ """Return the storage dtype of PXDepth learnable parameters.
101
+
102
+ No inputs are required. This reports parameter storage, which is
103
+ independent from local autocast contexts used inside attention.
104
+
105
+ Returns:
106
+ ``torch.dtype`` of the model's first parameter.
107
+ """
108
+ return next(self.parameters()).dtype
109
+
110
+ @classmethod
111
+ def from_pretrained(
112
+ cls,
113
+ path_or_repo: Union[str, Path, IO[bytes]],
114
+ model_kwargs: Optional[Dict[str, Any]] = None,
115
+ strict: bool = True,
116
+ **hf_kwargs: Any,
117
+ ) -> "PXDepth":
118
+ """Create a model from a local or Hugging Face ``model.pt`` checkpoint.
119
+
120
+ Args:
121
+ path_or_repo: Local checkpoint path, binary file object, or Hugging
122
+ Face model repository identifier.
123
+ model_kwargs: Optional constructor overrides applied after reading
124
+ ``model_config`` from the checkpoint.
125
+ strict: Forwarded to ``load_state_dict``. Published checkpoints
126
+ should use the default exact matching.
127
+ **hf_kwargs: Additional keyword arguments forwarded to
128
+ ``huggingface_hub.hf_hub_download`` for remote repositories.
129
+
130
+ Returns:
131
+ Initialized :class:`PXDepth` instance on CPU.
132
+ """
133
+ return load_pretrained(
134
+ cls,
135
+ path_or_repo,
136
+ model_kwargs=model_kwargs,
137
+ strict=strict,
138
+ **hf_kwargs,
139
+ )
140
+
141
+ def init_weights(self) -> None:
142
+ """Initialize the Global Context Encoder from official DINOv2 weights.
143
+
144
+ Predictor parameters retain the initialization created by their own
145
+ module constructors.
146
+
147
+ Returns:
148
+ ``None``. Encoder parameters are updated in place.
149
+ """
150
+ self.encoder.init_weights()
151
+
152
+ def enable_gradient_checkpointing(self) -> None:
153
+ """Enable activation checkpointing in both encoder and predictor.
154
+
155
+ This reduces saved activation memory during backward at the cost of
156
+ recomputing transformer blocks.
157
+
158
+ Returns:
159
+ ``None``. Child module runtime behavior is updated in place.
160
+ """
161
+ self.encoder.enable_gradient_checkpointing()
162
+ self.predictor.enable_gradient_checkpointing()
163
+
164
+ def enable_pytorch_native_sdpa(self) -> None:
165
+ """Enable the optimized SDPA attention path in the DINOv2 backbone.
166
+
167
+ Decoder CM-PiT attention already uses PyTorch SDPA directly and is not
168
+ modified by this method.
169
+
170
+ Returns:
171
+ ``None``. Encoder attention modules are wrapped in place.
172
+ """
173
+ self.encoder.enable_pytorch_native_sdpa()
174
+
175
+ def _remap(self, depth: torch.Tensor) -> torch.Tensor:
176
+ """Apply the configured output activation to raw depth predictions.
177
+
178
+ Args:
179
+ depth: Raw normalized-depth tensor with arbitrary batch/spatial
180
+ shape, normally ``[B, H, W]``.
181
+
182
+ Returns:
183
+ Tensor with the same shape. The released ``linear`` setting returns
184
+ the input unchanged.
185
+ """
186
+ return F.elu(depth) if self.remap_output == "elu" else depth
187
+
188
+ def forward(
189
+ self,
190
+ image: torch.Tensor,
191
+ use_fp16: bool = False,
192
+ use_fp32: bool = False,
193
+ ) -> Dict[str, torch.Tensor]:
194
+ """Run the network without metric-scale alignment.
195
+
196
+ Args:
197
+ image: RGB tensor ``[B, 3, H, W]`` with values in ``[0, 1]``. ``H``
198
+ and ``W`` must be divisible by the encoder patch size.
199
+ use_fp16: Run attention-heavy encoder and predictor regions under
200
+ FP16 autocast.
201
+ use_fp32: Disable reduced-precision autocast. It is mutually
202
+ exclusive with ``use_fp16``.
203
+
204
+ Returns:
205
+ Dictionary with normalized log-depth ``depth`` and finite-depth
206
+ probability ``mask``, both FP32 tensors ``[B, H, W]``.
207
+ """
208
+ height, width = image.shape[-2:]
209
+ if height % self.patch_size or width % self.patch_size:
210
+ raise ValueError(
211
+ f"Input resolution ({height}, {width}) must be divisible by patch size {self.patch_size}"
212
+ )
213
+ dtype = inference_dtype(use_fp16=use_fp16, use_fp32=use_fp32)
214
+
215
+ with full_precision(image.device):
216
+ image_norm = (image.float() - self.image_mean.float()) / self.image_std.float()
217
+ with reduced_precision(image.device, dtype):
218
+ context = self.encoder(image, height // self.patch_size, width // self.patch_size)
219
+ context = context.flatten(2).permute(0, 2, 1).contiguous()
220
+ depth, mask = self.predictor(image_norm, context, autocast_dtype=dtype)
221
+
222
+ with full_precision(image.device):
223
+ depth = self._remap(depth.float().squeeze(1))
224
+ mask = mask.float().squeeze(1).sigmoid()
225
+ return {"depth": depth, "mask": mask}
226
+
227
+ def infer(
228
+ self,
229
+ image: torch.Tensor,
230
+ gt_depth: Optional[torch.Tensor] = None,
231
+ intrinsics: Optional[torch.Tensor] = None,
232
+ fov_x: Optional[Union[float, torch.Tensor]] = None,
233
+ ref_image: Optional[torch.Tensor] = None,
234
+ apply_mask: bool = True,
235
+ use_fp16: bool = True,
236
+ use_fp32: bool = False,
237
+ ) -> Dict[str, torch.Tensor]:
238
+ """Recover metric-scale depth, validity, intrinsics, and 3D points.
239
+
240
+ Raw normalized log-depth is affine-aligned in log space to ``gt_depth``
241
+ when supplied, otherwise to a lazily loaded MoGe-2 reference. Alignment
242
+ parameters are estimated on a 64x64 nearest-resized valid subset. The
243
+ aligned depth is exponentiated and back-projected with normalized camera
244
+ intrinsics.
245
+
246
+ Args:
247
+ image: RGB tensor ``[3,H,W]`` or batch ``[B,3,H,W]`` in ``[0,1]``.
248
+ gt_depth: Optional reference depth ``[H,W]`` or ``[B,H,W]``. Finite
249
+ positive pixels define log-space alignment.
250
+ intrinsics: Optional normalized camera matrices ``[3,3]`` or
251
+ ``[B,3,3]`` corresponding to ``gt_depth``.
252
+ fov_x: Optional horizontal field of view in degrees, scalar or
253
+ tensor ``[B]``, used when intrinsics are unavailable.
254
+ ref_image: Optional original-resolution RGB tensor used only by the
255
+ reference model; PXDepth still consumes ``image``.
256
+ apply_mask: Replace invalid predicted depth/points with infinity.
257
+ use_fp16: Use FP16 for attention-heavy model regions.
258
+ use_fp32: Force those regions to FP32 and override the BF16 default.
259
+
260
+ Returns:
261
+ Dictionary containing aligned ``depth`` ``[B,H,W]``, boolean
262
+ ``mask`` ``[B,H,W]``, point map ``points`` ``[B,H,W,3]``, normalized
263
+ ``intrinsics`` ``[B,3,3]``, and horizontal ``fov_x`` ``[B]``. For an
264
+ unbatched input, the leading batch dimension is removed.
265
+ """
266
+ return infer_model(
267
+ self,
268
+ image,
269
+ gt_depth=gt_depth,
270
+ intrinsics=intrinsics,
271
+ fov_x=fov_x,
272
+ ref_image=ref_image,
273
+ apply_mask=apply_mask,
274
+ use_fp16=use_fp16,
275
+ use_fp32=use_fp32,
276
+ )
pxdepth/model/Pixel_Space_Depth_Predictor.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pixel-Space Depth Predictor that preserves the dense image lattice.
2
+
3
+ Normalized RGB is embedded with a 1x1 projection and processed by shared
4
+ CM-PiT trunk blocks before branching into depth and finite-mask predictors.
5
+ Linear patch compression is used only within transformer blocks, after which
6
+ features are expanded back to per-pixel tokens for dense output heads.
7
+ """
8
+
9
+ from typing import Iterable, Optional, Tuple
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ from torch.utils.checkpoint import checkpoint
14
+
15
+ from ..registry import PREDICTORS
16
+ from .CM_PiT import CMPiTBlock
17
+ from .RoPE import PositionGetter, RotaryPositionEmbedding2D
18
+ from .precision import full_precision, reduced_precision
19
+
20
+
21
+ @PREDICTORS.register()
22
+ class PixelSpaceDepthPredictor(nn.Module):
23
+ """Pixel-Space Depth Predictor built from cascaded CM-PiT blocks.
24
+
25
+ A 1x1 projection first embeds normalized RGB into dense pixel features.
26
+ Shared trunk blocks refine those features, after which independent depth
27
+ and validity branches predict normalized log-depth and finite-depth logits.
28
+ No convolution larger than 1x1 is applied to the pixel representation.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ in_channels: int = 3,
34
+ dim_ctx: int = 1024,
35
+ attn_dim: int = 1536,
36
+ ctx_patch_size: int = 14,
37
+ dim_pix: int = 16,
38
+ trunk_patch_size: int = 14,
39
+ depth_patch_size: int = 7,
40
+ mask_patch_size: int = 14,
41
+ num_heads: int = 24,
42
+ trunk_depth: int = 4,
43
+ depth_depth: int = 4,
44
+ mask_depth: int = 2,
45
+ mlp_ratio: float = 4.0,
46
+ qk_norm: bool = True,
47
+ rope_frequency: float = 100.0,
48
+ eps: float = 1e-6,
49
+ gradient_checkpointing: bool = True,
50
+ ) -> None:
51
+ """Construct the shared trunk and two prediction branches.
52
+
53
+ Args:
54
+ in_channels: Number of image channels, equal to three for RGB.
55
+ dim_ctx: Global context-token channel count ``C_ctx``.
56
+ attn_dim: Channel count ``D`` after linear patch compression.
57
+ ctx_patch_size: Encoder patch size ``P_ctx`` in image pixels.
58
+ dim_pix: Channel count ``C_pix`` of the dense pixel feature map.
59
+ trunk_patch_size: Attention patch size used by shared trunk blocks.
60
+ depth_patch_size: Attention patch size used by depth blocks.
61
+ mask_patch_size: Attention patch size used by validity-mask blocks.
62
+ num_heads: Number of gated-attention heads.
63
+ trunk_depth: Number of shared CM-PiT blocks.
64
+ depth_depth: Number of depth-branch CM-PiT blocks.
65
+ mask_depth: Number of validity-branch CM-PiT blocks.
66
+ mlp_ratio: SwiGLU expansion ratio inside every block.
67
+ qk_norm: Enable FP32 RMSNorm for attention queries and keys.
68
+ rope_frequency: Base frequency of the shared 2D RoPE module.
69
+ eps: Numerical epsilon for normalization layers.
70
+ gradient_checkpointing: Recompute CM-PiT blocks during backward to
71
+ reduce activation memory.
72
+
73
+ Returns:
74
+ ``None``. The complete pixel predictor is registered on the module.
75
+ """
76
+ super().__init__()
77
+ if attn_dim % num_heads != 0:
78
+ raise ValueError(f"attn_dim ({attn_dim}) must be divisible by num_heads ({num_heads})")
79
+ for name, patch_size in {
80
+ "trunk_patch_size": trunk_patch_size,
81
+ "depth_patch_size": depth_patch_size,
82
+ "mask_patch_size": mask_patch_size,
83
+ }.items():
84
+ if patch_size <= 0 or ctx_patch_size % patch_size != 0:
85
+ raise ValueError(
86
+ f"{name} ({patch_size}) must be positive and divide ctx_patch_size ({ctx_patch_size})"
87
+ )
88
+
89
+ self.dim_ctx = dim_ctx
90
+ self.dim_pix = dim_pix
91
+ self.attn_dim = attn_dim
92
+ self.ctx_patch_size = ctx_patch_size
93
+ self.trunk_patch_size = trunk_patch_size
94
+ self.depth_patch_size = depth_patch_size
95
+ self.mask_patch_size = mask_patch_size
96
+ self.gradient_checkpointing = gradient_checkpointing
97
+
98
+ self.pos = PositionGetter()
99
+ self.rope = RotaryPositionEmbedding2D(frequency=rope_frequency)
100
+ self.input_proj = nn.Conv2d(in_channels, dim_pix, kernel_size=1, bias=True)
101
+
102
+ block_args = dict(
103
+ dim_ctx=dim_ctx,
104
+ ctx_patch_size=ctx_patch_size,
105
+ dim_pix=dim_pix,
106
+ attn_dim=attn_dim,
107
+ num_heads=num_heads,
108
+ mlp_ratio=mlp_ratio,
109
+ qk_norm=qk_norm,
110
+ rope=self.rope,
111
+ eps=eps,
112
+ )
113
+ self.trunk_blocks = nn.ModuleList(
114
+ CMPiTBlock(patch_size=trunk_patch_size, **block_args) for _ in range(trunk_depth)
115
+ )
116
+ self.depth_blocks = nn.ModuleList(
117
+ CMPiTBlock(patch_size=depth_patch_size, **block_args) for _ in range(depth_depth)
118
+ )
119
+ self.mask_blocks = nn.ModuleList(
120
+ CMPiTBlock(patch_size=mask_patch_size, **block_args) for _ in range(mask_depth)
121
+ )
122
+ self.depth_head = nn.Conv2d(dim_pix, 1, kernel_size=1, bias=True)
123
+ self.mask_head = nn.Conv2d(dim_pix, 1, kernel_size=1, bias=True)
124
+ self.reset_parameters()
125
+
126
+ def _blocks(self) -> Iterable[CMPiTBlock]:
127
+ """Iterate over every CM-PiT block in execution-independent order.
128
+
129
+ Returns:
130
+ Iterable containing shared trunk, depth, and mask blocks. The method
131
+ takes no tensor inputs and is used for parameter initialization.
132
+ """
133
+ return (*self.trunk_blocks, *self.depth_blocks, *self.mask_blocks)
134
+
135
+ def reset_parameters(self) -> None:
136
+ """Initialize projections and start adaptive modulation at identity.
137
+
138
+ Linear and 1x1 convolution weights use Xavier uniform initialization.
139
+ Normalization scales start at one. The final adaptive-normalization
140
+ projections are zeroed so every CM-PiT residual branch initially has
141
+ zero modulation and zero gate.
142
+
143
+ Returns:
144
+ ``None``. Parameters are modified in place.
145
+ """
146
+ def init(module: nn.Module) -> None:
147
+ """Initialize one child module visited by :meth:`nn.Module.apply`.
148
+
149
+ Args:
150
+ module: Child ``nn.Module`` to initialize in place.
151
+
152
+ Returns:
153
+ ``None``.
154
+ """
155
+ if isinstance(module, (nn.Linear, nn.Conv2d)):
156
+ nn.init.xavier_uniform_(module.weight)
157
+ if module.bias is not None:
158
+ nn.init.zeros_(module.bias)
159
+ elif isinstance(module, (nn.LayerNorm, nn.RMSNorm)):
160
+ if module.weight is not None:
161
+ nn.init.ones_(module.weight)
162
+ if getattr(module, "bias", None) is not None:
163
+ nn.init.zeros_(module.bias)
164
+
165
+ self.apply(init)
166
+ for block in self._blocks():
167
+ nn.init.zeros_(block.ada_norm.proj[-1].weight)
168
+ nn.init.zeros_(block.ada_norm.proj[-1].bias)
169
+
170
+ def enable_gradient_checkpointing(self) -> None:
171
+ """Enable activation recomputation for CM-PiT blocks.
172
+
173
+ The flag is consulted only while the module is in training mode.
174
+
175
+ Returns:
176
+ ``None``. The runtime flag is changed in place.
177
+ """
178
+ self.gradient_checkpointing = True
179
+
180
+ def disable_gradient_checkpointing(self) -> None:
181
+ """Disable activation recomputation for CM-PiT blocks.
182
+
183
+ Subsequent training forwards retain block activations for backward.
184
+
185
+ Returns:
186
+ ``None``. The runtime flag is changed in place.
187
+ """
188
+ self.gradient_checkpointing = False
189
+
190
+ def _position(self, batch: int, height: int, width: int, patch_size: int, device: torch.device):
191
+ """Create cached 2D coordinates for one decoder patch grid.
192
+
193
+ Args:
194
+ batch: Batch size ``B``.
195
+ height: Dense image-feature height ``H``.
196
+ width: Dense image-feature width ``W``.
197
+ patch_size: Block patch side length ``P``.
198
+ device: Device on which coordinates are allocated.
199
+
200
+ Returns:
201
+ Integer position tensor ``[B, (H/P)*(W/P), 2]``.
202
+ """
203
+ return self.pos(batch, height // patch_size, width // patch_size, device=device).to(device)
204
+
205
+ def _run(
206
+ self,
207
+ x: torch.Tensor,
208
+ blocks: nn.ModuleList,
209
+ ctx: torch.Tensor,
210
+ pos: torch.Tensor,
211
+ ) -> torch.Tensor:
212
+ """Run a sequence of CM-PiT blocks with optional checkpointing.
213
+
214
+ Args:
215
+ x: Dense pixel features ``[B, C_pix, H, W]``.
216
+ blocks: Ordered CM-PiT block collection for one branch.
217
+ ctx: Global context tokens ``[B, N_ctx, C_ctx]``.
218
+ pos: 2D positions ``[B, N, 2]`` matching the blocks' patch grid.
219
+
220
+ Returns:
221
+ Refined dense features ``[B, C_pix, H, W]``.
222
+ """
223
+ for block in blocks:
224
+ if self.training and self.gradient_checkpointing:
225
+ x = checkpoint(block, x, ctx, pos, use_reentrant=False)
226
+ else:
227
+ x = block(x, ctx, pos)
228
+ return x
229
+
230
+ def forward(
231
+ self,
232
+ image: torch.Tensor,
233
+ ctx: torch.Tensor,
234
+ autocast_dtype: Optional[torch.dtype] = torch.bfloat16,
235
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
236
+ """Predict normalized log-depth and finite-depth logits from RGB.
237
+
238
+ Args:
239
+ image: ImageNet-normalized RGB tensor ``[B, 3, H, W]``.
240
+ ctx: Global context tokens ``[B, (H/P_ctx)*(W/P_ctx), C_ctx]``.
241
+ autocast_dtype: Decoder attention dtype. Use ``None`` for FP32,
242
+ ``torch.float16`` for FP16, or ``torch.bfloat16`` for BF16.
243
+
244
+ Returns:
245
+ depth: Raw normalized log-depth tensor ``[B, 1, H, W]``.
246
+ mask: Raw finite-depth logit tensor ``[B, 1, H, W]``.
247
+ """
248
+ batch, _, height, width = image.shape
249
+ p_ctx = self.ctx_patch_size
250
+ if height % p_ctx != 0 or width % p_ctx != 0:
251
+ raise ValueError(f"Input resolution ({height}, {width}) must be divisible by {p_ctx}")
252
+ expected = (height // p_ctx) * (width // p_ctx)
253
+ if tuple(ctx.shape) != (batch, expected, self.dim_ctx):
254
+ raise ValueError(
255
+ f"Context shape {tuple(ctx.shape)} does not match ({batch}, {expected}, {self.dim_ctx})"
256
+ )
257
+
258
+ with full_precision(image.device):
259
+ pix = self.input_proj(image.float())
260
+
261
+ with reduced_precision(image.device, autocast_dtype):
262
+ trunk_pos = self._position(batch, height, width, self.trunk_patch_size, image.device)
263
+ pix = self._run(pix, self.trunk_blocks, ctx, trunk_pos)
264
+
265
+ depth_pos = self._position(batch, height, width, self.depth_patch_size, image.device)
266
+ depth_feat = self._run(pix, self.depth_blocks, ctx, depth_pos)
267
+
268
+ mask_pos = self._position(batch, height, width, self.mask_patch_size, image.device)
269
+ mask_feat = self._run(pix, self.mask_blocks, ctx, mask_pos)
270
+
271
+ with full_precision(image.device):
272
+ depth = self.depth_head(depth_feat.float())
273
+ mask = self.mask_head(mask_feat.float())
274
+ return depth, mask
pxdepth/model/RoPE.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cached two-dimensional rotary position embeddings for image-token grids.
2
+
3
+ Positions are represented as integer ``(row, column)`` pairs. Half of every
4
+ attention-head feature is rotated by row and the other half by column, allowing
5
+ CM-PiT attention to operate at arbitrary patch-grid aspect ratios.
6
+ """
7
+
8
+ # This source code is licensed under the Apache License, Version 2.0
9
+ # found in the LICENSE file in the root directory of this source tree.
10
+
11
+
12
+ # Implementation of 2D Rotary Position Embeddings (RoPE).
13
+
14
+ # This module provides a clean implementation of 2D Rotary Position Embeddings,
15
+ # which extends the original RoPE concept to handle 2D spatial positions.
16
+
17
+ # Inspired by:
18
+ # https://github.com/meta-llama/codellama/blob/main/llama/model.py
19
+ # https://github.com/naver-ai/rope-vit
20
+
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ import torch.nn.functional as F
25
+ from typing import Dict, Tuple
26
+
27
+
28
+ class PositionGetter:
29
+ """Generates and caches 2D spatial positions for patches in a grid.
30
+
31
+ This class efficiently manages the generation of spatial coordinates for patches
32
+ in a 2D grid, caching results to avoid redundant computations.
33
+
34
+ Attributes:
35
+ position_cache: Dictionary storing precomputed position tensors for different
36
+ grid dimensions.
37
+ """
38
+
39
+ def __init__(self):
40
+ """Initialize the position generator with an empty grid cache.
41
+
42
+ The constructor takes no inputs. Coordinate tensors are generated lazily
43
+ for each unique ``(height,width)`` requested by decoder blocks.
44
+
45
+ Returns:
46
+ ``None``.
47
+ """
48
+ self.position_cache: Dict[Tuple[int, int], torch.Tensor] = {}
49
+
50
+ def __call__(self, batch_size: int, height: int, width: int, device: torch.device) -> torch.Tensor:
51
+ """Generates spatial positions for a batch of patches.
52
+
53
+ Args:
54
+ batch_size: Number of samples in the batch.
55
+ height: Height of the grid in patches.
56
+ width: Width of the grid in patches.
57
+ device: Target device for the position tensor.
58
+
59
+ Returns:
60
+ Tensor of shape (batch_size, height*width, 2) containing y,x coordinates
61
+ for each position in the grid, repeated for each batch item.
62
+ """
63
+ if (height, width) not in self.position_cache:
64
+ y_coords = torch.arange(height, device=device)
65
+ x_coords = torch.arange(width, device=device)
66
+ positions = torch.cartesian_prod(y_coords, x_coords)
67
+ self.position_cache[height, width] = positions
68
+
69
+ cached_positions = self.position_cache[height, width]
70
+ return cached_positions.view(1, height * width, 2).expand(batch_size, -1, -1).clone()
71
+
72
+
73
+ class RotaryPositionEmbedding2D(nn.Module):
74
+ """2D Rotary Position Embedding implementation.
75
+
76
+ This module applies rotary position embeddings to input tokens based on their
77
+ 2D spatial positions. It handles the position-dependent rotation of features
78
+ separately for vertical and horizontal dimensions.
79
+
80
+ Args:
81
+ frequency: Base frequency for the position embeddings. Default: 100.0
82
+ scaling_factor: Scaling factor for frequency computation. Default: 1.0
83
+
84
+ Attributes:
85
+ base_frequency: Base frequency for computing position embeddings.
86
+ scaling_factor: Factor to scale the computed frequencies.
87
+ frequency_cache: Cache for storing precomputed frequency components.
88
+ """
89
+
90
+ def __init__(self, frequency: float = 100.0, scaling_factor: float = 1.0):
91
+ """Initialize frequency settings and an empty component cache.
92
+
93
+ Args:
94
+ frequency: Base controlling the geometric frequency progression.
95
+ scaling_factor: Reserved multiplicative frequency scale retained
96
+ for checkpoint and API compatibility.
97
+
98
+ Returns:
99
+ ``None``. Frequency tables are generated lazily during ``forward``.
100
+ """
101
+ super().__init__()
102
+ self.base_frequency = frequency
103
+ self.scaling_factor = scaling_factor
104
+ self.frequency_cache: Dict[Tuple, Tuple[torch.Tensor, torch.Tensor]] = {}
105
+
106
+ def _compute_frequency_components(
107
+ self, dim: int, seq_len: int, device: torch.device, dtype: torch.dtype
108
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
109
+ """Computes frequency components for rotary embeddings.
110
+
111
+ Args:
112
+ dim: Feature dimension (must be even).
113
+ seq_len: Maximum sequence length.
114
+ device: Target device for computations.
115
+ dtype: Data type for the computed tensors.
116
+
117
+ Returns:
118
+ Tuple of (cosine, sine) tensors for frequency components.
119
+ """
120
+ cache_key = (dim, seq_len, device, dtype)
121
+ if cache_key not in self.frequency_cache:
122
+ # Compute frequency bands
123
+ exponents = torch.arange(0, dim, 2, device=device).float() / dim
124
+ inv_freq = 1.0 / (self.base_frequency**exponents)
125
+
126
+ # Generate position-dependent frequencies
127
+ positions = torch.arange(seq_len, device=device, dtype=inv_freq.dtype)
128
+ angles = torch.einsum("i,j->ij", positions, inv_freq)
129
+
130
+ # Compute and cache frequency components
131
+ angles = angles.to(dtype)
132
+ angles = torch.cat((angles, angles), dim=-1)
133
+ cos_components = angles.cos().to(dtype)
134
+ sin_components = angles.sin().to(dtype)
135
+ self.frequency_cache[cache_key] = (cos_components, sin_components)
136
+
137
+ return self.frequency_cache[cache_key]
138
+
139
+ @staticmethod
140
+ def _rotate_features(x: torch.Tensor) -> torch.Tensor:
141
+ """Performs feature rotation by splitting and recombining feature dimensions.
142
+
143
+ Args:
144
+ x: Tensor ``[..., D]`` whose final dimension is split in half.
145
+
146
+ Returns:
147
+ Rotated tensor with the same shape and dtype as ``x``.
148
+ """
149
+ feature_dim = x.shape[-1]
150
+ x1, x2 = x[..., : feature_dim // 2], x[..., feature_dim // 2 :]
151
+ return torch.cat((-x2, x1), dim=-1)
152
+
153
+ def _apply_1d_rope(
154
+ self, tokens: torch.Tensor, positions: torch.Tensor, cos_comp: torch.Tensor, sin_comp: torch.Tensor
155
+ ) -> torch.Tensor:
156
+ """Applies 1D rotary position embeddings along one dimension.
157
+
158
+ Args:
159
+ tokens: One-axis head features ``[B, H, N, D_axis]``.
160
+ positions: Integer position indices ``[B, N]``.
161
+ cos_comp: Cosine lookup table ``[L, D_axis]``.
162
+ sin_comp: Sine lookup table ``[L, D_axis]``.
163
+
164
+ Returns:
165
+ Rotated features ``[B, H, N, D_axis]``.
166
+ """
167
+ # Embed positions with frequency components
168
+ cos = F.embedding(positions, cos_comp)[:, None, :, :]
169
+ sin = F.embedding(positions, sin_comp)[:, None, :, :]
170
+
171
+ # Apply rotation
172
+ return (tokens * cos) + (self._rotate_features(tokens) * sin)
173
+
174
+ def forward(self, tokens: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
175
+ """Applies 2D rotary position embeddings to input tokens.
176
+
177
+ Args:
178
+ tokens: Input tensor of shape (batch_size, n_heads, n_tokens, dim).
179
+ The feature dimension (dim) must be divisible by 4.
180
+ positions: Position tensor of shape (batch_size, n_tokens, 2) containing
181
+ the y and x coordinates for each token.
182
+
183
+ Returns:
184
+ Tensor of same shape as input with applied 2D rotary position embeddings.
185
+
186
+ Raises:
187
+ AssertionError: If input dimensions are invalid or positions are malformed.
188
+ """
189
+ # Validate inputs
190
+ assert tokens.size(-1) % 2 == 0, "Feature dimension must be even"
191
+ assert positions.ndim == 3 and positions.shape[-1] == 2, "Positions must have shape (batch_size, n_tokens, 2)"
192
+
193
+ # Compute feature dimension for each spatial direction
194
+ feature_dim = tokens.size(-1) // 2
195
+
196
+ # Get frequency components
197
+ max_position = int(positions.max()) + 1
198
+ cos_comp, sin_comp = self._compute_frequency_components(feature_dim, max_position, tokens.device, tokens.dtype)
199
+
200
+ # Split features for vertical and horizontal processing
201
+ vertical_features, horizontal_features = tokens.chunk(2, dim=-1)
202
+
203
+ # Apply RoPE separately for each dimension
204
+ vertical_features = self._apply_1d_rope(vertical_features, positions[..., 0], cos_comp, sin_comp)
205
+ horizontal_features = self._apply_1d_rope(horizontal_features, positions[..., 1], cos_comp, sin_comp)
206
+
207
+ # Combine processed features
208
+ return torch.cat((vertical_features, horizontal_features), dim=-1)
pxdepth/model/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public model namespace for PXDepth.
2
+
3
+ Only the complete :class:`PXDepth` network is re-exported here. Internal
4
+ encoder, predictor, attention, and positional-encoding modules remain available
5
+ for development without becoming part of the stable package-level API.
6
+ """
7
+
8
+ from .PXDepth import PXDepth
9
+ from .Global_Context_Encoder import GlobalContextEncoder
10
+ from .Pixel_Space_Depth_Predictor import PixelSpaceDepthPredictor
11
+
12
+ __all__ = ["PXDepth", "GlobalContextEncoder", "PixelSpaceDepthPredictor"]
pxdepth/model/checkpoint.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load model-only checkpoints written in the public PXDepth format."""
2
+
3
+ from copy import deepcopy
4
+ from pathlib import Path
5
+ from typing import Any, Dict, IO, Optional, Type, TypeVar, Union
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from huggingface_hub import hf_hub_download
10
+
11
+
12
+ ModelT = TypeVar("ModelT", bound=nn.Module)
13
+
14
+
15
+ def _merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
16
+ """Recursively merge model-constructor overrides into a copied config.
17
+
18
+ Args:
19
+ base: Original nested model configuration. The dictionary is not
20
+ modified.
21
+ update: User overrides. Nested dictionaries update individual fields,
22
+ while lists and scalar values replace their counterparts.
23
+
24
+ Returns:
25
+ A new merged dictionary suitable for constructing the model.
26
+ """
27
+ result = deepcopy(base)
28
+ for key, value in update.items():
29
+ if isinstance(result.get(key), dict) and isinstance(value, dict):
30
+ result[key] = _merge(result[key], value)
31
+ else:
32
+ result[key] = deepcopy(value)
33
+ return result
34
+
35
+
36
+ def load_pretrained(
37
+ model_class: Type[ModelT],
38
+ path_or_repo: Union[str, Path, IO[bytes]],
39
+ model_kwargs: Optional[Dict[str, Any]] = None,
40
+ strict: bool = True,
41
+ **hf_kwargs: Any,
42
+ ) -> ModelT:
43
+ """Construct a model from a local or Hugging Face ``model.pt`` file.
44
+
45
+ Args:
46
+ model_class: Model class whose constructor accepts the canonical public
47
+ configuration.
48
+ path_or_repo: Existing local checkpoint path, binary file object, or
49
+ Hugging Face model repository identifier.
50
+ model_kwargs: Optional nested constructor overrides. Nested encoder or
51
+ predictor fields are merged without discarding sibling settings.
52
+ strict: Forwarded to :meth:`torch.nn.Module.load_state_dict`. Published
53
+ checkpoints should normally use the default strict loading.
54
+ **hf_kwargs: Extra arguments forwarded to ``hf_hub_download`` when
55
+ ``path_or_repo`` is a repository identifier.
56
+
57
+ Returns:
58
+ An initialized model instance on CPU.
59
+ """
60
+ path = Path(path_or_repo) if isinstance(path_or_repo, (str, Path)) else None
61
+ if path is not None and path.exists():
62
+ checkpoint_path: Union[Path, IO[bytes]] = path
63
+ elif isinstance(path_or_repo, str):
64
+ checkpoint_path = Path(
65
+ hf_hub_download(path_or_repo, repo_type="model", filename="model.pt", **hf_kwargs)
66
+ )
67
+ else:
68
+ checkpoint_path = path_or_repo
69
+
70
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
71
+ if "model_config" not in checkpoint or "model" not in checkpoint:
72
+ raise ValueError("PXDepth checkpoints must contain 'model_config' and 'model'.")
73
+ model_config = deepcopy(checkpoint["model_config"])
74
+ model_type = model_config.pop("type", None)
75
+ if model_type != model_class.__name__:
76
+ raise ValueError(
77
+ f"Expected a {model_class.__name__} checkpoint, got model type {model_type!r}."
78
+ )
79
+ if model_kwargs:
80
+ model_config = _merge(model_config, model_kwargs)
81
+ model = model_class(**model_config)
82
+ model.load_state_dict(checkpoint["model"], strict=strict)
83
+ return model
pxdepth/model/dinov2/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ __version__ = "0.0.1"
pxdepth/model/dinov2/hub/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
pxdepth/model/dinov2/hub/backbones.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ from enum import Enum
7
+ from typing import Union
8
+
9
+ import torch
10
+
11
+ from .utils import _DINOV2_BASE_URL, _make_dinov2_model_name
12
+
13
+
14
+ class Weights(Enum):
15
+ LVD142M = "LVD142M"
16
+
17
+
18
+ def _make_dinov2_model(
19
+ *,
20
+ arch_name: str = "vit_large",
21
+ img_size: int = 518,
22
+ patch_size: int = 14,
23
+ init_values: float = 1.0,
24
+ ffn_layer: str = "mlp",
25
+ block_chunks: int = 0,
26
+ num_register_tokens: int = 0,
27
+ interpolate_antialias: bool = False,
28
+ interpolate_offset: float = 0.1,
29
+ pretrained: bool = True,
30
+ weights: Union[Weights, str] = Weights.LVD142M,
31
+ **kwargs,
32
+ ):
33
+ from ..models import vision_transformer as vits
34
+
35
+ if isinstance(weights, str):
36
+ try:
37
+ weights = Weights[weights]
38
+ except KeyError:
39
+ raise AssertionError(f"Unsupported weights: {weights}")
40
+
41
+ model_base_name = _make_dinov2_model_name(arch_name, patch_size)
42
+ vit_kwargs = dict(
43
+ img_size=img_size,
44
+ patch_size=patch_size,
45
+ init_values=init_values,
46
+ ffn_layer=ffn_layer,
47
+ block_chunks=block_chunks,
48
+ num_register_tokens=num_register_tokens,
49
+ interpolate_antialias=interpolate_antialias,
50
+ interpolate_offset=interpolate_offset,
51
+ )
52
+ vit_kwargs.update(**kwargs)
53
+ model = vits.__dict__[arch_name](**vit_kwargs)
54
+
55
+ if pretrained:
56
+ model_full_name = _make_dinov2_model_name(arch_name, patch_size, num_register_tokens)
57
+ url = _DINOV2_BASE_URL + f"/{model_base_name}/{model_full_name}_pretrain.pth"
58
+ state_dict = torch.hub.load_state_dict_from_url(url, map_location="cpu")
59
+ model.load_state_dict(state_dict, strict=True)
60
+
61
+ return model
62
+
63
+
64
+ def dinov2_vits14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
65
+ """
66
+ DINOv2 ViT-S/14 model (optionally) pretrained on the LVD-142M dataset.
67
+ """
68
+ return _make_dinov2_model(arch_name="vit_small", pretrained=pretrained, weights=weights, **kwargs)
69
+
70
+
71
+ def dinov2_vitb14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
72
+ """
73
+ DINOv2 ViT-B/14 model (optionally) pretrained on the LVD-142M dataset.
74
+ """
75
+ return _make_dinov2_model(arch_name="vit_base", pretrained=pretrained, weights=weights, **kwargs)
76
+
77
+
78
+ def dinov2_vitl14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
79
+ """
80
+ DINOv2 ViT-L/14 model (optionally) pretrained on the LVD-142M dataset.
81
+ """
82
+ return _make_dinov2_model(arch_name="vit_large", pretrained=pretrained, weights=weights, **kwargs)
83
+
84
+
85
+ def dinov2_vitg14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
86
+ """
87
+ DINOv2 ViT-g/14 model (optionally) pretrained on the LVD-142M dataset.
88
+ """
89
+ return _make_dinov2_model(
90
+ arch_name="vit_giant2",
91
+ ffn_layer="swiglufused",
92
+ weights=weights,
93
+ pretrained=pretrained,
94
+ **kwargs,
95
+ )
96
+
97
+
98
+ def dinov2_vits14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
99
+ """
100
+ DINOv2 ViT-S/14 model with registers (optionally) pretrained on the LVD-142M dataset.
101
+ """
102
+ return _make_dinov2_model(
103
+ arch_name="vit_small",
104
+ pretrained=pretrained,
105
+ weights=weights,
106
+ num_register_tokens=4,
107
+ interpolate_antialias=True,
108
+ interpolate_offset=0.0,
109
+ **kwargs,
110
+ )
111
+
112
+
113
+ def dinov2_vitb14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
114
+ """
115
+ DINOv2 ViT-B/14 model with registers (optionally) pretrained on the LVD-142M dataset.
116
+ """
117
+ return _make_dinov2_model(
118
+ arch_name="vit_base",
119
+ pretrained=pretrained,
120
+ weights=weights,
121
+ num_register_tokens=4,
122
+ interpolate_antialias=True,
123
+ interpolate_offset=0.0,
124
+ **kwargs,
125
+ )
126
+
127
+
128
+ def dinov2_vitl14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
129
+ """
130
+ DINOv2 ViT-L/14 model with registers (optionally) pretrained on the LVD-142M dataset.
131
+ """
132
+ return _make_dinov2_model(
133
+ arch_name="vit_large",
134
+ pretrained=pretrained,
135
+ weights=weights,
136
+ num_register_tokens=4,
137
+ interpolate_antialias=True,
138
+ interpolate_offset=0.0,
139
+ **kwargs,
140
+ )
141
+
142
+
143
+ def dinov2_vitg14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
144
+ """
145
+ DINOv2 ViT-g/14 model with registers (optionally) pretrained on the LVD-142M dataset.
146
+ """
147
+ return _make_dinov2_model(
148
+ arch_name="vit_giant2",
149
+ ffn_layer="swiglufused",
150
+ weights=weights,
151
+ pretrained=pretrained,
152
+ num_register_tokens=4,
153
+ interpolate_antialias=True,
154
+ interpolate_offset=0.0,
155
+ **kwargs,
156
+ )
pxdepth/model/dinov2/hub/utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ import itertools
7
+ import math
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+
14
+ _DINOV2_BASE_URL = "https://dl.fbaipublicfiles.com/dinov2"
15
+
16
+
17
+ def _make_dinov2_model_name(arch_name: str, patch_size: int, num_register_tokens: int = 0) -> str:
18
+ compact_arch_name = arch_name.replace("_", "")[:4]
19
+ registers_suffix = f"_reg{num_register_tokens}" if num_register_tokens else ""
20
+ return f"dinov2_{compact_arch_name}{patch_size}{registers_suffix}"
21
+
22
+
23
+ class CenterPadding(nn.Module):
24
+ def __init__(self, multiple):
25
+ super().__init__()
26
+ self.multiple = multiple
27
+
28
+ def _get_pad(self, size):
29
+ new_size = math.ceil(size / self.multiple) * self.multiple
30
+ pad_size = new_size - size
31
+ pad_size_left = pad_size // 2
32
+ pad_size_right = pad_size - pad_size_left
33
+ return pad_size_left, pad_size_right
34
+
35
+ @torch.inference_mode()
36
+ def forward(self, x):
37
+ pads = list(itertools.chain.from_iterable(self._get_pad(m) for m in x.shape[:1:-1]))
38
+ output = F.pad(x, pads)
39
+ return output
pxdepth/model/dinov2/layers/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ from .mlp import Mlp
7
+ from .patch_embed import PatchEmbed
8
+ from .swiglu_ffn import SwiGLUFFN, SwiGLUFFNFused
9
+ from .block import NestedTensorBlock
10
+ from .attention import MemEffAttention
pxdepth/model/dinov2/layers/attention.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ # References:
7
+ # https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
8
+ # https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py
9
+
10
+ import logging
11
+ import os
12
+ import warnings
13
+
14
+ import torch.nn.functional as F
15
+ from torch import Tensor
16
+ from torch import nn
17
+ import torch
18
+
19
+
20
+ logger = logging.getLogger("dinov2")
21
+
22
+
23
+ XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
24
+ try:
25
+ if XFORMERS_ENABLED:
26
+ from xformers.ops import memory_efficient_attention, unbind
27
+
28
+ XFORMERS_AVAILABLE = True
29
+ # warnings.warn("xFormers is available (Attention)")
30
+ else:
31
+ # warnings.warn("xFormers is disabled (Attention)")
32
+ raise ImportError
33
+ except ImportError:
34
+ XFORMERS_AVAILABLE = False
35
+ # warnings.warn("xFormers is not available (Attention)")
36
+
37
+ try:
38
+ from flash_attn.flash_attn_interface import flash_attn_func
39
+ FLASH_ATTN_AVAILABLE = True
40
+ except Exception:
41
+ flash_attn_func = None
42
+ FLASH_ATTN_AVAILABLE = False
43
+
44
+
45
+ class Attention(nn.Module):
46
+ def __init__(
47
+ self,
48
+ dim: int,
49
+ num_heads: int = 8,
50
+ qkv_bias: bool = False,
51
+ proj_bias: bool = True,
52
+ attn_drop: float = 0.0,
53
+ proj_drop: float = 0.0,
54
+ ) -> None:
55
+ super().__init__()
56
+ self.num_heads = num_heads
57
+ head_dim = dim // num_heads
58
+ self.scale = head_dim**-0.5
59
+
60
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
61
+ self.attn_drop = nn.Dropout(attn_drop)
62
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
63
+ self.proj_drop = nn.Dropout(proj_drop)
64
+
65
+ # # Deprecated implementation, extremely slow
66
+ # def forward(self, x: Tensor, attn_bias=None) -> Tensor:
67
+ # B, N, C = x.shape
68
+ # qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
69
+ # q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
70
+ # attn = q @ k.transpose(-2, -1)
71
+ # attn = attn.softmax(dim=-1)
72
+ # attn = self.attn_drop(attn)
73
+ # x = (attn @ v).transpose(1, 2).reshape(B, N, C)
74
+ # x = self.proj(x)
75
+ # x = self.proj_drop(x)
76
+ # return x
77
+
78
+ def forward(self, x: Tensor, attn_bias=None) -> Tensor:
79
+ B, N, C = x.shape
80
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) # (3, B, H, N, C // H)
81
+
82
+ q, k, v = qkv.unbind(0) # (B, H, N, C // H)
83
+
84
+ use_flash_attn = (
85
+ FLASH_ATTN_AVAILABLE
86
+ and attn_bias is None
87
+ and q.is_cuda
88
+ and q.dtype in (torch.float16, torch.bfloat16)
89
+ )
90
+ if use_flash_attn:
91
+ q_f = q.permute(0, 2, 1, 3).contiguous()
92
+ k_f = k.permute(0, 2, 1, 3).contiguous()
93
+ v_f = v.permute(0, 2, 1, 3).contiguous()
94
+ x = flash_attn_func(
95
+ q_f,
96
+ k_f,
97
+ v_f,
98
+ dropout_p=0.0,
99
+ softmax_scale=self.scale,
100
+ causal=False,
101
+ )
102
+ x = x.reshape(B, N, C)
103
+ else:
104
+ x = F.scaled_dot_product_attention(q, k, v, attn_bias)
105
+ x = x.permute(0, 2, 1, 3).reshape(B, N, C)
106
+
107
+ x = self.proj(x)
108
+ x = self.proj_drop(x)
109
+ return x
110
+
111
+ class MemEffAttention(Attention):
112
+ def forward(self, x: Tensor, attn_bias=None) -> Tensor:
113
+ B, N, C = x.shape
114
+ use_flash_attn = (
115
+ FLASH_ATTN_AVAILABLE
116
+ and attn_bias is None
117
+ and x.is_cuda
118
+ and x.dtype in (torch.float16, torch.bfloat16)
119
+ )
120
+ if use_flash_attn:
121
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
122
+ q, k, v = qkv.unbind(2)
123
+ x = flash_attn_func(
124
+ q.contiguous(),
125
+ k.contiguous(),
126
+ v.contiguous(),
127
+ dropout_p=0.0,
128
+ softmax_scale=self.scale,
129
+ causal=False,
130
+ )
131
+ x = x.reshape([B, N, C])
132
+
133
+ x = self.proj(x)
134
+ x = self.proj_drop(x)
135
+ return x
136
+
137
+ if not XFORMERS_AVAILABLE:
138
+ if attn_bias is not None:
139
+ raise AssertionError("xFormers is required for using nested tensors")
140
+ return super().forward(x, attn_bias=attn_bias)
141
+
142
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
143
+
144
+ q, k, v = unbind(qkv, 2)
145
+
146
+ x = memory_efficient_attention(q, k, v, attn_bias=attn_bias)
147
+ x = x.reshape([B, N, C])
148
+
149
+ x = self.proj(x)
150
+ x = self.proj_drop(x)
151
+ return x
pxdepth/model/dinov2/layers/block.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ # References:
7
+ # https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
8
+ # https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py
9
+
10
+ import logging
11
+ import os
12
+ from typing import Callable, List, Any, Tuple, Dict
13
+ import warnings
14
+
15
+ import torch
16
+ from torch import nn, Tensor
17
+
18
+ from .attention import Attention, MemEffAttention
19
+ from .drop_path import DropPath
20
+ from .layer_scale import LayerScale
21
+ from .mlp import Mlp
22
+
23
+
24
+ logger = logging.getLogger("dinov2")
25
+
26
+
27
+ XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
28
+ try:
29
+ if XFORMERS_ENABLED:
30
+ from xformers.ops import fmha, scaled_index_add, index_select_cat
31
+
32
+ XFORMERS_AVAILABLE = True
33
+ # warnings.warn("xFormers is available (Block)")
34
+ else:
35
+ # warnings.warn("xFormers is disabled (Block)")
36
+ raise ImportError
37
+ except ImportError:
38
+ XFORMERS_AVAILABLE = False
39
+ # warnings.warn("xFormers is not available (Block)")
40
+
41
+
42
+ class Block(nn.Module):
43
+ def __init__(
44
+ self,
45
+ dim: int,
46
+ num_heads: int,
47
+ mlp_ratio: float = 4.0,
48
+ qkv_bias: bool = False,
49
+ proj_bias: bool = True,
50
+ ffn_bias: bool = True,
51
+ drop: float = 0.0,
52
+ attn_drop: float = 0.0,
53
+ init_values=None,
54
+ drop_path: float = 0.0,
55
+ act_layer: Callable[..., nn.Module] = nn.GELU,
56
+ norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
57
+ attn_class: Callable[..., nn.Module] = Attention,
58
+ ffn_layer: Callable[..., nn.Module] = Mlp,
59
+ ) -> None:
60
+ super().__init__()
61
+ # print(f"biases: qkv: {qkv_bias}, proj: {proj_bias}, ffn: {ffn_bias}")
62
+ self.norm1 = norm_layer(dim)
63
+ self.attn = attn_class(
64
+ dim,
65
+ num_heads=num_heads,
66
+ qkv_bias=qkv_bias,
67
+ proj_bias=proj_bias,
68
+ attn_drop=attn_drop,
69
+ proj_drop=drop,
70
+ )
71
+ self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
72
+ self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
73
+
74
+ self.norm2 = norm_layer(dim)
75
+ mlp_hidden_dim = int(dim * mlp_ratio)
76
+ self.mlp = ffn_layer(
77
+ in_features=dim,
78
+ hidden_features=mlp_hidden_dim,
79
+ act_layer=act_layer,
80
+ drop=drop,
81
+ bias=ffn_bias,
82
+ )
83
+ self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
84
+ self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
85
+
86
+ self.sample_drop_ratio = drop_path
87
+
88
+ def forward(self, x: Tensor) -> Tensor:
89
+ def attn_residual_func(x: Tensor) -> Tensor:
90
+ return self.ls1(self.attn(self.norm1(x)))
91
+
92
+ def ffn_residual_func(x: Tensor) -> Tensor:
93
+ return self.ls2(self.mlp(self.norm2(x)))
94
+
95
+ if self.training and self.sample_drop_ratio > 0.1:
96
+ # the overhead is compensated only for a drop path rate larger than 0.1
97
+ x = drop_add_residual_stochastic_depth(
98
+ x,
99
+ residual_func=attn_residual_func,
100
+ sample_drop_ratio=self.sample_drop_ratio,
101
+ )
102
+ x = drop_add_residual_stochastic_depth(
103
+ x,
104
+ residual_func=ffn_residual_func,
105
+ sample_drop_ratio=self.sample_drop_ratio,
106
+ )
107
+ elif self.training and self.sample_drop_ratio > 0.0:
108
+ x = x + self.drop_path1(attn_residual_func(x))
109
+ x = x + self.drop_path1(ffn_residual_func(x))
110
+ else:
111
+ x = x + attn_residual_func(x)
112
+ x = x + ffn_residual_func(x)
113
+ return x
114
+
115
+
116
+ def drop_add_residual_stochastic_depth(
117
+ x: Tensor,
118
+ residual_func: Callable[[Tensor], Tensor],
119
+ sample_drop_ratio: float = 0.0,
120
+ ) -> Tensor:
121
+ # 1) extract subset using permutation
122
+ b, n, d = x.shape
123
+ sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
124
+ brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
125
+ x_subset = x[brange]
126
+
127
+ # 2) apply residual_func to get residual
128
+ residual = residual_func(x_subset)
129
+
130
+ x_flat = x.flatten(1)
131
+ residual = residual.flatten(1)
132
+
133
+ residual_scale_factor = b / sample_subset_size
134
+
135
+ # 3) add the residual
136
+ x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor)
137
+ return x_plus_residual.view_as(x)
138
+
139
+
140
+ def get_branges_scales(x, sample_drop_ratio=0.0):
141
+ b, n, d = x.shape
142
+ sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
143
+ brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
144
+ residual_scale_factor = b / sample_subset_size
145
+ return brange, residual_scale_factor
146
+
147
+
148
+ def add_residual(x, brange, residual, residual_scale_factor, scaling_vector=None):
149
+ if scaling_vector is None:
150
+ x_flat = x.flatten(1)
151
+ residual = residual.flatten(1)
152
+ x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor)
153
+ else:
154
+ x_plus_residual = scaled_index_add(
155
+ x, brange, residual.to(dtype=x.dtype), scaling=scaling_vector, alpha=residual_scale_factor
156
+ )
157
+ return x_plus_residual
158
+
159
+
160
+ attn_bias_cache: Dict[Tuple, Any] = {}
161
+
162
+
163
+ def get_attn_bias_and_cat(x_list, branges=None):
164
+ """
165
+ this will perform the index select, cat the tensors, and provide the attn_bias from cache
166
+ """
167
+ batch_sizes = [b.shape[0] for b in branges] if branges is not None else [x.shape[0] for x in x_list]
168
+ all_shapes = tuple((b, x.shape[1]) for b, x in zip(batch_sizes, x_list))
169
+ if all_shapes not in attn_bias_cache.keys():
170
+ seqlens = []
171
+ for b, x in zip(batch_sizes, x_list):
172
+ for _ in range(b):
173
+ seqlens.append(x.shape[1])
174
+ attn_bias = fmha.BlockDiagonalMask.from_seqlens(seqlens)
175
+ attn_bias._batch_sizes = batch_sizes
176
+ attn_bias_cache[all_shapes] = attn_bias
177
+
178
+ if branges is not None:
179
+ cat_tensors = index_select_cat([x.flatten(1) for x in x_list], branges).view(1, -1, x_list[0].shape[-1])
180
+ else:
181
+ tensors_bs1 = tuple(x.reshape([1, -1, *x.shape[2:]]) for x in x_list)
182
+ cat_tensors = torch.cat(tensors_bs1, dim=1)
183
+
184
+ return attn_bias_cache[all_shapes], cat_tensors
185
+
186
+
187
+ def drop_add_residual_stochastic_depth_list(
188
+ x_list: List[Tensor],
189
+ residual_func: Callable[[Tensor, Any], Tensor],
190
+ sample_drop_ratio: float = 0.0,
191
+ scaling_vector=None,
192
+ ) -> Tensor:
193
+ # 1) generate random set of indices for dropping samples in the batch
194
+ branges_scales = [get_branges_scales(x, sample_drop_ratio=sample_drop_ratio) for x in x_list]
195
+ branges = [s[0] for s in branges_scales]
196
+ residual_scale_factors = [s[1] for s in branges_scales]
197
+
198
+ # 2) get attention bias and index+concat the tensors
199
+ attn_bias, x_cat = get_attn_bias_and_cat(x_list, branges)
200
+
201
+ # 3) apply residual_func to get residual, and split the result
202
+ residual_list = attn_bias.split(residual_func(x_cat, attn_bias=attn_bias)) # type: ignore
203
+
204
+ outputs = []
205
+ for x, brange, residual, residual_scale_factor in zip(x_list, branges, residual_list, residual_scale_factors):
206
+ outputs.append(add_residual(x, brange, residual, residual_scale_factor, scaling_vector).view_as(x))
207
+ return outputs
208
+
209
+
210
+ class NestedTensorBlock(Block):
211
+ def forward_nested(self, x_list: List[Tensor]) -> List[Tensor]:
212
+ """
213
+ x_list contains a list of tensors to nest together and run
214
+ """
215
+ assert isinstance(self.attn, MemEffAttention)
216
+
217
+ if self.training and self.sample_drop_ratio > 0.0:
218
+
219
+ def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
220
+ return self.attn(self.norm1(x), attn_bias=attn_bias)
221
+
222
+ def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
223
+ return self.mlp(self.norm2(x))
224
+
225
+ x_list = drop_add_residual_stochastic_depth_list(
226
+ x_list,
227
+ residual_func=attn_residual_func,
228
+ sample_drop_ratio=self.sample_drop_ratio,
229
+ scaling_vector=self.ls1.gamma if isinstance(self.ls1, LayerScale) else None,
230
+ )
231
+ x_list = drop_add_residual_stochastic_depth_list(
232
+ x_list,
233
+ residual_func=ffn_residual_func,
234
+ sample_drop_ratio=self.sample_drop_ratio,
235
+ scaling_vector=self.ls2.gamma if isinstance(self.ls1, LayerScale) else None,
236
+ )
237
+ return x_list
238
+ else:
239
+
240
+ def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
241
+ return self.ls1(self.attn(self.norm1(x), attn_bias=attn_bias))
242
+
243
+ def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
244
+ return self.ls2(self.mlp(self.norm2(x)))
245
+
246
+ attn_bias, x = get_attn_bias_and_cat(x_list)
247
+ x = x + attn_residual_func(x, attn_bias=attn_bias)
248
+ x = x + ffn_residual_func(x)
249
+ return attn_bias.split(x)
250
+
251
+ def forward(self, x_or_x_list):
252
+ if isinstance(x_or_x_list, Tensor):
253
+ return super().forward(x_or_x_list)
254
+ elif isinstance(x_or_x_list, list):
255
+ if not XFORMERS_AVAILABLE:
256
+ raise AssertionError("xFormers is required for using nested tensors")
257
+ return self.forward_nested(x_or_x_list)
258
+ else:
259
+ raise AssertionError
pxdepth/model/dinov2/layers/drop_path.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ # References:
7
+ # https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
8
+ # https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/drop.py
9
+
10
+
11
+ from torch import nn
12
+
13
+
14
+ def drop_path(x, drop_prob: float = 0.0, training: bool = False):
15
+ if drop_prob == 0.0 or not training:
16
+ return x
17
+ keep_prob = 1 - drop_prob
18
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
19
+ random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
20
+ if keep_prob > 0.0:
21
+ random_tensor.div_(keep_prob)
22
+ output = x * random_tensor
23
+ return output
24
+
25
+
26
+ class DropPath(nn.Module):
27
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
28
+
29
+ def __init__(self, drop_prob=None):
30
+ super(DropPath, self).__init__()
31
+ self.drop_prob = drop_prob
32
+
33
+ def forward(self, x):
34
+ return drop_path(x, self.drop_prob, self.training)
pxdepth/model/dinov2/layers/layer_scale.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ # Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110
7
+
8
+ from typing import Union
9
+
10
+ import torch
11
+ from torch import Tensor
12
+ from torch import nn
13
+
14
+
15
+ class LayerScale(nn.Module):
16
+ def __init__(
17
+ self,
18
+ dim: int,
19
+ init_values: Union[float, Tensor] = 1e-5,
20
+ inplace: bool = False,
21
+ ) -> None:
22
+ super().__init__()
23
+ self.inplace = inplace
24
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
25
+
26
+ def forward(self, x: Tensor) -> Tensor:
27
+ return x.mul_(self.gamma) if self.inplace else x * self.gamma
pxdepth/model/dinov2/layers/mlp.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the Apache License, Version 2.0
4
+ # found in the LICENSE file in the root directory of this source tree.
5
+
6
+ # References:
7
+ # https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
8
+ # https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/mlp.py
9
+
10
+
11
+ from typing import Callable, Optional
12
+
13
+ from torch import Tensor, nn
14
+
15
+
16
+ class Mlp(nn.Module):
17
+ def __init__(
18
+ self,
19
+ in_features: int,
20
+ hidden_features: Optional[int] = None,
21
+ out_features: Optional[int] = None,
22
+ act_layer: Callable[..., nn.Module] = nn.GELU,
23
+ drop: float = 0.0,
24
+ bias: bool = True,
25
+ ) -> None:
26
+ super().__init__()
27
+ out_features = out_features or in_features
28
+ hidden_features = hidden_features or in_features
29
+ self.fc1 = nn.Linear(in_features, hidden_features, bias=bias)
30
+ self.act = act_layer()
31
+ self.fc2 = nn.Linear(hidden_features, out_features, bias=bias)
32
+ self.drop = nn.Dropout(drop)
33
+
34
+ def forward(self, x: Tensor) -> Tensor:
35
+ x = self.fc1(x)
36
+ x = self.act(x)
37
+ x = self.drop(x)
38
+ x = self.fc2(x)
39
+ x = self.drop(x)
40
+ return x