Spaces:
Running on Zero
Running on Zero
Add PXDepth demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- README.md +11 -7
- app.py +323 -0
- example_images/KITTI_01.jpg +3 -0
- example_images/KITTI_02.jpg +3 -0
- example_images/KITTI_03.jpg +3 -0
- example_images/Libary_01.jpg +3 -0
- example_images/NYUv2_00175.jpg +3 -0
- example_images/bird_01.jpg +3 -0
- example_images/chair_01.jpg +3 -0
- example_images/courtyard_01.jpg +3 -0
- example_images/courtyard_02.jpg +3 -0
- example_images/pipe_01.jpg +3 -0
- example_images/room_01.jpg +3 -0
- example_images/room_02.jpg +3 -0
- example_images/room_03.jpg +3 -0
- example_images/stair_01.jpg +3 -0
- example_images/stand_01.jpg +3 -0
- example_images/street_01.jpg +3 -0
- example_images/street_02.png +3 -0
- example_images/street_03.jpg +3 -0
- example_images/umic_building.jpg +3 -0
- example_images/volterra.jpg +3 -0
- pxdepth/__init__.py +18 -0
- pxdepth/build.py +26 -0
- pxdepth/config.py +137 -0
- pxdepth/evaluation/__init__.py +12 -0
- pxdepth/evaluation/dataloader.py +675 -0
- pxdepth/evaluation/metrics.py +616 -0
- pxdepth/inference/__init__.py +20 -0
- pxdepth/inference/resize.py +158 -0
- pxdepth/inference/runner.py +84 -0
- pxdepth/model/CM_PiT.py +260 -0
- pxdepth/model/Gated_Attention.py +105 -0
- pxdepth/model/Global_Context_Encoder.py +185 -0
- pxdepth/model/PXDepth.py +276 -0
- pxdepth/model/Pixel_Space_Depth_Predictor.py +274 -0
- pxdepth/model/RoPE.py +208 -0
- pxdepth/model/__init__.py +12 -0
- pxdepth/model/checkpoint.py +83 -0
- pxdepth/model/dinov2/__init__.py +6 -0
- pxdepth/model/dinov2/hub/__init__.py +4 -0
- pxdepth/model/dinov2/hub/backbones.py +156 -0
- pxdepth/model/dinov2/hub/utils.py +39 -0
- pxdepth/model/dinov2/layers/__init__.py +10 -0
- pxdepth/model/dinov2/layers/attention.py +151 -0
- pxdepth/model/dinov2/layers/block.py +259 -0
- pxdepth/model/dinov2/layers/drop_path.py +34 -0
- pxdepth/model/dinov2/layers/layer_scale.py +27 -0
- 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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
example_images/KITTI_02.jpg
ADDED
|
Git LFS Details
|
example_images/KITTI_03.jpg
ADDED
|
Git LFS Details
|
example_images/Libary_01.jpg
ADDED
|
Git LFS Details
|
example_images/NYUv2_00175.jpg
ADDED
|
Git LFS Details
|
example_images/bird_01.jpg
ADDED
|
Git LFS Details
|
example_images/chair_01.jpg
ADDED
|
Git LFS Details
|
example_images/courtyard_01.jpg
ADDED
|
Git LFS Details
|
example_images/courtyard_02.jpg
ADDED
|
Git LFS Details
|
example_images/pipe_01.jpg
ADDED
|
Git LFS Details
|
example_images/room_01.jpg
ADDED
|
Git LFS Details
|
example_images/room_02.jpg
ADDED
|
Git LFS Details
|
example_images/room_03.jpg
ADDED
|
Git LFS Details
|
example_images/stair_01.jpg
ADDED
|
Git LFS Details
|
example_images/stand_01.jpg
ADDED
|
Git LFS Details
|
example_images/street_01.jpg
ADDED
|
Git LFS Details
|
example_images/street_02.png
ADDED
|
Git LFS Details
|
example_images/street_03.jpg
ADDED
|
Git LFS Details
|
example_images/umic_building.jpg
ADDED
|
Git LFS Details
|
example_images/volterra.jpg
ADDED
|
Git LFS Details
|
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
|