Instructions to use Qtn-Cls/LocalMeshEngine with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Trellis
How to use Qtn-Cls/LocalMeshEngine with Trellis:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Les quatre vues passent sous multivue/ : l'arborescence du depot devient celle du disque (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- multivue/LISEZ-MOI.md +23 -0
- multivue/cameras/source/src/depth_anything_3/model/gs_adapter.py +200 -0
- multivue/cameras/source/src/depth_anything_3/model/gsdpt.py +133 -0
- multivue/cameras/source/src/depth_anything_3/model/reference_view_selector.py +223 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/attention.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/block.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/head_utils.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/transform.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/attention.py +109 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/block.py +81 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/gs_renderer.py +340 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/head_utils.py +230 -0
- multivue/cameras/source/src/depth_anything_3/model/utils/transform.py +208 -0
- multivue/cameras/source/src/depth_anything_3/registry.py +50 -0
- multivue/cameras/source/src/depth_anything_3/services/__init__.py +24 -0
- multivue/cameras/source/src/depth_anything_3/services/backend.py +1556 -0
- multivue/cameras/source/src/depth_anything_3/services/gallery.py +806 -0
- multivue/cameras/source/src/depth_anything_3/services/inference_service.py +247 -0
- multivue/cameras/source/src/depth_anything_3/services/input_handlers.py +266 -0
- multivue/cameras/source/src/depth_anything_3/specs.py +45 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/alignment.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/constants.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/geometry.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/logger.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/parallel_utils.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/__pycache__/ray_utils.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/alignment.py +163 -0
- multivue/cameras/source/src/depth_anything_3/utils/api_helpers.py +58 -0
- multivue/cameras/source/src/depth_anything_3/utils/camera_trj_helpers.py +479 -0
- multivue/cameras/source/src/depth_anything_3/utils/constants.py +270 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/__init__.py +75 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/colmap.py +150 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/depth_vis.py +41 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/feat_vis.py +75 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/glb.py +437 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/gs.py +154 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/npz.py +73 -0
- multivue/cameras/source/src/depth_anything_3/utils/export/utils.py +30 -0
- multivue/cameras/source/src/depth_anything_3/utils/geometry.py +498 -0
- multivue/cameras/source/src/depth_anything_3/utils/gsply_helpers.py +173 -0
- multivue/cameras/source/src/depth_anything_3/utils/io/__pycache__/input_processor.cpython-312.pyc +0 -0
- multivue/cameras/source/src/depth_anything_3/utils/io/input_processor.py +501 -0
- multivue/cameras/source/src/depth_anything_3/utils/io/output_processor.py +172 -0
- multivue/cameras/source/src/depth_anything_3/utils/layout_helpers.py +216 -0
- multivue/cameras/source/src/depth_anything_3/utils/logger.py +82 -0
- multivue/cameras/source/src/depth_anything_3/utils/memory.py +127 -0
- multivue/cameras/source/src/depth_anything_3/utils/model_loading.py +149 -0
- multivue/cameras/source/src/depth_anything_3/utils/parallel_utils.py +133 -0
- multivue/cameras/source/src/depth_anything_3/utils/pca_utils.py +284 -0
- multivue/cameras/source/src/depth_anything_3/utils/pose_align.py +347 -0
multivue/LISEZ-MOI.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Les poids du multivue
|
| 2 |
+
|
| 3 |
+
Quatre fichiers obligatoires et un optionnel. Aucun n'est telecharge par le
|
| 4 |
+
moteur : c'est l'application qui les pose, ou la personne qui installe.
|
| 5 |
+
|
| 6 |
+
structure_mv_fp8.safetensors fusionne les quatre vues en un volume
|
| 7 |
+
forme_512_mv_fp8.safetensors premiere descente de la forme
|
| 8 |
+
forme_1024_mv_fp8.safetensors seconde descente, celle qui rend la surface
|
| 9 |
+
champ.safetensors rattrape le detail entre deux jetons
|
| 10 |
+
|
| 11 |
+
cameras/ mesure sous quel angle chaque photo a ete
|
| 12 |
+
prise, et en deduit si les deux profils
|
| 13 |
+
sont a l'endroit ou inverses.
|
| 14 |
+
|
| 15 |
+
CE DERNIER A ETE ECRIT « OPTIONNEL » ICI, ET C'ETAIT FAUX (corrige le 8
|
| 16 |
+
septembre 2026). Sans lui le moteur SUPPOSE la lateralite, et quand il suppose
|
| 17 |
+
mal l'objet sort avec un visage sculpte a l'avant ET a l'arriere. Mesure sur
|
| 18 |
+
quatre sujets : les quatre demandent l'inverse de la convention. Le moteur
|
| 19 |
+
tourne encore sans ce dossier, mais il devine, et il le dit dans son resultat.
|
| 20 |
+
A ne pas retirer pour gagner 517 Mo.
|
| 21 |
+
|
| 22 |
+
Chaque .safetensors va avec son .json, qui decrit l'architecture : sans lui le
|
| 23 |
+
modele ne peut pas etre construit.
|
multivue/cameras/source/src/depth_anything_3/model/gs_adapter.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Optional
|
| 16 |
+
import torch
|
| 17 |
+
from einops import einsum, rearrange, repeat
|
| 18 |
+
from torch import nn
|
| 19 |
+
|
| 20 |
+
from depth_anything_3.model.utils.transform import cam_quat_xyzw_to_world_quat_wxyz
|
| 21 |
+
from depth_anything_3.specs import Gaussians
|
| 22 |
+
from depth_anything_3.utils.geometry import affine_inverse, get_world_rays, sample_image_grid
|
| 23 |
+
from depth_anything_3.utils.pose_align import batch_align_poses_umeyama
|
| 24 |
+
from depth_anything_3.utils.sh_helpers import rotate_sh
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class GaussianAdapter(nn.Module):
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
sh_degree: int = 0,
|
| 32 |
+
pred_color: bool = False,
|
| 33 |
+
pred_offset_depth: bool = False,
|
| 34 |
+
pred_offset_xy: bool = True,
|
| 35 |
+
gaussian_scale_min: float = 1e-5,
|
| 36 |
+
gaussian_scale_max: float = 30.0,
|
| 37 |
+
):
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.sh_degree = sh_degree
|
| 40 |
+
self.pred_color = pred_color
|
| 41 |
+
self.pred_offset_depth = pred_offset_depth
|
| 42 |
+
self.pred_offset_xy = pred_offset_xy
|
| 43 |
+
self.gaussian_scale_min = gaussian_scale_min
|
| 44 |
+
self.gaussian_scale_max = gaussian_scale_max
|
| 45 |
+
|
| 46 |
+
# Create a mask for the spherical harmonics coefficients. This ensures that at
|
| 47 |
+
# initialization, the coefficients are biased towards having a large DC
|
| 48 |
+
# component and small view-dependent components.
|
| 49 |
+
if not pred_color:
|
| 50 |
+
self.register_buffer(
|
| 51 |
+
"sh_mask",
|
| 52 |
+
torch.ones((self.d_sh,), dtype=torch.float32),
|
| 53 |
+
persistent=False,
|
| 54 |
+
)
|
| 55 |
+
for degree in range(1, sh_degree + 1):
|
| 56 |
+
self.sh_mask[degree**2 : (degree + 1) ** 2] = 0.1 * 0.25**degree
|
| 57 |
+
|
| 58 |
+
def forward(
|
| 59 |
+
self,
|
| 60 |
+
extrinsics: torch.Tensor, # "*#batch 4 4"
|
| 61 |
+
intrinsics: torch.Tensor, # "*#batch 3 3"
|
| 62 |
+
depths: torch.Tensor, # "*#batch"
|
| 63 |
+
opacities: torch.Tensor, # "*#batch" | "*#batch _"
|
| 64 |
+
raw_gaussians: torch.Tensor, # "*#batch _"
|
| 65 |
+
image_shape: tuple[int, int],
|
| 66 |
+
eps: float = 1e-8,
|
| 67 |
+
gt_extrinsics: Optional[torch.Tensor] = None, # "*#batch 4 4"
|
| 68 |
+
**kwargs,
|
| 69 |
+
) -> Gaussians:
|
| 70 |
+
device = extrinsics.device
|
| 71 |
+
dtype = raw_gaussians.dtype
|
| 72 |
+
H, W = image_shape
|
| 73 |
+
b, v = raw_gaussians.shape[:2]
|
| 74 |
+
|
| 75 |
+
# get cam2worlds and intr_normed to adapt to 3DGS codebase
|
| 76 |
+
cam2worlds = affine_inverse(extrinsics)
|
| 77 |
+
intr_normed = intrinsics.clone().detach()
|
| 78 |
+
intr_normed[..., 0, :] /= W
|
| 79 |
+
intr_normed[..., 1, :] /= H
|
| 80 |
+
|
| 81 |
+
# 1. compute 3DGS means
|
| 82 |
+
# 1.1) offset the predicted depth if needed
|
| 83 |
+
if self.pred_offset_depth:
|
| 84 |
+
gs_depths = depths + raw_gaussians[..., -1]
|
| 85 |
+
raw_gaussians = raw_gaussians[..., :-1]
|
| 86 |
+
else:
|
| 87 |
+
gs_depths = depths
|
| 88 |
+
# 1.2) align predicted poses with GT if needed
|
| 89 |
+
if gt_extrinsics is not None and not torch.equal(extrinsics, gt_extrinsics):
|
| 90 |
+
try:
|
| 91 |
+
_, _, pose_scales = batch_align_poses_umeyama(
|
| 92 |
+
gt_extrinsics.detach().float(),
|
| 93 |
+
extrinsics.detach().float(),
|
| 94 |
+
)
|
| 95 |
+
except Exception:
|
| 96 |
+
pose_scales = torch.ones_like(extrinsics[:, 0, 0, 0])
|
| 97 |
+
pose_scales = torch.clamp(pose_scales, min=1 / 3.0, max=3.0)
|
| 98 |
+
cam2worlds[:, :, :3, 3] = cam2worlds[:, :, :3, 3] * rearrange(
|
| 99 |
+
pose_scales, "b -> b () ()"
|
| 100 |
+
) # [b, i, j]
|
| 101 |
+
gs_depths = gs_depths * rearrange(pose_scales, "b -> b () () ()") # [b, v, h, w]
|
| 102 |
+
# 1.3) casting xy in image space
|
| 103 |
+
xy_ray, _ = sample_image_grid((H, W), device)
|
| 104 |
+
xy_ray = xy_ray[None, None, ...].expand(b, v, -1, -1, -1) # b v h w xy
|
| 105 |
+
# offset xy if needed
|
| 106 |
+
if self.pred_offset_xy:
|
| 107 |
+
pixel_size = 1 / torch.tensor((W, H), dtype=xy_ray.dtype, device=device)
|
| 108 |
+
offset_xy = raw_gaussians[..., :2]
|
| 109 |
+
xy_ray = xy_ray + offset_xy * pixel_size
|
| 110 |
+
raw_gaussians = raw_gaussians[..., 2:] # skip the offset_xy
|
| 111 |
+
# 1.4) unproject depth + xy to world ray
|
| 112 |
+
origins, directions = get_world_rays(
|
| 113 |
+
xy_ray,
|
| 114 |
+
repeat(cam2worlds, "b v i j -> b v h w i j", h=H, w=W),
|
| 115 |
+
repeat(intr_normed, "b v i j -> b v h w i j", h=H, w=W),
|
| 116 |
+
)
|
| 117 |
+
gs_means_world = origins + directions * gs_depths[..., None]
|
| 118 |
+
gs_means_world = rearrange(gs_means_world, "b v h w d -> b (v h w) d")
|
| 119 |
+
|
| 120 |
+
# 2. compute other GS attributes
|
| 121 |
+
scales, rotations, sh = raw_gaussians.split((3, 4, 3 * self.d_sh), dim=-1)
|
| 122 |
+
|
| 123 |
+
# 2.1) 3DGS scales
|
| 124 |
+
# make the scale invarient to resolution
|
| 125 |
+
scale_min = self.gaussian_scale_min
|
| 126 |
+
scale_max = self.gaussian_scale_max
|
| 127 |
+
scales = scale_min + (scale_max - scale_min) * scales.sigmoid()
|
| 128 |
+
pixel_size = 1 / torch.tensor((W, H), dtype=dtype, device=device)
|
| 129 |
+
multiplier = self.get_scale_multiplier(intr_normed, pixel_size)
|
| 130 |
+
gs_scales = scales * gs_depths[..., None] * multiplier[..., None, None, None]
|
| 131 |
+
gs_scales = rearrange(gs_scales, "b v h w d -> b (v h w) d")
|
| 132 |
+
|
| 133 |
+
# 2.2) 3DGS quaternion (world space)
|
| 134 |
+
# due to historical issue, assume quaternion in order xyzw, not wxyz
|
| 135 |
+
# Normalize the quaternion features to yield a valid quaternion.
|
| 136 |
+
rotations = rotations / (rotations.norm(dim=-1, keepdim=True) + eps)
|
| 137 |
+
# rotate them to world space
|
| 138 |
+
cam_quat_xyzw = rearrange(rotations, "b v h w c -> b (v h w) c")
|
| 139 |
+
c2w_mat = repeat(
|
| 140 |
+
cam2worlds,
|
| 141 |
+
"b v i j -> b (v h w) i j",
|
| 142 |
+
h=H,
|
| 143 |
+
w=W,
|
| 144 |
+
)
|
| 145 |
+
world_quat_wxyz = cam_quat_xyzw_to_world_quat_wxyz(cam_quat_xyzw, c2w_mat)
|
| 146 |
+
gs_rotations_world = world_quat_wxyz # b (v h w) c
|
| 147 |
+
|
| 148 |
+
# 2.3) 3DGS color / SH coefficient (world space)
|
| 149 |
+
sh = rearrange(sh, "... (xyz d_sh) -> ... xyz d_sh", xyz=3)
|
| 150 |
+
if not self.pred_color:
|
| 151 |
+
sh = sh * self.sh_mask
|
| 152 |
+
|
| 153 |
+
if self.pred_color or self.sh_degree == 0:
|
| 154 |
+
# predict pre-computed color or predict only DC band, no need to transform
|
| 155 |
+
gs_sh_world = sh
|
| 156 |
+
else:
|
| 157 |
+
gs_sh_world = rotate_sh(sh, cam2worlds[:, :, None, None, None, :3, :3])
|
| 158 |
+
gs_sh_world = rearrange(gs_sh_world, "b v h w xyz d_sh -> b (v h w) xyz d_sh")
|
| 159 |
+
|
| 160 |
+
# 2.4) 3DGS opacity
|
| 161 |
+
gs_opacities = rearrange(opacities, "b v h w ... -> b (v h w) ...")
|
| 162 |
+
|
| 163 |
+
return Gaussians(
|
| 164 |
+
means=gs_means_world,
|
| 165 |
+
harmonics=gs_sh_world,
|
| 166 |
+
opacities=gs_opacities,
|
| 167 |
+
scales=gs_scales,
|
| 168 |
+
rotations=gs_rotations_world,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def get_scale_multiplier(
|
| 172 |
+
self,
|
| 173 |
+
intrinsics: torch.Tensor, # "*#batch 3 3"
|
| 174 |
+
pixel_size: torch.Tensor, # "*#batch 2"
|
| 175 |
+
multiplier: float = 0.1,
|
| 176 |
+
) -> torch.Tensor: # " *batch"
|
| 177 |
+
xy_multipliers = multiplier * einsum(
|
| 178 |
+
intrinsics[..., :2, :2].float().inverse().to(intrinsics),
|
| 179 |
+
pixel_size,
|
| 180 |
+
"... i j, j -> ... i",
|
| 181 |
+
)
|
| 182 |
+
return xy_multipliers.sum(dim=-1)
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def d_sh(self) -> int:
|
| 186 |
+
return 1 if self.pred_color else (self.sh_degree + 1) ** 2
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def d_in(self) -> int:
|
| 190 |
+
# provided as reference to the gs_dpt output dim
|
| 191 |
+
raw_gs_dim = 0
|
| 192 |
+
if self.pred_offset_xy:
|
| 193 |
+
raw_gs_dim += 2
|
| 194 |
+
raw_gs_dim += 3 # scales
|
| 195 |
+
raw_gs_dim += 4 # quaternion
|
| 196 |
+
raw_gs_dim += 3 * self.d_sh # color
|
| 197 |
+
if self.pred_offset_depth:
|
| 198 |
+
raw_gs_dim += 1
|
| 199 |
+
|
| 200 |
+
return raw_gs_dim
|
multivue/cameras/source/src/depth_anything_3/model/gsdpt.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Dict as TyDict
|
| 16 |
+
from typing import List, Sequence
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
|
| 20 |
+
from depth_anything_3.model.dpt import DPT
|
| 21 |
+
from depth_anything_3.model.utils.head_utils import activate_head_gs, custom_interpolate
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class GSDPT(DPT):
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
dim_in: int,
|
| 29 |
+
patch_size: int = 14,
|
| 30 |
+
output_dim: int = 4,
|
| 31 |
+
activation: str = "linear",
|
| 32 |
+
conf_activation: str = "sigmoid",
|
| 33 |
+
features: int = 256,
|
| 34 |
+
out_channels: Sequence[int] = (256, 512, 1024, 1024),
|
| 35 |
+
pos_embed: bool = True,
|
| 36 |
+
feature_only: bool = False,
|
| 37 |
+
down_ratio: int = 1,
|
| 38 |
+
conf_dim: int = 1,
|
| 39 |
+
norm_type: str = "idt", # use to match legacy GS-DPT head, "idt" / "layer"
|
| 40 |
+
fusion_block_inplace: bool = False,
|
| 41 |
+
) -> None:
|
| 42 |
+
super().__init__(
|
| 43 |
+
dim_in=dim_in,
|
| 44 |
+
patch_size=patch_size,
|
| 45 |
+
output_dim=output_dim,
|
| 46 |
+
activation=activation,
|
| 47 |
+
conf_activation=conf_activation,
|
| 48 |
+
features=features,
|
| 49 |
+
out_channels=out_channels,
|
| 50 |
+
pos_embed=pos_embed,
|
| 51 |
+
down_ratio=down_ratio,
|
| 52 |
+
head_name="raw_gs",
|
| 53 |
+
use_sky_head=False,
|
| 54 |
+
norm_type=norm_type,
|
| 55 |
+
fusion_block_inplace=fusion_block_inplace,
|
| 56 |
+
)
|
| 57 |
+
self.conf_dim = conf_dim
|
| 58 |
+
if conf_dim and conf_dim > 1:
|
| 59 |
+
assert (
|
| 60 |
+
conf_activation == "linear"
|
| 61 |
+
), "use linear prediction when using view-dependent opacity"
|
| 62 |
+
|
| 63 |
+
merger_out_dim = features if feature_only else features // 2
|
| 64 |
+
self.images_merger = nn.Sequential(
|
| 65 |
+
nn.Conv2d(3, merger_out_dim // 4, 3, 1, 1), # fewer channels first
|
| 66 |
+
nn.GELU(),
|
| 67 |
+
nn.Conv2d(merger_out_dim // 4, merger_out_dim // 2, 3, 1, 1),
|
| 68 |
+
nn.GELU(),
|
| 69 |
+
nn.Conv2d(merger_out_dim // 2, merger_out_dim, 3, 1, 1),
|
| 70 |
+
nn.GELU(),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# -------------------------------------------------------------------------
|
| 74 |
+
# Internal forward (single chunk)
|
| 75 |
+
# -------------------------------------------------------------------------
|
| 76 |
+
def _forward_impl(
|
| 77 |
+
self,
|
| 78 |
+
feats: List[torch.Tensor],
|
| 79 |
+
H: int,
|
| 80 |
+
W: int,
|
| 81 |
+
patch_start_idx: int,
|
| 82 |
+
images: torch.Tensor,
|
| 83 |
+
) -> TyDict[str, torch.Tensor]:
|
| 84 |
+
B, _, C = feats[0].shape
|
| 85 |
+
ph, pw = H // self.patch_size, W // self.patch_size
|
| 86 |
+
resized_feats = []
|
| 87 |
+
for stage_idx, take_idx in enumerate(self.intermediate_layer_idx):
|
| 88 |
+
x = feats[take_idx][:, patch_start_idx:] # [B*S, N_patch, C]
|
| 89 |
+
x = self.norm(x)
|
| 90 |
+
x = x.permute(0, 2, 1).reshape(B, C, ph, pw) # [B*S, C, ph, pw]
|
| 91 |
+
|
| 92 |
+
x = self.projects[stage_idx](x)
|
| 93 |
+
if self.pos_embed:
|
| 94 |
+
x = self._add_pos_embed(x, W, H)
|
| 95 |
+
x = self.resize_layers[stage_idx](x) # Align scale
|
| 96 |
+
resized_feats.append(x)
|
| 97 |
+
|
| 98 |
+
# 2) Fusion pyramid (main branch only)
|
| 99 |
+
fused = self._fuse(resized_feats)
|
| 100 |
+
fused = self.scratch.output_conv1(fused)
|
| 101 |
+
|
| 102 |
+
# 3) Upsample to target resolution, optionally add position encoding again
|
| 103 |
+
h_out = int(ph * self.patch_size / self.down_ratio)
|
| 104 |
+
w_out = int(pw * self.patch_size / self.down_ratio)
|
| 105 |
+
|
| 106 |
+
fused = custom_interpolate(fused, (h_out, w_out), mode="bilinear", align_corners=True)
|
| 107 |
+
|
| 108 |
+
# inject the image information here
|
| 109 |
+
fused = fused + self.images_merger(images)
|
| 110 |
+
|
| 111 |
+
if self.pos_embed:
|
| 112 |
+
fused = self._add_pos_embed(fused, W, H)
|
| 113 |
+
|
| 114 |
+
# 4) Shared neck1
|
| 115 |
+
# feat = self.scratch.output_conv1(fused)
|
| 116 |
+
feat = fused
|
| 117 |
+
|
| 118 |
+
# 5) Main head: logits -> activate_head or single channel activation
|
| 119 |
+
main_logits = self.scratch.output_conv2(feat)
|
| 120 |
+
outs: TyDict[str, torch.Tensor] = {}
|
| 121 |
+
if self.has_conf:
|
| 122 |
+
pred, conf = activate_head_gs(
|
| 123 |
+
main_logits,
|
| 124 |
+
activation=self.activation,
|
| 125 |
+
conf_activation=self.conf_activation,
|
| 126 |
+
conf_dim=self.conf_dim,
|
| 127 |
+
)
|
| 128 |
+
outs[self.head_main] = pred.squeeze(1)
|
| 129 |
+
outs[f"{self.head_main}_conf"] = conf.squeeze(1)
|
| 130 |
+
else:
|
| 131 |
+
outs[self.head_main] = self._apply_activation_single(main_logits).squeeze(1)
|
| 132 |
+
|
| 133 |
+
return outs
|
multivue/cameras/source/src/depth_anything_3/model/reference_view_selector.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Reference View Selection Strategies
|
| 17 |
+
|
| 18 |
+
This module provides different strategies for selecting a reference view
|
| 19 |
+
from multiple input views in multi-view depth estimation.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
from typing import Literal
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
RefViewStrategy = Literal["first", "middle", "saddle_balanced", "saddle_sim_range"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def select_reference_view(
|
| 30 |
+
x: torch.Tensor,
|
| 31 |
+
strategy: RefViewStrategy = "saddle_balanced",
|
| 32 |
+
) -> torch.Tensor:
|
| 33 |
+
"""
|
| 34 |
+
Select a reference view from multiple views using the specified strategy.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
x: Input tensor of shape (B, S, N, C) where
|
| 38 |
+
B = batch size
|
| 39 |
+
S = number of views
|
| 40 |
+
N = number of tokens
|
| 41 |
+
C = channel dimension
|
| 42 |
+
strategy: Selection strategy, one of:
|
| 43 |
+
- "first": Always select the first view
|
| 44 |
+
- "middle": Select the middle view
|
| 45 |
+
- "saddle_balanced": Select view with balanced features across multiple metrics
|
| 46 |
+
- "saddle_sim_range": Select view with largest similarity range
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
b_idx: Tensor of shape (B,) containing the selected view index for each batch
|
| 50 |
+
"""
|
| 51 |
+
B, S, N, C = x.shape
|
| 52 |
+
|
| 53 |
+
# For single view, no reordering needed
|
| 54 |
+
if S <= 1:
|
| 55 |
+
return torch.zeros(B, dtype=torch.long, device=x.device)
|
| 56 |
+
|
| 57 |
+
# Simple position-based strategies
|
| 58 |
+
if strategy == "first":
|
| 59 |
+
return torch.zeros(B, dtype=torch.long, device=x.device)
|
| 60 |
+
|
| 61 |
+
elif strategy == "middle":
|
| 62 |
+
return torch.full((B,), S // 2, dtype=torch.long, device=x.device)
|
| 63 |
+
|
| 64 |
+
# Feature-based strategies require normalized class tokens
|
| 65 |
+
# Extract and normalize class tokens (first token of each view)
|
| 66 |
+
img_class_feat = x[:, :, 0] / x[:, :, 0].norm(dim=-1, keepdim=True) # B S C
|
| 67 |
+
|
| 68 |
+
if strategy == "saddle_balanced":
|
| 69 |
+
# Select view with balanced features across multiple metrics
|
| 70 |
+
# Compute similarity matrix
|
| 71 |
+
sim = torch.matmul(img_class_feat, img_class_feat.transpose(1, 2)) # B S S
|
| 72 |
+
sim_no_diag = sim - torch.eye(S, device=sim.device).unsqueeze(0)
|
| 73 |
+
sim_score = sim_no_diag.sum(dim=-1) / (S - 1) # B S
|
| 74 |
+
|
| 75 |
+
feat_norm = x[:, :, 0].norm(dim=-1) # B S
|
| 76 |
+
feat_var = img_class_feat.var(dim=-1) # B S
|
| 77 |
+
|
| 78 |
+
# Normalize all metrics to [0, 1]
|
| 79 |
+
def normalize_metric(metric):
|
| 80 |
+
min_val = metric.min(dim=1, keepdim=True).values
|
| 81 |
+
max_val = metric.max(dim=1, keepdim=True).values
|
| 82 |
+
return (metric - min_val) / (max_val - min_val + 1e-8)
|
| 83 |
+
|
| 84 |
+
sim_score_norm = normalize_metric(sim_score)
|
| 85 |
+
norm_norm = normalize_metric(feat_norm)
|
| 86 |
+
var_norm = normalize_metric(feat_var)
|
| 87 |
+
|
| 88 |
+
# Select view closest to the median (0.5) across all metrics
|
| 89 |
+
balance_score = (
|
| 90 |
+
(sim_score_norm - 0.5).abs() +
|
| 91 |
+
(norm_norm - 0.5).abs() +
|
| 92 |
+
(var_norm - 0.5).abs()
|
| 93 |
+
)
|
| 94 |
+
b_idx = balance_score.argmin(dim=1)
|
| 95 |
+
|
| 96 |
+
elif strategy == "saddle_sim_range":
|
| 97 |
+
# Select view with largest similarity range (max - min)
|
| 98 |
+
sim = torch.matmul(img_class_feat, img_class_feat.transpose(1, 2)) # B S S
|
| 99 |
+
sim_no_diag = sim - torch.eye(S, device=sim.device).unsqueeze(0)
|
| 100 |
+
|
| 101 |
+
sim_max = sim_no_diag.max(dim=-1).values # B S
|
| 102 |
+
sim_min = sim_no_diag.min(dim=-1).values # B S
|
| 103 |
+
sim_range = sim_max - sim_min
|
| 104 |
+
b_idx = sim_range.argmax(dim=1)
|
| 105 |
+
|
| 106 |
+
else:
|
| 107 |
+
raise ValueError(
|
| 108 |
+
f"Unknown reference view selection strategy: {strategy}. "
|
| 109 |
+
f"Must be one of: 'first', 'middle', 'saddle_balanced', 'saddle_sim_range'"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
return b_idx
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def reorder_by_reference(
|
| 116 |
+
x: torch.Tensor,
|
| 117 |
+
b_idx: torch.Tensor,
|
| 118 |
+
) -> torch.Tensor:
|
| 119 |
+
"""
|
| 120 |
+
Reorder views to place the selected reference view first.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
x: Input tensor of shape (B, S, N, C)
|
| 124 |
+
b_idx: Reference view indices of shape (B,)
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
Reordered tensor with reference view at position 0
|
| 128 |
+
|
| 129 |
+
Example:
|
| 130 |
+
If b_idx = [2] and S = 5 (views [0,1,2,3,4]),
|
| 131 |
+
result order is [2,0,1,3,4] (ref_idx first, then others in order)
|
| 132 |
+
"""
|
| 133 |
+
B, S = x.shape[0], x.shape[1]
|
| 134 |
+
|
| 135 |
+
# For single view, no reordering needed
|
| 136 |
+
if S <= 1:
|
| 137 |
+
return x
|
| 138 |
+
|
| 139 |
+
# Create position indices: (B, S) where each row is [0, 1, 2, ..., S-1]
|
| 140 |
+
positions = torch.arange(S, device=x.device).unsqueeze(0).expand(B, -1) # B S
|
| 141 |
+
|
| 142 |
+
# For each position, determine which original index it should take
|
| 143 |
+
# Position 0 gets ref_idx
|
| 144 |
+
# Position 1 to ref_idx gets indices 0 to ref_idx-1
|
| 145 |
+
# Position ref_idx+1 to S-1 gets indices ref_idx+1 to S-1
|
| 146 |
+
|
| 147 |
+
b_idx_expanded = b_idx.unsqueeze(1) # B 1
|
| 148 |
+
|
| 149 |
+
# Create the reordering indices
|
| 150 |
+
# For positions 1 to ref_idx: map to indices 0 to ref_idx-1 (shift by -1)
|
| 151 |
+
# For positions > ref_idx: keep the same
|
| 152 |
+
reorder_indices = positions.clone()
|
| 153 |
+
reorder_indices = torch.where(
|
| 154 |
+
(positions > 0) & (positions <= b_idx_expanded),
|
| 155 |
+
positions - 1,
|
| 156 |
+
positions
|
| 157 |
+
)
|
| 158 |
+
# Set position 0 to ref_idx
|
| 159 |
+
reorder_indices[:, 0] = b_idx
|
| 160 |
+
|
| 161 |
+
# Gather using advanced indexing
|
| 162 |
+
batch_indices = torch.arange(B, device=x.device).unsqueeze(1) # B 1
|
| 163 |
+
x_reordered = x[batch_indices, reorder_indices]
|
| 164 |
+
|
| 165 |
+
return x_reordered
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def restore_original_order(
|
| 169 |
+
x: torch.Tensor,
|
| 170 |
+
b_idx: torch.Tensor,
|
| 171 |
+
) -> torch.Tensor:
|
| 172 |
+
"""
|
| 173 |
+
Restore original view order after processing.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
x: Reordered tensor of shape (B, S, ...)
|
| 177 |
+
b_idx: Original reference view indices of shape (B,)
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
Tensor with original view order restored
|
| 181 |
+
|
| 182 |
+
Example:
|
| 183 |
+
If original order was [0, 1, 2, 3, 4] and b_idx=2,
|
| 184 |
+
reordered becomes [2, 0, 1, 3, 4] (reference at position 0),
|
| 185 |
+
restore should return [0, 1, 2, 3, 4] (original order).
|
| 186 |
+
"""
|
| 187 |
+
B, S = x.shape[0], x.shape[1]
|
| 188 |
+
|
| 189 |
+
# For single view, no restoration needed
|
| 190 |
+
if S <= 1:
|
| 191 |
+
return x
|
| 192 |
+
|
| 193 |
+
# Create target position indices: (B, S) where each row is [0, 1, 2, ..., S-1]
|
| 194 |
+
target_positions = torch.arange(S, device=x.device).unsqueeze(0).expand(B, -1) # B S
|
| 195 |
+
|
| 196 |
+
# For each target position, determine which current position it comes from
|
| 197 |
+
# Target position 0 to ref_idx-1 <- Current position 1 to ref_idx (shift by +1)
|
| 198 |
+
# Target position ref_idx <- Current position 0
|
| 199 |
+
# Target position ref_idx+1 to S-1 <- Current position ref_idx+1 to S-1 (no change)
|
| 200 |
+
|
| 201 |
+
b_idx_expanded = b_idx.unsqueeze(1) # B 1
|
| 202 |
+
|
| 203 |
+
# Create the restore indices
|
| 204 |
+
restore_indices = torch.where(
|
| 205 |
+
target_positions < b_idx_expanded,
|
| 206 |
+
target_positions + 1, # Positions before ref_idx come from current position + 1
|
| 207 |
+
target_positions # Positions after ref_idx stay the same
|
| 208 |
+
)
|
| 209 |
+
# Target position = ref_idx comes from current position 0
|
| 210 |
+
# Use scatter to set specific positions
|
| 211 |
+
restore_indices = torch.scatter(
|
| 212 |
+
restore_indices,
|
| 213 |
+
dim=1,
|
| 214 |
+
index=b_idx_expanded,
|
| 215 |
+
src=torch.zeros_like(b_idx_expanded)
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Gather using advanced indexing
|
| 219 |
+
batch_indices = torch.arange(B, device=x.device).unsqueeze(1) # B 1
|
| 220 |
+
x_restored = x[batch_indices, restore_indices]
|
| 221 |
+
|
| 222 |
+
return x_restored
|
| 223 |
+
|
multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/attention.cpython-312.pyc
ADDED
|
Binary file (5.98 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/block.cpython-312.pyc
ADDED
|
Binary file (3.33 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/head_utils.cpython-312.pyc
ADDED
|
Binary file (9.25 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/model/utils/__pycache__/transform.cpython-312.pyc
ADDED
|
Binary file (8.71 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/model/utils/attention.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
# Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110 # noqa
|
| 16 |
+
|
| 17 |
+
from typing import Callable, Optional, Union
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from torch import Tensor, nn
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Attention(nn.Module):
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
dim: int,
|
| 27 |
+
num_heads: int = 8,
|
| 28 |
+
qkv_bias: bool = True,
|
| 29 |
+
proj_bias: bool = True,
|
| 30 |
+
attn_drop: float = 0.0,
|
| 31 |
+
proj_drop: float = 0.0,
|
| 32 |
+
norm_layer: nn.Module = nn.LayerNorm,
|
| 33 |
+
qk_norm: bool = False,
|
| 34 |
+
rope=None,
|
| 35 |
+
) -> None:
|
| 36 |
+
super().__init__()
|
| 37 |
+
assert dim % num_heads == 0, "dim should be divisible by num_heads"
|
| 38 |
+
self.num_heads = num_heads
|
| 39 |
+
self.head_dim = dim // num_heads
|
| 40 |
+
self.scale = self.head_dim**-0.5
|
| 41 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
| 42 |
+
self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
| 43 |
+
self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
| 44 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 45 |
+
self.proj = nn.Linear(dim, dim, bias=proj_bias)
|
| 46 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 47 |
+
self.rope = rope
|
| 48 |
+
|
| 49 |
+
def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor:
|
| 50 |
+
# Debug breakpoint removed for production
|
| 51 |
+
B, N, C = x.shape
|
| 52 |
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
|
| 53 |
+
q, k, v = qkv.unbind(0)
|
| 54 |
+
q, k = self.q_norm(q), self.k_norm(k)
|
| 55 |
+
q = self.rope(q, pos) if self.rope is not None else q
|
| 56 |
+
k = self.rope(k, pos) if self.rope is not None else k
|
| 57 |
+
x = F.scaled_dot_product_attention(
|
| 58 |
+
q,
|
| 59 |
+
k,
|
| 60 |
+
v,
|
| 61 |
+
dropout_p=self.attn_drop.p if self.training else 0.0,
|
| 62 |
+
attn_mask=attn_mask,
|
| 63 |
+
)
|
| 64 |
+
x = x.transpose(1, 2).reshape(B, N, C)
|
| 65 |
+
x = self.proj(x)
|
| 66 |
+
x = self.proj_drop(x)
|
| 67 |
+
return x
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class LayerScale(nn.Module):
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
dim: int,
|
| 74 |
+
init_values: Union[float, Tensor] = 1e-5,
|
| 75 |
+
inplace: bool = False,
|
| 76 |
+
) -> None:
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.inplace = inplace
|
| 79 |
+
self.gamma = nn.Parameter(init_values * torch.ones(dim))
|
| 80 |
+
|
| 81 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 82 |
+
return x.mul_(self.gamma) if self.inplace else x * self.gamma
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class Mlp(nn.Module):
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
in_features: int,
|
| 89 |
+
hidden_features: Optional[int] = None,
|
| 90 |
+
out_features: Optional[int] = None,
|
| 91 |
+
act_layer: Callable[..., nn.Module] = nn.GELU,
|
| 92 |
+
drop: float = 0.0,
|
| 93 |
+
bias: bool = True,
|
| 94 |
+
) -> None:
|
| 95 |
+
super().__init__()
|
| 96 |
+
out_features = out_features or in_features
|
| 97 |
+
hidden_features = hidden_features or in_features
|
| 98 |
+
self.fc1 = nn.Linear(in_features, hidden_features, bias=bias)
|
| 99 |
+
self.act = act_layer()
|
| 100 |
+
self.fc2 = nn.Linear(hidden_features, out_features, bias=bias)
|
| 101 |
+
self.drop = nn.Dropout(drop)
|
| 102 |
+
|
| 103 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 104 |
+
x = self.fc1(x)
|
| 105 |
+
x = self.act(x)
|
| 106 |
+
x = self.drop(x)
|
| 107 |
+
x = self.fc2(x)
|
| 108 |
+
x = self.drop(x)
|
| 109 |
+
return x
|
multivue/cameras/source/src/depth_anything_3/model/utils/block.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from typing import Callable
|
| 17 |
+
from torch import Tensor, nn
|
| 18 |
+
|
| 19 |
+
from .attention import Attention, LayerScale, Mlp
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Block(nn.Module):
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
dim: int,
|
| 26 |
+
num_heads: int,
|
| 27 |
+
mlp_ratio: float = 4.0,
|
| 28 |
+
qkv_bias: bool = True,
|
| 29 |
+
proj_bias: bool = True,
|
| 30 |
+
ffn_bias: bool = True,
|
| 31 |
+
drop: float = 0.0,
|
| 32 |
+
attn_drop: float = 0.0,
|
| 33 |
+
init_values=None,
|
| 34 |
+
drop_path: float = 0.0,
|
| 35 |
+
act_layer: Callable[..., nn.Module] = nn.GELU,
|
| 36 |
+
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
|
| 37 |
+
attn_class: Callable[..., nn.Module] = Attention,
|
| 38 |
+
ffn_layer: Callable[..., nn.Module] = Mlp,
|
| 39 |
+
qk_norm: bool = False,
|
| 40 |
+
rope=None,
|
| 41 |
+
) -> None:
|
| 42 |
+
super().__init__()
|
| 43 |
+
|
| 44 |
+
self.norm1 = norm_layer(dim)
|
| 45 |
+
|
| 46 |
+
self.attn = attn_class(
|
| 47 |
+
dim,
|
| 48 |
+
num_heads=num_heads,
|
| 49 |
+
qkv_bias=qkv_bias,
|
| 50 |
+
proj_bias=proj_bias,
|
| 51 |
+
attn_drop=attn_drop,
|
| 52 |
+
proj_drop=drop,
|
| 53 |
+
qk_norm=qk_norm,
|
| 54 |
+
rope=rope,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
| 58 |
+
self.norm2 = norm_layer(dim)
|
| 59 |
+
mlp_hidden_dim = int(dim * mlp_ratio)
|
| 60 |
+
self.mlp = ffn_layer(
|
| 61 |
+
in_features=dim,
|
| 62 |
+
hidden_features=mlp_hidden_dim,
|
| 63 |
+
act_layer=act_layer,
|
| 64 |
+
drop=drop,
|
| 65 |
+
bias=ffn_bias,
|
| 66 |
+
)
|
| 67 |
+
self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
| 68 |
+
|
| 69 |
+
self.sample_drop_ratio = 0.0 # Equivalent to always having drop_path=0
|
| 70 |
+
|
| 71 |
+
def forward(self, x: Tensor, pos=None, attn_mask=None) -> Tensor:
|
| 72 |
+
def attn_residual_func(x: Tensor, pos=None, attn_mask=None) -> Tensor:
|
| 73 |
+
return self.ls1(self.attn(self.norm1(x), pos=pos, attn_mask=attn_mask))
|
| 74 |
+
|
| 75 |
+
def ffn_residual_func(x: Tensor) -> Tensor:
|
| 76 |
+
return self.ls2(self.mlp(self.norm2(x)))
|
| 77 |
+
|
| 78 |
+
# drop_path is always 0, so always take the else branch
|
| 79 |
+
x = x + attn_residual_func(x, pos=pos, attn_mask=attn_mask)
|
| 80 |
+
x = x + ffn_residual_func(x)
|
| 81 |
+
return x
|
multivue/cameras/source/src/depth_anything_3/model/utils/gs_renderer.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from math import isqrt
|
| 17 |
+
from typing import Literal, Optional
|
| 18 |
+
import torch
|
| 19 |
+
from einops import rearrange, repeat
|
| 20 |
+
from tqdm import tqdm
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.specs import Gaussians
|
| 23 |
+
from depth_anything_3.utils.camera_trj_helpers import (
|
| 24 |
+
interpolate_extrinsics,
|
| 25 |
+
interpolate_intrinsics,
|
| 26 |
+
render_dolly_zoom_path,
|
| 27 |
+
render_stabilization_path,
|
| 28 |
+
render_wander_path,
|
| 29 |
+
render_wobble_inter_path,
|
| 30 |
+
)
|
| 31 |
+
from depth_anything_3.utils.geometry import affine_inverse, as_homogeneous, get_fov
|
| 32 |
+
from depth_anything_3.utils.logger import logger
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
from gsplat import rasterization
|
| 36 |
+
except ImportError:
|
| 37 |
+
logger.warn(
|
| 38 |
+
"Dependency `gsplat` is required for rendering 3DGS. "
|
| 39 |
+
"Install via: pip install git+https://github.com/nerfstudio-project/"
|
| 40 |
+
"gsplat.git@0b4dddf04cb687367602c01196913cde6a743d70"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def render_3dgs(
|
| 45 |
+
extrinsics: torch.Tensor, # "batch_views 4 4", w2c
|
| 46 |
+
intrinsics: torch.Tensor, # "batch_views 3 3", normalized
|
| 47 |
+
image_shape: tuple[int, int],
|
| 48 |
+
gaussian: Gaussians,
|
| 49 |
+
background_color: Optional[torch.Tensor] = None, # "batch_views 3"
|
| 50 |
+
use_sh: bool = True,
|
| 51 |
+
num_view: int = 1,
|
| 52 |
+
color_mode: Literal["RGB+D", "RGB+ED"] = "RGB+D",
|
| 53 |
+
**kwargs,
|
| 54 |
+
) -> tuple[
|
| 55 |
+
torch.Tensor, # "batch_views 3 height width"
|
| 56 |
+
torch.Tensor, # "batch_views height width"
|
| 57 |
+
]:
|
| 58 |
+
# extract gaussian params
|
| 59 |
+
gaussian_means = gaussian.means
|
| 60 |
+
gaussian_scales = gaussian.scales
|
| 61 |
+
gaussian_quats = gaussian.rotations
|
| 62 |
+
gaussian_opacities = gaussian.opacities
|
| 63 |
+
gaussian_sh_coefficients = gaussian.harmonics
|
| 64 |
+
b, _, _ = extrinsics.shape
|
| 65 |
+
|
| 66 |
+
if background_color is None:
|
| 67 |
+
background_color = repeat(torch.tensor([0.0, 0.0, 0.0]), "c -> b c", b=b).to(
|
| 68 |
+
gaussian_sh_coefficients
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if use_sh:
|
| 72 |
+
_, _, _, n = gaussian_sh_coefficients.shape
|
| 73 |
+
degree = isqrt(n) - 1
|
| 74 |
+
shs = rearrange(gaussian_sh_coefficients, "b g xyz n -> b g n xyz").contiguous()
|
| 75 |
+
else: # use color
|
| 76 |
+
shs = (
|
| 77 |
+
gaussian_sh_coefficients.squeeze(-1).sigmoid().contiguous()
|
| 78 |
+
) # (b, g, c), normed to (0, 1)
|
| 79 |
+
|
| 80 |
+
h, w = image_shape
|
| 81 |
+
|
| 82 |
+
fov_x, fov_y = get_fov(intrinsics).unbind(dim=-1)
|
| 83 |
+
tan_fov_x = (0.5 * fov_x).tan()
|
| 84 |
+
tan_fov_y = (0.5 * fov_y).tan()
|
| 85 |
+
focal_length_x = w / (2 * tan_fov_x)
|
| 86 |
+
focal_length_y = h / (2 * tan_fov_y)
|
| 87 |
+
|
| 88 |
+
view_matrix = extrinsics.float()
|
| 89 |
+
|
| 90 |
+
all_images = []
|
| 91 |
+
all_radii = []
|
| 92 |
+
all_depths = []
|
| 93 |
+
# render view in a batch based, each batch contains one scene
|
| 94 |
+
# assume the Gaussian parameters are originally repeated along the view dim
|
| 95 |
+
batch_scene = b // num_view
|
| 96 |
+
|
| 97 |
+
def index_i_gs_attr(full_attr, idx):
|
| 98 |
+
# return rearrange(full_attr, "(b v) ... -> b v ...", v=num_view)[idx, 0]
|
| 99 |
+
return full_attr[idx]
|
| 100 |
+
|
| 101 |
+
for i in range(batch_scene):
|
| 102 |
+
K = repeat(
|
| 103 |
+
torch.tensor(
|
| 104 |
+
[
|
| 105 |
+
[0, 0, w / 2.0],
|
| 106 |
+
[0, 0, h / 2.0],
|
| 107 |
+
[0, 0, 1],
|
| 108 |
+
]
|
| 109 |
+
),
|
| 110 |
+
"i j -> v i j",
|
| 111 |
+
v=num_view,
|
| 112 |
+
).to(gaussian_means)
|
| 113 |
+
K[:, 0, 0] = focal_length_x.reshape(batch_scene, num_view)[i]
|
| 114 |
+
K[:, 1, 1] = focal_length_y.reshape(batch_scene, num_view)[i]
|
| 115 |
+
|
| 116 |
+
i_means = index_i_gs_attr(gaussian_means, i) # [N, 3]
|
| 117 |
+
i_scales = index_i_gs_attr(gaussian_scales, i)
|
| 118 |
+
i_quats = index_i_gs_attr(gaussian_quats, i)
|
| 119 |
+
i_opacities = index_i_gs_attr(gaussian_opacities, i) # [N,]
|
| 120 |
+
i_colors = index_i_gs_attr(shs, i) # [N, K, 3]
|
| 121 |
+
i_viewmats = rearrange(view_matrix, "(b v) ... -> b v ...", v=num_view)[i] # [v, 4, 4]
|
| 122 |
+
i_backgrounds = rearrange(background_color, "(b v) ... -> b v ...", v=num_view)[
|
| 123 |
+
i
|
| 124 |
+
] # [v, 3]
|
| 125 |
+
|
| 126 |
+
render_colors, render_alphas, info = rasterization(
|
| 127 |
+
means=i_means,
|
| 128 |
+
quats=i_quats, # [N, 4]
|
| 129 |
+
scales=i_scales, # [N, 3]
|
| 130 |
+
opacities=i_opacities,
|
| 131 |
+
colors=i_colors,
|
| 132 |
+
viewmats=i_viewmats, # [v, 4, 4]
|
| 133 |
+
Ks=K, # [v, 3, 3]
|
| 134 |
+
backgrounds=i_backgrounds,
|
| 135 |
+
render_mode=color_mode,
|
| 136 |
+
width=w,
|
| 137 |
+
height=h,
|
| 138 |
+
packed=False,
|
| 139 |
+
sh_degree=degree if use_sh else None,
|
| 140 |
+
)
|
| 141 |
+
depth = render_colors[..., -1].unbind(dim=0)
|
| 142 |
+
|
| 143 |
+
image = rearrange(render_colors[..., :3], "v h w c -> v c h w").unbind(dim=0)
|
| 144 |
+
radii = info["radii"].unbind(dim=0)
|
| 145 |
+
try:
|
| 146 |
+
info["means2d"].retain_grad() # [1, N, 2]
|
| 147 |
+
except Exception:
|
| 148 |
+
pass
|
| 149 |
+
all_images.extend(image)
|
| 150 |
+
all_depths.extend(depth)
|
| 151 |
+
all_radii.extend(radii)
|
| 152 |
+
|
| 153 |
+
return torch.stack(all_images), torch.stack(all_depths)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def run_renderer_in_chunk_w_trj_mode(
|
| 157 |
+
gaussians: Gaussians,
|
| 158 |
+
extrinsics: torch.Tensor, # world2cam, "batch view 4 4" | "batch view 3 4"
|
| 159 |
+
intrinsics: torch.Tensor, # unnormed intrinsics, "batch view 3 3"
|
| 160 |
+
image_shape: tuple[int, int],
|
| 161 |
+
chunk_size: Optional[int] = 8,
|
| 162 |
+
trj_mode: Literal[
|
| 163 |
+
"original",
|
| 164 |
+
"smooth",
|
| 165 |
+
"interpolate",
|
| 166 |
+
"interpolate_smooth",
|
| 167 |
+
"wander",
|
| 168 |
+
"dolly_zoom",
|
| 169 |
+
"extend",
|
| 170 |
+
"wobble_inter",
|
| 171 |
+
] = "smooth",
|
| 172 |
+
input_shape: Optional[tuple[int, int]] = None,
|
| 173 |
+
enable_tqdm: Optional[bool] = False,
|
| 174 |
+
**kwargs,
|
| 175 |
+
) -> tuple[
|
| 176 |
+
torch.Tensor, # color, "batch view 3 height width"
|
| 177 |
+
torch.Tensor, # depth, "batch view height width"
|
| 178 |
+
]:
|
| 179 |
+
cam2world = affine_inverse(as_homogeneous(extrinsics))
|
| 180 |
+
if input_shape is not None:
|
| 181 |
+
in_h, in_w = input_shape
|
| 182 |
+
else:
|
| 183 |
+
in_h, in_w = image_shape
|
| 184 |
+
intr_normed = intrinsics.clone().detach()
|
| 185 |
+
intr_normed[..., 0, :] /= in_w
|
| 186 |
+
intr_normed[..., 1, :] /= in_h
|
| 187 |
+
if extrinsics.shape[1] <= 1:
|
| 188 |
+
assert trj_mode in [
|
| 189 |
+
"wander",
|
| 190 |
+
"dolly_zoom",
|
| 191 |
+
], "Please set trj_mode to 'wander' or 'dolly_zoom' when n_views=1"
|
| 192 |
+
|
| 193 |
+
def _smooth_trj_fn_batch(raw_c2ws, k_size=50):
|
| 194 |
+
try:
|
| 195 |
+
smooth_c2ws = torch.stack(
|
| 196 |
+
[render_stabilization_path(c2w_i, k_size) for c2w_i in raw_c2ws],
|
| 197 |
+
dim=0,
|
| 198 |
+
)
|
| 199 |
+
except Exception as e:
|
| 200 |
+
print(f"[DEBUG] Path smoothing failed with error: {e}.")
|
| 201 |
+
smooth_c2ws = raw_c2ws
|
| 202 |
+
return smooth_c2ws
|
| 203 |
+
|
| 204 |
+
# get rendered trj
|
| 205 |
+
if trj_mode == "original":
|
| 206 |
+
tgt_c2w = cam2world
|
| 207 |
+
tgt_intr = intr_normed
|
| 208 |
+
elif trj_mode == "smooth":
|
| 209 |
+
tgt_c2w = _smooth_trj_fn_batch(cam2world)
|
| 210 |
+
tgt_intr = intr_normed
|
| 211 |
+
elif trj_mode in ["interpolate", "interpolate_smooth", "extend"]:
|
| 212 |
+
inter_len = 8
|
| 213 |
+
total_len = (cam2world.shape[1] - 1) * inter_len
|
| 214 |
+
if total_len > 24 * 18: # no more than 18s
|
| 215 |
+
inter_len = max(1, 24 * 10 // (cam2world.shape[1] - 1))
|
| 216 |
+
if total_len < 24 * 2: # no less than 2s
|
| 217 |
+
inter_len = max(1, 24 * 2 // (cam2world.shape[1] - 1))
|
| 218 |
+
|
| 219 |
+
if inter_len > 2:
|
| 220 |
+
t = torch.linspace(0, 1, inter_len, dtype=torch.float32, device=cam2world.device)
|
| 221 |
+
t = (torch.cos(torch.pi * (t + 1)) + 1) / 2
|
| 222 |
+
tgt_c2w_b = []
|
| 223 |
+
tgt_intr_b = []
|
| 224 |
+
for b_idx in range(cam2world.shape[0]):
|
| 225 |
+
tgt_c2w = []
|
| 226 |
+
tgt_intr = []
|
| 227 |
+
for cur_idx in range(cam2world.shape[1] - 1):
|
| 228 |
+
tgt_c2w.append(
|
| 229 |
+
interpolate_extrinsics(
|
| 230 |
+
cam2world[b_idx, cur_idx], cam2world[b_idx, cur_idx + 1], t
|
| 231 |
+
)[(0 if cur_idx == 0 else 1) :]
|
| 232 |
+
)
|
| 233 |
+
tgt_intr.append(
|
| 234 |
+
interpolate_intrinsics(
|
| 235 |
+
intr_normed[b_idx, cur_idx], intr_normed[b_idx, cur_idx + 1], t
|
| 236 |
+
)[(0 if cur_idx == 0 else 1) :]
|
| 237 |
+
)
|
| 238 |
+
tgt_c2w_b.append(torch.cat(tgt_c2w))
|
| 239 |
+
tgt_intr_b.append(torch.cat(tgt_intr))
|
| 240 |
+
tgt_c2w = torch.stack(tgt_c2w_b) # b v 4 4
|
| 241 |
+
tgt_intr = torch.stack(tgt_intr_b) # b v 3 3
|
| 242 |
+
else:
|
| 243 |
+
tgt_c2w = cam2world
|
| 244 |
+
tgt_intr = intr_normed
|
| 245 |
+
if trj_mode in ["interpolate_smooth", "extend"]:
|
| 246 |
+
tgt_c2w = _smooth_trj_fn_batch(tgt_c2w)
|
| 247 |
+
if trj_mode == "extend":
|
| 248 |
+
# apply dolly_zoom and wander in the middle frame
|
| 249 |
+
assert cam2world.shape[0] == 1, "extend only supports for batch_size=1 currently."
|
| 250 |
+
mid_idx = tgt_c2w.shape[1] // 2
|
| 251 |
+
c2w_wd, intr_wd = render_wander_path(
|
| 252 |
+
tgt_c2w[0, mid_idx],
|
| 253 |
+
tgt_intr[0, mid_idx],
|
| 254 |
+
h=in_h,
|
| 255 |
+
w=in_w,
|
| 256 |
+
num_frames=max(36, min(60, mid_idx // 2)),
|
| 257 |
+
max_disp=24.0,
|
| 258 |
+
)
|
| 259 |
+
c2w_dz, intr_dz = render_dolly_zoom_path(
|
| 260 |
+
tgt_c2w[0, mid_idx],
|
| 261 |
+
tgt_intr[0, mid_idx],
|
| 262 |
+
h=in_h,
|
| 263 |
+
w=in_w,
|
| 264 |
+
num_frames=max(36, min(60, mid_idx // 2)),
|
| 265 |
+
)
|
| 266 |
+
tgt_c2w = torch.cat(
|
| 267 |
+
[
|
| 268 |
+
tgt_c2w[:, :mid_idx],
|
| 269 |
+
c2w_wd.unsqueeze(0),
|
| 270 |
+
c2w_dz.unsqueeze(0),
|
| 271 |
+
tgt_c2w[:, mid_idx:],
|
| 272 |
+
],
|
| 273 |
+
dim=1,
|
| 274 |
+
)
|
| 275 |
+
tgt_intr = torch.cat(
|
| 276 |
+
[
|
| 277 |
+
tgt_intr[:, :mid_idx],
|
| 278 |
+
intr_wd.unsqueeze(0),
|
| 279 |
+
intr_dz.unsqueeze(0),
|
| 280 |
+
tgt_intr[:, mid_idx:],
|
| 281 |
+
],
|
| 282 |
+
dim=1,
|
| 283 |
+
)
|
| 284 |
+
elif trj_mode in ["wander", "dolly_zoom"]:
|
| 285 |
+
if trj_mode == "wander":
|
| 286 |
+
render_fn = render_wander_path
|
| 287 |
+
extra_kwargs = {"max_disp": 24.0}
|
| 288 |
+
else:
|
| 289 |
+
render_fn = render_dolly_zoom_path
|
| 290 |
+
extra_kwargs = {"D_focus": 30.0, "max_disp": 2.0}
|
| 291 |
+
tgt_c2w = []
|
| 292 |
+
tgt_intr = []
|
| 293 |
+
for b_idx in range(cam2world.shape[0]):
|
| 294 |
+
c2w_i, intr_i = render_fn(
|
| 295 |
+
cam2world[b_idx, 0], intr_normed[b_idx, 0], h=in_h, w=in_w, **extra_kwargs
|
| 296 |
+
)
|
| 297 |
+
tgt_c2w.append(c2w_i)
|
| 298 |
+
tgt_intr.append(intr_i)
|
| 299 |
+
tgt_c2w = torch.stack(tgt_c2w)
|
| 300 |
+
tgt_intr = torch.stack(tgt_intr)
|
| 301 |
+
elif trj_mode == "wobble_inter":
|
| 302 |
+
tgt_c2w, tgt_intr = render_wobble_inter_path(
|
| 303 |
+
cam2world=cam2world,
|
| 304 |
+
intr_normed=intr_normed,
|
| 305 |
+
inter_len=10,
|
| 306 |
+
n_skip=3,
|
| 307 |
+
)
|
| 308 |
+
else:
|
| 309 |
+
raise Exception(f"trj mode [{trj_mode}] is not implemented.")
|
| 310 |
+
|
| 311 |
+
_, v = tgt_c2w.shape[:2]
|
| 312 |
+
tgt_extr = affine_inverse(tgt_c2w)
|
| 313 |
+
if chunk_size is None:
|
| 314 |
+
chunk_size = v
|
| 315 |
+
chunk_size = min(v, chunk_size)
|
| 316 |
+
all_colors = []
|
| 317 |
+
all_depths = []
|
| 318 |
+
for chunk_idx in tqdm(
|
| 319 |
+
range(math.ceil(v / chunk_size)),
|
| 320 |
+
desc="Rendering novel views",
|
| 321 |
+
disable=(not enable_tqdm),
|
| 322 |
+
leave=False,
|
| 323 |
+
):
|
| 324 |
+
s = int(chunk_idx * chunk_size)
|
| 325 |
+
e = int((chunk_idx + 1) * chunk_size)
|
| 326 |
+
cur_n_view = tgt_extr[:, s:e].shape[1]
|
| 327 |
+
color, depth = render_3dgs(
|
| 328 |
+
extrinsics=rearrange(tgt_extr[:, s:e], "b v ... -> (b v) ..."), # w2c
|
| 329 |
+
intrinsics=rearrange(tgt_intr[:, s:e], "b v ... -> (b v) ..."), # normed
|
| 330 |
+
image_shape=image_shape,
|
| 331 |
+
gaussian=gaussians,
|
| 332 |
+
num_view=cur_n_view,
|
| 333 |
+
**kwargs,
|
| 334 |
+
)
|
| 335 |
+
all_colors.append(rearrange(color, "(b v) ... -> b v ...", v=cur_n_view))
|
| 336 |
+
all_depths.append(rearrange(depth, "(b v) ... -> b v ...", v=cur_n_view))
|
| 337 |
+
all_colors = torch.cat(all_colors, dim=1)
|
| 338 |
+
all_depths = torch.cat(all_depths, dim=1)
|
| 339 |
+
|
| 340 |
+
return all_colors, all_depths
|
multivue/cameras/source/src/depth_anything_3/model/utils/head_utils.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Tuple, Union
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
|
| 20 |
+
# -----------------------------------------------------------------------------
|
| 21 |
+
# Activation functions
|
| 22 |
+
# -----------------------------------------------------------------------------
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def activate_head_gs(out, activation="norm_exp", conf_activation="expp1", conf_dim=None):
|
| 26 |
+
"""
|
| 27 |
+
Process network output to extract GS params and density values.
|
| 28 |
+
Density could be view-dependent as SH coefficient
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
out: Network output tensor (B, C, H, W)
|
| 33 |
+
activation: Activation type for 3D points
|
| 34 |
+
conf_activation: Activation type for confidence values
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
Tuple of (3D points tensor, confidence tensor)
|
| 38 |
+
"""
|
| 39 |
+
# Move channels from last dim to the 4th dimension => (B, H, W, C)
|
| 40 |
+
fmap = out.permute(0, 2, 3, 1) # B,H,W,C expected
|
| 41 |
+
|
| 42 |
+
# Split into xyz (first C-1 channels) and confidence (last channel)
|
| 43 |
+
conf_dim = 1 if conf_dim is None else conf_dim
|
| 44 |
+
xyz = fmap[:, :, :, :-conf_dim]
|
| 45 |
+
conf = fmap[:, :, :, -1] if conf_dim == 1 else fmap[:, :, :, -conf_dim:]
|
| 46 |
+
|
| 47 |
+
if activation == "norm_exp":
|
| 48 |
+
d = xyz.norm(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 49 |
+
xyz_normed = xyz / d
|
| 50 |
+
pts3d = xyz_normed * torch.expm1(d)
|
| 51 |
+
elif activation == "norm":
|
| 52 |
+
pts3d = xyz / xyz.norm(dim=-1, keepdim=True)
|
| 53 |
+
elif activation == "exp":
|
| 54 |
+
pts3d = torch.exp(xyz)
|
| 55 |
+
elif activation == "relu":
|
| 56 |
+
pts3d = F.relu(xyz)
|
| 57 |
+
elif activation == "sigmoid":
|
| 58 |
+
pts3d = torch.sigmoid(xyz)
|
| 59 |
+
elif activation == "linear":
|
| 60 |
+
pts3d = xyz
|
| 61 |
+
else:
|
| 62 |
+
raise ValueError(f"Unknown activation: {activation}")
|
| 63 |
+
|
| 64 |
+
if conf_activation == "expp1":
|
| 65 |
+
conf_out = 1 + conf.exp()
|
| 66 |
+
elif conf_activation == "expp0":
|
| 67 |
+
conf_out = conf.exp()
|
| 68 |
+
elif conf_activation == "sigmoid":
|
| 69 |
+
conf_out = torch.sigmoid(conf)
|
| 70 |
+
elif conf_activation == "linear":
|
| 71 |
+
conf_out = conf
|
| 72 |
+
else:
|
| 73 |
+
raise ValueError(f"Unknown conf_activation: {conf_activation}")
|
| 74 |
+
|
| 75 |
+
return pts3d, conf_out
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# -----------------------------------------------------------------------------
|
| 79 |
+
# Other utilities
|
| 80 |
+
# -----------------------------------------------------------------------------
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class Permute(nn.Module):
|
| 84 |
+
"""nn.Module wrapper around Tensor.permute for cleaner nn.Sequential usage."""
|
| 85 |
+
|
| 86 |
+
dims: Tuple[int, ...]
|
| 87 |
+
|
| 88 |
+
def __init__(self, dims: Tuple[int, ...]) -> None:
|
| 89 |
+
super().__init__()
|
| 90 |
+
self.dims = dims
|
| 91 |
+
|
| 92 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor: # type: ignore[override]
|
| 93 |
+
return x.permute(*self.dims)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def position_grid_to_embed(
|
| 97 |
+
pos_grid: torch.Tensor, embed_dim: int, omega_0: float = 100
|
| 98 |
+
) -> torch.Tensor:
|
| 99 |
+
"""
|
| 100 |
+
Convert 2D position grid (HxWx2) to sinusoidal embeddings (HxWxC)
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
pos_grid: Tensor of shape (H, W, 2) containing 2D coordinates
|
| 104 |
+
embed_dim: Output channel dimension for embeddings
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
Tensor of shape (H, W, embed_dim) with positional embeddings
|
| 108 |
+
"""
|
| 109 |
+
H, W, grid_dim = pos_grid.shape
|
| 110 |
+
assert grid_dim == 2
|
| 111 |
+
pos_flat = pos_grid.reshape(-1, grid_dim) # Flatten to (H*W, 2)
|
| 112 |
+
|
| 113 |
+
# Process x and y coordinates separately
|
| 114 |
+
emb_x = make_sincos_pos_embed(embed_dim // 2, pos_flat[:, 0], omega_0=omega_0) # [1, H*W, D/2]
|
| 115 |
+
emb_y = make_sincos_pos_embed(embed_dim // 2, pos_flat[:, 1], omega_0=omega_0) # [1, H*W, D/2]
|
| 116 |
+
|
| 117 |
+
# Combine and reshape
|
| 118 |
+
emb = torch.cat([emb_x, emb_y], dim=-1) # [1, H*W, D]
|
| 119 |
+
|
| 120 |
+
return emb.view(H, W, embed_dim) # [H, W, D]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def make_sincos_pos_embed(embed_dim: int, pos: torch.Tensor, omega_0: float = 100) -> torch.Tensor:
|
| 124 |
+
"""
|
| 125 |
+
This function generates a 1D positional embedding from a given grid using sine and cosine functions. # noqa
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
- embed_dim: The embedding dimension.
|
| 129 |
+
- pos: The position to generate the embedding from.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
- emb: The generated 1D positional embedding.
|
| 133 |
+
"""
|
| 134 |
+
assert embed_dim % 2 == 0
|
| 135 |
+
omega = torch.arange(embed_dim // 2, dtype=torch.float32, device=pos.device)
|
| 136 |
+
omega /= embed_dim / 2.0
|
| 137 |
+
omega = 1.0 / omega_0**omega # (D/2,)
|
| 138 |
+
|
| 139 |
+
pos = pos.reshape(-1) # (M,)
|
| 140 |
+
out = torch.einsum("m,d->md", pos, omega) # (M, D/2), outer product
|
| 141 |
+
|
| 142 |
+
emb_sin = torch.sin(out) # (M, D/2)
|
| 143 |
+
emb_cos = torch.cos(out) # (M, D/2)
|
| 144 |
+
|
| 145 |
+
emb = torch.cat([emb_sin, emb_cos], dim=1) # (M, D)
|
| 146 |
+
return emb.float()
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# Inspired by https://github.com/microsoft/moge
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def create_uv_grid(
|
| 153 |
+
width: int,
|
| 154 |
+
height: int,
|
| 155 |
+
aspect_ratio: float = None,
|
| 156 |
+
dtype: torch.dtype = None,
|
| 157 |
+
device: torch.device = None,
|
| 158 |
+
) -> torch.Tensor:
|
| 159 |
+
"""
|
| 160 |
+
Create a normalized UV grid of shape (width, height, 2).
|
| 161 |
+
|
| 162 |
+
The grid spans horizontally and vertically according to an aspect ratio,
|
| 163 |
+
ensuring the top-left corner is at (-x_span, -y_span) and the bottom-right
|
| 164 |
+
corner is at (x_span, y_span), normalized by the diagonal of the plane.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
width (int): Number of points horizontally.
|
| 168 |
+
height (int): Number of points vertically.
|
| 169 |
+
aspect_ratio (float, optional): Width-to-height ratio. Defaults to width/height.
|
| 170 |
+
dtype (torch.dtype, optional): Data type of the resulting tensor.
|
| 171 |
+
device (torch.device, optional): Device on which the tensor is created.
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
torch.Tensor: A (width, height, 2) tensor of UV coordinates.
|
| 175 |
+
"""
|
| 176 |
+
# Derive aspect ratio if not explicitly provided
|
| 177 |
+
if aspect_ratio is None:
|
| 178 |
+
aspect_ratio = float(width) / float(height)
|
| 179 |
+
|
| 180 |
+
# Compute normalized spans for X and Y
|
| 181 |
+
diag_factor = (aspect_ratio**2 + 1.0) ** 0.5
|
| 182 |
+
span_x = aspect_ratio / diag_factor
|
| 183 |
+
span_y = 1.0 / diag_factor
|
| 184 |
+
|
| 185 |
+
# Establish the linspace boundaries
|
| 186 |
+
left_x = -span_x * (width - 1) / width
|
| 187 |
+
right_x = span_x * (width - 1) / width
|
| 188 |
+
top_y = -span_y * (height - 1) / height
|
| 189 |
+
bottom_y = span_y * (height - 1) / height
|
| 190 |
+
|
| 191 |
+
# Generate 1D coordinates
|
| 192 |
+
x_coords = torch.linspace(left_x, right_x, steps=width, dtype=dtype, device=device)
|
| 193 |
+
y_coords = torch.linspace(top_y, bottom_y, steps=height, dtype=dtype, device=device)
|
| 194 |
+
|
| 195 |
+
# Create 2D meshgrid (width x height) and stack into UV
|
| 196 |
+
uu, vv = torch.meshgrid(x_coords, y_coords, indexing="xy")
|
| 197 |
+
uv_grid = torch.stack((uu, vv), dim=-1)
|
| 198 |
+
|
| 199 |
+
return uv_grid
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# -----------------------------------------------------------------------------
|
| 203 |
+
# Interpolation (safe interpolation, avoid INT_MAX overflow)
|
| 204 |
+
# -----------------------------------------------------------------------------
|
| 205 |
+
def custom_interpolate(
|
| 206 |
+
x: torch.Tensor,
|
| 207 |
+
size: Union[Tuple[int, int], None] = None,
|
| 208 |
+
scale_factor: Union[float, None] = None,
|
| 209 |
+
mode: str = "bilinear",
|
| 210 |
+
align_corners: bool = True,
|
| 211 |
+
) -> torch.Tensor:
|
| 212 |
+
"""
|
| 213 |
+
Safe interpolation implementation to avoid INT_MAX overflow in torch.nn.functional.interpolate.
|
| 214 |
+
"""
|
| 215 |
+
if size is None:
|
| 216 |
+
assert scale_factor is not None, "Either size or scale_factor must be provided."
|
| 217 |
+
size = (int(x.shape[-2] * scale_factor), int(x.shape[-1] * scale_factor))
|
| 218 |
+
|
| 219 |
+
INT_MAX = 1610612736
|
| 220 |
+
total = size[0] * size[1] * x.shape[0] * x.shape[1]
|
| 221 |
+
|
| 222 |
+
if total > INT_MAX:
|
| 223 |
+
chunks = torch.chunk(x, chunks=(total // INT_MAX) + 1, dim=0)
|
| 224 |
+
outs = [
|
| 225 |
+
nn.functional.interpolate(c, size=size, mode=mode, align_corners=align_corners)
|
| 226 |
+
for c in chunks
|
| 227 |
+
]
|
| 228 |
+
return torch.cat(outs, dim=0).contiguous()
|
| 229 |
+
|
| 230 |
+
return nn.functional.interpolate(x, size=size, mode=mode, align_corners=align_corners)
|
multivue/cameras/source/src/depth_anything_3/model/utils/transform.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def extri_intri_to_pose_encoding(
|
| 20 |
+
extrinsics,
|
| 21 |
+
intrinsics,
|
| 22 |
+
image_size_hw=None,
|
| 23 |
+
):
|
| 24 |
+
"""Convert camera extrinsics and intrinsics to a compact pose encoding."""
|
| 25 |
+
|
| 26 |
+
# extrinsics: BxSx3x4
|
| 27 |
+
# intrinsics: BxSx3x3
|
| 28 |
+
R = extrinsics[:, :, :3, :3] # BxSx3x3
|
| 29 |
+
T = extrinsics[:, :, :3, 3] # BxSx3
|
| 30 |
+
|
| 31 |
+
quat = mat_to_quat(R)
|
| 32 |
+
# Note the order of h and w here
|
| 33 |
+
H, W = image_size_hw
|
| 34 |
+
fov_h = 2 * torch.atan((H / 2) / intrinsics[..., 1, 1])
|
| 35 |
+
fov_w = 2 * torch.atan((W / 2) / intrinsics[..., 0, 0])
|
| 36 |
+
pose_encoding = torch.cat([T, quat, fov_h[..., None], fov_w[..., None]], dim=-1).float()
|
| 37 |
+
|
| 38 |
+
return pose_encoding
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def pose_encoding_to_extri_intri(
|
| 42 |
+
pose_encoding,
|
| 43 |
+
image_size_hw=None,
|
| 44 |
+
):
|
| 45 |
+
"""Convert a pose encoding back to camera extrinsics and intrinsics."""
|
| 46 |
+
|
| 47 |
+
T = pose_encoding[..., :3]
|
| 48 |
+
quat = pose_encoding[..., 3:7]
|
| 49 |
+
fov_h = pose_encoding[..., 7]
|
| 50 |
+
fov_w = pose_encoding[..., 8]
|
| 51 |
+
|
| 52 |
+
R = quat_to_mat(quat)
|
| 53 |
+
extrinsics = torch.cat([R, T[..., None]], dim=-1)
|
| 54 |
+
|
| 55 |
+
H, W = image_size_hw
|
| 56 |
+
fy = (H / 2.0) / torch.clamp(torch.tan(fov_h / 2.0), 1e-6)
|
| 57 |
+
fx = (W / 2.0) / torch.clamp(torch.tan(fov_w / 2.0), 1e-6)
|
| 58 |
+
intrinsics = torch.zeros(pose_encoding.shape[:2] + (3, 3), device=pose_encoding.device)
|
| 59 |
+
intrinsics[..., 0, 0] = fx
|
| 60 |
+
intrinsics[..., 1, 1] = fy
|
| 61 |
+
intrinsics[..., 0, 2] = W / 2
|
| 62 |
+
intrinsics[..., 1, 2] = H / 2
|
| 63 |
+
intrinsics[..., 2, 2] = 1.0 # Set the homogeneous coordinate to 1
|
| 64 |
+
|
| 65 |
+
return extrinsics, intrinsics
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def quat_to_mat(quaternions: torch.Tensor) -> torch.Tensor:
|
| 69 |
+
"""
|
| 70 |
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
| 71 |
+
|
| 72 |
+
Convert rotations given as quaternions to rotation matrices.
|
| 73 |
+
Args:
|
| 74 |
+
quaternions: quaternions with real part last,
|
| 75 |
+
as tensor of shape (..., 4).
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Rotation matrices as tensor of shape (..., 3, 3).
|
| 79 |
+
"""
|
| 80 |
+
i, j, k, r = torch.unbind(quaternions, -1)
|
| 81 |
+
two_s = 2.0 / (quaternions * quaternions).sum(-1)
|
| 82 |
+
|
| 83 |
+
o = torch.stack(
|
| 84 |
+
(
|
| 85 |
+
1 - two_s * (j * j + k * k),
|
| 86 |
+
two_s * (i * j - k * r),
|
| 87 |
+
two_s * (i * k + j * r),
|
| 88 |
+
two_s * (i * j + k * r),
|
| 89 |
+
1 - two_s * (i * i + k * k),
|
| 90 |
+
two_s * (j * k - i * r),
|
| 91 |
+
two_s * (i * k - j * r),
|
| 92 |
+
two_s * (j * k + i * r),
|
| 93 |
+
1 - two_s * (i * i + j * j),
|
| 94 |
+
),
|
| 95 |
+
-1,
|
| 96 |
+
)
|
| 97 |
+
return o.reshape(quaternions.shape[:-1] + (3, 3))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def mat_to_quat(matrix: torch.Tensor) -> torch.Tensor:
|
| 101 |
+
"""
|
| 102 |
+
Convert rotations given as rotation matrices to quaternions.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
matrix: Rotation matrices as tensor of shape (..., 3, 3).
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
quaternions with real part last, as tensor of shape (..., 4).
|
| 109 |
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
| 110 |
+
"""
|
| 111 |
+
if matrix.size(-1) != 3 or matrix.size(-2) != 3:
|
| 112 |
+
raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
|
| 113 |
+
|
| 114 |
+
batch_dim = matrix.shape[:-2]
|
| 115 |
+
m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
|
| 116 |
+
matrix.reshape(batch_dim + (9,)), dim=-1
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
q_abs = _sqrt_positive_part(
|
| 120 |
+
torch.stack(
|
| 121 |
+
[
|
| 122 |
+
1.0 + m00 + m11 + m22,
|
| 123 |
+
1.0 + m00 - m11 - m22,
|
| 124 |
+
1.0 - m00 + m11 - m22,
|
| 125 |
+
1.0 - m00 - m11 + m22,
|
| 126 |
+
],
|
| 127 |
+
dim=-1,
|
| 128 |
+
)
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
quat_by_rijk = torch.stack(
|
| 132 |
+
[
|
| 133 |
+
torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1),
|
| 134 |
+
torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1),
|
| 135 |
+
torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1),
|
| 136 |
+
torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1),
|
| 137 |
+
],
|
| 138 |
+
dim=-2,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
|
| 142 |
+
quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
|
| 143 |
+
|
| 144 |
+
out = quat_candidates[F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :].reshape(
|
| 145 |
+
batch_dim + (4,)
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
out = out[..., [1, 2, 3, 0]]
|
| 149 |
+
|
| 150 |
+
out = standardize_quaternion(out)
|
| 151 |
+
|
| 152 |
+
return out
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor:
|
| 156 |
+
"""
|
| 157 |
+
Returns torch.sqrt(torch.max(0, x))
|
| 158 |
+
but with a zero subgradient where x is 0.
|
| 159 |
+
"""
|
| 160 |
+
ret = torch.zeros_like(x)
|
| 161 |
+
positive_mask = x > 0
|
| 162 |
+
if torch.is_grad_enabled():
|
| 163 |
+
ret[positive_mask] = torch.sqrt(x[positive_mask])
|
| 164 |
+
else:
|
| 165 |
+
ret = torch.where(positive_mask, torch.sqrt(x), ret)
|
| 166 |
+
return ret
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor:
|
| 170 |
+
"""
|
| 171 |
+
Convert a unit quaternion to a standard form: one in which the real
|
| 172 |
+
part is non negative.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
quaternions: Quaternions with real part last,
|
| 176 |
+
as tensor of shape (..., 4).
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
Standardized quaternions as tensor of shape (..., 4).
|
| 180 |
+
"""
|
| 181 |
+
return torch.where(quaternions[..., 3:4] < 0, -quaternions, quaternions)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def cam_quat_xyzw_to_world_quat_wxyz(cam_quat_xyzw, c2w):
|
| 185 |
+
# cam_quat_xyzw: (b, n, 4) in xyzw
|
| 186 |
+
# c2w: (b, n, 4, 4)
|
| 187 |
+
b, n = cam_quat_xyzw.shape[:2]
|
| 188 |
+
# 1. xyzw -> wxyz
|
| 189 |
+
cam_quat_wxyz = torch.cat(
|
| 190 |
+
[
|
| 191 |
+
cam_quat_xyzw[..., 3:4], # w
|
| 192 |
+
cam_quat_xyzw[..., 0:1], # x
|
| 193 |
+
cam_quat_xyzw[..., 1:2], # y
|
| 194 |
+
cam_quat_xyzw[..., 2:3], # z
|
| 195 |
+
],
|
| 196 |
+
dim=-1,
|
| 197 |
+
)
|
| 198 |
+
# 2. Quaternion to matrix
|
| 199 |
+
cam_quat_wxyz_flat = cam_quat_wxyz.reshape(-1, 4)
|
| 200 |
+
rotmat_cam = quat_to_mat(cam_quat_wxyz_flat).reshape(b, n, 3, 3)
|
| 201 |
+
# 3. Transform to world space
|
| 202 |
+
rotmat_c2w = c2w[..., :3, :3]
|
| 203 |
+
rotmat_world = torch.matmul(rotmat_c2w, rotmat_cam)
|
| 204 |
+
# 4. Matrix to quaternion (wxyz)
|
| 205 |
+
rotmat_world_flat = rotmat_world.reshape(-1, 3, 3)
|
| 206 |
+
world_quat_wxyz_flat = mat_to_quat(rotmat_world_flat)
|
| 207 |
+
world_quat_wxyz = world_quat_wxyz_flat.reshape(b, n, 4)
|
| 208 |
+
return world_quat_wxyz
|
multivue/cameras/source/src/depth_anything_3/registry.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from collections import OrderedDict
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_all_models() -> OrderedDict:
|
| 20 |
+
"""
|
| 21 |
+
Scans all YAML files in the configs directory and returns a sorted dictionary where:
|
| 22 |
+
- Keys are model names (YAML filenames without the .yaml extension)
|
| 23 |
+
- Values are absolute paths to the corresponding YAML files
|
| 24 |
+
"""
|
| 25 |
+
# Get path to the configs directory within the da3 package
|
| 26 |
+
# Works both in development and after pip installation
|
| 27 |
+
# configs_dir = files("depth_anything_3").joinpath("configs")
|
| 28 |
+
configs_dir = Path(__file__).resolve().parent / "configs"
|
| 29 |
+
|
| 30 |
+
# Ensure path is a Path object for consistent cross-platform handling
|
| 31 |
+
configs_dir = Path(configs_dir)
|
| 32 |
+
|
| 33 |
+
model_entries = []
|
| 34 |
+
# Iterate through all items in the configs directory
|
| 35 |
+
for item in configs_dir.iterdir():
|
| 36 |
+
# Filter for YAML files (excluding directories)
|
| 37 |
+
if item.is_file() and item.suffix == ".yaml":
|
| 38 |
+
# Extract model name (filename without .yaml extension)
|
| 39 |
+
model_name = item.stem
|
| 40 |
+
# Get absolute path (resolve() handles symlinks)
|
| 41 |
+
file_abs_path = str(item.resolve())
|
| 42 |
+
model_entries.append((model_name, file_abs_path))
|
| 43 |
+
|
| 44 |
+
# Sort entries by model name and convert to OrderedDict
|
| 45 |
+
sorted_entries = sorted(model_entries, key=lambda x: x[0])
|
| 46 |
+
return OrderedDict(sorted_entries)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Global registry for external imports
|
| 50 |
+
MODEL_REGISTRY = get_all_models()
|
multivue/cameras/source/src/depth_anything_3/services/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Services module for Depth Anything 3.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from depth_anything_3.services.backend import create_app, start_server
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
start_server,
|
| 23 |
+
create_app,
|
| 24 |
+
]
|
multivue/cameras/source/src/depth_anything_3/services/backend.py
ADDED
|
@@ -0,0 +1,1556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# flake8: noqa: E501
|
| 2 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
Model backend service for Depth Anything 3.
|
| 18 |
+
Provides HTTP API for model inference with persistent model loading.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import hmac
|
| 22 |
+
import html
|
| 23 |
+
import os
|
| 24 |
+
import posixpath
|
| 25 |
+
import re
|
| 26 |
+
import secrets
|
| 27 |
+
import time
|
| 28 |
+
import uuid
|
| 29 |
+
|
| 30 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 31 |
+
from typing import Any, Dict, List, Optional
|
| 32 |
+
from urllib.parse import quote
|
| 33 |
+
import numpy as np
|
| 34 |
+
|
| 35 |
+
import uvicorn
|
| 36 |
+
from fastapi import Depends, FastAPI, Header, HTTPException
|
| 37 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 38 |
+
from pydantic import BaseModel, field_validator
|
| 39 |
+
|
| 40 |
+
from ..api import DepthAnything3
|
| 41 |
+
from ..utils.export import SUPPORTED_EXPORT_FORMATS
|
| 42 |
+
from ..utils.memory import (
|
| 43 |
+
get_gpu_memory_info,
|
| 44 |
+
cleanup_cuda_memory,
|
| 45 |
+
check_memory_availability,
|
| 46 |
+
estimate_memory_requirement,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class InferenceRequest(BaseModel):
|
| 51 |
+
"""Request model for inference API."""
|
| 52 |
+
|
| 53 |
+
image_paths: List[str]
|
| 54 |
+
export_dir: Optional[str] = None
|
| 55 |
+
export_format: str = "mini_npz-glb"
|
| 56 |
+
extrinsics: Optional[List[List[List[float]]]] = None
|
| 57 |
+
intrinsics: Optional[List[List[List[float]]]] = None
|
| 58 |
+
process_res: int = 504
|
| 59 |
+
process_res_method: str = "upper_bound_resize"
|
| 60 |
+
export_feat_layers: List[int] = []
|
| 61 |
+
align_to_input_ext_scale: bool = True
|
| 62 |
+
# GLB export parameters
|
| 63 |
+
conf_thresh_percentile: float = 40.0
|
| 64 |
+
num_max_points: int = 1_000_000
|
| 65 |
+
show_cameras: bool = True
|
| 66 |
+
# Feat_vis export parameters
|
| 67 |
+
feat_vis_fps: int = 15
|
| 68 |
+
|
| 69 |
+
@field_validator("export_format")
|
| 70 |
+
@classmethod
|
| 71 |
+
def _validate_export_format(cls, value: str) -> str:
|
| 72 |
+
"""Restrict export_format to the server's known/trusted export dispatcher formats,
|
| 73 |
+
instead of letting an arbitrary client-supplied string reach the export() dispatcher."""
|
| 74 |
+
tokens = value.split("-") if value else [value]
|
| 75 |
+
unknown = sorted(set(tokens) - SUPPORTED_EXPORT_FORMATS)
|
| 76 |
+
if unknown:
|
| 77 |
+
raise ValueError(
|
| 78 |
+
f"Unsupported export_format value(s): {unknown}. "
|
| 79 |
+
f"Supported formats: {sorted(SUPPORTED_EXPORT_FORMATS)}"
|
| 80 |
+
)
|
| 81 |
+
return value
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class InferenceResponse(BaseModel):
|
| 85 |
+
"""Response model for inference API."""
|
| 86 |
+
|
| 87 |
+
success: bool
|
| 88 |
+
message: str
|
| 89 |
+
task_id: Optional[str] = None
|
| 90 |
+
export_dir: Optional[str] = None
|
| 91 |
+
export_format: str = "mini_npz-glb"
|
| 92 |
+
processing_time: Optional[float] = None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class TaskStatus(BaseModel):
|
| 96 |
+
"""Task status model."""
|
| 97 |
+
|
| 98 |
+
task_id: str
|
| 99 |
+
status: str # "pending", "running", "completed", "failed"
|
| 100 |
+
message: str
|
| 101 |
+
progress: Optional[float] = None # 0.0 to 1.0
|
| 102 |
+
created_at: float
|
| 103 |
+
started_at: Optional[float] = None
|
| 104 |
+
completed_at: Optional[float] = None
|
| 105 |
+
export_dir: Optional[str] = None
|
| 106 |
+
request: Optional[InferenceRequest] = None # Store the original request
|
| 107 |
+
|
| 108 |
+
# Essential task parameters
|
| 109 |
+
num_images: Optional[int] = None # Number of input images
|
| 110 |
+
export_format: Optional[str] = None # Export format
|
| 111 |
+
process_res_method: Optional[str] = None # Processing resolution method
|
| 112 |
+
video_path: Optional[str] = None # Source video path
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _render_task_card(task: "TaskStatus", css_class: str) -> str:
|
| 116 |
+
"""Render a single task as an HTML fragment for the dashboard.
|
| 117 |
+
|
| 118 |
+
Every field is HTML-escaped: task fields originate from a client request and
|
| 119 |
+
are rendered on a page other users may view.
|
| 120 |
+
"""
|
| 121 |
+
e = html.escape
|
| 122 |
+
|
| 123 |
+
video_row = ""
|
| 124 |
+
if task.video_path:
|
| 125 |
+
video_row = f"<br><small>Video: {e(task.video_path)}</small>"
|
| 126 |
+
|
| 127 |
+
return f"""
|
| 128 |
+
<div class="task-item {e(css_class)}">
|
| 129 |
+
<div class="task-header">
|
| 130 |
+
<span class="task-id">{e(task.task_id)}</span>
|
| 131 |
+
<span class="task-status status-{e(task.status)}">{e(task.status)}</span>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="task-message">{e(task.message)}</div>
|
| 134 |
+
<div class="task-params">
|
| 135 |
+
<small>
|
| 136 |
+
Images: {e(str(task.num_images)) if task.num_images is not None else 'N/A'} |
|
| 137 |
+
Format: {e(task.export_format) if task.export_format else 'N/A'} |
|
| 138 |
+
Method: {e(task.process_res_method) if task.process_res_method else 'N/A'} |
|
| 139 |
+
Export Dir: {e(task.export_dir) if task.export_dir else 'N/A'}
|
| 140 |
+
</small>
|
| 141 |
+
{video_row}
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class ModelBackend:
|
| 148 |
+
"""Model backend service with persistent model loading."""
|
| 149 |
+
|
| 150 |
+
def __init__(self, model_dir: str, device: str = "cuda"):
|
| 151 |
+
self.model_dir = model_dir
|
| 152 |
+
self.device = device
|
| 153 |
+
self.model = None
|
| 154 |
+
self.model_loaded = False
|
| 155 |
+
self.load_time = None
|
| 156 |
+
self.load_start_time = None # Time when model loading started
|
| 157 |
+
self.load_completed_time = None # Time when model loading completed
|
| 158 |
+
self.last_used = None
|
| 159 |
+
|
| 160 |
+
def load_model(self):
|
| 161 |
+
"""Load model if not already loaded."""
|
| 162 |
+
if self.model_loaded and self.model is not None:
|
| 163 |
+
self.last_used = time.time()
|
| 164 |
+
return self.model
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
print(f"Loading model from {self.model_dir}...")
|
| 168 |
+
self.load_start_time = time.time()
|
| 169 |
+
start_time = time.time()
|
| 170 |
+
|
| 171 |
+
self.model = DepthAnything3.from_pretrained(self.model_dir).to(self.device)
|
| 172 |
+
self.model.eval()
|
| 173 |
+
|
| 174 |
+
self.model_loaded = True
|
| 175 |
+
self.load_time = time.time() - start_time
|
| 176 |
+
self.load_completed_time = time.time()
|
| 177 |
+
self.last_used = time.time()
|
| 178 |
+
|
| 179 |
+
print(f"Model loaded successfully in {self.load_time:.2f}s")
|
| 180 |
+
return self.model
|
| 181 |
+
|
| 182 |
+
except Exception as e:
|
| 183 |
+
print(f"Failed to load model: {e}")
|
| 184 |
+
raise e
|
| 185 |
+
|
| 186 |
+
def get_model(self):
|
| 187 |
+
"""Get model, loading if necessary."""
|
| 188 |
+
if not self.model_loaded:
|
| 189 |
+
return self.load_model()
|
| 190 |
+
self.last_used = time.time()
|
| 191 |
+
return self.model
|
| 192 |
+
|
| 193 |
+
def get_status(self) -> Dict[str, Any]:
|
| 194 |
+
"""Get backend status information."""
|
| 195 |
+
# Calculate uptime from when model loading completed
|
| 196 |
+
uptime = 0
|
| 197 |
+
if self.model_loaded and self.load_completed_time:
|
| 198 |
+
uptime = time.time() - self.load_completed_time
|
| 199 |
+
|
| 200 |
+
return {
|
| 201 |
+
"model_loaded": self.model_loaded,
|
| 202 |
+
"model_dir": self.model_dir,
|
| 203 |
+
"device": self.device,
|
| 204 |
+
"load_time": self.load_time,
|
| 205 |
+
"last_used": self.last_used,
|
| 206 |
+
"uptime": uptime,
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# Global backend instance
|
| 211 |
+
_backend: Optional[ModelBackend] = None
|
| 212 |
+
_app: Optional[FastAPI] = None
|
| 213 |
+
_gallery_dir: Optional[str] = None
|
| 214 |
+
_tasks: Dict[str, TaskStatus] = {}
|
| 215 |
+
_executor = ThreadPoolExecutor(max_workers=1) # Restrict to single-task execution
|
| 216 |
+
_running_task_id: Optional[str] = None # Currently running task ID
|
| 217 |
+
_task_queue: List[str] = [] # Pending task queue
|
| 218 |
+
|
| 219 |
+
# Task cleanup configuration
|
| 220 |
+
MAX_TASK_HISTORY = 100 # Maximum number of tasks to keep in memory
|
| 221 |
+
CLEANUP_INTERVAL = 300 # Cleanup interval in seconds (5 minutes)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _process_next_task():
|
| 225 |
+
"""Process the next task in the queue."""
|
| 226 |
+
global _task_queue, _running_task_id
|
| 227 |
+
|
| 228 |
+
if not _task_queue or _running_task_id is not None:
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
# Get next task from queue
|
| 232 |
+
task_id = _task_queue.pop(0)
|
| 233 |
+
|
| 234 |
+
# Get task request from tasks dict (we need to store the request)
|
| 235 |
+
if task_id not in _tasks:
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
# Submit task to executor
|
| 239 |
+
_executor.submit(_run_inference_task, task_id)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
# get_gpu_memory_info imported from depth_anything_3.utils.memory
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# cleanup_cuda_memory imported from depth_anything_3.utils.memory
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# check_memory_availability imported from depth_anything_3.utils.memory
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# estimate_memory_requirement imported from depth_anything_3.utils.memory
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _run_inference_task(task_id: str):
|
| 255 |
+
"""Run inference task in background thread with OOM protection."""
|
| 256 |
+
global _tasks, _backend, _running_task_id, _task_queue
|
| 257 |
+
|
| 258 |
+
model = None
|
| 259 |
+
inference_started = False
|
| 260 |
+
start_time = time.time()
|
| 261 |
+
|
| 262 |
+
try:
|
| 263 |
+
# Get task request
|
| 264 |
+
if task_id not in _tasks or _tasks[task_id].request is None:
|
| 265 |
+
print(f"[{task_id}] Task not found or request missing")
|
| 266 |
+
return
|
| 267 |
+
|
| 268 |
+
request = _tasks[task_id].request
|
| 269 |
+
num_images = len(request.image_paths)
|
| 270 |
+
|
| 271 |
+
# Set current running task
|
| 272 |
+
_running_task_id = task_id
|
| 273 |
+
|
| 274 |
+
# Update task status to running
|
| 275 |
+
_tasks[task_id].status = "running"
|
| 276 |
+
_tasks[task_id].started_at = start_time
|
| 277 |
+
_tasks[task_id].message = f"[{task_id}] Starting inference on {num_images} frames..."
|
| 278 |
+
print(f"[{task_id}] Starting inference on {num_images} frames")
|
| 279 |
+
|
| 280 |
+
# Pre-inference cleanup to ensure maximum available memory
|
| 281 |
+
print(f"[{task_id}] Pre-inference cleanup...")
|
| 282 |
+
cleanup_cuda_memory()
|
| 283 |
+
|
| 284 |
+
# Check memory availability
|
| 285 |
+
estimated_memory = estimate_memory_requirement(num_images, request.process_res)
|
| 286 |
+
mem_available, mem_msg = check_memory_availability(estimated_memory)
|
| 287 |
+
print(f"[{task_id}] {mem_msg}")
|
| 288 |
+
|
| 289 |
+
if not mem_available:
|
| 290 |
+
# Try aggressive cleanup
|
| 291 |
+
print(f"[{task_id}] Insufficient memory, attempting aggressive cleanup...")
|
| 292 |
+
cleanup_cuda_memory()
|
| 293 |
+
time.sleep(0.5) # Give system time to reclaim memory
|
| 294 |
+
|
| 295 |
+
# Check again
|
| 296 |
+
mem_available, mem_msg = check_memory_availability(estimated_memory)
|
| 297 |
+
if not mem_available:
|
| 298 |
+
raise RuntimeError(
|
| 299 |
+
f"Insufficient GPU memory after cleanup. {mem_msg}\n"
|
| 300 |
+
f"Suggestions:\n"
|
| 301 |
+
f" 1. Reduce process_res (current: {request.process_res})\n"
|
| 302 |
+
f" 2. Process fewer images at once (current: {num_images})\n"
|
| 303 |
+
f" 3. Clear other GPU processes"
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
# Get model (with error handling)
|
| 307 |
+
print(f"[{task_id}] Loading model...")
|
| 308 |
+
_tasks[task_id].message = f"[{task_id}] Loading model..."
|
| 309 |
+
_tasks[task_id].progress = 0.1
|
| 310 |
+
|
| 311 |
+
try:
|
| 312 |
+
model = _backend.get_model()
|
| 313 |
+
except RuntimeError as e:
|
| 314 |
+
if "out of memory" in str(e).lower():
|
| 315 |
+
cleanup_cuda_memory()
|
| 316 |
+
raise RuntimeError(
|
| 317 |
+
f"OOM during model loading: {str(e)}\n"
|
| 318 |
+
f"Try reducing the batch size or resolution."
|
| 319 |
+
)
|
| 320 |
+
raise
|
| 321 |
+
|
| 322 |
+
print(f"[{task_id}] Model loaded successfully")
|
| 323 |
+
_tasks[task_id].progress = 0.2
|
| 324 |
+
|
| 325 |
+
# Prepare inference parameters
|
| 326 |
+
inference_kwargs = {
|
| 327 |
+
"image": request.image_paths,
|
| 328 |
+
"export_format": request.export_format,
|
| 329 |
+
"process_res": request.process_res,
|
| 330 |
+
"process_res_method": request.process_res_method,
|
| 331 |
+
"export_feat_layers": request.export_feat_layers,
|
| 332 |
+
"align_to_input_ext_scale": request.align_to_input_ext_scale,
|
| 333 |
+
"conf_thresh_percentile": request.conf_thresh_percentile,
|
| 334 |
+
"num_max_points": request.num_max_points,
|
| 335 |
+
"show_cameras": request.show_cameras,
|
| 336 |
+
"feat_vis_fps": request.feat_vis_fps,
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
if request.export_dir:
|
| 340 |
+
# Re-check against the current configuration: this task may have sat in the
|
| 341 |
+
# queue for a while after the initial request-time check, and configuration
|
| 342 |
+
# can differ if the process was restarted with a different gallery_dir.
|
| 343 |
+
validate_export_dir(request.export_dir, _gallery_dir)
|
| 344 |
+
inference_kwargs["export_dir"] = request.export_dir
|
| 345 |
+
|
| 346 |
+
if request.extrinsics:
|
| 347 |
+
inference_kwargs["extrinsics"] = np.array(request.extrinsics, dtype=np.float32)
|
| 348 |
+
|
| 349 |
+
if request.intrinsics:
|
| 350 |
+
inference_kwargs["intrinsics"] = np.array(request.intrinsics, dtype=np.float32)
|
| 351 |
+
|
| 352 |
+
# Run inference with timing
|
| 353 |
+
inference_start_time = time.time()
|
| 354 |
+
print(f"[{task_id}] Running model inference...")
|
| 355 |
+
_tasks[task_id].message = f"[{task_id}] Running model inference on {num_images} images..."
|
| 356 |
+
_tasks[task_id].progress = 0.3
|
| 357 |
+
|
| 358 |
+
inference_started = True
|
| 359 |
+
|
| 360 |
+
try:
|
| 361 |
+
model.inference(**inference_kwargs)
|
| 362 |
+
inference_time = time.time() - inference_start_time
|
| 363 |
+
avg_time_per_image = inference_time / num_images if num_images > 0 else 0
|
| 364 |
+
|
| 365 |
+
print(
|
| 366 |
+
f"[{task_id}] Inference completed in {inference_time:.2f}s "
|
| 367 |
+
f"({avg_time_per_image:.2f}s per image)"
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
except RuntimeError as e:
|
| 371 |
+
if "out of memory" in str(e).lower():
|
| 372 |
+
cleanup_cuda_memory()
|
| 373 |
+
raise RuntimeError(
|
| 374 |
+
f"OOM during inference: {str(e)}\n"
|
| 375 |
+
f"Settings: {num_images} images, resolution={request.process_res}\n"
|
| 376 |
+
f"Suggestions:\n"
|
| 377 |
+
f" 1. Reduce process_res to {int(request.process_res * 0.75)}\n"
|
| 378 |
+
f" 2. Process images in smaller batches\n"
|
| 379 |
+
f" 3. Use process_res_method='resize' instead of 'upper_bound_resize'"
|
| 380 |
+
)
|
| 381 |
+
raise
|
| 382 |
+
|
| 383 |
+
_tasks[task_id].progress = 0.9
|
| 384 |
+
|
| 385 |
+
# Post-inference cleanup
|
| 386 |
+
print(f"[{task_id}] Post-inference cleanup...")
|
| 387 |
+
cleanup_cuda_memory()
|
| 388 |
+
|
| 389 |
+
# Calculate total processing time
|
| 390 |
+
total_time = time.time() - start_time
|
| 391 |
+
|
| 392 |
+
# Update task status to completed
|
| 393 |
+
_tasks[task_id].status = "completed"
|
| 394 |
+
_tasks[task_id].completed_at = time.time()
|
| 395 |
+
_tasks[task_id].message = (
|
| 396 |
+
f"[{task_id}] Completed in {total_time:.2f}s " f"({avg_time_per_image:.2f}s per image)"
|
| 397 |
+
)
|
| 398 |
+
_tasks[task_id].progress = 1.0
|
| 399 |
+
_tasks[task_id].export_dir = request.export_dir
|
| 400 |
+
|
| 401 |
+
# Clear running state
|
| 402 |
+
_running_task_id = None
|
| 403 |
+
|
| 404 |
+
# Process next task in queue
|
| 405 |
+
_process_next_task()
|
| 406 |
+
|
| 407 |
+
print(f"[{task_id}] Task completed successfully")
|
| 408 |
+
print(
|
| 409 |
+
f"[{task_id}] Total time: {total_time:.2f}s, "
|
| 410 |
+
f"Inference time: {inference_time:.2f}s, "
|
| 411 |
+
f"Avg per image: {avg_time_per_image:.2f}s"
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
except Exception as e:
|
| 415 |
+
# Update task status to failed
|
| 416 |
+
error_msg = str(e)
|
| 417 |
+
total_time = time.time() - start_time
|
| 418 |
+
|
| 419 |
+
print(f"[{task_id}] Task failed after {total_time:.2f}s: {error_msg}")
|
| 420 |
+
|
| 421 |
+
# Always attempt cleanup on failure
|
| 422 |
+
cleanup_cuda_memory()
|
| 423 |
+
|
| 424 |
+
_tasks[task_id].status = "failed"
|
| 425 |
+
_tasks[task_id].completed_at = time.time()
|
| 426 |
+
_tasks[task_id].message = f"[{task_id}] Failed after {total_time:.2f}s: {error_msg}"
|
| 427 |
+
|
| 428 |
+
# Clear running state
|
| 429 |
+
_running_task_id = None
|
| 430 |
+
|
| 431 |
+
# Process next task in queue
|
| 432 |
+
_process_next_task()
|
| 433 |
+
|
| 434 |
+
finally:
|
| 435 |
+
# Final cleanup in finally block to ensure it always runs
|
| 436 |
+
# This is critical for releasing resources even if unexpected errors occur
|
| 437 |
+
try:
|
| 438 |
+
if inference_started:
|
| 439 |
+
print(f"[{task_id}] Final cleanup in finally block...")
|
| 440 |
+
cleanup_cuda_memory()
|
| 441 |
+
except Exception as e:
|
| 442 |
+
print(f"[{task_id}] Warning: Finally block cleanup failed: {e}")
|
| 443 |
+
|
| 444 |
+
# Schedule cleanup after task completion
|
| 445 |
+
_schedule_task_cleanup()
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def _cleanup_old_tasks():
|
| 449 |
+
"""Clean up old completed/failed tasks to prevent memory buildup."""
|
| 450 |
+
global _tasks
|
| 451 |
+
|
| 452 |
+
current_time = time.time()
|
| 453 |
+
tasks_to_remove = []
|
| 454 |
+
|
| 455 |
+
# Find tasks to remove - more aggressive cleanup
|
| 456 |
+
for task_id, task in _tasks.items():
|
| 457 |
+
# Remove completed/failed tasks older than 10 minutes (instead of 1 hour)
|
| 458 |
+
if (
|
| 459 |
+
task.status in ["completed", "failed"]
|
| 460 |
+
and task.completed_at
|
| 461 |
+
and current_time - task.completed_at > 600
|
| 462 |
+
): # 10 minutes
|
| 463 |
+
tasks_to_remove.append(task_id)
|
| 464 |
+
|
| 465 |
+
# Remove old tasks
|
| 466 |
+
for task_id in tasks_to_remove:
|
| 467 |
+
del _tasks[task_id]
|
| 468 |
+
print(f"[CLEANUP] Removed old task: {task_id}")
|
| 469 |
+
|
| 470 |
+
# If still too many tasks, remove oldest completed/failed tasks
|
| 471 |
+
if len(_tasks) > MAX_TASK_HISTORY:
|
| 472 |
+
completed_tasks = [
|
| 473 |
+
(task_id, task)
|
| 474 |
+
for task_id, task in _tasks.items()
|
| 475 |
+
if task.status in ["completed", "failed"]
|
| 476 |
+
]
|
| 477 |
+
completed_tasks.sort(key=lambda x: x[1].completed_at or 0)
|
| 478 |
+
|
| 479 |
+
excess_count = len(_tasks) - MAX_TASK_HISTORY
|
| 480 |
+
for i in range(min(excess_count, len(completed_tasks))):
|
| 481 |
+
task_id = completed_tasks[i][0]
|
| 482 |
+
del _tasks[task_id]
|
| 483 |
+
print(f"[CLEANUP] Removed excess task: {task_id}")
|
| 484 |
+
|
| 485 |
+
# Count active tasks (only pending and running)
|
| 486 |
+
active_count = sum(1 for task in _tasks.values() if task.status in ["pending", "running"])
|
| 487 |
+
print(
|
| 488 |
+
"[CLEANUP] Task cleanup completed. "
|
| 489 |
+
f"Total tasks: {len(_tasks)}, Active tasks: {active_count}"
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def _schedule_task_cleanup():
|
| 494 |
+
"""Schedule task cleanup in background."""
|
| 495 |
+
|
| 496 |
+
def cleanup_worker():
|
| 497 |
+
try:
|
| 498 |
+
time.sleep(2) # Small delay to ensure task status is updated
|
| 499 |
+
_cleanup_old_tasks()
|
| 500 |
+
except Exception as e:
|
| 501 |
+
print(f"[CLEANUP] Cleanup worker failed: {e}")
|
| 502 |
+
|
| 503 |
+
# Run cleanup in background thread
|
| 504 |
+
_executor.submit(cleanup_worker)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
# ============================================================================
|
| 508 |
+
# Gallery utilities (extracted from gallery.py)
|
| 509 |
+
# ============================================================================
|
| 510 |
+
|
| 511 |
+
GALLERY_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp")
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _load_gallery_html() -> str:
|
| 515 |
+
"""
|
| 516 |
+
Load and modify gallery HTML to work under /gallery/ subdirectory.
|
| 517 |
+
Replaces API paths from root to /gallery/ prefix.
|
| 518 |
+
"""
|
| 519 |
+
from ..services.gallery import HTML_PAGE
|
| 520 |
+
|
| 521 |
+
# Replace API paths to be under /gallery/ subdirectory
|
| 522 |
+
html = (
|
| 523 |
+
HTML_PAGE.replace("fetch('/manifest.json'", "fetch('/gallery/manifest.json'")
|
| 524 |
+
.replace("fetch('/manifest/'+", "fetch('/gallery/manifest/'+")
|
| 525 |
+
.replace(
|
| 526 |
+
"if(location.pathname!=\"/\")history.replaceState(null,'','/'+location.search)",
|
| 527 |
+
"if(!location.pathname.startsWith(\"/gallery\"))history.replaceState(null,'','/gallery/'+location.search)",
|
| 528 |
+
)
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
return html
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def _gallery_url_join(*parts: str) -> str:
|
| 535 |
+
"""Join URL parts safely."""
|
| 536 |
+
norm = posixpath.join(*[p.replace("\\", "/") for p in parts])
|
| 537 |
+
segs = [s for s in norm.split("/") if s not in ("", ".")]
|
| 538 |
+
return "/".join(quote(s) for s in segs)
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def _is_plain_name(name: str) -> bool:
|
| 542 |
+
"""Check if name is safe for use in paths."""
|
| 543 |
+
return all(c not in name for c in ("/", "\\")) and name not in (".", "..")
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def build_group_list(root_dir: str) -> dict:
|
| 547 |
+
"""Build list of groups from gallery directory."""
|
| 548 |
+
groups = []
|
| 549 |
+
try:
|
| 550 |
+
for gname in sorted(os.listdir(root_dir)):
|
| 551 |
+
gpath = os.path.join(root_dir, gname)
|
| 552 |
+
if not os.path.isdir(gpath):
|
| 553 |
+
continue
|
| 554 |
+
has_scene = False
|
| 555 |
+
try:
|
| 556 |
+
for sname in os.listdir(gpath):
|
| 557 |
+
spath = os.path.join(gpath, sname)
|
| 558 |
+
if not os.path.isdir(spath):
|
| 559 |
+
continue
|
| 560 |
+
if os.path.exists(os.path.join(spath, "scene.glb")) and os.path.exists(
|
| 561 |
+
os.path.join(spath, "scene.jpg")
|
| 562 |
+
):
|
| 563 |
+
has_scene = True
|
| 564 |
+
break
|
| 565 |
+
except Exception:
|
| 566 |
+
pass
|
| 567 |
+
if has_scene:
|
| 568 |
+
groups.append({"id": gname, "title": gname})
|
| 569 |
+
except Exception as e:
|
| 570 |
+
print(f"[warn] build_group_list failed: {e}")
|
| 571 |
+
return {"groups": groups}
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def build_group_manifest(root_dir: str, group: str) -> dict:
|
| 575 |
+
"""Build manifest for a specific group."""
|
| 576 |
+
items = []
|
| 577 |
+
gpath = os.path.join(root_dir, group)
|
| 578 |
+
try:
|
| 579 |
+
if not os.path.isdir(gpath):
|
| 580 |
+
return {"group": group, "items": []}
|
| 581 |
+
for sname in sorted(os.listdir(gpath)):
|
| 582 |
+
spath = os.path.join(gpath, sname)
|
| 583 |
+
if not os.path.isdir(spath):
|
| 584 |
+
continue
|
| 585 |
+
glb_fs = os.path.join(spath, "scene.glb")
|
| 586 |
+
jpg_fs = os.path.join(spath, "scene.jpg")
|
| 587 |
+
if not (os.path.exists(glb_fs) and os.path.exists(jpg_fs)):
|
| 588 |
+
continue
|
| 589 |
+
depth_images = []
|
| 590 |
+
dpath = os.path.join(spath, "depth_vis")
|
| 591 |
+
if os.path.isdir(dpath):
|
| 592 |
+
files = [
|
| 593 |
+
f
|
| 594 |
+
for f in os.listdir(dpath)
|
| 595 |
+
if os.path.splitext(f)[1].lower() in GALLERY_IMAGE_EXTS
|
| 596 |
+
]
|
| 597 |
+
for fn in sorted(files):
|
| 598 |
+
depth_images.append(
|
| 599 |
+
"/gallery/" + _gallery_url_join(group, sname, "depth_vis", fn)
|
| 600 |
+
)
|
| 601 |
+
items.append(
|
| 602 |
+
{
|
| 603 |
+
"id": sname,
|
| 604 |
+
"title": sname,
|
| 605 |
+
"model": "/gallery/" + _gallery_url_join(group, sname, "scene.glb"),
|
| 606 |
+
"thumbnail": "/gallery/" + _gallery_url_join(group, sname, "scene.jpg"),
|
| 607 |
+
"depth_images": depth_images,
|
| 608 |
+
}
|
| 609 |
+
)
|
| 610 |
+
except Exception as e:
|
| 611 |
+
print(f"[warn] build_group_manifest failed for {group}: {e}")
|
| 612 |
+
return {"group": group, "items": items}
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
# ============================================================================
|
| 616 |
+
# Inference request validation
|
| 617 |
+
# ============================================================================
|
| 618 |
+
|
| 619 |
+
# export_dir is expected to be a simple relative path; anything else is rejected below.
|
| 620 |
+
_EXPORT_DIR_METACHAR_RE = re.compile(r"[;&|`$\n\r<>\x00]")
|
| 621 |
+
|
| 622 |
+
# Hosts considered "local only" for the purposes of deciding whether an unauthenticated
|
| 623 |
+
# backend is safe to start.
|
| 624 |
+
_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1", "0:0:0:0:0:0:0:1"}
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
def validate_export_dir(export_dir: Optional[str], gallery_dir: Optional[str]) -> Optional[str]:
|
| 628 |
+
"""Validate a client-supplied export_dir before it reaches any inference/export code.
|
| 629 |
+
|
| 630 |
+
export_dir must be a simple relative path that resolves within the server's
|
| 631 |
+
configured gallery directory.
|
| 632 |
+
|
| 633 |
+
Raises:
|
| 634 |
+
ValueError: with a human-readable reason, if export_dir is rejected.
|
| 635 |
+
"""
|
| 636 |
+
if not export_dir:
|
| 637 |
+
return export_dir
|
| 638 |
+
|
| 639 |
+
if _EXPORT_DIR_METACHAR_RE.search(export_dir):
|
| 640 |
+
raise ValueError("export_dir contains disallowed characters")
|
| 641 |
+
|
| 642 |
+
if os.path.isabs(export_dir) or export_dir.startswith("~"):
|
| 643 |
+
raise ValueError("export_dir must be a relative path")
|
| 644 |
+
|
| 645 |
+
if any(part == ".." for part in export_dir.replace("\\", "/").split("/")):
|
| 646 |
+
raise ValueError("export_dir must not contain '..' path traversal")
|
| 647 |
+
|
| 648 |
+
if not gallery_dir:
|
| 649 |
+
raise ValueError(
|
| 650 |
+
"export_dir is not permitted: no gallery directory is configured on this server"
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
gallery_root = os.path.realpath(gallery_dir)
|
| 654 |
+
resolved = os.path.realpath(export_dir)
|
| 655 |
+
if resolved != gallery_root and not resolved.startswith(gallery_root + os.sep):
|
| 656 |
+
raise ValueError(
|
| 657 |
+
"export_dir must resolve within the server's configured gallery directory"
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
return export_dir
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def create_app(
|
| 664 |
+
model_dir: str,
|
| 665 |
+
device: str = "cuda",
|
| 666 |
+
gallery_dir: Optional[str] = None,
|
| 667 |
+
api_key: Optional[str] = None,
|
| 668 |
+
) -> FastAPI:
|
| 669 |
+
"""Create FastAPI application with model backend.
|
| 670 |
+
|
| 671 |
+
Note: the non-loopback-without-authentication guard lives in start_server(), not
|
| 672 |
+
here. Callers that take this app and serve it themselves (e.g. via a custom
|
| 673 |
+
uvicorn.run() or by mounting it into a larger ASGI app) are responsible for their
|
| 674 |
+
own host-binding and authentication decisions.
|
| 675 |
+
"""
|
| 676 |
+
global _backend, _app, _gallery_dir
|
| 677 |
+
|
| 678 |
+
_backend = ModelBackend(model_dir, device)
|
| 679 |
+
_app = FastAPI(
|
| 680 |
+
title="Depth Anything 3 Backend",
|
| 681 |
+
description="Model inference service for Depth Anything 3",
|
| 682 |
+
version="1.0.0",
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
# Module-level (not just closure-local) so background tasks can re-check it too.
|
| 686 |
+
_gallery_dir = gallery_dir
|
| 687 |
+
|
| 688 |
+
# Optional shared-secret gate for task-submitting endpoints. Falls back to the
|
| 689 |
+
# DA3_BACKEND_API_KEY env var so the key never needs to appear in a command line.
|
| 690 |
+
_api_key = api_key or os.environ.get("DA3_BACKEND_API_KEY")
|
| 691 |
+
|
| 692 |
+
async def _require_api_key(x_api_key: Optional[str] = Header(default=None, alias="X-API-Key")):
|
| 693 |
+
"""FastAPI dependency enforcing the API key when one is configured."""
|
| 694 |
+
if _api_key and not hmac.compare_digest(x_api_key or "", _api_key):
|
| 695 |
+
raise HTTPException(status_code=401, detail="Missing or invalid X-API-Key header")
|
| 696 |
+
|
| 697 |
+
@_app.get("/", response_class=HTMLResponse)
|
| 698 |
+
async def root():
|
| 699 |
+
"""Home page with navigation to dashboard and gallery."""
|
| 700 |
+
html_content = (
|
| 701 |
+
"""
|
| 702 |
+
<!DOCTYPE html>
|
| 703 |
+
<html lang="en">
|
| 704 |
+
<head>
|
| 705 |
+
<meta charset="UTF-8">
|
| 706 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 707 |
+
<title>Depth Anything 3 Backend</title>
|
| 708 |
+
<style>
|
| 709 |
+
:root {
|
| 710 |
+
--tech-blue: #00d4ff;
|
| 711 |
+
--tech-cyan: #00ffcc;
|
| 712 |
+
--tech-purple: #7877c6;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
* {
|
| 716 |
+
box-sizing: border-box;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
/* Dark mode styles */
|
| 720 |
+
@media (prefers-color-scheme: dark) {
|
| 721 |
+
body {
|
| 722 |
+
margin: 0;
|
| 723 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 724 |
+
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
| 725 |
+
color: #e8eaed;
|
| 726 |
+
min-height: 100vh;
|
| 727 |
+
display: flex;
|
| 728 |
+
align-items: center;
|
| 729 |
+
justify-content: center;
|
| 730 |
+
position: relative;
|
| 731 |
+
overflow-x: hidden;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
body::before {
|
| 735 |
+
content: '';
|
| 736 |
+
position: fixed;
|
| 737 |
+
top: 0;
|
| 738 |
+
left: 0;
|
| 739 |
+
right: 0;
|
| 740 |
+
bottom: 0;
|
| 741 |
+
background:
|
| 742 |
+
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
| 743 |
+
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
| 744 |
+
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%);
|
| 745 |
+
animation: techPulse 8s ease-in-out infinite;
|
| 746 |
+
z-index: -1;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.container {
|
| 750 |
+
max-width: 800px;
|
| 751 |
+
padding: 40px;
|
| 752 |
+
text-align: center;
|
| 753 |
+
z-index: 1;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
h1 {
|
| 757 |
+
font-size: 3em;
|
| 758 |
+
margin: 0 0 20px 0;
|
| 759 |
+
background: linear-gradient(45deg, var(--tech-blue), var(--tech-cyan), var(--tech-purple));
|
| 760 |
+
background-size: 400% 400%;
|
| 761 |
+
-webkit-background-clip: text;
|
| 762 |
+
background-clip: text;
|
| 763 |
+
color: transparent;
|
| 764 |
+
animation: techGradient 3s ease infinite;
|
| 765 |
+
text-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.subtitle {
|
| 769 |
+
font-size: 1.2em;
|
| 770 |
+
opacity: 0.8;
|
| 771 |
+
margin-bottom: 50px;
|
| 772 |
+
color: #a0a0a0;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.nav-grid {
|
| 776 |
+
display: grid;
|
| 777 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 778 |
+
gap: 24px;
|
| 779 |
+
margin-top: 40px;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.nav-card {
|
| 783 |
+
background: rgba(0, 0, 0, 0.3);
|
| 784 |
+
border: 1px solid rgba(0, 212, 255, 0.2);
|
| 785 |
+
border-radius: 16px;
|
| 786 |
+
padding: 30px;
|
| 787 |
+
text-decoration: none;
|
| 788 |
+
color: inherit;
|
| 789 |
+
transition: all 0.3s ease;
|
| 790 |
+
backdrop-filter: blur(10px);
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.nav-card:hover {
|
| 794 |
+
transform: translateY(-4px);
|
| 795 |
+
border-color: var(--tech-blue);
|
| 796 |
+
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.2);
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
.nav-card h2 {
|
| 800 |
+
margin: 0 0 15px 0;
|
| 801 |
+
font-size: 1.8em;
|
| 802 |
+
color: var(--tech-blue);
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.nav-card p {
|
| 806 |
+
margin: 0;
|
| 807 |
+
opacity: 0.8;
|
| 808 |
+
line-height: 1.6;
|
| 809 |
+
}
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
/* Light mode styles */
|
| 813 |
+
@media (prefers-color-scheme: light) {
|
| 814 |
+
body {
|
| 815 |
+
margin: 0;
|
| 816 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 817 |
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);
|
| 818 |
+
color: #1e293b;
|
| 819 |
+
min-height: 100vh;
|
| 820 |
+
display: flex;
|
| 821 |
+
align-items: center;
|
| 822 |
+
justify-content: center;
|
| 823 |
+
position: relative;
|
| 824 |
+
overflow-x: hidden;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
body::before {
|
| 828 |
+
content: '';
|
| 829 |
+
position: fixed;
|
| 830 |
+
top: 0;
|
| 831 |
+
left: 0;
|
| 832 |
+
right: 0;
|
| 833 |
+
bottom: 0;
|
| 834 |
+
background:
|
| 835 |
+
radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
| 836 |
+
radial-gradient(circle at 80% 20%, rgba(0, 102, 255, 0.1) 0%, transparent 50%),
|
| 837 |
+
radial-gradient(circle at 40% 40%, rgba(0, 255, 204, 0.08) 0%, transparent 50%);
|
| 838 |
+
animation: techPulse 8s ease-in-out infinite;
|
| 839 |
+
z-index: -1;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
.container {
|
| 843 |
+
max-width: 800px;
|
| 844 |
+
padding: 40px;
|
| 845 |
+
text-align: center;
|
| 846 |
+
z-index: 1;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
h1 {
|
| 850 |
+
font-size: 3em;
|
| 851 |
+
margin: 0 0 20px 0;
|
| 852 |
+
background: linear-gradient(45deg, #0066ff, #00d4ff, #00ffcc);
|
| 853 |
+
background-size: 400% 400%;
|
| 854 |
+
-webkit-background-clip: text;
|
| 855 |
+
background-clip: text;
|
| 856 |
+
color: transparent;
|
| 857 |
+
animation: techGradient 3s ease infinite;
|
| 858 |
+
text-shadow: 0 0 20px rgba(0, 102, 255, 0.3);
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.subtitle {
|
| 862 |
+
font-size: 1.2em;
|
| 863 |
+
opacity: 0.8;
|
| 864 |
+
margin-bottom: 50px;
|
| 865 |
+
color: #64748b;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.nav-grid {
|
| 869 |
+
display: grid;
|
| 870 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 871 |
+
gap: 24px;
|
| 872 |
+
margin-top: 40px;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
.nav-card {
|
| 876 |
+
background: rgba(255, 255, 255, 0.8);
|
| 877 |
+
border: 1px solid rgba(0, 212, 255, 0.3);
|
| 878 |
+
border-radius: 16px;
|
| 879 |
+
padding: 30px;
|
| 880 |
+
text-decoration: none;
|
| 881 |
+
color: inherit;
|
| 882 |
+
transition: all 0.3s ease;
|
| 883 |
+
backdrop-filter: blur(10px);
|
| 884 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
.nav-card:hover {
|
| 888 |
+
transform: translateY(-4px);
|
| 889 |
+
border-color: #0066ff;
|
| 890 |
+
box-shadow: 0 8px 25px rgba(0, 102, 255, 0.2);
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.nav-card h2 {
|
| 894 |
+
margin: 0 0 15px 0;
|
| 895 |
+
font-size: 1.8em;
|
| 896 |
+
color: #0066ff;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.nav-card p {
|
| 900 |
+
margin: 0;
|
| 901 |
+
opacity: 0.8;
|
| 902 |
+
line-height: 1.6;
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
@keyframes techPulse {
|
| 907 |
+
0%, 100% { opacity: 0.5; }
|
| 908 |
+
50% { opacity: 0.8; }
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
@keyframes techGradient {
|
| 912 |
+
0% { background-position: 0% 50%; }
|
| 913 |
+
50% { background-position: 100% 50%; }
|
| 914 |
+
100% { background-position: 0% 50%; }
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
.footer {
|
| 918 |
+
margin-top: 50px;
|
| 919 |
+
opacity: 0.6;
|
| 920 |
+
font-size: 0.9em;
|
| 921 |
+
}
|
| 922 |
+
</style>
|
| 923 |
+
</head>
|
| 924 |
+
<body>
|
| 925 |
+
<div class="container">
|
| 926 |
+
<h1>Depth Anything 3</h1>
|
| 927 |
+
<p class="subtitle">Model Backend Service</p>
|
| 928 |
+
<div class="nav-grid">
|
| 929 |
+
<a href="/dashboard" class="nav-card">
|
| 930 |
+
<h2>📊 Dashboard</h2>
|
| 931 |
+
<p>Monitor backend status, model information, and inference tasks in real-time.</p>
|
| 932 |
+
</a>
|
| 933 |
+
"""
|
| 934 |
+
+ (
|
| 935 |
+
'<a href="/gallery/" class="nav-card">'
|
| 936 |
+
"<h2>🎨 Gallery</h2>"
|
| 937 |
+
"<p>Browse 3D reconstructions and depth visualizations from processed scenes.</p>"
|
| 938 |
+
"</a>"
|
| 939 |
+
if _gallery_dir and os.path.exists(_gallery_dir)
|
| 940 |
+
else ""
|
| 941 |
+
)
|
| 942 |
+
+ """
|
| 943 |
+
</div>
|
| 944 |
+
<div class="footer">
|
| 945 |
+
<p>Depth Anything 3 Backend API</p>
|
| 946 |
+
</div>
|
| 947 |
+
</div>
|
| 948 |
+
</body>
|
| 949 |
+
</html>
|
| 950 |
+
"""
|
| 951 |
+
)
|
| 952 |
+
return HTMLResponse(html_content)
|
| 953 |
+
|
| 954 |
+
@_app.get("/dashboard", response_class=HTMLResponse)
|
| 955 |
+
async def dashboard():
|
| 956 |
+
"""HTML dashboard for monitoring backend status and tasks."""
|
| 957 |
+
if _backend is None:
|
| 958 |
+
return HTMLResponse("<h1>Backend not initialized</h1>", status_code=500)
|
| 959 |
+
|
| 960 |
+
# Get backend status
|
| 961 |
+
status = _backend.get_status()
|
| 962 |
+
|
| 963 |
+
# Safely format status values
|
| 964 |
+
if status["load_time"] is not None:
|
| 965 |
+
load_time_str = f"{status['load_time']:.2f}s"
|
| 966 |
+
else:
|
| 967 |
+
load_time_str = "Not loaded"
|
| 968 |
+
|
| 969 |
+
if status["uptime"] is not None:
|
| 970 |
+
uptime_str = f"{status['uptime']:.2f}s"
|
| 971 |
+
else:
|
| 972 |
+
uptime_str = "Not running"
|
| 973 |
+
|
| 974 |
+
# Get tasks information
|
| 975 |
+
active_tasks = [task for task in _tasks.values() if task.status in ["pending", "running"]]
|
| 976 |
+
completed_tasks = [
|
| 977 |
+
task for task in _tasks.values() if task.status in ["completed", "failed"]
|
| 978 |
+
]
|
| 979 |
+
|
| 980 |
+
# Generate task HTML
|
| 981 |
+
if active_tasks:
|
| 982 |
+
active_tasks_html = "".join(
|
| 983 |
+
_render_task_card(task, "running") for task in active_tasks
|
| 984 |
+
)
|
| 985 |
+
else:
|
| 986 |
+
active_tasks_html = "<p>No active tasks</p>"
|
| 987 |
+
|
| 988 |
+
if completed_tasks:
|
| 989 |
+
completed_tasks_html = "".join(
|
| 990 |
+
_render_task_card(task, "completed") for task in completed_tasks[-10:]
|
| 991 |
+
)
|
| 992 |
+
else:
|
| 993 |
+
completed_tasks_html = "<p>No completed tasks</p>"
|
| 994 |
+
|
| 995 |
+
# Generate HTML
|
| 996 |
+
html_content = f"""
|
| 997 |
+
<!DOCTYPE html>
|
| 998 |
+
<html lang="en">
|
| 999 |
+
<head>
|
| 1000 |
+
<meta charset="UTF-8">
|
| 1001 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1002 |
+
<title>Depth Anything 3 Backend Dashboard</title>
|
| 1003 |
+
<style>
|
| 1004 |
+
body {{
|
| 1005 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 1006 |
+
margin: 0;
|
| 1007 |
+
padding: 20px;
|
| 1008 |
+
background-color: #f5f5f5;
|
| 1009 |
+
}}
|
| 1010 |
+
.container {{
|
| 1011 |
+
max-width: 1200px;
|
| 1012 |
+
margin: 0 auto;
|
| 1013 |
+
}}
|
| 1014 |
+
.header {{
|
| 1015 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1016 |
+
color: white;
|
| 1017 |
+
padding: 20px;
|
| 1018 |
+
border-radius: 10px;
|
| 1019 |
+
margin-bottom: 20px;
|
| 1020 |
+
text-align: center;
|
| 1021 |
+
}}
|
| 1022 |
+
.status-grid {{
|
| 1023 |
+
display: grid;
|
| 1024 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 1025 |
+
gap: 20px;
|
| 1026 |
+
margin-bottom: 30px;
|
| 1027 |
+
}}
|
| 1028 |
+
.status-card {{
|
| 1029 |
+
background: white;
|
| 1030 |
+
padding: 20px;
|
| 1031 |
+
border-radius: 10px;
|
| 1032 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1033 |
+
}}
|
| 1034 |
+
.status-card h3 {{
|
| 1035 |
+
margin-top: 0;
|
| 1036 |
+
color: #333;
|
| 1037 |
+
}}
|
| 1038 |
+
.status-item {{
|
| 1039 |
+
display: flex;
|
| 1040 |
+
justify-content: space-between;
|
| 1041 |
+
margin: 10px 0;
|
| 1042 |
+
padding: 8px 0;
|
| 1043 |
+
border-bottom: 1px solid #eee;
|
| 1044 |
+
}}
|
| 1045 |
+
.status-item:last-child {{
|
| 1046 |
+
border-bottom: none;
|
| 1047 |
+
}}
|
| 1048 |
+
.status-value {{
|
| 1049 |
+
font-weight: bold;
|
| 1050 |
+
color: #666;
|
| 1051 |
+
}}
|
| 1052 |
+
.status-online {{
|
| 1053 |
+
color: #28a745;
|
| 1054 |
+
}}
|
| 1055 |
+
.status-offline {{
|
| 1056 |
+
color: #dc3545;
|
| 1057 |
+
}}
|
| 1058 |
+
.tasks-section {{
|
| 1059 |
+
background: white;
|
| 1060 |
+
padding: 20px;
|
| 1061 |
+
border-radius: 10px;
|
| 1062 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 1063 |
+
margin-bottom: 20px;
|
| 1064 |
+
}}
|
| 1065 |
+
.task-item {{
|
| 1066 |
+
background: #f8f9fa;
|
| 1067 |
+
padding: 15px;
|
| 1068 |
+
margin: 10px 0;
|
| 1069 |
+
border-radius: 8px;
|
| 1070 |
+
border-left: 4px solid #007bff;
|
| 1071 |
+
}}
|
| 1072 |
+
.task-item.completed {{
|
| 1073 |
+
border-left-color: #28a745;
|
| 1074 |
+
}}
|
| 1075 |
+
.task-item.failed {{
|
| 1076 |
+
border-left-color: #dc3545;
|
| 1077 |
+
}}
|
| 1078 |
+
.task-item.running {{
|
| 1079 |
+
border-left-color: #ffc107;
|
| 1080 |
+
}}
|
| 1081 |
+
.task-header {{
|
| 1082 |
+
display: flex;
|
| 1083 |
+
justify-content: space-between;
|
| 1084 |
+
align-items: center;
|
| 1085 |
+
margin-bottom: 8px;
|
| 1086 |
+
}}
|
| 1087 |
+
.task-id {{
|
| 1088 |
+
font-family: monospace;
|
| 1089 |
+
font-size: 12px;
|
| 1090 |
+
color: #666;
|
| 1091 |
+
}}
|
| 1092 |
+
.task-status {{
|
| 1093 |
+
padding: 4px 8px;
|
| 1094 |
+
border-radius: 4px;
|
| 1095 |
+
font-size: 12px;
|
| 1096 |
+
font-weight: bold;
|
| 1097 |
+
}}
|
| 1098 |
+
.status-pending {{
|
| 1099 |
+
background: #fff3cd;
|
| 1100 |
+
color: #856404;
|
| 1101 |
+
}}
|
| 1102 |
+
.status-running {{
|
| 1103 |
+
background: #d4edda;
|
| 1104 |
+
color: #155724;
|
| 1105 |
+
}}
|
| 1106 |
+
.status-completed {{
|
| 1107 |
+
background: #d1ecf1;
|
| 1108 |
+
color: #0c5460;
|
| 1109 |
+
}}
|
| 1110 |
+
.status-failed {{
|
| 1111 |
+
background: #f8d7da;
|
| 1112 |
+
color: #721c24;
|
| 1113 |
+
}}
|
| 1114 |
+
.refresh-btn {{
|
| 1115 |
+
background: #007bff;
|
| 1116 |
+
color: white;
|
| 1117 |
+
border: none;
|
| 1118 |
+
padding: 10px 20px;
|
| 1119 |
+
border-radius: 5px;
|
| 1120 |
+
cursor: pointer;
|
| 1121 |
+
font-size: 14px;
|
| 1122 |
+
}}
|
| 1123 |
+
.refresh-btn:hover {{
|
| 1124 |
+
background: #0056b3;
|
| 1125 |
+
}}
|
| 1126 |
+
.auto-refresh {{
|
| 1127 |
+
margin-left: 10px;
|
| 1128 |
+
}}
|
| 1129 |
+
.timestamp {{
|
| 1130 |
+
font-size: 12px;
|
| 1131 |
+
color: #666;
|
| 1132 |
+
margin-top: 10px;
|
| 1133 |
+
}}
|
| 1134 |
+
.task-message {{
|
| 1135 |
+
font-size: 14px;
|
| 1136 |
+
color: #333;
|
| 1137 |
+
margin-bottom: 8px;
|
| 1138 |
+
}}
|
| 1139 |
+
.task-params {{
|
| 1140 |
+
font-size: 12px;
|
| 1141 |
+
color: #666;
|
| 1142 |
+
background: #f8f9fa;
|
| 1143 |
+
padding: 6px 8px;
|
| 1144 |
+
border-radius: 4px;
|
| 1145 |
+
margin-top: 8px;
|
| 1146 |
+
}}
|
| 1147 |
+
</style>
|
| 1148 |
+
</head>
|
| 1149 |
+
<body>
|
| 1150 |
+
<div class="container">
|
| 1151 |
+
<div class="header">
|
| 1152 |
+
<h1>Depth Anything 3 Backend Dashboard</h1>
|
| 1153 |
+
<p>Real-time monitoring of model status and inference tasks</p>
|
| 1154 |
+
</div>
|
| 1155 |
+
|
| 1156 |
+
<div class="status-grid">
|
| 1157 |
+
<div class="status-card">
|
| 1158 |
+
<h3>Model Status</h3>
|
| 1159 |
+
<div class="status-item">
|
| 1160 |
+
<span>Status:</span>
|
| 1161 |
+
<span class="status-value {'status-online' if status['model_loaded'] else 'status-offline'}">
|
| 1162 |
+
{'Online' if status['model_loaded'] else 'Offline'}
|
| 1163 |
+
</span>
|
| 1164 |
+
</div>
|
| 1165 |
+
<div class="status-item">
|
| 1166 |
+
<span>Model Directory:</span>
|
| 1167 |
+
<span class="status-value">{html.escape(str(status['model_dir']))}</span>
|
| 1168 |
+
</div>
|
| 1169 |
+
<div class="status-item">
|
| 1170 |
+
<span>Device:</span>
|
| 1171 |
+
<span class="status-value">{html.escape(str(status['device']))}</span>
|
| 1172 |
+
</div>
|
| 1173 |
+
<div class="status-item">
|
| 1174 |
+
<span>Load Time:</span>
|
| 1175 |
+
<span class="status-value">{load_time_str}</span>
|
| 1176 |
+
</div>
|
| 1177 |
+
<div class="status-item">
|
| 1178 |
+
<span>Uptime:</span>
|
| 1179 |
+
<span class="status-value">{uptime_str}</span>
|
| 1180 |
+
</div>
|
| 1181 |
+
</div>
|
| 1182 |
+
|
| 1183 |
+
<div class="status-card">
|
| 1184 |
+
<h3>Task Summary</h3>
|
| 1185 |
+
<div class="status-item">
|
| 1186 |
+
<span>Active Tasks:</span>
|
| 1187 |
+
<span class="status-value">{len(active_tasks)}</span>
|
| 1188 |
+
</div>
|
| 1189 |
+
<div class="status-item">
|
| 1190 |
+
<span>Completed Tasks:</span>
|
| 1191 |
+
<span class="status-value">{len(completed_tasks)}</span>
|
| 1192 |
+
</div>
|
| 1193 |
+
<div class="status-item">
|
| 1194 |
+
<span>Total Tasks:</span>
|
| 1195 |
+
<span class="status-value">{len(_tasks)}</span>
|
| 1196 |
+
</div>
|
| 1197 |
+
</div>
|
| 1198 |
+
</div>
|
| 1199 |
+
|
| 1200 |
+
<div class="tasks-section">
|
| 1201 |
+
<h3>Active Tasks</h3>
|
| 1202 |
+
<button class="refresh-btn" onclick="location.reload()">Refresh</button>
|
| 1203 |
+
<label class="auto-refresh">
|
| 1204 |
+
<input type="checkbox" id="autoRefresh" onchange="toggleAutoRefresh()"> Auto-refresh (5s)
|
| 1205 |
+
</label>
|
| 1206 |
+
<div class="timestamp">Last updated: <span id="lastUpdate">{time.strftime('%Y-%m-%d %H:%M:%S')}</span></div>
|
| 1207 |
+
|
| 1208 |
+
{active_tasks_html}
|
| 1209 |
+
</div>
|
| 1210 |
+
|
| 1211 |
+
<div class="tasks-section">
|
| 1212 |
+
<h3>Recent Completed Tasks</h3>
|
| 1213 |
+
{completed_tasks_html}
|
| 1214 |
+
</div>
|
| 1215 |
+
</div>
|
| 1216 |
+
|
| 1217 |
+
<script>
|
| 1218 |
+
let autoRefreshInterval;
|
| 1219 |
+
|
| 1220 |
+
function toggleAutoRefresh() {{
|
| 1221 |
+
const checkbox = document.getElementById('autoRefresh');
|
| 1222 |
+
if (checkbox.checked) {{
|
| 1223 |
+
autoRefreshInterval = setInterval(() => {{
|
| 1224 |
+
location.reload();
|
| 1225 |
+
}}, 5000);
|
| 1226 |
+
}} else {{
|
| 1227 |
+
clearInterval(autoRefreshInterval);
|
| 1228 |
+
}}
|
| 1229 |
+
}}
|
| 1230 |
+
|
| 1231 |
+
// Update timestamp every second
|
| 1232 |
+
setInterval(() => {{
|
| 1233 |
+
const now = new Date();
|
| 1234 |
+
document.getElementById('lastUpdate').textContent = now.toLocaleString();
|
| 1235 |
+
}}, 1000);
|
| 1236 |
+
</script>
|
| 1237 |
+
</body>
|
| 1238 |
+
</html>
|
| 1239 |
+
"""
|
| 1240 |
+
|
| 1241 |
+
return HTMLResponse(html_content)
|
| 1242 |
+
|
| 1243 |
+
@_app.get("/status")
|
| 1244 |
+
async def get_status():
|
| 1245 |
+
"""Get backend status with GPU memory information."""
|
| 1246 |
+
if _backend is None:
|
| 1247 |
+
raise HTTPException(status_code=500, detail="Backend not initialized")
|
| 1248 |
+
|
| 1249 |
+
status = _backend.get_status()
|
| 1250 |
+
|
| 1251 |
+
# Add GPU memory information
|
| 1252 |
+
gpu_memory = get_gpu_memory_info()
|
| 1253 |
+
if gpu_memory:
|
| 1254 |
+
status["gpu_memory"] = {
|
| 1255 |
+
"total_gb": round(gpu_memory["total_gb"], 2),
|
| 1256 |
+
"allocated_gb": round(gpu_memory["allocated_gb"], 2),
|
| 1257 |
+
"reserved_gb": round(gpu_memory["reserved_gb"], 2),
|
| 1258 |
+
"free_gb": round(gpu_memory["free_gb"], 2),
|
| 1259 |
+
"utilization_percent": round(gpu_memory["utilization"], 1),
|
| 1260 |
+
}
|
| 1261 |
+
else:
|
| 1262 |
+
status["gpu_memory"] = None
|
| 1263 |
+
|
| 1264 |
+
return status
|
| 1265 |
+
|
| 1266 |
+
@_app.post(
|
| 1267 |
+
"/inference", response_model=InferenceResponse, dependencies=[Depends(_require_api_key)]
|
| 1268 |
+
)
|
| 1269 |
+
async def run_inference(request: InferenceRequest):
|
| 1270 |
+
"""Submit inference task and return task ID."""
|
| 1271 |
+
global _running_task_id
|
| 1272 |
+
|
| 1273 |
+
if _backend is None:
|
| 1274 |
+
raise HTTPException(status_code=500, detail="Backend not initialized")
|
| 1275 |
+
|
| 1276 |
+
if request.export_dir:
|
| 1277 |
+
try:
|
| 1278 |
+
validate_export_dir(request.export_dir, _gallery_dir)
|
| 1279 |
+
except ValueError as e:
|
| 1280 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 1281 |
+
|
| 1282 |
+
# Generate unique task ID
|
| 1283 |
+
task_id = str(uuid.uuid4())
|
| 1284 |
+
|
| 1285 |
+
# Create task status
|
| 1286 |
+
if _running_task_id is not None:
|
| 1287 |
+
status_msg = f"[{task_id}] Task queued (waiting for {_running_task_id} to complete)"
|
| 1288 |
+
else:
|
| 1289 |
+
status_msg = f"[{task_id}] Task submitted"
|
| 1290 |
+
|
| 1291 |
+
_tasks[task_id] = TaskStatus(
|
| 1292 |
+
task_id=task_id,
|
| 1293 |
+
status="pending",
|
| 1294 |
+
message=status_msg,
|
| 1295 |
+
created_at=time.time(),
|
| 1296 |
+
export_dir=request.export_dir,
|
| 1297 |
+
request=request,
|
| 1298 |
+
# Record essential parameters
|
| 1299 |
+
num_images=len(request.image_paths),
|
| 1300 |
+
export_format=request.export_format,
|
| 1301 |
+
process_res_method=request.process_res_method,
|
| 1302 |
+
video_path=(
|
| 1303 |
+
request.image_paths[0] if request.image_paths else None
|
| 1304 |
+
), # Use first image path as video reference
|
| 1305 |
+
)
|
| 1306 |
+
|
| 1307 |
+
# Add task to queue
|
| 1308 |
+
_task_queue.append(task_id)
|
| 1309 |
+
|
| 1310 |
+
# If no task is running, start processing the queue
|
| 1311 |
+
if _running_task_id is None:
|
| 1312 |
+
_process_next_task()
|
| 1313 |
+
|
| 1314 |
+
return InferenceResponse(
|
| 1315 |
+
success=True,
|
| 1316 |
+
message="Task submitted successfully",
|
| 1317 |
+
task_id=task_id,
|
| 1318 |
+
export_dir=request.export_dir,
|
| 1319 |
+
export_format=request.export_format,
|
| 1320 |
+
)
|
| 1321 |
+
|
| 1322 |
+
@_app.get("/task/{task_id}", response_model=TaskStatus)
|
| 1323 |
+
async def get_task_status(task_id: str):
|
| 1324 |
+
"""Get task status by task ID."""
|
| 1325 |
+
if task_id not in _tasks:
|
| 1326 |
+
raise HTTPException(status_code=404, detail="Task not found")
|
| 1327 |
+
|
| 1328 |
+
return _tasks[task_id]
|
| 1329 |
+
|
| 1330 |
+
@_app.get("/gpu-memory")
|
| 1331 |
+
async def get_gpu_memory():
|
| 1332 |
+
"""Get detailed GPU memory information."""
|
| 1333 |
+
gpu_memory = get_gpu_memory_info()
|
| 1334 |
+
if gpu_memory is None:
|
| 1335 |
+
return {
|
| 1336 |
+
"available": False,
|
| 1337 |
+
"message": "CUDA not available or memory info cannot be retrieved",
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
return {
|
| 1341 |
+
"available": True,
|
| 1342 |
+
"total_gb": round(gpu_memory["total_gb"], 2),
|
| 1343 |
+
"allocated_gb": round(gpu_memory["allocated_gb"], 2),
|
| 1344 |
+
"reserved_gb": round(gpu_memory["reserved_gb"], 2),
|
| 1345 |
+
"free_gb": round(gpu_memory["free_gb"], 2),
|
| 1346 |
+
"utilization_percent": round(gpu_memory["utilization"], 1),
|
| 1347 |
+
"status": (
|
| 1348 |
+
"healthy"
|
| 1349 |
+
if gpu_memory["utilization"] < 80
|
| 1350 |
+
else "warning" if gpu_memory["utilization"] < 95 else "critical"
|
| 1351 |
+
),
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
@_app.get("/tasks")
|
| 1355 |
+
async def list_tasks():
|
| 1356 |
+
"""List all tasks."""
|
| 1357 |
+
# Separate active and completed tasks
|
| 1358 |
+
active_tasks = [task for task in _tasks.values() if task.status in ["pending", "running"]]
|
| 1359 |
+
completed_tasks = [
|
| 1360 |
+
task for task in _tasks.values() if task.status in ["completed", "failed"]
|
| 1361 |
+
]
|
| 1362 |
+
|
| 1363 |
+
return {
|
| 1364 |
+
"tasks": list(_tasks.values()),
|
| 1365 |
+
"active_tasks": active_tasks,
|
| 1366 |
+
"completed_tasks": completed_tasks,
|
| 1367 |
+
"active_count": len(active_tasks),
|
| 1368 |
+
"total_count": len(_tasks),
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
@_app.post("/cleanup")
|
| 1372 |
+
async def manual_cleanup():
|
| 1373 |
+
"""Manually trigger task cleanup."""
|
| 1374 |
+
try:
|
| 1375 |
+
_cleanup_old_tasks()
|
| 1376 |
+
return {"message": "Cleanup completed", "active_tasks": len(_tasks)}
|
| 1377 |
+
except Exception as e:
|
| 1378 |
+
raise HTTPException(status_code=500, detail=f"Cleanup failed: {str(e)}")
|
| 1379 |
+
|
| 1380 |
+
@_app.delete("/task/{task_id}")
|
| 1381 |
+
async def delete_task(task_id: str):
|
| 1382 |
+
"""Delete a specific task."""
|
| 1383 |
+
if task_id not in _tasks:
|
| 1384 |
+
raise HTTPException(status_code=404, detail="Task not found")
|
| 1385 |
+
|
| 1386 |
+
# Only allow deletion of completed/failed tasks
|
| 1387 |
+
if _tasks[task_id].status not in ["completed", "failed"]:
|
| 1388 |
+
raise HTTPException(status_code=400, detail="Cannot delete running or pending tasks")
|
| 1389 |
+
|
| 1390 |
+
del _tasks[task_id]
|
| 1391 |
+
return {"message": f"Task {task_id} deleted successfully"}
|
| 1392 |
+
|
| 1393 |
+
@_app.post("/reload")
|
| 1394 |
+
async def reload_model():
|
| 1395 |
+
"""Reload the model."""
|
| 1396 |
+
if _backend is None:
|
| 1397 |
+
raise HTTPException(status_code=500, detail="Backend not initialized")
|
| 1398 |
+
|
| 1399 |
+
try:
|
| 1400 |
+
_backend.model = None
|
| 1401 |
+
_backend.model_loaded = False
|
| 1402 |
+
_backend.load_model()
|
| 1403 |
+
return {"message": "Model reloaded successfully"}
|
| 1404 |
+
except Exception as e:
|
| 1405 |
+
raise HTTPException(status_code=500, detail=f"Failed to reload model: {str(e)}")
|
| 1406 |
+
|
| 1407 |
+
# ============================================================================
|
| 1408 |
+
# Gallery routes
|
| 1409 |
+
# ============================================================================
|
| 1410 |
+
|
| 1411 |
+
if _gallery_dir and os.path.exists(_gallery_dir):
|
| 1412 |
+
# Load gallery HTML page (with modified paths for /gallery/ subdirectory)
|
| 1413 |
+
_gallery_html = _load_gallery_html()
|
| 1414 |
+
|
| 1415 |
+
@_app.get("/gallery/", response_class=HTMLResponse)
|
| 1416 |
+
@_app.get("/gallery", response_class=HTMLResponse)
|
| 1417 |
+
async def gallery_home():
|
| 1418 |
+
"""Gallery home page."""
|
| 1419 |
+
return HTMLResponse(_gallery_html)
|
| 1420 |
+
|
| 1421 |
+
@_app.get("/gallery/manifest.json")
|
| 1422 |
+
async def gallery_manifest():
|
| 1423 |
+
"""Get gallery group list."""
|
| 1424 |
+
try:
|
| 1425 |
+
return build_group_list(_gallery_dir)
|
| 1426 |
+
except Exception as e:
|
| 1427 |
+
raise HTTPException(
|
| 1428 |
+
status_code=500, detail=f"Failed to build group list: {str(e)}"
|
| 1429 |
+
)
|
| 1430 |
+
|
| 1431 |
+
@_app.get("/gallery/manifest/{group}.json")
|
| 1432 |
+
async def gallery_group_manifest(group: str):
|
| 1433 |
+
"""Get manifest for a specific group."""
|
| 1434 |
+
if not _is_plain_name(group):
|
| 1435 |
+
raise HTTPException(status_code=400, detail="Invalid group name")
|
| 1436 |
+
try:
|
| 1437 |
+
return build_group_manifest(_gallery_dir, group)
|
| 1438 |
+
except Exception as e:
|
| 1439 |
+
raise HTTPException(
|
| 1440 |
+
status_code=500, detail=f"Failed to build group manifest: {str(e)}"
|
| 1441 |
+
)
|
| 1442 |
+
|
| 1443 |
+
@_app.get("/gallery/{path:path}")
|
| 1444 |
+
async def gallery_files(path: str):
|
| 1445 |
+
"""Serve gallery static files (GLB, JPG, etc.)."""
|
| 1446 |
+
# Security check: prevent directory traversal
|
| 1447 |
+
path_parts = path.split("/")
|
| 1448 |
+
if any(not _is_plain_name(part) for part in path_parts if part):
|
| 1449 |
+
raise HTTPException(status_code=400, detail="Invalid path")
|
| 1450 |
+
|
| 1451 |
+
file_path = os.path.join(_gallery_dir, *path_parts)
|
| 1452 |
+
|
| 1453 |
+
# Ensure the file is within gallery directory
|
| 1454 |
+
real_file_path = os.path.realpath(file_path)
|
| 1455 |
+
real_gallery_dir = os.path.realpath(_gallery_dir)
|
| 1456 |
+
if not real_file_path.startswith(real_gallery_dir):
|
| 1457 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 1458 |
+
|
| 1459 |
+
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
| 1460 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 1461 |
+
|
| 1462 |
+
return FileResponse(file_path)
|
| 1463 |
+
|
| 1464 |
+
return _app
|
| 1465 |
+
|
| 1466 |
+
|
| 1467 |
+
def start_server(
|
| 1468 |
+
model_dir: str,
|
| 1469 |
+
device: str = "cuda",
|
| 1470 |
+
host: str = "127.0.0.1",
|
| 1471 |
+
port: int = 8000,
|
| 1472 |
+
gallery_dir: Optional[str] = None,
|
| 1473 |
+
api_key: Optional[str] = None,
|
| 1474 |
+
allow_unauthenticated: bool = False,
|
| 1475 |
+
):
|
| 1476 |
+
"""Start the backend server."""
|
| 1477 |
+
api_key = api_key or os.environ.get("DA3_BACKEND_API_KEY")
|
| 1478 |
+
auto_generated_key = False
|
| 1479 |
+
|
| 1480 |
+
if host not in _LOOPBACK_HOSTS and not api_key and not allow_unauthenticated:
|
| 1481 |
+
# No key was configured for a host reachable beyond this machine: generate one
|
| 1482 |
+
# for this run rather than requiring the operator to come up with one first.
|
| 1483 |
+
api_key = secrets.token_urlsafe(32)
|
| 1484 |
+
auto_generated_key = True
|
| 1485 |
+
|
| 1486 |
+
app = create_app(model_dir, device, gallery_dir, api_key=api_key)
|
| 1487 |
+
|
| 1488 |
+
print("Starting Depth Anything 3 Backend...")
|
| 1489 |
+
print(f"Model directory: {model_dir}")
|
| 1490 |
+
print(f"Device: {device}")
|
| 1491 |
+
print(f"Server: http://{host}:{port}")
|
| 1492 |
+
print(f"Dashboard: http://{host}:{port}/dashboard")
|
| 1493 |
+
print(f"API Status: http://{host}:{port}/status")
|
| 1494 |
+
|
| 1495 |
+
if gallery_dir and os.path.exists(gallery_dir):
|
| 1496 |
+
print(f"Gallery: http://{host}:{port}/gallery/")
|
| 1497 |
+
|
| 1498 |
+
if auto_generated_key:
|
| 1499 |
+
print("=" * 60)
|
| 1500 |
+
print("No --api-key was set, so one was generated for this run:")
|
| 1501 |
+
print(f" X-API-Key: {api_key}")
|
| 1502 |
+
print("Set DA3_BACKEND_API_KEY to this value wherever you submit jobs from")
|
| 1503 |
+
print("(e.g. `da3 ... --use-backend`), or pass --api-key / set")
|
| 1504 |
+
print("DA3_BACKEND_API_KEY before starting the backend for a key that stays")
|
| 1505 |
+
print("the same across restarts. Use --allow-unauthenticated to skip this.")
|
| 1506 |
+
elif api_key:
|
| 1507 |
+
print("Authentication: X-API-Key header required for /inference")
|
| 1508 |
+
|
| 1509 |
+
print("=" * 60)
|
| 1510 |
+
print("Backend is running! You can now:")
|
| 1511 |
+
print(f" • Open home page: http://{host}:{port}")
|
| 1512 |
+
print(f" • Open dashboard: http://{host}:{port}/dashboard")
|
| 1513 |
+
print(f" • Check API status: http://{host}:{port}/status")
|
| 1514 |
+
|
| 1515 |
+
if gallery_dir and os.path.exists(gallery_dir):
|
| 1516 |
+
print(f" • Browse gallery: http://{host}:{port}/gallery/")
|
| 1517 |
+
|
| 1518 |
+
print(" • Submit inference tasks via API")
|
| 1519 |
+
print("=" * 60)
|
| 1520 |
+
|
| 1521 |
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
| 1522 |
+
|
| 1523 |
+
|
| 1524 |
+
if __name__ == "__main__":
|
| 1525 |
+
import argparse
|
| 1526 |
+
|
| 1527 |
+
parser = argparse.ArgumentParser(description="Depth Anything 3 Backend Server")
|
| 1528 |
+
parser.add_argument("--model-dir", required=True, help="Model directory path")
|
| 1529 |
+
parser.add_argument("--device", default="cuda", help="Device to use")
|
| 1530 |
+
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
|
| 1531 |
+
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
| 1532 |
+
parser.add_argument("--gallery-dir", help="Gallery directory path (optional)")
|
| 1533 |
+
parser.add_argument(
|
| 1534 |
+
"--api-key",
|
| 1535 |
+
default=None,
|
| 1536 |
+
help="Require this API key (X-API-Key header) on inference requests. Falls back "
|
| 1537 |
+
"to the DA3_BACKEND_API_KEY env var, or an auto-generated key when binding to a "
|
| 1538 |
+
"non-loopback host.",
|
| 1539 |
+
)
|
| 1540 |
+
parser.add_argument(
|
| 1541 |
+
"--allow-unauthenticated",
|
| 1542 |
+
action="store_true",
|
| 1543 |
+
help="Skip authentication entirely on a non-loopback host, instead of using an "
|
| 1544 |
+
"auto-generated API key.",
|
| 1545 |
+
)
|
| 1546 |
+
|
| 1547 |
+
args = parser.parse_args()
|
| 1548 |
+
start_server(
|
| 1549 |
+
args.model_dir,
|
| 1550 |
+
args.device,
|
| 1551 |
+
args.host,
|
| 1552 |
+
args.port,
|
| 1553 |
+
args.gallery_dir,
|
| 1554 |
+
api_key=args.api_key,
|
| 1555 |
+
allow_unauthenticated=args.allow_unauthenticated,
|
| 1556 |
+
)
|
multivue/cameras/source/src/depth_anything_3/services/gallery.py
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# flake8: noqa: E501
|
| 3 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
Depth Anything 3 Gallery Server (two-level, single-file)
|
| 19 |
+
Now supports paginated depth preview (4 per page).
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import argparse
|
| 23 |
+
import json
|
| 24 |
+
import mimetypes
|
| 25 |
+
import os
|
| 26 |
+
import posixpath
|
| 27 |
+
import sys
|
| 28 |
+
from functools import partial
|
| 29 |
+
from http import HTTPStatus
|
| 30 |
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
| 31 |
+
from urllib.parse import quote, unquote
|
| 32 |
+
|
| 33 |
+
# ------------------------------ Embedded HTML ------------------------------ #
|
| 34 |
+
|
| 35 |
+
HTML_PAGE = r"""<!doctype html>
|
| 36 |
+
<html lang="en">
|
| 37 |
+
<head>
|
| 38 |
+
<meta charset="utf-8" />
|
| 39 |
+
<title>Depth Anything 3 Gallery</title>
|
| 40 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 41 |
+
<link rel="icon" href="https://i.postimg.cc/rFSzGJ7J/light-icon.jpg" media="(prefers-color-scheme: light)">
|
| 42 |
+
<link rel="icon" href="https://i.postimg.cc/P5gZfJsf/dark-icon.jpg" media="(prefers-color-scheme: dark)">
|
| 43 |
+
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
|
| 44 |
+
<style>
|
| 45 |
+
:root {
|
| 46 |
+
--gap:16px; --card-radius:16px; --shadow:0 8px 24px rgba(0,0,0,.12);
|
| 47 |
+
--maxW:1036px; --maxH:518px;
|
| 48 |
+
--tech-blue: #00d4ff;
|
| 49 |
+
--tech-cyan: #00ffcc;
|
| 50 |
+
--tech-purple: #7877c6;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
*{ box-sizing:border-box }
|
| 54 |
+
|
| 55 |
+
/* Dark mode tech theme */
|
| 56 |
+
@media (prefers-color-scheme: dark) {
|
| 57 |
+
body{
|
| 58 |
+
margin:0; font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
| 59 |
+
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
| 60 |
+
color:#e8eaed;
|
| 61 |
+
position: relative;
|
| 62 |
+
overflow-x: hidden;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
body::before {
|
| 66 |
+
content: '';
|
| 67 |
+
position: fixed;
|
| 68 |
+
top: 0;
|
| 69 |
+
left: 0;
|
| 70 |
+
right: 0;
|
| 71 |
+
bottom: 0;
|
| 72 |
+
background:
|
| 73 |
+
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
| 74 |
+
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
| 75 |
+
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%);
|
| 76 |
+
animation: techPulse 8s ease-in-out infinite;
|
| 77 |
+
z-index: -1;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Light mode tech theme */
|
| 82 |
+
@media (prefers-color-scheme: light) {
|
| 83 |
+
body{
|
| 84 |
+
margin:0; font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
| 85 |
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);
|
| 86 |
+
color:#1e293b;
|
| 87 |
+
position: relative;
|
| 88 |
+
overflow-x: hidden;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
body::before {
|
| 92 |
+
content: '';
|
| 93 |
+
position: fixed;
|
| 94 |
+
top: 0;
|
| 95 |
+
left: 0;
|
| 96 |
+
right: 0;
|
| 97 |
+
bottom: 0;
|
| 98 |
+
background:
|
| 99 |
+
radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
| 100 |
+
radial-gradient(circle at 80% 20%, rgba(0, 102, 255, 0.1) 0%, transparent 50%),
|
| 101 |
+
radial-gradient(circle at 40% 40%, rgba(0, 255, 204, 0.08) 0%, transparent 50%);
|
| 102 |
+
animation: techPulse 8s ease-in-out infinite;
|
| 103 |
+
z-index: -1;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
@keyframes techPulse {
|
| 108 |
+
0%, 100% { opacity: 0.5; }
|
| 109 |
+
50% { opacity: 0.8; }
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
@keyframes techGradient {
|
| 113 |
+
0% { background-position: 0% 50%; }
|
| 114 |
+
50% { background-position: 100% 50%; }
|
| 115 |
+
100% { background-position: 0% 50%; }
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Dark mode header */
|
| 119 |
+
@media (prefers-color-scheme: dark) {
|
| 120 |
+
header{
|
| 121 |
+
padding:20px 24px; position:sticky; top:0;
|
| 122 |
+
background:linear-gradient(180deg,rgba(10,10,10,0.9) 60%,rgba(10,10,10,0));
|
| 123 |
+
z-index:2; border-bottom:1px solid rgba(0, 212, 255, 0.2);
|
| 124 |
+
backdrop-filter: blur(10px);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
h1{
|
| 128 |
+
margin:0; font-size:22px;
|
| 129 |
+
background: linear-gradient(45deg, var(--tech-blue), var(--tech-cyan), var(--tech-purple));
|
| 130 |
+
background-size: 400% 400%;
|
| 131 |
+
-webkit-background-clip: text;
|
| 132 |
+
background-clip: text;
|
| 133 |
+
color: transparent;
|
| 134 |
+
animation: techGradient 3s ease infinite;
|
| 135 |
+
text-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.muted{ opacity:.7; font-size:13px; color: #a0a0a0; }
|
| 139 |
+
|
| 140 |
+
#backBtn{
|
| 141 |
+
display:none; padding:6px 10px; border-radius:10px;
|
| 142 |
+
border:1px solid rgba(0, 212, 255, 0.3);
|
| 143 |
+
background:rgba(0, 0, 0, 0.3);
|
| 144 |
+
color:#e8eaed; cursor:pointer;
|
| 145 |
+
transition: all 0.3s ease;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#backBtn:hover {
|
| 149 |
+
border-color: var(--tech-blue);
|
| 150 |
+
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
#search{
|
| 154 |
+
flex:1 1 260px; min-width:240px; max-width:520px;
|
| 155 |
+
padding:10px 14px; border-radius:12px;
|
| 156 |
+
border:1px solid rgba(0, 212, 255, 0.3);
|
| 157 |
+
background:rgba(0, 0, 0, 0.3);
|
| 158 |
+
color:#e8eaed; outline:none;
|
| 159 |
+
transition: all 0.3s ease;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#search:focus {
|
| 163 |
+
border-color: var(--tech-blue);
|
| 164 |
+
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Light mode header */
|
| 169 |
+
@media (prefers-color-scheme: light) {
|
| 170 |
+
header{
|
| 171 |
+
padding:20px 24px; position:sticky; top:0;
|
| 172 |
+
background:linear-gradient(180deg,rgba(248,250,252,0.9) 60%,rgba(248,250,252,0));
|
| 173 |
+
z-index:2; border-bottom:1px solid rgba(0, 212, 255, 0.3);
|
| 174 |
+
backdrop-filter: blur(10px);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
h1{
|
| 178 |
+
margin:0; font-size:22px;
|
| 179 |
+
background: linear-gradient(45deg, #0066ff, #00d4ff, #00ffcc);
|
| 180 |
+
background-size: 400% 400%;
|
| 181 |
+
-webkit-background-clip: text;
|
| 182 |
+
background-clip: text;
|
| 183 |
+
color: transparent;
|
| 184 |
+
animation: techGradient 3s ease infinite;
|
| 185 |
+
text-shadow: 0 0 20px rgba(0, 102, 255, 0.3);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.muted{ opacity:.7; font-size:13px; color: #64748b; }
|
| 189 |
+
|
| 190 |
+
#backBtn{
|
| 191 |
+
display:none; padding:6px 10px; border-radius:10px;
|
| 192 |
+
border:1px solid rgba(0, 212, 255, 0.4);
|
| 193 |
+
background:rgba(255, 255, 255, 0.8);
|
| 194 |
+
color:#1e293b; cursor:pointer;
|
| 195 |
+
transition: all 0.3s ease;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
#backBtn:hover {
|
| 199 |
+
border-color: #0066ff;
|
| 200 |
+
box-shadow: 0 0 10px rgba(0, 102, 255, 0.3);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
#search{
|
| 204 |
+
flex:1 1 260px; min-width:240px; max-width:520px;
|
| 205 |
+
padding:10px 14px; border-radius:12px;
|
| 206 |
+
border:1px solid rgba(0, 212, 255, 0.4);
|
| 207 |
+
background:rgba(255, 255, 255, 0.8);
|
| 208 |
+
color:#1e293b; outline:none;
|
| 209 |
+
transition: all 0.3s ease;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
#search:focus {
|
| 213 |
+
border-color: #0066ff;
|
| 214 |
+
box-shadow: 0 0 10px rgba(0, 102, 255, 0.3);
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.row{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; justify-content:center; }
|
| 219 |
+
|
| 220 |
+
main{ padding:16px 24px 24px; display:grid; place-items:center; }
|
| 221 |
+
|
| 222 |
+
.group-wrap{ width:min(900px,100%); }
|
| 223 |
+
.group-list{ list-style:none; margin:0; padding:0; display:grid; gap:10px; }
|
| 224 |
+
|
| 225 |
+
/* Dark mode cards */
|
| 226 |
+
@media (prefers-color-scheme: dark) {
|
| 227 |
+
.group-item{
|
| 228 |
+
display:flex; align-items:center; gap:12px; padding:12px 14px;
|
| 229 |
+
background:rgba(0, 0, 0, 0.3); border:1px solid rgba(0, 212, 255, 0.2); border-radius:14px; cursor:pointer;
|
| 230 |
+
transition: all 0.3s ease;
|
| 231 |
+
backdrop-filter: blur(10px);
|
| 232 |
+
}
|
| 233 |
+
.group-item:hover{
|
| 234 |
+
transform: translateY(-1px);
|
| 235 |
+
border-color:var(--tech-blue);
|
| 236 |
+
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.2);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.card{
|
| 240 |
+
background:rgba(0, 0, 0, 0.3); border:1px solid rgba(0, 212, 255, 0.2); border-radius:var(--card-radius);
|
| 241 |
+
overflow:hidden; box-shadow:var(--shadow);
|
| 242 |
+
transition:all 0.3s ease; cursor:pointer; display:flex; flex-direction:column; max-width:var(--maxW);
|
| 243 |
+
backdrop-filter: blur(10px);
|
| 244 |
+
}
|
| 245 |
+
.card:hover{
|
| 246 |
+
transform:translateY(-2px);
|
| 247 |
+
border-color:var(--tech-blue);
|
| 248 |
+
box-shadow: 0 8px 25px rgba(0, 212, 255, 0.2);
|
| 249 |
+
}
|
| 250 |
+
.thumb-box{
|
| 251 |
+
position:relative; width:100%; aspect-ratio:2/1;
|
| 252 |
+
background:linear-gradient(135deg, #0e121b 0%, #1a1a2e 100%);
|
| 253 |
+
display:grid; place-items:center; overflow:hidden;
|
| 254 |
+
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
|
| 255 |
+
}
|
| 256 |
+
.open{
|
| 257 |
+
font-size:12px; opacity:.7; padding:6px 8px;
|
| 258 |
+
border:1px solid rgba(0, 212, 255, 0.3);
|
| 259 |
+
border-radius:10px;
|
| 260 |
+
background:rgba(0, 212, 255, 0.1);
|
| 261 |
+
transition: all 0.3s ease;
|
| 262 |
+
}
|
| 263 |
+
.open:hover {
|
| 264 |
+
background:rgba(0, 212, 255, 0.2);
|
| 265 |
+
border-color: var(--tech-blue);
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/* Light mode cards */
|
| 270 |
+
@media (prefers-color-scheme: light) {
|
| 271 |
+
.group-item{
|
| 272 |
+
display:flex; align-items:center; gap:12px; padding:12px 14px;
|
| 273 |
+
background:rgba(255, 255, 255, 0.8); border:1px solid rgba(0, 212, 255, 0.3); border-radius:14px; cursor:pointer;
|
| 274 |
+
transition: all 0.3s ease;
|
| 275 |
+
backdrop-filter: blur(10px);
|
| 276 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 277 |
+
}
|
| 278 |
+
.group-item:hover{
|
| 279 |
+
transform: translateY(-1px);
|
| 280 |
+
border-color:#0066ff;
|
| 281 |
+
box-shadow: 0 4px 15px rgba(0, 102, 255, 0.2);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.card{
|
| 285 |
+
background:rgba(255, 255, 255, 0.8); border:1px solid rgba(0, 212, 255, 0.3); border-radius:var(--card-radius);
|
| 286 |
+
overflow:hidden; box-shadow:0 4px 6px rgba(0, 0, 0, 0.1);
|
| 287 |
+
transition:all 0.3s ease; cursor:pointer; display:flex; flex-direction:column; max-width:var(--maxW);
|
| 288 |
+
backdrop-filter: blur(10px);
|
| 289 |
+
}
|
| 290 |
+
.card:hover{
|
| 291 |
+
transform:translateY(-2px);
|
| 292 |
+
border-color:#0066ff;
|
| 293 |
+
box-shadow: 0 8px 25px rgba(0, 102, 255, 0.2);
|
| 294 |
+
}
|
| 295 |
+
.thumb-box{
|
| 296 |
+
position:relative; width:100%; aspect-ratio:2/1;
|
| 297 |
+
background:linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
| 298 |
+
display:grid; place-items:center; overflow:hidden;
|
| 299 |
+
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
| 300 |
+
}
|
| 301 |
+
.open{
|
| 302 |
+
font-size:12px; opacity:.7; padding:6px 8px;
|
| 303 |
+
border:1px solid rgba(0, 212, 255, 0.4);
|
| 304 |
+
border-radius:10px;
|
| 305 |
+
background:rgba(0, 212, 255, 0.1);
|
| 306 |
+
transition: all 0.3s ease;
|
| 307 |
+
}
|
| 308 |
+
.open:hover {
|
| 309 |
+
background:rgba(0, 212, 255, 0.2);
|
| 310 |
+
border-color: #0066ff;
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.gname{ font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; width:100%; }
|
| 315 |
+
.grid{
|
| 316 |
+
width:min(1200px,100%);
|
| 317 |
+
display:grid;
|
| 318 |
+
grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
|
| 319 |
+
gap:var(--gap);
|
| 320 |
+
align-items:start;
|
| 321 |
+
justify-items:stretch;
|
| 322 |
+
margin: 0 auto;
|
| 323 |
+
padding: 0 20px;
|
| 324 |
+
}
|
| 325 |
+
.thumb{ max-width:100%; max-height:100%; object-fit:contain; display:block; }
|
| 326 |
+
.meta{ padding:12px 14px; display:flex; justify-content:space-between; align-items:center; gap:8px; }
|
| 327 |
+
.title{ font-weight:600; font-size:14px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 328 |
+
.empty{ opacity:.6; padding:40px 0; text-align:center; }
|
| 329 |
+
.crumb{ font-size:13px; opacity:.8; }
|
| 330 |
+
|
| 331 |
+
.overlay{ position:fixed; inset:0; background:rgba(0,0,0,.6); display:none; place-items:center; padding:20px; z-index:10; }
|
| 332 |
+
.overlay.show{ display:grid; }
|
| 333 |
+
|
| 334 |
+
/* Dark mode viewer */
|
| 335 |
+
@media (prefers-color-scheme: dark) {
|
| 336 |
+
.viewer{
|
| 337 |
+
inline-size:min(92vw,var(--maxW));
|
| 338 |
+
block-size:min(82vh,var(--maxH));
|
| 339 |
+
background:#0e121b; border:1px solid rgba(0, 212, 255, 0.3); border-radius:18px; overflow:hidden; position:relative; box-shadow:0 12px 36px rgba(0,0,0,.35);
|
| 340 |
+
display:grid;
|
| 341 |
+
}
|
| 342 |
+
.chip{ background:rgba(0,0,0,.45); border:1px solid rgba(0, 212, 255, 0.3); color:#e8eaed; padding:6px 10px; border-radius:12px; font-size:12px; max-width:60%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 343 |
+
.btn{ margin-left:auto; background:rgba(0, 0, 0, 0.3); color:#e8eaed; border:1px solid rgba(0, 212, 255, 0.3); border-radius:10px; padding:6px 10px; cursor:pointer; transition: all 0.3s ease; }
|
| 344 |
+
.btn:hover { border-color: var(--tech-blue); box-shadow: 0 0 10px rgba(0, 212, 255, 0.3); }
|
| 345 |
+
.mv-box{ width:100%; aspect-ratio:1036/518; background:#0b0d12; border:1px solid rgba(0, 212, 255, 0.2); border-radius:12px; overflow:hidden; }
|
| 346 |
+
.mv-box model-viewer{ width:100%; height:100%; background:#0b0d12; }
|
| 347 |
+
.res-cell{ position:relative; width:100%; aspect-ratio:2/1; background:#0e121b; border:1px solid rgba(0, 212, 255, 0.2); border-radius:12px; overflow:hidden; display:grid; place-items:center; }
|
| 348 |
+
.res-empty{ position:absolute; inset:0; display:grid; place-items:center; opacity:.55; font-size:12px; color:#9aa0a6; }
|
| 349 |
+
.download-icon{ background:rgba(0, 0, 0, 0.6); border:1px solid rgba(0, 212, 255, 0.3); color:#e8eaed; box-shadow:0 4px 12px rgba(0,0,0,0.3); }
|
| 350 |
+
.download-icon:hover{ background:rgba(0, 212, 255, 0.2); border-color:var(--tech-blue); box-shadow:0 0 20px rgba(0, 212, 255, 0.4); transform:scale(1.05); }
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/* Light mode viewer */
|
| 354 |
+
@media (prefers-color-scheme: light) {
|
| 355 |
+
.viewer{
|
| 356 |
+
inline-size:min(92vw,var(--maxW));
|
| 357 |
+
block-size:min(82vh,var(--maxH));
|
| 358 |
+
background:#f8fafc; border:1px solid rgba(0, 212, 255, 0.4); border-radius:18px; overflow:hidden; position:relative; box-shadow:0 12px 36px rgba(0,0,0,.15);
|
| 359 |
+
display:grid;
|
| 360 |
+
}
|
| 361 |
+
.chip{ background:rgba(255,255,255,0.8); border:1px solid rgba(0, 212, 255, 0.4); color:#1e293b; padding:6px 10px; border-radius:12px; font-size:12px; max-width:60%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 362 |
+
.btn{ margin-left:auto; background:rgba(255, 255, 255, 0.8); color:#1e293b; border:1px solid rgba(0, 212, 255, 0.4); border-radius:10px; padding:6px 10px; cursor:pointer; transition: all 0.3s ease; }
|
| 363 |
+
.btn:hover { border-color: #0066ff; box-shadow: 0 0 10px rgba(0, 102, 255, 0.3); }
|
| 364 |
+
.mv-box{ width:100%; aspect-ratio:1036/518; background:#f8fafc; border:1px solid rgba(0, 212, 255, 0.3); border-radius:12px; overflow:hidden; }
|
| 365 |
+
.mv-box model-viewer{ width:100%; height:100%; background:#f8fafc; }
|
| 366 |
+
.res-cell{ position:relative; width:100%; aspect-ratio:2/1; background:#f8fafc; border:1px solid rgba(0, 212, 255, 0.3); border-radius:12px; overflow:hidden; display:grid; place-items:center; }
|
| 367 |
+
.res-empty{ position:absolute; inset:0; display:grid; place-items:center; opacity:.55; font-size:12px; color:#64748b; }
|
| 368 |
+
.download-icon{ background:rgba(255, 255, 255, 0.9); border:1px solid rgba(0, 212, 255, 0.4); color:#1e293b; box-shadow:0 4px 12px rgba(0,0,0,0.15); }
|
| 369 |
+
.download-icon:hover{ background:rgba(0, 212, 255, 0.2); border-color:#0066ff; box-shadow:0 0 20px rgba(0, 102, 255, 0.4); transform:scale(1.05); }
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.viewer-header{ position:absolute; top:8px; left:8px; right:8px; display:flex; gap:8px; align-items:center; z-index:2; }
|
| 373 |
+
.viewer-body{ height:100%; display:grid; grid-template-rows:auto auto; gap:12px; padding:36px 8px 8px 8px; overflow:auto; }
|
| 374 |
+
.res-grid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
| 375 |
+
.res-img{ max-width:100%; max-height:100%; object-fit:contain; display:block; }
|
| 376 |
+
.download-icon{ position:absolute; bottom:16px; right:16px; width:44px; height:44px; border-radius:50%; display:grid; place-items:center; font-size:20px; cursor:pointer; z-index:3; transition:all 0.3s ease; }
|
| 377 |
+
|
| 378 |
+
/* Pagination controls */
|
| 379 |
+
.pager {
|
| 380 |
+
grid-column: 1 / -1;
|
| 381 |
+
justify-content: center;
|
| 382 |
+
align-items: center;
|
| 383 |
+
display: flex;
|
| 384 |
+
gap: 16px;
|
| 385 |
+
margin-top: 8px;
|
| 386 |
+
font-size: 13px;
|
| 387 |
+
text-align: center;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
/* Dark mode pagination */
|
| 391 |
+
@media (prefers-color-scheme: dark) {
|
| 392 |
+
.pager {
|
| 393 |
+
color: #ccc;
|
| 394 |
+
}
|
| 395 |
+
.pager button {
|
| 396 |
+
padding: 4px 10px;
|
| 397 |
+
border-radius: 8px;
|
| 398 |
+
border: 1px solid rgba(0, 212, 255, 0.3);
|
| 399 |
+
background: rgba(0, 0, 0, 0.3);
|
| 400 |
+
color: #e8eaed;
|
| 401 |
+
cursor: pointer;
|
| 402 |
+
transition: all 0.3s ease;
|
| 403 |
+
}
|
| 404 |
+
.pager button:hover:not(:disabled) {
|
| 405 |
+
border-color: var(--tech-blue);
|
| 406 |
+
box-shadow: 0 0 8px rgba(0, 212, 255, 0.2);
|
| 407 |
+
}
|
| 408 |
+
.pager button:disabled {
|
| 409 |
+
opacity: 0.4;
|
| 410 |
+
cursor: not-allowed;
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* Light mode pagination */
|
| 415 |
+
@media (prefers-color-scheme: light) {
|
| 416 |
+
.pager {
|
| 417 |
+
color: #64748b;
|
| 418 |
+
}
|
| 419 |
+
.pager button {
|
| 420 |
+
padding: 4px 10px;
|
| 421 |
+
border-radius: 8px;
|
| 422 |
+
border: 1px solid rgba(0, 212, 255, 0.4);
|
| 423 |
+
background: rgba(255, 255, 255, 0.8);
|
| 424 |
+
color: #1e293b;
|
| 425 |
+
cursor: pointer;
|
| 426 |
+
transition: all 0.3s ease;
|
| 427 |
+
}
|
| 428 |
+
.pager button:hover:not(:disabled) {
|
| 429 |
+
border-color: #0066ff;
|
| 430 |
+
box-shadow: 0 0 8px rgba(0, 102, 255, 0.2);
|
| 431 |
+
}
|
| 432 |
+
.pager button:disabled {
|
| 433 |
+
opacity: 0.4;
|
| 434 |
+
cursor: not-allowed;
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
/* Intro card styles */
|
| 439 |
+
@media (prefers-color-scheme: dark) {
|
| 440 |
+
.intro-card {
|
| 441 |
+
background: linear-gradient(135deg, rgba(0, 212, 255, 0.1) 0%, rgba(0, 102, 255, 0.1) 100%);
|
| 442 |
+
border: 1px solid rgba(0, 212, 255, 0.2);
|
| 443 |
+
backdrop-filter: blur(10px);
|
| 444 |
+
}
|
| 445 |
+
.intro-title {
|
| 446 |
+
background: linear-gradient(45deg, var(--tech-blue), var(--tech-cyan), var(--tech-purple));
|
| 447 |
+
background-size: 400% 400%;
|
| 448 |
+
-webkit-background-clip: text;
|
| 449 |
+
background-clip: text;
|
| 450 |
+
color: transparent;
|
| 451 |
+
animation: techGradient 3s ease infinite;
|
| 452 |
+
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
|
| 453 |
+
}
|
| 454 |
+
.intro-description {
|
| 455 |
+
color: #e0e0e0;
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
@media (prefers-color-scheme: light) {
|
| 460 |
+
.intro-card {
|
| 461 |
+
background: linear-gradient(135deg, rgba(0, 212, 255, 0.05) 0%, rgba(0, 102, 255, 0.05) 100%);
|
| 462 |
+
border: 1px solid rgba(0, 212, 255, 0.3);
|
| 463 |
+
backdrop-filter: blur(10px);
|
| 464 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 465 |
+
}
|
| 466 |
+
.intro-title {
|
| 467 |
+
background: linear-gradient(45deg, #0066ff, #00d4ff, #00ffcc);
|
| 468 |
+
background-size: 400% 400%;
|
| 469 |
+
-webkit-background-clip: text;
|
| 470 |
+
background-clip: text;
|
| 471 |
+
color: transparent;
|
| 472 |
+
animation: techGradient 3s ease infinite;
|
| 473 |
+
text-shadow: 0 0 15px rgba(0, 102, 255, 0.2);
|
| 474 |
+
}
|
| 475 |
+
.intro-description {
|
| 476 |
+
color: #334155;
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
footer{
|
| 481 |
+
opacity:.55;
|
| 482 |
+
font-size:12px;
|
| 483 |
+
padding:12px 24px 24px;
|
| 484 |
+
text-align:center;
|
| 485 |
+
display:flex;
|
| 486 |
+
justify-content:center;
|
| 487 |
+
align-items:center;
|
| 488 |
+
width:100%;
|
| 489 |
+
}
|
| 490 |
+
</style>
|
| 491 |
+
</head>
|
| 492 |
+
<body>
|
| 493 |
+
<header>
|
| 494 |
+
<div class="row">
|
| 495 |
+
<button id="backBtn">← Back</button>
|
| 496 |
+
<h1 id="pageTitle">Depth Anything 3 Gallery</h1>
|
| 497 |
+
<span id="crumb" class="crumb"></span>
|
| 498 |
+
<input id="search" placeholder="Search…" />
|
| 499 |
+
</div>
|
| 500 |
+
<div class="muted" id="hint" style="text-align: center;">Level 1 shows groups only; click a group to browse scenes and previews.</div>
|
| 501 |
+
</header>
|
| 502 |
+
|
| 503 |
+
<main>
|
| 504 |
+
<!-- Tech intro card -->
|
| 505 |
+
<div class="intro-card" style="margin-bottom: 30px; padding: 25px; border-radius: 15px; text-align: center; max-width: 800px;">
|
| 506 |
+
<h2 class="intro-title" style="margin: 0 0 15px 0; font-size: 1.8em; font-weight: 700;">
|
| 507 |
+
🎯 Depth Anything 3 Gallery
|
| 508 |
+
</h2>
|
| 509 |
+
<p class="intro-description" style="margin: 0; font-size: 1.1em; line-height: 1.6;">
|
| 510 |
+
Explore 3D reconstructions and depth visualizations from Depth Anything 3.
|
| 511 |
+
Browse through groups of scenes, preview 3D models, and examine depth maps interactively.
|
| 512 |
+
</p>
|
| 513 |
+
</div>
|
| 514 |
+
|
| 515 |
+
<div id="level1" class="group-wrap" aria-live="polite">
|
| 516 |
+
<ul id="groupList" class="group-list"></ul>
|
| 517 |
+
<div id="groupEmpty" class="empty" style="display:none;">No available groups</div>
|
| 518 |
+
</div>
|
| 519 |
+
|
| 520 |
+
<div id="level2" style="display:none; width:100%;" aria-live="polite">
|
| 521 |
+
<div id="topPager" class="pager" style="margin-bottom: 16px;"></div>
|
| 522 |
+
<div id="grid" class="grid"></div>
|
| 523 |
+
<div id="sceneEmpty" class="empty" style="display:none;">No available scenes in this group</div>
|
| 524 |
+
</div>
|
| 525 |
+
</main>
|
| 526 |
+
|
| 527 |
+
<div id="overlay" class="overlay" role="dialog" aria-modal="true" aria-label="3D Preview">
|
| 528 |
+
<div class="viewer" id="viewer">
|
| 529 |
+
<div class="viewer-header">
|
| 530 |
+
<div id="viewerTitle" class="chip">Loading…</div>
|
| 531 |
+
<button id="toggleView" class="btn" title="Toggle between 3D-only and resource view">Resource View</button>
|
| 532 |
+
<button id="closeBtn" class="btn">Close</button>
|
| 533 |
+
</div>
|
| 534 |
+
<div id="downloadBtn" class="download-icon" title="Download GLB model">⬇</div>
|
| 535 |
+
<div class="viewer-body">
|
| 536 |
+
<div class="mv-box"><model-viewer id="mv"
|
| 537 |
+
src=""
|
| 538 |
+
ar
|
| 539 |
+
camera-controls
|
| 540 |
+
auto-rotate
|
| 541 |
+
interaction-prompt="auto"
|
| 542 |
+
shadow-intensity="0.7"
|
| 543 |
+
exposure="1.0"
|
| 544 |
+
alt="GLB Preview"></model-viewer></div>
|
| 545 |
+
<div class="res-grid" id="resGrid" hidden></div>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
|
| 550 |
+
<footer>Depth Anything 3 Gallery. Copyright 2025 Depth Anything 3 authors.</footer>
|
| 551 |
+
|
| 552 |
+
<script>
|
| 553 |
+
const level1=document.getElementById('level1'),level2=document.getElementById('level2'),pageTitle=document.getElementById('pageTitle'),crumb=document.getElementById('crumb'),backBtn=document.getElementById('backBtn'),hint=document.getElementById('hint'),searchInput=document.getElementById('search'),groupList=document.getElementById('groupList'),groupEmpty=document.getElementById('groupEmpty'),topPager=document.getElementById('topPager'),grid=document.getElementById('grid'),sceneEmpty=document.getElementById('sceneEmpty'),overlay=document.getElementById('overlay'),viewer=document.getElementById('viewer'),mv=document.getElementById('mv'),viewerTitle=document.getElementById('viewerTitle'),downloadBtn=document.getElementById('downloadBtn'),toggleViewBtn=document.getElementById('toggleView'),closeBtn=document.getElementById('closeBtn'),resGrid=document.getElementById('resGrid');
|
| 554 |
+
let GROUPS=[],SCENES=[],currentGroup=null,currentScene=null,currentPage=1,currentScenePage=1;
|
| 555 |
+
|
| 556 |
+
const qs=()=>new URLSearchParams(location.search);
|
| 557 |
+
async function loadGroups(){const r=await fetch('/manifest.json',{cache:'no-store'});if(!r.ok)throw new Error(r.status+' '+r.statusText);const j=await r.json();GROUPS=j.groups||[];renderGroups(GROUPS);}
|
| 558 |
+
async function loadScenes(g){const r=await fetch('/manifest/'+encodeURIComponent(g)+'.json',{cache:'no-store'});if(!r.ok)throw new Error(r.status+' '+r.statusText);const j=await r.json();SCENES=j.items||[];const p=parseInt(qs().get('page'))||1;renderScenes(SCENES,p);}
|
| 559 |
+
function renderGroups(list){groupList.innerHTML='';const q=searchInput.value.trim().toLowerCase();const f=list.filter(g=>(g.title||g.id||'').toLowerCase().includes(q));if(!f.length){groupEmpty.style.display='';return;}groupEmpty.style.display='none';for(const g of f){const li=document.createElement('li');li.className='group-item';li.title=g.title||g.id;li.onclick=()=>enterLevel2(g.id,{push:true});const name=document.createElement('div');name.className='gname';name.textContent=g.title||g.id;li.appendChild(name);groupList.appendChild(li);}}
|
| 560 |
+
function renderScenes(list,page=1){topPager.innerHTML='';grid.innerHTML='';const q=searchInput.value.trim().toLowerCase();const f=list.filter(x=>(x.title||'').toLowerCase().includes(q)||(x.id||'').toLowerCase().includes(q));if(!f.length){sceneEmpty.style.display='';topPager.style.display='none';return;}sceneEmpty.style.display='none';topPager.style.display='flex';const perPage=16;const total=f.length;const totalPages=Math.max(1,Math.ceil(total/perPage));currentScenePage=page;const u=new URL(location.href);u.searchParams.set('page',page);history.replaceState(null,'',u);const subset=f.slice((page-1)*perPage,page*perPage);for(const i of subset){const c=document.createElement('div');c.className='card';c.title=i.title;const b=document.createElement('div');b.className='thumb-box';const img=document.createElement('img');img.className='thumb';img.loading='lazy';img.alt=i.title;img.src=i.thumbnail;b.appendChild(img);const m=document.createElement('div');m.className='meta';const t=document.createElement('div');t.className='title';t.textContent=i.title;const o=document.createElement('div');o.className='open';o.textContent='Preview';m.appendChild(t);m.appendChild(o);c.appendChild(b);c.appendChild(m);c.onclick=()=>openViewer(i,{push:true});grid.appendChild(c);}function buildPager(){const pg=document.createElement('div');pg.className='pager';const prev=document.createElement('button');prev.textContent='← Prev';prev.disabled=page<=1;prev.onclick=()=>renderScenes(list,page-1);const info=document.createElement('span');info.textContent=`${page} / ${totalPages}`;const next=document.createElement('button');next.textContent='Next →';next.disabled=page>=totalPages;next.onclick=()=>renderScenes(list,page+1);pg.appendChild(prev);pg.appendChild(info);pg.appendChild(next);return pg;}topPager.innerHTML='';topPager.appendChild(buildPager());grid.appendChild(buildPager());}
|
| 561 |
+
function enterLevel1({push=false}={}){currentGroup=null;pageTitle.textContent='Depth Anything 3 Gallery';crumb.textContent='';backBtn.style.display='none';hint.style.display='';level1.style.display='';level2.style.display='none';overlay.classList.remove('show');mv.src='';const u=new URL(location.href);u.searchParams.delete('group');u.searchParams.delete('id');u.searchParams.delete('page');push?history.pushState(null,'',u):history.replaceState(null,'',u);searchInput.value='';loadGroups().catch(e=>{groupList.innerHTML='';groupEmpty.style.display='';groupEmpty.textContent='Failed to load groups: '+e;});}
|
| 562 |
+
async function enterLevel2(g,{push=false}={}){currentGroup=g;pageTitle.textContent=g;crumb.textContent='(group)';backBtn.style.display='';hint.style.display='none';level1.style.display='none';level2.style.display='';overlay.classList.remove('show');mv.src='';const u=new URL(location.href);u.searchParams.set('group',g);u.searchParams.delete('id');push?history.pushState(null,'',u):history.replaceState(null,'',u);searchInput.value='';try{await loadScenes(g);const id=qs().get('id');if(id){const hit=SCENES.find(x=>x.id===id);if(hit)openViewer(hit,{push:false});}}catch(e){grid.innerHTML='';sceneEmpty.style.display='';sceneEmpty.textContent='Failed to load scenes: '+e;}}
|
| 563 |
+
function buildResGrid(i,page=1){
|
| 564 |
+
resGrid.innerHTML='';
|
| 565 |
+
const imgs=i.depth_images||[];
|
| 566 |
+
const perPage=4;
|
| 567 |
+
const total=imgs.length;
|
| 568 |
+
const totalPages=Math.max(1, Math.ceil(total/perPage));
|
| 569 |
+
currentPage=page;
|
| 570 |
+
|
| 571 |
+
const subset=imgs.slice((page-1)*perPage,(page-1)*perPage+perPage);
|
| 572 |
+
for(let k=0;k<4;k++){
|
| 573 |
+
const cell=document.createElement('div');
|
| 574 |
+
cell.className='res-cell';
|
| 575 |
+
if(subset[k]){
|
| 576 |
+
const im=document.createElement('img');
|
| 577 |
+
im.className='res-img';
|
| 578 |
+
im.src=subset[k];
|
| 579 |
+
im.alt=(i.title||'scene')+' depth '+(k+1+(page-1)*perPage);
|
| 580 |
+
im.loading='lazy';
|
| 581 |
+
cell.appendChild(im);
|
| 582 |
+
} else {
|
| 583 |
+
const ph=document.createElement('div');
|
| 584 |
+
ph.className='res-empty';
|
| 585 |
+
ph.textContent='N/A';
|
| 586 |
+
cell.appendChild(ph);
|
| 587 |
+
}
|
| 588 |
+
resGrid.appendChild(cell);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
// pagination bar (always rebuilt)
|
| 592 |
+
const pager=document.createElement('div');
|
| 593 |
+
pager.className='pager';
|
| 594 |
+
|
| 595 |
+
const prev=document.createElement('button');
|
| 596 |
+
prev.textContent='← Prev';
|
| 597 |
+
prev.disabled=page<=1;
|
| 598 |
+
prev.onclick=()=>buildResGrid(i,page-1);
|
| 599 |
+
|
| 600 |
+
const info=document.createElement('span');
|
| 601 |
+
info.textContent=`${page} / ${totalPages}`;
|
| 602 |
+
|
| 603 |
+
const next=document.createElement('button');
|
| 604 |
+
next.textContent='Next →';
|
| 605 |
+
next.disabled=page>=totalPages;
|
| 606 |
+
next.onclick=()=>buildResGrid(i,page+1);
|
| 607 |
+
|
| 608 |
+
pager.appendChild(prev);
|
| 609 |
+
pager.appendChild(info);
|
| 610 |
+
pager.appendChild(next);
|
| 611 |
+
resGrid.appendChild(pager);
|
| 612 |
+
}
|
| 613 |
+
function openViewer(i,{push=false}={}){currentScene=i;viewerTitle.textContent=i.title;mv.src=i.model;overlay.classList.add('show');resGrid.hidden=true;toggleViewBtn.textContent='Resource View';viewer.style.blockSize='min(82vh,var(--maxH))';buildResGrid(i,1);downloadBtn.onclick=()=>{const a=document.createElement('a');a.href=i.model;a.download=i.title+'.glb';a.click();};if(push){const u=new URL(location.href);if(!u.searchParams.get('group'))u.searchParams.set('group',currentGroup||'');u.searchParams.set('id',i.id);history.pushState(null,'',u);}}
|
| 614 |
+
function toggleView(){const hidden=!resGrid.hidden;resGrid.hidden=hidden;toggleViewBtn.textContent=hidden?'Resource View':'3D Only';viewer.style.blockSize=hidden?'min(82vh,var(--maxH))':'min(92vh,900px)';}
|
| 615 |
+
function closeViewer(){const hasId=!!qs().get('id');if(hasId&&history.length>1){history.back();return;}const u=new URL(location.href);u.searchParams.delete('id');history.replaceState(null,'',u);overlay.classList.remove('show');mv.src='';}
|
| 616 |
+
overlay.onclick=e=>{if(e.target===overlay)closeViewer();};closeBtn.onclick=closeViewer;toggleViewBtn.onclick=toggleView;backBtn.onclick=()=>history.back();
|
| 617 |
+
searchInput.oninput=()=>{!qs().get('group')?renderGroups(GROUPS):renderScenes(SCENES,1);};
|
| 618 |
+
window.onpopstate=()=>routeFromURL();
|
| 619 |
+
async function routeFromURL(){if(location.pathname!="/")history.replaceState(null,'','/'+location.search);const g=qs().get('group');const id=qs().get('id');if(!g){enterLevel1({push:false});return;}await enterLevel2(g,{push:false});if(id){const hit=SCENES.find(x=>x.id===id);if(hit)openViewer(hit,{push:false});else{overlay.classList.remove('show');mv.src='';}}else{overlay.classList.remove('show');mv.src='';}}
|
| 620 |
+
routeFromURL();
|
| 621 |
+
</script>
|
| 622 |
+
</body>
|
| 623 |
+
</html>
|
| 624 |
+
"""
|
| 625 |
+
|
| 626 |
+
# ------------------------------ Utilities ------------------------------ #
|
| 627 |
+
|
| 628 |
+
IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp")
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def _url_join(*parts: str) -> str:
|
| 632 |
+
norm = posixpath.join(*[p.replace("\\", "/") for p in parts])
|
| 633 |
+
segs = [s for s in norm.split("/") if s not in ("", ".")]
|
| 634 |
+
return "/".join(quote(s) for s in segs)
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def _is_plain_name(name: str) -> bool:
|
| 638 |
+
return all(c not in name for c in ("/", "\\")) and name not in (".", "..")
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def build_group_list(root_dir: str) -> dict:
|
| 642 |
+
groups = []
|
| 643 |
+
try:
|
| 644 |
+
for gname in sorted(os.listdir(root_dir)):
|
| 645 |
+
gpath = os.path.join(root_dir, gname)
|
| 646 |
+
if not os.path.isdir(gpath):
|
| 647 |
+
continue
|
| 648 |
+
has_scene = False
|
| 649 |
+
try:
|
| 650 |
+
for sname in os.listdir(gpath):
|
| 651 |
+
spath = os.path.join(gpath, sname)
|
| 652 |
+
if not os.path.isdir(spath):
|
| 653 |
+
continue
|
| 654 |
+
if os.path.exists(os.path.join(spath, "scene.glb")) and os.path.exists(
|
| 655 |
+
os.path.join(spath, "scene.jpg")
|
| 656 |
+
):
|
| 657 |
+
has_scene = True
|
| 658 |
+
break
|
| 659 |
+
except Exception:
|
| 660 |
+
pass
|
| 661 |
+
if has_scene:
|
| 662 |
+
groups.append({"id": gname, "title": gname})
|
| 663 |
+
except Exception as e:
|
| 664 |
+
print(f"[warn] build_group_list failed: {e}", file=sys.stderr)
|
| 665 |
+
return {"groups": groups}
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def build_group_manifest(root_dir: str, group: str) -> dict:
|
| 669 |
+
items = []
|
| 670 |
+
gpath = os.path.join(root_dir, group)
|
| 671 |
+
try:
|
| 672 |
+
if not os.path.isdir(gpath):
|
| 673 |
+
return {"group": group, "items": []}
|
| 674 |
+
for sname in sorted(os.listdir(gpath)):
|
| 675 |
+
spath = os.path.join(gpath, sname)
|
| 676 |
+
if not os.path.isdir(spath):
|
| 677 |
+
continue
|
| 678 |
+
glb_fs = os.path.join(spath, "scene.glb")
|
| 679 |
+
jpg_fs = os.path.join(spath, "scene.jpg")
|
| 680 |
+
if not (os.path.exists(glb_fs) and os.path.exists(jpg_fs)):
|
| 681 |
+
continue
|
| 682 |
+
depth_images = []
|
| 683 |
+
dpath = os.path.join(spath, "depth_vis")
|
| 684 |
+
if os.path.isdir(dpath):
|
| 685 |
+
files = [
|
| 686 |
+
f for f in os.listdir(dpath) if os.path.splitext(f)[1].lower() in IMAGE_EXTS
|
| 687 |
+
]
|
| 688 |
+
for fn in sorted(files):
|
| 689 |
+
depth_images.append("/" + _url_join(group, sname, "depth_vis", fn))
|
| 690 |
+
items.append(
|
| 691 |
+
{
|
| 692 |
+
"id": sname,
|
| 693 |
+
"title": sname,
|
| 694 |
+
"model": "/" + _url_join(group, sname, "scene.glb"),
|
| 695 |
+
"thumbnail": "/" + _url_join(group, sname, "scene.jpg"),
|
| 696 |
+
"depth_images": depth_images,
|
| 697 |
+
}
|
| 698 |
+
)
|
| 699 |
+
except Exception as e:
|
| 700 |
+
print(f"[warn] build_group_manifest failed for {group}: {e}", file=sys.stderr)
|
| 701 |
+
return {"group": group, "items": items}
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
class GalleryHandler(SimpleHTTPRequestHandler):
|
| 705 |
+
def __init__(self, *args, directory=None, **kwargs):
|
| 706 |
+
super().__init__(*args, directory=directory, **kwargs)
|
| 707 |
+
|
| 708 |
+
def do_GET(self):
|
| 709 |
+
if self.path in ("/", "/index.html") or self.path.startswith("/?"):
|
| 710 |
+
content = HTML_PAGE.encode("utf-8")
|
| 711 |
+
self.send_response(HTTPStatus.OK)
|
| 712 |
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
| 713 |
+
self.send_header("Content-Length", str(len(content)))
|
| 714 |
+
self.send_header("Cache-Control", "no-store")
|
| 715 |
+
self.end_headers()
|
| 716 |
+
self.wfile.write(content)
|
| 717 |
+
return
|
| 718 |
+
if self.path == "/manifest.json":
|
| 719 |
+
data = json.dumps(
|
| 720 |
+
build_group_list(self.directory), ensure_ascii=False, indent=2
|
| 721 |
+
).encode("utf-8")
|
| 722 |
+
self.send_response(HTTPStatus.OK)
|
| 723 |
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
| 724 |
+
self.send_header("Content-Length", str(len(data)))
|
| 725 |
+
self.send_header("Cache-Control", "no-store")
|
| 726 |
+
self.end_headers()
|
| 727 |
+
self.wfile.write(data)
|
| 728 |
+
return
|
| 729 |
+
if self.path.startswith("/manifest/") and self.path.endswith(".json"):
|
| 730 |
+
group_enc = self.path[len("/manifest/") : -len(".json")]
|
| 731 |
+
try:
|
| 732 |
+
group = unquote(group_enc)
|
| 733 |
+
except Exception:
|
| 734 |
+
group = group_enc
|
| 735 |
+
if not _is_plain_name(group):
|
| 736 |
+
self.send_error(HTTPStatus.BAD_REQUEST, "Invalid group name")
|
| 737 |
+
return
|
| 738 |
+
data = json.dumps(
|
| 739 |
+
build_group_manifest(self.directory, group), ensure_ascii=False, indent=2
|
| 740 |
+
).encode("utf-8")
|
| 741 |
+
self.send_response(HTTPStatus.OK)
|
| 742 |
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
| 743 |
+
self.send_header("Content-Length", str(len(data)))
|
| 744 |
+
self.send_header("Cache-Control", "no-store")
|
| 745 |
+
self.end_headers()
|
| 746 |
+
self.wfile.write(data)
|
| 747 |
+
return
|
| 748 |
+
if self.path == "/favicon.ico":
|
| 749 |
+
self.send_response(HTTPStatus.NO_CONTENT)
|
| 750 |
+
self.end_headers()
|
| 751 |
+
return
|
| 752 |
+
return super().do_GET()
|
| 753 |
+
|
| 754 |
+
def list_directory(self, path):
|
| 755 |
+
self.send_error(HTTPStatus.NOT_FOUND, "Directory listing disabled")
|
| 756 |
+
return None
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
def gallery():
|
| 760 |
+
parser = argparse.ArgumentParser(
|
| 761 |
+
description="Depth Anything 3 Gallery Server (two-level, with pagination)"
|
| 762 |
+
)
|
| 763 |
+
parser.add_argument(
|
| 764 |
+
"-d", "--dir", required=True, help="Gallery root directory (two-level: group/scene)"
|
| 765 |
+
)
|
| 766 |
+
parser.add_argument("-p", "--port", type=int, default=8000, help="Port (default 8000)")
|
| 767 |
+
parser.add_argument("--host", default="127.0.0.1", help="Host address (default 127.0.0.1)")
|
| 768 |
+
parser.add_argument("--open", action="store_true", help="Open browser after launch")
|
| 769 |
+
args = parser.parse_args()
|
| 770 |
+
|
| 771 |
+
root_dir = os.path.abspath(args.dir)
|
| 772 |
+
if not os.path.isdir(root_dir):
|
| 773 |
+
print(f"[error] Directory not found: {root_dir}", file=sys.stderr)
|
| 774 |
+
sys.exit(1)
|
| 775 |
+
|
| 776 |
+
Handler = partial(GalleryHandler, directory=root_dir)
|
| 777 |
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
| 778 |
+
|
| 779 |
+
addr = f"http://{args.host}:{args.port}/"
|
| 780 |
+
print(f"[info] Serving gallery from: {root_dir}")
|
| 781 |
+
print(f"[info] Open: {addr}")
|
| 782 |
+
|
| 783 |
+
if args.open:
|
| 784 |
+
try:
|
| 785 |
+
import webbrowser
|
| 786 |
+
|
| 787 |
+
webbrowser.open(addr)
|
| 788 |
+
except Exception as e:
|
| 789 |
+
print(f"[warn] Failed to open browser: {e}", file=sys.stderr)
|
| 790 |
+
|
| 791 |
+
try:
|
| 792 |
+
server.serve_forever()
|
| 793 |
+
except KeyboardInterrupt:
|
| 794 |
+
print("\n[info] Shutting down...")
|
| 795 |
+
finally:
|
| 796 |
+
server.server_close()
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
def main():
|
| 800 |
+
"""Main entry point for gallery server."""
|
| 801 |
+
mimetypes.add_type("model/gltf-binary", ".glb")
|
| 802 |
+
gallery()
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
if __name__ == "__main__":
|
| 806 |
+
main()
|
multivue/cameras/source/src/depth_anything_3/services/inference_service.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Unified Inference Service
|
| 17 |
+
Provides unified interface for local and remote inference
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
from typing import Any, Dict, List, Optional, Union
|
| 23 |
+
import numpy as np
|
| 24 |
+
import requests
|
| 25 |
+
import typer
|
| 26 |
+
|
| 27 |
+
from ..api import DepthAnything3
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class InferenceService:
|
| 31 |
+
"""Unified inference service class"""
|
| 32 |
+
|
| 33 |
+
def __init__(self, model_dir: str, device: str = "cuda"):
|
| 34 |
+
self.model_dir = model_dir
|
| 35 |
+
self.device = device
|
| 36 |
+
self.model = None
|
| 37 |
+
|
| 38 |
+
def load_model(self):
|
| 39 |
+
"""Load model"""
|
| 40 |
+
if self.model is None:
|
| 41 |
+
typer.echo(f"Loading model from {self.model_dir}...")
|
| 42 |
+
self.model = DepthAnything3.from_pretrained(self.model_dir).to(self.device)
|
| 43 |
+
return self.model
|
| 44 |
+
|
| 45 |
+
def run_local_inference(
|
| 46 |
+
self,
|
| 47 |
+
image_paths: List[str],
|
| 48 |
+
export_dir: str,
|
| 49 |
+
export_format: str = "mini_npz-glb",
|
| 50 |
+
process_res: int = 504,
|
| 51 |
+
process_res_method: str = "upper_bound_resize",
|
| 52 |
+
export_feat_layers: List[int] = None,
|
| 53 |
+
extrinsics: Optional[np.ndarray] = None,
|
| 54 |
+
intrinsics: Optional[np.ndarray] = None,
|
| 55 |
+
align_to_input_ext_scale: bool = True,
|
| 56 |
+
use_ray_pose: bool = False,
|
| 57 |
+
ref_view_strategy: str = "saddle_balanced",
|
| 58 |
+
conf_thresh_percentile: float = 40.0,
|
| 59 |
+
num_max_points: int = 1_000_000,
|
| 60 |
+
show_cameras: bool = True,
|
| 61 |
+
feat_vis_fps: int = 15,
|
| 62 |
+
) -> Any:
|
| 63 |
+
"""Run local inference"""
|
| 64 |
+
if export_feat_layers is None:
|
| 65 |
+
export_feat_layers = []
|
| 66 |
+
|
| 67 |
+
model = self.load_model()
|
| 68 |
+
|
| 69 |
+
# Prepare inference parameters
|
| 70 |
+
inference_kwargs = {
|
| 71 |
+
"image": image_paths,
|
| 72 |
+
"export_dir": export_dir,
|
| 73 |
+
"export_format": export_format,
|
| 74 |
+
"process_res": process_res,
|
| 75 |
+
"process_res_method": process_res_method,
|
| 76 |
+
"export_feat_layers": export_feat_layers,
|
| 77 |
+
"align_to_input_ext_scale": align_to_input_ext_scale,
|
| 78 |
+
"use_ray_pose": use_ray_pose,
|
| 79 |
+
"ref_view_strategy": ref_view_strategy,
|
| 80 |
+
"conf_thresh_percentile": conf_thresh_percentile,
|
| 81 |
+
"num_max_points": num_max_points,
|
| 82 |
+
"show_cameras": show_cameras,
|
| 83 |
+
"feat_vis_fps": feat_vis_fps,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Add pose data (if exists)
|
| 87 |
+
if extrinsics is not None:
|
| 88 |
+
inference_kwargs["extrinsics"] = extrinsics
|
| 89 |
+
if intrinsics is not None:
|
| 90 |
+
inference_kwargs["intrinsics"] = intrinsics
|
| 91 |
+
|
| 92 |
+
# Run inference
|
| 93 |
+
typer.echo(f"Running inference on {len(image_paths)} images...")
|
| 94 |
+
prediction = model.inference(**inference_kwargs)
|
| 95 |
+
|
| 96 |
+
typer.echo(f"Results saved to {export_dir}")
|
| 97 |
+
typer.echo(f"Export format: {export_format}")
|
| 98 |
+
|
| 99 |
+
return prediction
|
| 100 |
+
|
| 101 |
+
def run_backend_inference(
|
| 102 |
+
self,
|
| 103 |
+
image_paths: List[str],
|
| 104 |
+
export_dir: str,
|
| 105 |
+
backend_url: str,
|
| 106 |
+
export_format: str = "mini_npz-glb",
|
| 107 |
+
process_res: int = 504,
|
| 108 |
+
process_res_method: str = "upper_bound_resize",
|
| 109 |
+
export_feat_layers: List[int] = None,
|
| 110 |
+
extrinsics: Optional[np.ndarray] = None,
|
| 111 |
+
intrinsics: Optional[np.ndarray] = None,
|
| 112 |
+
align_to_input_ext_scale: bool = True,
|
| 113 |
+
use_ray_pose: bool = False,
|
| 114 |
+
ref_view_strategy: str = "saddle_balanced",
|
| 115 |
+
conf_thresh_percentile: float = 40.0,
|
| 116 |
+
num_max_points: int = 1_000_000,
|
| 117 |
+
show_cameras: bool = True,
|
| 118 |
+
feat_vis_fps: int = 15,
|
| 119 |
+
) -> Dict[str, Any]:
|
| 120 |
+
"""Run backend inference"""
|
| 121 |
+
if export_feat_layers is None:
|
| 122 |
+
export_feat_layers = []
|
| 123 |
+
|
| 124 |
+
# Check backend status
|
| 125 |
+
if not self._check_backend_status(backend_url):
|
| 126 |
+
raise typer.BadParameter(f"Backend service is not running at {backend_url}")
|
| 127 |
+
|
| 128 |
+
# Prepare payload
|
| 129 |
+
payload = {
|
| 130 |
+
"image_paths": image_paths,
|
| 131 |
+
"export_dir": export_dir,
|
| 132 |
+
"export_format": export_format,
|
| 133 |
+
"process_res": process_res,
|
| 134 |
+
"process_res_method": process_res_method,
|
| 135 |
+
"export_feat_layers": export_feat_layers,
|
| 136 |
+
"align_to_input_ext_scale": align_to_input_ext_scale,
|
| 137 |
+
"use_ray_pose": use_ray_pose,
|
| 138 |
+
"ref_view_strategy": ref_view_strategy,
|
| 139 |
+
"conf_thresh_percentile": conf_thresh_percentile,
|
| 140 |
+
"num_max_points": num_max_points,
|
| 141 |
+
"show_cameras": show_cameras,
|
| 142 |
+
"feat_vis_fps": feat_vis_fps,
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Add pose data (if exists)
|
| 146 |
+
if extrinsics is not None:
|
| 147 |
+
payload["extrinsics"] = [ext.astype(np.float64).tolist() for ext in extrinsics]
|
| 148 |
+
if intrinsics is not None:
|
| 149 |
+
payload["intrinsics"] = [intr.astype(np.float64).tolist() for intr in intrinsics]
|
| 150 |
+
|
| 151 |
+
# Submit task
|
| 152 |
+
typer.echo("Submitting inference task to backend...")
|
| 153 |
+
# Sent automatically when the backend was started with a matching --api-key /
|
| 154 |
+
# DA3_BACKEND_API_KEY, so client and server share the same env-var convention.
|
| 155 |
+
api_key = os.environ.get("DA3_BACKEND_API_KEY")
|
| 156 |
+
headers = {"X-API-Key": api_key} if api_key else None
|
| 157 |
+
try:
|
| 158 |
+
response = requests.post(
|
| 159 |
+
f"{backend_url}/inference", json=payload, headers=headers, timeout=30
|
| 160 |
+
)
|
| 161 |
+
response.raise_for_status()
|
| 162 |
+
result = response.json()
|
| 163 |
+
|
| 164 |
+
if result["success"]:
|
| 165 |
+
task_id = result["task_id"]
|
| 166 |
+
typer.echo("Task submitted successfully!")
|
| 167 |
+
typer.echo(f"Task ID: {task_id}")
|
| 168 |
+
typer.echo(f"Results will be saved to: {export_dir}")
|
| 169 |
+
typer.echo(f"Check backend logs for progress updates with task ID: {task_id}")
|
| 170 |
+
return result
|
| 171 |
+
else:
|
| 172 |
+
raise typer.BadParameter(
|
| 173 |
+
f"Backend inference submission failed: {result['message']}"
|
| 174 |
+
)
|
| 175 |
+
except requests.exceptions.RequestException as e:
|
| 176 |
+
raise typer.BadParameter(f"Backend inference submission failed: {e}")
|
| 177 |
+
|
| 178 |
+
def _check_backend_status(self, backend_url: str) -> bool:
|
| 179 |
+
"""Check backend status"""
|
| 180 |
+
try:
|
| 181 |
+
response = requests.get(f"{backend_url}/status", timeout=5)
|
| 182 |
+
return response.status_code == 200
|
| 183 |
+
except Exception:
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def run_inference(
|
| 188 |
+
image_paths: List[str],
|
| 189 |
+
export_dir: str,
|
| 190 |
+
model_dir: str,
|
| 191 |
+
device: str = "cuda",
|
| 192 |
+
backend_url: Optional[str] = None,
|
| 193 |
+
export_format: str = "mini_npz-glb",
|
| 194 |
+
process_res: int = 504,
|
| 195 |
+
process_res_method: str = "upper_bound_resize",
|
| 196 |
+
export_feat_layers: List[int] = None,
|
| 197 |
+
extrinsics: Optional[np.ndarray] = None,
|
| 198 |
+
intrinsics: Optional[np.ndarray] = None,
|
| 199 |
+
align_to_input_ext_scale: bool = True,
|
| 200 |
+
use_ray_pose: bool = False,
|
| 201 |
+
ref_view_strategy: str = "saddle_balanced",
|
| 202 |
+
conf_thresh_percentile: float = 40.0,
|
| 203 |
+
num_max_points: int = 1_000_000,
|
| 204 |
+
show_cameras: bool = True,
|
| 205 |
+
feat_vis_fps: int = 15,
|
| 206 |
+
) -> Union[Any, Dict[str, Any]]:
|
| 207 |
+
"""Unified inference interface"""
|
| 208 |
+
|
| 209 |
+
service = InferenceService(model_dir, device)
|
| 210 |
+
|
| 211 |
+
if backend_url:
|
| 212 |
+
return service.run_backend_inference(
|
| 213 |
+
image_paths=image_paths,
|
| 214 |
+
export_dir=export_dir,
|
| 215 |
+
backend_url=backend_url,
|
| 216 |
+
export_format=export_format,
|
| 217 |
+
process_res=process_res,
|
| 218 |
+
process_res_method=process_res_method,
|
| 219 |
+
export_feat_layers=export_feat_layers,
|
| 220 |
+
extrinsics=extrinsics,
|
| 221 |
+
intrinsics=intrinsics,
|
| 222 |
+
align_to_input_ext_scale=align_to_input_ext_scale,
|
| 223 |
+
use_ray_pose=use_ray_pose,
|
| 224 |
+
ref_view_strategy=ref_view_strategy,
|
| 225 |
+
conf_thresh_percentile=conf_thresh_percentile,
|
| 226 |
+
num_max_points=num_max_points,
|
| 227 |
+
show_cameras=show_cameras,
|
| 228 |
+
feat_vis_fps=feat_vis_fps,
|
| 229 |
+
)
|
| 230 |
+
else:
|
| 231 |
+
return service.run_local_inference(
|
| 232 |
+
image_paths=image_paths,
|
| 233 |
+
export_dir=export_dir,
|
| 234 |
+
export_format=export_format,
|
| 235 |
+
process_res=process_res,
|
| 236 |
+
process_res_method=process_res_method,
|
| 237 |
+
export_feat_layers=export_feat_layers,
|
| 238 |
+
extrinsics=extrinsics,
|
| 239 |
+
intrinsics=intrinsics,
|
| 240 |
+
align_to_input_ext_scale=align_to_input_ext_scale,
|
| 241 |
+
use_ray_pose=use_ray_pose,
|
| 242 |
+
ref_view_strategy=ref_view_strategy,
|
| 243 |
+
conf_thresh_percentile=conf_thresh_percentile,
|
| 244 |
+
num_max_points=num_max_points,
|
| 245 |
+
show_cameras=show_cameras,
|
| 246 |
+
feat_vis_fps=feat_vis_fps,
|
| 247 |
+
)
|
multivue/cameras/source/src/depth_anything_3/services/input_handlers.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Input Processing Service
|
| 17 |
+
Handles different types of inputs (image, images, colmap, video)
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import glob
|
| 21 |
+
import os
|
| 22 |
+
from typing import List, Tuple
|
| 23 |
+
import cv2
|
| 24 |
+
import numpy as np
|
| 25 |
+
import typer
|
| 26 |
+
|
| 27 |
+
from ..utils.read_write_model import read_model
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class InputHandler:
|
| 31 |
+
"""Base input handler class"""
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
def validate_path(path: str, path_type: str = "file") -> str:
|
| 35 |
+
"""Validate path"""
|
| 36 |
+
if not os.path.exists(path):
|
| 37 |
+
raise typer.BadParameter(f"{path_type} not found: {path}")
|
| 38 |
+
return path
|
| 39 |
+
|
| 40 |
+
@staticmethod
|
| 41 |
+
def handle_export_dir(export_dir: str, auto_cleanup: bool = False) -> str:
|
| 42 |
+
"""Handle export directory"""
|
| 43 |
+
if os.path.exists(export_dir):
|
| 44 |
+
if auto_cleanup:
|
| 45 |
+
typer.echo(f"Auto-cleaning existing export directory: {export_dir}")
|
| 46 |
+
import shutil
|
| 47 |
+
|
| 48 |
+
shutil.rmtree(export_dir)
|
| 49 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 50 |
+
else:
|
| 51 |
+
typer.echo(f"Export directory '{export_dir}' already exists.")
|
| 52 |
+
if typer.confirm("Do you want to clean it and continue?"):
|
| 53 |
+
import shutil
|
| 54 |
+
|
| 55 |
+
shutil.rmtree(export_dir)
|
| 56 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 57 |
+
typer.echo(f"Cleaned export directory: {export_dir}")
|
| 58 |
+
else:
|
| 59 |
+
typer.echo("Operation cancelled.")
|
| 60 |
+
raise typer.Exit(0)
|
| 61 |
+
else:
|
| 62 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 63 |
+
return export_dir
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ImageHandler(InputHandler):
|
| 67 |
+
"""Single image handler"""
|
| 68 |
+
|
| 69 |
+
@staticmethod
|
| 70 |
+
def process(image_path: str) -> List[str]:
|
| 71 |
+
"""Process single image"""
|
| 72 |
+
InputHandler.validate_path(image_path, "Image file")
|
| 73 |
+
return [image_path]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ImagesHandler(InputHandler):
|
| 77 |
+
"""Image directory handler"""
|
| 78 |
+
|
| 79 |
+
@staticmethod
|
| 80 |
+
def process(images_dir: str, image_extensions: str = "png,jpg,jpeg") -> List[str]:
|
| 81 |
+
"""Process image directory"""
|
| 82 |
+
InputHandler.validate_path(images_dir, "Images directory")
|
| 83 |
+
|
| 84 |
+
# Parse extensions
|
| 85 |
+
extensions = [ext.strip().lower() for ext in image_extensions.split(",")]
|
| 86 |
+
extensions = [ext if ext.startswith(".") else f".{ext}" for ext in extensions]
|
| 87 |
+
|
| 88 |
+
# Find image files
|
| 89 |
+
image_files = []
|
| 90 |
+
for ext in extensions:
|
| 91 |
+
pattern = f"*{ext}"
|
| 92 |
+
image_files.extend(glob.glob(os.path.join(images_dir, pattern)))
|
| 93 |
+
image_files.extend(glob.glob(os.path.join(images_dir, pattern.upper())))
|
| 94 |
+
|
| 95 |
+
image_files = sorted(list(set(image_files))) # Remove duplicates and sort
|
| 96 |
+
|
| 97 |
+
if not image_files:
|
| 98 |
+
raise typer.BadParameter(
|
| 99 |
+
f"No image files found in {images_dir} with extensions: {extensions}"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
typer.echo(f"Found {len(image_files)} images to process")
|
| 103 |
+
return image_files
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class ColmapHandler(InputHandler):
|
| 107 |
+
"""COLMAP data handler"""
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
def process(
|
| 111 |
+
colmap_dir: str, sparse_subdir: str = ""
|
| 112 |
+
) -> Tuple[List[str], np.ndarray, np.ndarray]:
|
| 113 |
+
"""Process COLMAP data"""
|
| 114 |
+
InputHandler.validate_path(colmap_dir, "COLMAP directory")
|
| 115 |
+
|
| 116 |
+
# Build paths
|
| 117 |
+
images_dir = os.path.join(colmap_dir, "images")
|
| 118 |
+
if sparse_subdir:
|
| 119 |
+
sparse_dir = os.path.join(colmap_dir, "sparse", sparse_subdir)
|
| 120 |
+
else:
|
| 121 |
+
sparse_dir = os.path.join(colmap_dir, "sparse")
|
| 122 |
+
|
| 123 |
+
InputHandler.validate_path(images_dir, "Images directory")
|
| 124 |
+
InputHandler.validate_path(sparse_dir, "Sparse reconstruction directory")
|
| 125 |
+
|
| 126 |
+
# Load COLMAP data
|
| 127 |
+
typer.echo("Loading COLMAP reconstruction data...")
|
| 128 |
+
try:
|
| 129 |
+
cameras, images, points3D = read_model(sparse_dir)
|
| 130 |
+
|
| 131 |
+
typer.echo(
|
| 132 |
+
f"Loaded COLMAP data: {len(cameras)} cameras, {len(images)} images, "
|
| 133 |
+
f"{len(points3D)} 3D points."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Get image files and pose data
|
| 137 |
+
image_files = []
|
| 138 |
+
extrinsics = []
|
| 139 |
+
intrinsics = []
|
| 140 |
+
|
| 141 |
+
for image_id, image_data in images.items():
|
| 142 |
+
image_name = image_data.name
|
| 143 |
+
image_path = os.path.join(images_dir, image_name)
|
| 144 |
+
|
| 145 |
+
if os.path.exists(image_path):
|
| 146 |
+
image_files.append(image_path)
|
| 147 |
+
|
| 148 |
+
# Get camera parameters
|
| 149 |
+
camera = cameras[image_data.camera_id]
|
| 150 |
+
|
| 151 |
+
# Convert quaternion to rotation matrix
|
| 152 |
+
R = image_data.qvec2rotmat()
|
| 153 |
+
t = image_data.tvec
|
| 154 |
+
|
| 155 |
+
# Create extrinsic matrix (world to camera)
|
| 156 |
+
extrinsic = np.eye(4)
|
| 157 |
+
extrinsic[:3, :3] = R
|
| 158 |
+
extrinsic[:3, 3] = t
|
| 159 |
+
extrinsics.append(extrinsic)
|
| 160 |
+
|
| 161 |
+
# Create intrinsic matrix
|
| 162 |
+
if camera.model == "PINHOLE":
|
| 163 |
+
fx, fy, cx, cy = camera.params
|
| 164 |
+
elif camera.model == "SIMPLE_PINHOLE":
|
| 165 |
+
f, cx, cy = camera.params
|
| 166 |
+
fx = fy = f
|
| 167 |
+
else:
|
| 168 |
+
# For other models, use basic pinhole approximation
|
| 169 |
+
fx = fy = camera.params[0] if len(camera.params) > 0 else 1000
|
| 170 |
+
cx = camera.width / 2
|
| 171 |
+
cy = camera.height / 2
|
| 172 |
+
|
| 173 |
+
intrinsic = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
|
| 174 |
+
intrinsics.append(intrinsic)
|
| 175 |
+
|
| 176 |
+
if not image_files:
|
| 177 |
+
raise typer.BadParameter("No valid images found in COLMAP data")
|
| 178 |
+
|
| 179 |
+
typer.echo(f"Found {len(image_files)} valid images with pose data")
|
| 180 |
+
|
| 181 |
+
return image_files, np.array(extrinsics), np.array(intrinsics)
|
| 182 |
+
|
| 183 |
+
except Exception as e:
|
| 184 |
+
raise typer.BadParameter(f"Failed to load COLMAP data: {e}")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class VideoHandler(InputHandler):
|
| 188 |
+
"""Video handler"""
|
| 189 |
+
|
| 190 |
+
@staticmethod
|
| 191 |
+
def process(video_path: str, output_dir: str, fps: float = 1.0) -> List[str]:
|
| 192 |
+
"""Process video, extract frames"""
|
| 193 |
+
InputHandler.validate_path(video_path, "Video file")
|
| 194 |
+
|
| 195 |
+
cap = cv2.VideoCapture(video_path)
|
| 196 |
+
if not cap.isOpened():
|
| 197 |
+
raise typer.BadParameter(f"Cannot open video: {video_path}")
|
| 198 |
+
|
| 199 |
+
# Get video properties
|
| 200 |
+
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
| 201 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 202 |
+
duration = total_frames / video_fps
|
| 203 |
+
|
| 204 |
+
# Calculate frame interval (ensure at least 1)
|
| 205 |
+
frame_interval = max(1, int(video_fps / fps))
|
| 206 |
+
actual_fps = video_fps / frame_interval
|
| 207 |
+
|
| 208 |
+
typer.echo(f"Video FPS: {video_fps:.2f}, Duration: {duration:.2f}s")
|
| 209 |
+
|
| 210 |
+
# Warn if requested FPS is higher than video FPS
|
| 211 |
+
if fps > video_fps:
|
| 212 |
+
typer.echo(
|
| 213 |
+
f"⚠️ Warning: Requested sampling FPS ({fps:.2f}) exceeds video FPS ({video_fps:.2f})", # noqa: E501
|
| 214 |
+
err=True,
|
| 215 |
+
)
|
| 216 |
+
typer.echo(
|
| 217 |
+
f"⚠️ Using maximum available FPS: {actual_fps:.2f} (extracting every frame)",
|
| 218 |
+
err=True,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
typer.echo(f"Extracting frames at {actual_fps:.2f} FPS (every {frame_interval} frame(s))")
|
| 222 |
+
|
| 223 |
+
# Create output directory
|
| 224 |
+
frames_dir = os.path.join(output_dir, "input_images")
|
| 225 |
+
os.makedirs(frames_dir, exist_ok=True)
|
| 226 |
+
|
| 227 |
+
frame_count = 0
|
| 228 |
+
saved_count = 0
|
| 229 |
+
|
| 230 |
+
while True:
|
| 231 |
+
ret, frame = cap.read()
|
| 232 |
+
if not ret:
|
| 233 |
+
break
|
| 234 |
+
|
| 235 |
+
if frame_count % frame_interval == 0:
|
| 236 |
+
frame_path = os.path.join(frames_dir, f"{saved_count:06d}.png")
|
| 237 |
+
cv2.imwrite(frame_path, frame)
|
| 238 |
+
saved_count += 1
|
| 239 |
+
|
| 240 |
+
frame_count += 1
|
| 241 |
+
|
| 242 |
+
cap.release()
|
| 243 |
+
typer.echo(f"Extracted {saved_count} frames to {frames_dir}")
|
| 244 |
+
|
| 245 |
+
# Get frame file list
|
| 246 |
+
frame_files = sorted(
|
| 247 |
+
[f for f in os.listdir(frames_dir) if f.endswith((".png", ".jpg", ".jpeg"))]
|
| 248 |
+
)
|
| 249 |
+
if not frame_files:
|
| 250 |
+
raise typer.BadParameter("No frames extracted from video")
|
| 251 |
+
|
| 252 |
+
return [os.path.join(frames_dir, f) for f in frame_files]
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def parse_export_feat(export_feat_str: str) -> List[int]:
|
| 256 |
+
"""Parse export_feat parameter"""
|
| 257 |
+
if not export_feat_str:
|
| 258 |
+
return []
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
return [int(x.strip()) for x in export_feat_str.split(",") if x.strip()]
|
| 262 |
+
except ValueError:
|
| 263 |
+
raise typer.BadParameter(
|
| 264 |
+
f"Invalid export_feat format: {export_feat_str}. "
|
| 265 |
+
"Use comma-separated integers like '0,1,2'"
|
| 266 |
+
)
|
multivue/cameras/source/src/depth_anything_3/specs.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from typing import Any, Optional
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class Gaussians:
|
| 25 |
+
"""3DGS parameters, all in world space"""
|
| 26 |
+
|
| 27 |
+
means: torch.Tensor # world points, "batch gaussian dim"
|
| 28 |
+
scales: torch.Tensor # scales_std, "batch gaussian 3"
|
| 29 |
+
rotations: torch.Tensor # world_quat_wxyz, "batch gaussian 4"
|
| 30 |
+
harmonics: torch.Tensor # world SH, "batch gaussian 3 d_sh"
|
| 31 |
+
opacities: torch.Tensor # opacity | opacity SH, "batch gaussian" | "batch gaussian 1 d_sh"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class Prediction:
|
| 36 |
+
depth: np.ndarray # N, H, W
|
| 37 |
+
is_metric: int
|
| 38 |
+
sky: np.ndarray | None = None # N, H, W
|
| 39 |
+
conf: np.ndarray | None = None # N, H, W
|
| 40 |
+
extrinsics: np.ndarray | None = None # N, 4, 4
|
| 41 |
+
intrinsics: np.ndarray | None = None # N, 3, 3
|
| 42 |
+
processed_images: np.ndarray | None = None # N, H, W, 3 - processed images for visualization
|
| 43 |
+
gaussians: Gaussians | None = None # 3D gaussians
|
| 44 |
+
aux: dict[str, Any] = None #
|
| 45 |
+
scale_factor: Optional[float] = None # metric scale
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/alignment.cpython-312.pyc
ADDED
|
Binary file (6.05 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (3.83 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/geometry.cpython-312.pyc
ADDED
|
Binary file (20.3 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/logger.cpython-312.pyc
ADDED
|
Binary file (3.65 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/parallel_utils.cpython-312.pyc
ADDED
|
Binary file (5 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/__pycache__/ray_utils.cpython-312.pyc
ADDED
|
Binary file (21.8 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/alignment.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Alignment utilities for depth estimation and metric scaling.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from typing import Tuple
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def least_squares_scale_scalar(
|
| 24 |
+
a: torch.Tensor, b: torch.Tensor, eps: float = 1e-12
|
| 25 |
+
) -> torch.Tensor:
|
| 26 |
+
"""
|
| 27 |
+
Compute least squares scale factor s such that a ≈ s * b.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
a: First tensor
|
| 31 |
+
b: Second tensor
|
| 32 |
+
eps: Small epsilon for numerical stability
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Scalar tensor containing the scale factor
|
| 36 |
+
|
| 37 |
+
Raises:
|
| 38 |
+
ValueError: If tensors have mismatched shapes or devices
|
| 39 |
+
TypeError: If tensors are not floating point
|
| 40 |
+
"""
|
| 41 |
+
if a.shape != b.shape:
|
| 42 |
+
raise ValueError(f"Shape mismatch: {a.shape} vs {b.shape}")
|
| 43 |
+
if a.device != b.device:
|
| 44 |
+
raise ValueError(f"Device mismatch: {a.device} vs {b.device}")
|
| 45 |
+
if not a.is_floating_point() or not b.is_floating_point():
|
| 46 |
+
raise TypeError("Tensors must be floating point type")
|
| 47 |
+
|
| 48 |
+
# Compute dot products for least squares solution
|
| 49 |
+
num = torch.dot(a.reshape(-1), b.reshape(-1))
|
| 50 |
+
den = torch.dot(b.reshape(-1), b.reshape(-1)).clamp_min(eps)
|
| 51 |
+
return num / den
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def compute_sky_mask(sky_prediction: torch.Tensor, threshold: float = 0.3) -> torch.Tensor:
|
| 55 |
+
"""
|
| 56 |
+
Compute non-sky mask from sky prediction.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
sky_prediction: Sky prediction tensor
|
| 60 |
+
threshold: Threshold for sky classification
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Boolean mask where True indicates non-sky regions
|
| 64 |
+
"""
|
| 65 |
+
return sky_prediction < threshold
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def compute_alignment_mask(
|
| 69 |
+
depth_conf: torch.Tensor,
|
| 70 |
+
non_sky_mask: torch.Tensor,
|
| 71 |
+
depth: torch.Tensor,
|
| 72 |
+
metric_depth: torch.Tensor,
|
| 73 |
+
median_conf: torch.Tensor,
|
| 74 |
+
min_depth_threshold: float = 1e-3,
|
| 75 |
+
min_metric_depth_threshold: float = 1e-2,
|
| 76 |
+
) -> torch.Tensor:
|
| 77 |
+
"""
|
| 78 |
+
Compute mask for depth alignment based on confidence and depth thresholds.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
depth_conf: Depth confidence tensor
|
| 82 |
+
non_sky_mask: Non-sky region mask
|
| 83 |
+
depth: Predicted depth tensor
|
| 84 |
+
metric_depth: Metric depth tensor
|
| 85 |
+
median_conf: Median confidence threshold
|
| 86 |
+
min_depth_threshold: Minimum depth threshold
|
| 87 |
+
min_metric_depth_threshold: Minimum metric depth threshold
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Boolean mask for valid alignment regions
|
| 91 |
+
"""
|
| 92 |
+
return (
|
| 93 |
+
(depth_conf >= median_conf)
|
| 94 |
+
& non_sky_mask
|
| 95 |
+
& (metric_depth > min_metric_depth_threshold)
|
| 96 |
+
& (depth > min_depth_threshold)
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def sample_tensor_for_quantile(tensor: torch.Tensor, max_samples: int = 100000) -> torch.Tensor:
|
| 101 |
+
"""
|
| 102 |
+
Sample tensor elements for quantile computation to reduce memory usage.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
tensor: Input tensor to sample
|
| 106 |
+
max_samples: Maximum number of samples to take
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Sampled tensor
|
| 110 |
+
"""
|
| 111 |
+
if tensor.numel() <= max_samples:
|
| 112 |
+
return tensor
|
| 113 |
+
|
| 114 |
+
idx = torch.randperm(tensor.numel(), device=tensor.device)[:max_samples]
|
| 115 |
+
return tensor.flatten()[idx]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def apply_metric_scaling(
|
| 119 |
+
depth: torch.Tensor, intrinsics: torch.Tensor, scale_factor: float = 300.0
|
| 120 |
+
) -> torch.Tensor:
|
| 121 |
+
"""
|
| 122 |
+
Apply metric scaling to depth based on camera intrinsics.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
depth: Input depth tensor
|
| 126 |
+
intrinsics: Camera intrinsics tensor
|
| 127 |
+
scale_factor: Scaling factor for metric conversion
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
Scaled depth tensor
|
| 131 |
+
"""
|
| 132 |
+
focal_length = (intrinsics[:, :, 0, 0] + intrinsics[:, :, 1, 1]) / 2
|
| 133 |
+
return depth * (focal_length[:, :, None, None] / scale_factor)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def set_sky_regions_to_max_depth(
|
| 137 |
+
depth: torch.Tensor,
|
| 138 |
+
depth_conf: torch.Tensor,
|
| 139 |
+
non_sky_mask: torch.Tensor,
|
| 140 |
+
max_depth: float = 200.0,
|
| 141 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 142 |
+
"""
|
| 143 |
+
Set sky regions to maximum depth and high confidence.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
depth: Depth tensor
|
| 147 |
+
depth_conf: Depth confidence tensor
|
| 148 |
+
non_sky_mask: Non-sky region mask
|
| 149 |
+
max_depth: Maximum depth value for sky regions
|
| 150 |
+
|
| 151 |
+
Returns:
|
| 152 |
+
Tuple of (updated_depth, updated_depth_conf)
|
| 153 |
+
"""
|
| 154 |
+
depth = depth.clone()
|
| 155 |
+
|
| 156 |
+
# Set sky regions to max depth and high confidence
|
| 157 |
+
depth[~non_sky_mask] = max_depth
|
| 158 |
+
if depth_conf is not None:
|
| 159 |
+
depth_conf = depth_conf.clone()
|
| 160 |
+
depth_conf[~non_sky_mask] = 1.0
|
| 161 |
+
return depth, depth_conf
|
| 162 |
+
else:
|
| 163 |
+
return depth, None
|
multivue/cameras/source/src/depth_anything_3/utils/api_helpers.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def parse_scalar(s):
|
| 5 |
+
if not isinstance(s, str):
|
| 6 |
+
return s
|
| 7 |
+
t = s.strip()
|
| 8 |
+
l = t.lower()
|
| 9 |
+
if l == "true":
|
| 10 |
+
return True
|
| 11 |
+
if l == "false":
|
| 12 |
+
return False
|
| 13 |
+
if l in ("none", "null"):
|
| 14 |
+
return None
|
| 15 |
+
try:
|
| 16 |
+
return int(t, 10)
|
| 17 |
+
except Exception:
|
| 18 |
+
pass
|
| 19 |
+
try:
|
| 20 |
+
return float(t)
|
| 21 |
+
except Exception:
|
| 22 |
+
return s
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def fn_kv_csv(s: str) -> dict[str, dict[str, object]]:
|
| 26 |
+
"""
|
| 27 |
+
Parse a string of comma-separated triplets: fn:key:value
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
dict[fn_name] -> dict[key] = parsed_value
|
| 31 |
+
|
| 32 |
+
Example:
|
| 33 |
+
"fn1:width:1920,fn1:height:1080,fn2:quality:0.8"
|
| 34 |
+
-> {"fn1": {"width": 1920, "height": 1080}, "fn2": {"quality": 0.8}}
|
| 35 |
+
"""
|
| 36 |
+
result: dict[str, dict[str, object]] = {}
|
| 37 |
+
if not s:
|
| 38 |
+
return result
|
| 39 |
+
|
| 40 |
+
for item in s.split(","):
|
| 41 |
+
if not item:
|
| 42 |
+
continue
|
| 43 |
+
parts = item.split(":", 2) # allow value to contain ":" beyond first two separators
|
| 44 |
+
if len(parts) < 3:
|
| 45 |
+
raise argparse.ArgumentTypeError(f"Bad item '{item}', expected FN:KEY:VALUE")
|
| 46 |
+
fn, key, raw_val = parts[0], parts[1], parts[2]
|
| 47 |
+
# If you need to allow colons in values, join leftover parts:
|
| 48 |
+
# fn, key, raw_val = parts[0], parts[1], ":".join(parts[2:])
|
| 49 |
+
|
| 50 |
+
if not fn:
|
| 51 |
+
raise argparse.ArgumentTypeError(f"Bad item '{item}': empty function name")
|
| 52 |
+
if not key:
|
| 53 |
+
raise argparse.ArgumentTypeError(f"Bad item '{item}': empty key")
|
| 54 |
+
|
| 55 |
+
val = parse_scalar(raw_val)
|
| 56 |
+
bucket = result.setdefault(fn, {})
|
| 57 |
+
bucket[key] = val
|
| 58 |
+
return result
|
multivue/cameras/source/src/depth_anything_3/utils/camera_trj_helpers.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import cv2
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from einops import einsum, rearrange, reduce
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from scipy.spatial.transform import Rotation as R
|
| 23 |
+
except ImportError:
|
| 24 |
+
from depth_anything_3.utils.logger import logger
|
| 25 |
+
|
| 26 |
+
logger.warn("Dependency 'scipy' not found. Required for interpolating camera trajectory.")
|
| 27 |
+
|
| 28 |
+
from depth_anything_3.utils.geometry import as_homogeneous
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@torch.no_grad()
|
| 32 |
+
def render_stabilization_path(poses, k_size=45):
|
| 33 |
+
"""Rendering stabilized camera path.
|
| 34 |
+
poses: [batch, 4, 4] or [batch, 3, 4],
|
| 35 |
+
return:
|
| 36 |
+
smooth path: [batch 4 4]"""
|
| 37 |
+
num_frames = poses.shape[0]
|
| 38 |
+
device = poses.device
|
| 39 |
+
dtype = poses.dtype
|
| 40 |
+
|
| 41 |
+
# Early exit for trivial cases
|
| 42 |
+
if num_frames <= 1:
|
| 43 |
+
return as_homogeneous(poses)
|
| 44 |
+
|
| 45 |
+
# Make k_size safe: positive odd and not larger than num_frames
|
| 46 |
+
# 1) Ensure odd
|
| 47 |
+
if k_size < 1:
|
| 48 |
+
k_size = 1
|
| 49 |
+
if k_size % 2 == 0:
|
| 50 |
+
k_size += 1
|
| 51 |
+
# 2) Cap to num_frames (keep odd)
|
| 52 |
+
max_odd = num_frames if (num_frames % 2 == 1) else (num_frames - 1)
|
| 53 |
+
if max_odd < 1:
|
| 54 |
+
max_odd = 1 # covers num_frames == 0 theoretically
|
| 55 |
+
k_size = min(k_size, max_odd)
|
| 56 |
+
# 3) enforce a minimum of 3 when possible (for better smoothing)
|
| 57 |
+
if num_frames >= 3 and k_size < 3:
|
| 58 |
+
k_size = 3
|
| 59 |
+
|
| 60 |
+
input_poses = []
|
| 61 |
+
for i in range(num_frames):
|
| 62 |
+
input_poses.append(
|
| 63 |
+
torch.cat([poses[i, :3, 0:1], poses[i, :3, 1:2], poses[i, :3, 3:4]], dim=-1)
|
| 64 |
+
)
|
| 65 |
+
input_poses = torch.stack(input_poses) # (num_frames, 3, 3)
|
| 66 |
+
|
| 67 |
+
# Prepare Gaussian kernel
|
| 68 |
+
gaussian_kernel = cv2.getGaussianKernel(ksize=k_size, sigma=-1).astype(np.float32).squeeze()
|
| 69 |
+
gaussian_kernel = torch.tensor(gaussian_kernel, dtype=dtype, device=device).view(1, 1, -1)
|
| 70 |
+
pad = k_size // 2
|
| 71 |
+
|
| 72 |
+
output_vectors = []
|
| 73 |
+
for idx in range(3): # For r1, r2, t
|
| 74 |
+
vec = (
|
| 75 |
+
input_poses[:, :, idx].T.unsqueeze(0).unsqueeze(0)
|
| 76 |
+
) # (1, 1, 3, num_frames) -> (1, 1, 3, num_frames)
|
| 77 |
+
# But actually, we want (batch=3, channel=1, width=num_frames)
|
| 78 |
+
# So:
|
| 79 |
+
vec = input_poses[:, :, idx].T.unsqueeze(1) # (3, 1, num_frames)
|
| 80 |
+
vec_padded = F.pad(vec, (pad, pad), mode="reflect")
|
| 81 |
+
filtered = F.conv1d(vec_padded, gaussian_kernel)
|
| 82 |
+
output_vectors.append(filtered.squeeze(1).T) # (num_frames, 3)
|
| 83 |
+
|
| 84 |
+
output_r1, output_r2, output_t = output_vectors # Each is (num_frames, 3)
|
| 85 |
+
|
| 86 |
+
# Normalize r1 and r2
|
| 87 |
+
output_r1 = output_r1 / output_r1.norm(dim=-1, keepdim=True)
|
| 88 |
+
output_r2 = output_r2 / output_r2.norm(dim=-1, keepdim=True)
|
| 89 |
+
|
| 90 |
+
output_poses = []
|
| 91 |
+
for i in range(num_frames):
|
| 92 |
+
output_r3 = torch.linalg.cross(output_r1[i], output_r2[i])
|
| 93 |
+
render_pose = torch.cat(
|
| 94 |
+
[
|
| 95 |
+
output_r1[i].unsqueeze(-1),
|
| 96 |
+
output_r2[i].unsqueeze(-1),
|
| 97 |
+
output_r3.unsqueeze(-1),
|
| 98 |
+
output_t[i].unsqueeze(-1),
|
| 99 |
+
],
|
| 100 |
+
dim=-1,
|
| 101 |
+
)
|
| 102 |
+
output_poses.append(render_pose[:3, :])
|
| 103 |
+
output_poses = as_homogeneous(torch.stack(output_poses, dim=0))
|
| 104 |
+
|
| 105 |
+
return output_poses
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@torch.no_grad()
|
| 109 |
+
def render_wander_path(
|
| 110 |
+
cam2world: torch.Tensor,
|
| 111 |
+
intrinsic: torch.Tensor,
|
| 112 |
+
h: int,
|
| 113 |
+
w: int,
|
| 114 |
+
num_frames: int = 120,
|
| 115 |
+
max_disp: float = 48.0,
|
| 116 |
+
):
|
| 117 |
+
device, dtype = cam2world.device, cam2world.dtype
|
| 118 |
+
fx = intrinsic[0, 0] * w
|
| 119 |
+
r = max_disp / fx
|
| 120 |
+
th = torch.linspace(0, 2.0 * torch.pi, steps=num_frames, device=device, dtype=dtype)
|
| 121 |
+
x = r * torch.sin(th)
|
| 122 |
+
yz = r * torch.cos(th) / 3.0
|
| 123 |
+
T = torch.eye(4, device=device, dtype=dtype).unsqueeze(0).repeat(num_frames, 1, 1)
|
| 124 |
+
T[:, :3, 3] = torch.stack([x, yz, yz], dim=-1) * -1.0
|
| 125 |
+
c2ws = cam2world.unsqueeze(0) @ T
|
| 126 |
+
# Start at reference pose and end back at reference pose
|
| 127 |
+
c2ws = torch.cat([cam2world.unsqueeze(0), c2ws, cam2world.unsqueeze(0)], dim=0)
|
| 128 |
+
Ks = intrinsic.unsqueeze(0).repeat(c2ws.shape[0], 1, 1)
|
| 129 |
+
return c2ws, Ks
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@torch.no_grad()
|
| 133 |
+
def render_dolly_zoom_path(
|
| 134 |
+
cam2world: torch.Tensor,
|
| 135 |
+
intrinsic: torch.Tensor,
|
| 136 |
+
h: int,
|
| 137 |
+
w: int,
|
| 138 |
+
num_frames: int = 120,
|
| 139 |
+
max_disp: float = 0.1,
|
| 140 |
+
D_focus: float = 10.0,
|
| 141 |
+
):
|
| 142 |
+
device, dtype = cam2world.device, cam2world.dtype
|
| 143 |
+
fx0, fy0 = intrinsic[0, 0] * w, intrinsic[1, 1] * h
|
| 144 |
+
t = torch.linspace(0.0, 2.0, steps=num_frames, device=device, dtype=dtype)
|
| 145 |
+
z = 0.5 * (1.0 - torch.cos(torch.pi * t)) * max_disp
|
| 146 |
+
T = torch.eye(4, device=device, dtype=dtype).unsqueeze(0).repeat(num_frames, 1, 1)
|
| 147 |
+
T[:, 2, 3] = -z
|
| 148 |
+
c2ws = cam2world.unsqueeze(0) @ T
|
| 149 |
+
Df = torch.as_tensor(D_focus, device=device, dtype=dtype)
|
| 150 |
+
scale = (Df / (Df + z)).clamp(min=1e-6)
|
| 151 |
+
Ks = intrinsic.unsqueeze(0).repeat(num_frames, 1, 1)
|
| 152 |
+
Ks[:, 0, 0] = (fx0 * scale) / w
|
| 153 |
+
Ks[:, 1, 1] = (fy0 * scale) / h
|
| 154 |
+
return c2ws, Ks
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@torch.no_grad()
|
| 158 |
+
def interpolate_intrinsics(
|
| 159 |
+
initial: torch.Tensor, # "*#batch 3 3"
|
| 160 |
+
final: torch.Tensor, # "*#batch 3 3"
|
| 161 |
+
t: torch.Tensor, # " time_step"
|
| 162 |
+
) -> torch.Tensor: # "*batch time_step 3 3"
|
| 163 |
+
initial = rearrange(initial, "... i j -> ... () i j")
|
| 164 |
+
final = rearrange(final, "... i j -> ... () i j")
|
| 165 |
+
t = rearrange(t, "t -> t () ()")
|
| 166 |
+
return initial + (final - initial) * t
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def intersect_rays(
|
| 170 |
+
a_origins: torch.Tensor, # "*#batch dim"
|
| 171 |
+
a_directions: torch.Tensor, # "*#batch dim"
|
| 172 |
+
b_origins: torch.Tensor, # "*#batch dim"
|
| 173 |
+
b_directions: torch.Tensor, # "*#batch dim"
|
| 174 |
+
) -> torch.Tensor: # "*batch dim"
|
| 175 |
+
"""Compute the least-squares intersection of rays. Uses the math from here:
|
| 176 |
+
https://math.stackexchange.com/a/1762491/286022
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
# Broadcast and stack the tensors.
|
| 180 |
+
a_origins, a_directions, b_origins, b_directions = torch.broadcast_tensors(
|
| 181 |
+
a_origins, a_directions, b_origins, b_directions
|
| 182 |
+
)
|
| 183 |
+
origins = torch.stack((a_origins, b_origins), dim=-2)
|
| 184 |
+
directions = torch.stack((a_directions, b_directions), dim=-2)
|
| 185 |
+
|
| 186 |
+
# Compute n_i * n_i^T - eye(3) from the equation.
|
| 187 |
+
n = einsum(directions, directions, "... n i, ... n j -> ... n i j")
|
| 188 |
+
n = n - torch.eye(3, dtype=origins.dtype, device=origins.device)
|
| 189 |
+
|
| 190 |
+
# Compute the left-hand side of the equation.
|
| 191 |
+
lhs = reduce(n, "... n i j -> ... i j", "sum")
|
| 192 |
+
|
| 193 |
+
# Compute the right-hand side of the equation.
|
| 194 |
+
rhs = einsum(n, origins, "... n i j, ... n j -> ... n i")
|
| 195 |
+
rhs = reduce(rhs, "... n i -> ... i", "sum")
|
| 196 |
+
|
| 197 |
+
# Left-matrix-multiply both sides by the inverse of lhs to find p.
|
| 198 |
+
return torch.linalg.lstsq(lhs, rhs).solution
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def normalize(a: torch.Tensor) -> torch.Tensor: # "*#batch dim" -> "*#batch dim"
|
| 202 |
+
return a / a.norm(dim=-1, keepdim=True)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def generate_coordinate_frame(
|
| 206 |
+
y: torch.Tensor, # "*#batch 3"
|
| 207 |
+
z: torch.Tensor, # "*#batch 3"
|
| 208 |
+
) -> torch.Tensor: # "*batch 3 3"
|
| 209 |
+
"""Generate a coordinate frame given perpendicular, unit-length Y and Z vectors."""
|
| 210 |
+
y, z = torch.broadcast_tensors(y, z)
|
| 211 |
+
return torch.stack([y.cross(z, dim=-1), y, z], dim=-1)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def generate_rotation_coordinate_frame(
|
| 215 |
+
a: torch.Tensor, # "*#batch 3"
|
| 216 |
+
b: torch.Tensor, # "*#batch 3"
|
| 217 |
+
eps: float = 1e-4,
|
| 218 |
+
) -> torch.Tensor: # "*batch 3 3"
|
| 219 |
+
"""Generate a coordinate frame where the Y direction is normal to the plane defined
|
| 220 |
+
by unit vectors a and b. The other axes are arbitrary."""
|
| 221 |
+
device = a.device
|
| 222 |
+
|
| 223 |
+
# Replace every entry in b that's parallel to the corresponding entry in a with an
|
| 224 |
+
# arbitrary vector.
|
| 225 |
+
b = b.detach().clone()
|
| 226 |
+
parallel = (einsum(a, b, "... i, ... i -> ...").abs() - 1).abs() < eps
|
| 227 |
+
b[parallel] = torch.tensor([0, 0, 1], dtype=b.dtype, device=device)
|
| 228 |
+
parallel = (einsum(a, b, "... i, ... i -> ...").abs() - 1).abs() < eps
|
| 229 |
+
b[parallel] = torch.tensor([0, 1, 0], dtype=b.dtype, device=device)
|
| 230 |
+
|
| 231 |
+
# Generate the coordinate frame. The initial cross product defines the plane.
|
| 232 |
+
return generate_coordinate_frame(normalize(torch.linalg.cross(a, b)), a)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def matrix_to_euler(
|
| 236 |
+
rotations: torch.Tensor, # "*batch 3 3"
|
| 237 |
+
pattern: str,
|
| 238 |
+
) -> torch.Tensor: # "*batch 3"
|
| 239 |
+
*batch, _, _ = rotations.shape
|
| 240 |
+
rotations = rotations.reshape(-1, 3, 3)
|
| 241 |
+
angles_np = R.from_matrix(rotations.detach().cpu().numpy()).as_euler(pattern)
|
| 242 |
+
rotations = torch.tensor(angles_np, dtype=rotations.dtype, device=rotations.device)
|
| 243 |
+
return rotations.reshape(*batch, 3)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def euler_to_matrix(
|
| 247 |
+
rotations: torch.Tensor, # "*batch 3"
|
| 248 |
+
pattern: str,
|
| 249 |
+
) -> torch.Tensor: # "*batch 3 3"
|
| 250 |
+
*batch, _ = rotations.shape
|
| 251 |
+
rotations = rotations.reshape(-1, 3)
|
| 252 |
+
matrix_np = R.from_euler(pattern, rotations.detach().cpu().numpy()).as_matrix()
|
| 253 |
+
rotations = torch.tensor(matrix_np, dtype=rotations.dtype, device=rotations.device)
|
| 254 |
+
return rotations.reshape(*batch, 3, 3)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def extrinsics_to_pivot_parameters(
|
| 258 |
+
extrinsics: torch.Tensor, # "*#batch 4 4"
|
| 259 |
+
pivot_coordinate_frame: torch.Tensor, # "*#batch 3 3"
|
| 260 |
+
pivot_point: torch.Tensor, # "*#batch 3"
|
| 261 |
+
) -> torch.Tensor: # "*batch 5"
|
| 262 |
+
"""Convert the extrinsics to a representation with 5 degrees of freedom:
|
| 263 |
+
1. Distance from pivot point in the "X" (look cross pivot axis) direction.
|
| 264 |
+
2. Distance from pivot point in the "Y" (pivot axis) direction.
|
| 265 |
+
3. Distance from pivot point in the Z (look) direction
|
| 266 |
+
4. Angle in plane
|
| 267 |
+
5. Twist (rotation not in plane)
|
| 268 |
+
"""
|
| 269 |
+
|
| 270 |
+
# The pivot coordinate frame's Z axis is normal to the plane.
|
| 271 |
+
pivot_axis = pivot_coordinate_frame[..., :, 1]
|
| 272 |
+
|
| 273 |
+
# Compute the translation elements of the pivot parametrization.
|
| 274 |
+
translation_frame = generate_coordinate_frame(pivot_axis, extrinsics[..., :3, 2])
|
| 275 |
+
origin = extrinsics[..., :3, 3]
|
| 276 |
+
delta = pivot_point - origin
|
| 277 |
+
translation = einsum(translation_frame, delta, "... i j, ... i -> ... j")
|
| 278 |
+
|
| 279 |
+
# Add the rotation elements of the pivot parametrization.
|
| 280 |
+
inverted = pivot_coordinate_frame.inverse() @ extrinsics[..., :3, :3]
|
| 281 |
+
y, _, z = matrix_to_euler(inverted, "YXZ").unbind(dim=-1)
|
| 282 |
+
|
| 283 |
+
return torch.cat([translation, y[..., None], z[..., None]], dim=-1)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def pivot_parameters_to_extrinsics(
|
| 287 |
+
parameters: torch.Tensor, # "*#batch 5"
|
| 288 |
+
pivot_coordinate_frame: torch.Tensor, # "*#batch 3 3"
|
| 289 |
+
pivot_point: torch.Tensor, # "*#batch 3"
|
| 290 |
+
) -> torch.Tensor: # "*batch 4 4"
|
| 291 |
+
translation, y, z = parameters.split((3, 1, 1), dim=-1)
|
| 292 |
+
|
| 293 |
+
euler = torch.cat((y, torch.zeros_like(y), z), dim=-1)
|
| 294 |
+
rotation = pivot_coordinate_frame @ euler_to_matrix(euler, "YXZ")
|
| 295 |
+
|
| 296 |
+
# The pivot coordinate frame's Z axis is normal to the plane.
|
| 297 |
+
pivot_axis = pivot_coordinate_frame[..., :, 1]
|
| 298 |
+
|
| 299 |
+
translation_frame = generate_coordinate_frame(pivot_axis, rotation[..., :3, 2])
|
| 300 |
+
delta = einsum(translation_frame, translation, "... i j, ... j -> ... i")
|
| 301 |
+
origin = pivot_point - delta
|
| 302 |
+
|
| 303 |
+
*batch, _ = origin.shape
|
| 304 |
+
extrinsics = torch.eye(4, dtype=parameters.dtype, device=parameters.device)
|
| 305 |
+
extrinsics = extrinsics.broadcast_to((*batch, 4, 4)).clone()
|
| 306 |
+
extrinsics[..., 3, 3] = 1
|
| 307 |
+
extrinsics[..., :3, :3] = rotation
|
| 308 |
+
extrinsics[..., :3, 3] = origin
|
| 309 |
+
return extrinsics
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def interpolate_circular(
|
| 313 |
+
a: torch.Tensor, # "*#batch"
|
| 314 |
+
b: torch.Tensor, # "*#batch"
|
| 315 |
+
t: torch.Tensor, # "*#batch"
|
| 316 |
+
) -> torch.Tensor: # " *batch"
|
| 317 |
+
a, b, t = torch.broadcast_tensors(a, b, t)
|
| 318 |
+
|
| 319 |
+
tau = 2 * torch.pi
|
| 320 |
+
a = a % tau
|
| 321 |
+
b = b % tau
|
| 322 |
+
|
| 323 |
+
# Consider piecewise edge cases.
|
| 324 |
+
d = (b - a).abs()
|
| 325 |
+
a_left = a - tau
|
| 326 |
+
d_left = (b - a_left).abs()
|
| 327 |
+
a_right = a + tau
|
| 328 |
+
d_right = (b - a_right).abs()
|
| 329 |
+
use_d = (d < d_left) & (d < d_right)
|
| 330 |
+
use_d_left = (d_left < d_right) & (~use_d)
|
| 331 |
+
use_d_right = (~use_d) & (~use_d_left)
|
| 332 |
+
|
| 333 |
+
result = a + (b - a) * t
|
| 334 |
+
result[use_d_left] = (a_left + (b - a_left) * t)[use_d_left]
|
| 335 |
+
result[use_d_right] = (a_right + (b - a_right) * t)[use_d_right]
|
| 336 |
+
|
| 337 |
+
return result
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def interpolate_pivot_parameters(
|
| 341 |
+
initial: torch.Tensor, # "*#batch 5"
|
| 342 |
+
final: torch.Tensor, # "*#batch 5"
|
| 343 |
+
t: torch.Tensor, # " time_step"
|
| 344 |
+
) -> torch.Tensor: # "*batch time_step 5"
|
| 345 |
+
initial = rearrange(initial, "... d -> ... () d")
|
| 346 |
+
final = rearrange(final, "... d -> ... () d")
|
| 347 |
+
t = rearrange(t, "t -> t ()")
|
| 348 |
+
ti, ri = initial.split((3, 2), dim=-1)
|
| 349 |
+
tf, rf = final.split((3, 2), dim=-1)
|
| 350 |
+
|
| 351 |
+
t_lerp = ti + (tf - ti) * t
|
| 352 |
+
r_lerp = interpolate_circular(ri, rf, t)
|
| 353 |
+
|
| 354 |
+
return torch.cat((t_lerp, r_lerp), dim=-1)
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
@torch.no_grad()
|
| 358 |
+
def interpolate_extrinsics(
|
| 359 |
+
initial: torch.Tensor, # "*#batch 4 4"
|
| 360 |
+
final: torch.Tensor, # "*#batch 4 4"
|
| 361 |
+
t: torch.Tensor, # " time_step"
|
| 362 |
+
eps: float = 1e-4,
|
| 363 |
+
) -> torch.Tensor: # "*batch time_step 4 4"
|
| 364 |
+
"""Interpolate extrinsics by rotating around their "focus point," which is the
|
| 365 |
+
least-squares intersection between the look vectors of the initial and final
|
| 366 |
+
extrinsics.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
initial = initial.type(torch.float64)
|
| 370 |
+
final = final.type(torch.float64)
|
| 371 |
+
t = t.type(torch.float64)
|
| 372 |
+
|
| 373 |
+
# Based on the dot product between the look vectors, pick from one of two cases:
|
| 374 |
+
# 1. Look vectors are parallel: interpolate about their origins' midpoint.
|
| 375 |
+
# 3. Look vectors aren't parallel: interpolate about their focus point.
|
| 376 |
+
initial_look = initial[..., :3, 2]
|
| 377 |
+
final_look = final[..., :3, 2]
|
| 378 |
+
dot_products = einsum(initial_look, final_look, "... i, ... i -> ...")
|
| 379 |
+
parallel_mask = (dot_products.abs() - 1).abs() < eps
|
| 380 |
+
|
| 381 |
+
# Pick focus points.
|
| 382 |
+
initial_origin = initial[..., :3, 3]
|
| 383 |
+
final_origin = final[..., :3, 3]
|
| 384 |
+
pivot_point = 0.5 * (initial_origin + final_origin)
|
| 385 |
+
pivot_point[~parallel_mask] = intersect_rays(
|
| 386 |
+
initial_origin[~parallel_mask],
|
| 387 |
+
initial_look[~parallel_mask],
|
| 388 |
+
final_origin[~parallel_mask],
|
| 389 |
+
final_look[~parallel_mask],
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
# Convert to pivot parameters.
|
| 393 |
+
pivot_frame = generate_rotation_coordinate_frame(initial_look, final_look, eps=eps)
|
| 394 |
+
initial_params = extrinsics_to_pivot_parameters(initial, pivot_frame, pivot_point)
|
| 395 |
+
final_params = extrinsics_to_pivot_parameters(final, pivot_frame, pivot_point)
|
| 396 |
+
|
| 397 |
+
# Interpolate the pivot parameters.
|
| 398 |
+
interpolated_params = interpolate_pivot_parameters(initial_params, final_params, t)
|
| 399 |
+
|
| 400 |
+
# Convert back.
|
| 401 |
+
return pivot_parameters_to_extrinsics(
|
| 402 |
+
interpolated_params.type(torch.float32),
|
| 403 |
+
rearrange(pivot_frame, "... i j -> ... () i j").type(torch.float32),
|
| 404 |
+
rearrange(pivot_point, "... xyz -> ... () xyz").type(torch.float32),
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
@torch.no_grad()
|
| 409 |
+
def generate_wobble_transformation(
|
| 410 |
+
radius: torch.Tensor, # "*#batch"
|
| 411 |
+
t: torch.Tensor, # " time_step"
|
| 412 |
+
num_rotations: int = 1,
|
| 413 |
+
scale_radius_with_t: bool = True,
|
| 414 |
+
) -> torch.Tensor: # "*batch time_step 4 4"]:
|
| 415 |
+
# Generate a translation in the image plane.
|
| 416 |
+
tf = torch.eye(4, dtype=torch.float32, device=t.device)
|
| 417 |
+
tf = tf.broadcast_to((*radius.shape, t.shape[0], 4, 4)).clone()
|
| 418 |
+
radius = radius[..., None]
|
| 419 |
+
if scale_radius_with_t:
|
| 420 |
+
radius = radius * t
|
| 421 |
+
tf[..., 0, 3] = torch.sin(2 * torch.pi * num_rotations * t) * radius
|
| 422 |
+
tf[..., 1, 3] = -torch.cos(2 * torch.pi * num_rotations * t) * radius
|
| 423 |
+
return tf
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
@torch.no_grad()
|
| 427 |
+
def render_wobble_inter_path(
|
| 428 |
+
cam2world: torch.Tensor, intr_normed: torch.Tensor, inter_len: int, n_skip: int = 3
|
| 429 |
+
):
|
| 430 |
+
"""
|
| 431 |
+
cam2world: [batch, 4, 4],
|
| 432 |
+
intr_normed: [batch, 3, 3]
|
| 433 |
+
"""
|
| 434 |
+
frame_per_round = n_skip * inter_len
|
| 435 |
+
num_rotations = 1
|
| 436 |
+
|
| 437 |
+
t = torch.linspace(0, 1, frame_per_round, dtype=torch.float32, device=cam2world.device)
|
| 438 |
+
# t = (torch.cos(torch.pi * (t + 1)) + 1) / 2
|
| 439 |
+
tgt_c2w_b = []
|
| 440 |
+
tgt_intr_b = []
|
| 441 |
+
for b_idx in range(cam2world.shape[0]):
|
| 442 |
+
tgt_c2w = []
|
| 443 |
+
tgt_intr = []
|
| 444 |
+
for cur_idx in range(0, cam2world.shape[1] - n_skip, n_skip):
|
| 445 |
+
origin_a = cam2world[b_idx, cur_idx, :3, 3]
|
| 446 |
+
origin_b = cam2world[b_idx, cur_idx + n_skip, :3, 3]
|
| 447 |
+
delta = (origin_a - origin_b).norm(dim=-1)
|
| 448 |
+
if cur_idx == 0:
|
| 449 |
+
delta_prev = delta
|
| 450 |
+
else:
|
| 451 |
+
delta = (delta_prev + delta) / 2
|
| 452 |
+
delta_prev = delta
|
| 453 |
+
tf = generate_wobble_transformation(
|
| 454 |
+
radius=delta * 0.5,
|
| 455 |
+
t=t,
|
| 456 |
+
num_rotations=num_rotations,
|
| 457 |
+
scale_radius_with_t=False,
|
| 458 |
+
)
|
| 459 |
+
cur_extrs = (
|
| 460 |
+
interpolate_extrinsics(
|
| 461 |
+
cam2world[b_idx, cur_idx],
|
| 462 |
+
cam2world[b_idx, cur_idx + n_skip],
|
| 463 |
+
t,
|
| 464 |
+
)
|
| 465 |
+
@ tf
|
| 466 |
+
)
|
| 467 |
+
tgt_c2w.append(cur_extrs[(0 if cur_idx == 0 else 1) :])
|
| 468 |
+
tgt_intr.append(
|
| 469 |
+
interpolate_intrinsics(
|
| 470 |
+
intr_normed[b_idx, cur_idx],
|
| 471 |
+
intr_normed[b_idx, cur_idx + n_skip],
|
| 472 |
+
t,
|
| 473 |
+
)[(0 if cur_idx == 0 else 1) :]
|
| 474 |
+
)
|
| 475 |
+
tgt_c2w_b.append(torch.cat(tgt_c2w))
|
| 476 |
+
tgt_intr_b.append(torch.cat(tgt_intr))
|
| 477 |
+
tgt_c2w = torch.stack(tgt_c2w_b) # b v 4 4
|
| 478 |
+
tgt_intr = torch.stack(tgt_intr_b) # b v 3 3
|
| 479 |
+
return tgt_c2w, tgt_intr
|
multivue/cameras/source/src/depth_anything_3/utils/constants.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
DEFAULT_MODEL = "depth-anything/DA3NESTED-GIANT-LARGE-1.1"
|
| 16 |
+
DEFAULT_EXPORT_DIR = "workspace/gallery/scene"
|
| 17 |
+
DEFAULT_GALLERY_DIR = "workspace/gallery"
|
| 18 |
+
DEFAULT_GRADIO_DIR = "workspace/gradio"
|
| 19 |
+
THRESH_FOR_REF_SELECTION = 3
|
| 20 |
+
|
| 21 |
+
# =============================================================================
|
| 22 |
+
# Benchmark Evaluation Constants
|
| 23 |
+
# =============================================================================
|
| 24 |
+
|
| 25 |
+
# Default evaluation workspace directory
|
| 26 |
+
DEFAULT_EVAL_WORKSPACE = "workspace/evaluation"
|
| 27 |
+
|
| 28 |
+
# Default reference view selection strategy for evaluation
|
| 29 |
+
# Use "first" for consistent and reproducible evaluation results
|
| 30 |
+
# Other options: "saddle_balanced", "auto", "mid"
|
| 31 |
+
EVAL_REF_VIEW_STRATEGY = "first"
|
| 32 |
+
|
| 33 |
+
# -----------------------------------------------------------------------------
|
| 34 |
+
# DTU Dataset Configuration
|
| 35 |
+
# Reference: https://roboimagedata.compute.dtu.dk/
|
| 36 |
+
# Note: DepthAnything3 was never trained on any images from DTU.
|
| 37 |
+
# -----------------------------------------------------------------------------
|
| 38 |
+
|
| 39 |
+
# Root directory for DTU evaluation data (MVSNet format)
|
| 40 |
+
# Download from: https://drive.google.com/file/d/1rX0EXlUL4prRxrRu2DgLJv2j7-tpUD4D/view
|
| 41 |
+
DTU_EVAL_DATA_ROOT = "workspace/benchmark_dataset/dtu"
|
| 42 |
+
|
| 43 |
+
# List of DTU evaluation scenes
|
| 44 |
+
DTU_SCENES = [
|
| 45 |
+
"scan1",
|
| 46 |
+
"scan4",
|
| 47 |
+
"scan9",
|
| 48 |
+
"scan10",
|
| 49 |
+
"scan11",
|
| 50 |
+
"scan12",
|
| 51 |
+
"scan13",
|
| 52 |
+
"scan15",
|
| 53 |
+
"scan23",
|
| 54 |
+
"scan24",
|
| 55 |
+
"scan29",
|
| 56 |
+
"scan32",
|
| 57 |
+
"scan33",
|
| 58 |
+
"scan34",
|
| 59 |
+
"scan48",
|
| 60 |
+
"scan49",
|
| 61 |
+
"scan62",
|
| 62 |
+
"scan75",
|
| 63 |
+
"scan77",
|
| 64 |
+
"scan110",
|
| 65 |
+
"scan114",
|
| 66 |
+
"scan118",
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
# Point cloud fusion hyperparameters
|
| 70 |
+
DTU_DIST_THRESH = 0.2 # Distance threshold for geometric consistency (mm)
|
| 71 |
+
DTU_NUM_CONSIST = 4 # Minimum number of consistent views for a point
|
| 72 |
+
DTU_MAX_POINTS = 4_000_000 # Maximum points in fused point cloud
|
| 73 |
+
|
| 74 |
+
# 3D reconstruction evaluation hyperparameters
|
| 75 |
+
DTU_DOWN_DENSE = 0.2 # Downsample density for evaluation (mm)
|
| 76 |
+
DTU_PATCH_SIZE = 60 # Patch size for boundary handling
|
| 77 |
+
DTU_MAX_DIST = 20 # Outlier threshold for accuracy/completeness (mm)
|
| 78 |
+
|
| 79 |
+
# -----------------------------------------------------------------------------
|
| 80 |
+
# DTU-64 Dataset Configuration (Pose Evaluation Only)
|
| 81 |
+
# This is a subset of DTU with 64 images per scene for pose evaluation.
|
| 82 |
+
# Note: This dataset is ONLY for pose evaluation, not 3D reconstruction.
|
| 83 |
+
# -----------------------------------------------------------------------------
|
| 84 |
+
|
| 85 |
+
# Root directory for DTU-64 evaluation data
|
| 86 |
+
DTU64_EVAL_DATA_ROOT = "workspace/benchmark_dataset/dtu64"
|
| 87 |
+
DTU64_CAMERA_ROOT = "workspace/benchmark_dataset/dtu64/Cameras"
|
| 88 |
+
|
| 89 |
+
# List of DTU-64 evaluation scenes (13 scenes)
|
| 90 |
+
DTU64_SCENES = [
|
| 91 |
+
"scan105",
|
| 92 |
+
"scan114",
|
| 93 |
+
"scan118",
|
| 94 |
+
"scan122",
|
| 95 |
+
"scan24",
|
| 96 |
+
"scan37",
|
| 97 |
+
"scan40",
|
| 98 |
+
"scan55",
|
| 99 |
+
"scan63",
|
| 100 |
+
"scan65",
|
| 101 |
+
"scan69",
|
| 102 |
+
"scan83",
|
| 103 |
+
"scan97",
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
# -----------------------------------------------------------------------------
|
| 107 |
+
# ETH3D Dataset Configuration
|
| 108 |
+
# Reference: https://www.eth3d.net/
|
| 109 |
+
# High-resolution multi-view stereo benchmark with laser-scanned ground truth.
|
| 110 |
+
# Note: DepthAnything3 was never trained on any images from ETH3D.
|
| 111 |
+
# -----------------------------------------------------------------------------
|
| 112 |
+
|
| 113 |
+
# Root directory for ETH3D evaluation data
|
| 114 |
+
ETH3D_EVAL_DATA_ROOT = "workspace/benchmark_dataset/eth3d"
|
| 115 |
+
|
| 116 |
+
# List of ETH3D evaluation scenes (indoor and outdoor)
|
| 117 |
+
ETH3D_SCENES = [
|
| 118 |
+
"courtyard",
|
| 119 |
+
"electro",
|
| 120 |
+
"kicker",
|
| 121 |
+
"pipes",
|
| 122 |
+
"relief",
|
| 123 |
+
# "terrace", # Excluded: known issues
|
| 124 |
+
"delivery_area",
|
| 125 |
+
"facade",
|
| 126 |
+
# "meadow", # Excluded: known issues
|
| 127 |
+
"office",
|
| 128 |
+
"playground",
|
| 129 |
+
"relief_2",
|
| 130 |
+
"terrains",
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
# Images to filter out (known problematic views per scene)
|
| 134 |
+
ETH3D_FILTER_KEYS = {
|
| 135 |
+
"delivery_area": ["711.JPG", "712.JPG", "713.JPG", "714.JPG"],
|
| 136 |
+
"electro": ["9289.JPG", "9290.JPG", "9291.JPG", "9292.JPG", "9293.JPG", "9298.JPG"],
|
| 137 |
+
"playground": ["587.JPG", "588.JPG", "589.JPG", "590.JPG", "591.JPG", "592.JPG"],
|
| 138 |
+
"relief": [
|
| 139 |
+
"427.JPG", "428.JPG", "429.JPG", "430.JPG", "431.JPG", "432.JPG",
|
| 140 |
+
"433.JPG", "434.JPG", "435.JPG", "436.JPG", "437.JPG", "438.JPG",
|
| 141 |
+
],
|
| 142 |
+
"relief_2": [
|
| 143 |
+
"458.JPG", "459.JPG", "460.JPG", "461.JPG", "462.JPG", "463.JPG",
|
| 144 |
+
"464.JPG", "465.JPG", "466.JPG", "467.JPG", "468.JPG",
|
| 145 |
+
],
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
# TSDF fusion hyperparameters (scaled for outdoor scenes)
|
| 149 |
+
ETH3D_VOXEL_LENGTH = 4.0 / 512.0 * 5 # Voxel size for TSDF (meters)
|
| 150 |
+
ETH3D_SDF_TRUNC = 0.04 * 5 # SDF truncation distance (meters)
|
| 151 |
+
ETH3D_MAX_DEPTH = 100000.0 # Maximum depth for integration (effectively no truncation)
|
| 152 |
+
|
| 153 |
+
# Point cloud sampling
|
| 154 |
+
ETH3D_SAMPLING_NUMBER = 1_000_000 # Number of points to sample from mesh
|
| 155 |
+
|
| 156 |
+
# 3D reconstruction evaluation hyperparameters
|
| 157 |
+
ETH3D_EVAL_THRESHOLD = 0.05 * 5 # Distance threshold for precision/recall (meters)
|
| 158 |
+
ETH3D_DOWN_SAMPLE = 4.0 / 512.0 * 5 # Voxel size for evaluation downsampling (meters)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# ==============================================================================
|
| 162 |
+
# 7Scenes Dataset Configuration
|
| 163 |
+
# ==============================================================================
|
| 164 |
+
# Reference: https://www.microsoft.com/en-us/research/project/rgb-d-dataset-7-scenes/
|
| 165 |
+
# Note: Indoor RGB-D dataset with ground truth poses and meshes.
|
| 166 |
+
|
| 167 |
+
# Root directory for 7Scenes evaluation data
|
| 168 |
+
SEVENSCENES_EVAL_DATA_ROOT = "workspace/benchmark_dataset/7scenes"
|
| 169 |
+
|
| 170 |
+
# List of 7Scenes evaluation scenes
|
| 171 |
+
SEVENSCENES_SCENES = [
|
| 172 |
+
"chess",
|
| 173 |
+
"fire",
|
| 174 |
+
"heads",
|
| 175 |
+
"office",
|
| 176 |
+
"pumpkin",
|
| 177 |
+
"redkitchen",
|
| 178 |
+
"stairs",
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
# Fixed camera intrinsics for 7Scenes (all images share same intrinsics)
|
| 182 |
+
SEVENSCENES_FX = 585.0
|
| 183 |
+
SEVENSCENES_FY = 585.0
|
| 184 |
+
SEVENSCENES_CX = 320.0
|
| 185 |
+
SEVENSCENES_CY = 240.0
|
| 186 |
+
|
| 187 |
+
# TSDF fusion hyperparameters (indoor scenes, smaller voxels)
|
| 188 |
+
SEVENSCENES_VOXEL_LENGTH = 4.0 / 512.0 # Voxel size for TSDF (meters)
|
| 189 |
+
SEVENSCENES_SDF_TRUNC = 0.04 # SDF truncation distance (meters)
|
| 190 |
+
SEVENSCENES_MAX_DEPTH = 1000000.0 # Maximum depth for integration (no truncation)
|
| 191 |
+
|
| 192 |
+
# Point cloud sampling
|
| 193 |
+
SEVENSCENES_SAMPLING_NUMBER = 1_000_000 # Number of points to sample from mesh
|
| 194 |
+
|
| 195 |
+
# 3D reconstruction evaluation hyperparameters
|
| 196 |
+
SEVENSCENES_EVAL_THRESHOLD = 0.05 # Distance threshold for precision/recall (meters)
|
| 197 |
+
SEVENSCENES_DOWN_SAMPLE = 4.0 / 512.0 # Voxel size for evaluation downsampling (meters)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ==============================================================================
|
| 201 |
+
# ScanNet++ Dataset Configuration
|
| 202 |
+
# ==============================================================================
|
| 203 |
+
# Reference: https://kaldir.vc.in.tum.de/scannetpp/
|
| 204 |
+
# Note: High-quality indoor RGB-D dataset with iPhone and DSLR images.
|
| 205 |
+
|
| 206 |
+
# Root directory for ScanNet++ evaluation data
|
| 207 |
+
SCANNETPP_EVAL_DATA_ROOT = "workspace/benchmark_dataset/scannetpp"
|
| 208 |
+
|
| 209 |
+
# List of ScanNet++ evaluation scenes
|
| 210 |
+
SCANNETPP_SCENES = [
|
| 211 |
+
"09c1414f1b",
|
| 212 |
+
"1ada7a0617",
|
| 213 |
+
"40aec5fffa",
|
| 214 |
+
"3e8bba0176",
|
| 215 |
+
"acd95847c5",
|
| 216 |
+
"578511c8a9",
|
| 217 |
+
"5f99900f09",
|
| 218 |
+
"c4c04e6d6c",
|
| 219 |
+
"f3d64c30f8",
|
| 220 |
+
"7bc286c1b6",
|
| 221 |
+
"c5439f4607",
|
| 222 |
+
"286b55a2bf",
|
| 223 |
+
"fb5a96b1a2",
|
| 224 |
+
"7831862f02",
|
| 225 |
+
"38d58a7a31",
|
| 226 |
+
"bde1e479ad",
|
| 227 |
+
"9071e139d9",
|
| 228 |
+
"21d970d8de",
|
| 229 |
+
"bcd2436daf",
|
| 230 |
+
"cc5237fd77",
|
| 231 |
+
]
|
| 232 |
+
|
| 233 |
+
# Input resolution for ScanNet++ (after undistortion and resize)
|
| 234 |
+
SCANNETPP_INPUT_H = 768
|
| 235 |
+
SCANNETPP_INPUT_W = 1024
|
| 236 |
+
|
| 237 |
+
# TSDF fusion hyperparameters (indoor scenes)
|
| 238 |
+
SCANNETPP_VOXEL_LENGTH = 0.02 # Voxel size for TSDF (meters)
|
| 239 |
+
SCANNETPP_SDF_TRUNC = 0.15 # SDF truncation distance (meters)
|
| 240 |
+
SCANNETPP_MAX_DEPTH = 5.0 # Maximum depth for integration (meters)
|
| 241 |
+
|
| 242 |
+
# Point cloud sampling
|
| 243 |
+
SCANNETPP_SAMPLING_NUMBER = 1_000_000 # Number of points to sample from mesh
|
| 244 |
+
|
| 245 |
+
# 3D reconstruction evaluation hyperparameters
|
| 246 |
+
SCANNETPP_EVAL_THRESHOLD = 0.05 # Distance threshold for precision/recall (meters)
|
| 247 |
+
SCANNETPP_DOWN_SAMPLE = 0.02 # Voxel size for evaluation downsampling (meters)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# ==============================================================================
|
| 251 |
+
# HiRoom Dataset Configuration
|
| 252 |
+
# ==============================================================================
|
| 253 |
+
# Note: Indoor RGB-D dataset.
|
| 254 |
+
|
| 255 |
+
# Root directory for HiRoom evaluation data
|
| 256 |
+
HIROOM_EVAL_DATA_ROOT = "workspace/benchmark_dataset/hiroom/data"
|
| 257 |
+
HIROOM_GT_ROOT_PATH = "workspace/benchmark_dataset/hiroom/fused_pcd"
|
| 258 |
+
HIROOM_SCENE_LIST_PATH = "workspace/benchmark_dataset/hiroom/selected_scene_list_val.txt"
|
| 259 |
+
|
| 260 |
+
# TSDF fusion hyperparameters (indoor scenes)
|
| 261 |
+
HIROOM_VOXEL_LENGTH = 4.0 / 512.0 # Voxel size for TSDF (meters)
|
| 262 |
+
HIROOM_SDF_TRUNC = 0.04 # SDF truncation distance (meters)
|
| 263 |
+
HIROOM_MAX_DEPTH = 10000.0 # Maximum depth for integration (no truncation)
|
| 264 |
+
|
| 265 |
+
# Point cloud sampling
|
| 266 |
+
HIROOM_SAMPLING_NUMBER = 1_000_000 # Number of points to sample from mesh
|
| 267 |
+
|
| 268 |
+
# 3D reconstruction evaluation hyperparameters
|
| 269 |
+
HIROOM_EVAL_THRESHOLD = 0.05 # Distance threshold for precision/recall (meters)
|
| 270 |
+
HIROOM_DOWN_SAMPLE = 4.0 / 512.0 # Voxel size for evaluation downsampling (meters)
|
multivue/cameras/source/src/depth_anything_3/utils/export/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from depth_anything_3.specs import Prediction
|
| 16 |
+
from depth_anything_3.utils.export.gs import export_to_gs_ply, export_to_gs_video
|
| 17 |
+
|
| 18 |
+
from .colmap import export_to_colmap
|
| 19 |
+
from .depth_vis import export_to_depth_vis
|
| 20 |
+
from .feat_vis import export_to_feat_vis
|
| 21 |
+
from .glb import export_to_glb
|
| 22 |
+
from .npz import export_to_mini_npz, export_to_npz
|
| 23 |
+
|
| 24 |
+
# Canonical set of export formats this dispatcher understands. Treat this as the
|
| 25 |
+
# server-trusted enum for any request-facing validation (e.g. the HTTP backend) instead
|
| 26 |
+
# of letting a client-supplied export_format string reach the dispatcher unchecked.
|
| 27 |
+
SUPPORTED_EXPORT_FORMATS = frozenset(
|
| 28 |
+
{
|
| 29 |
+
"glb",
|
| 30 |
+
"mini_npz",
|
| 31 |
+
"npz",
|
| 32 |
+
"feat_vis",
|
| 33 |
+
"depth_vis",
|
| 34 |
+
"gs_ply",
|
| 35 |
+
"gs_video",
|
| 36 |
+
"colmap",
|
| 37 |
+
}
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def export(
|
| 42 |
+
prediction: Prediction,
|
| 43 |
+
export_format: str,
|
| 44 |
+
export_dir: str,
|
| 45 |
+
**kwargs,
|
| 46 |
+
):
|
| 47 |
+
if "-" in export_format:
|
| 48 |
+
export_formats = export_format.split("-")
|
| 49 |
+
for export_format in export_formats:
|
| 50 |
+
export(prediction, export_format, export_dir, **kwargs)
|
| 51 |
+
return # Prevent falling through to single-format handling
|
| 52 |
+
|
| 53 |
+
if export_format == "glb":
|
| 54 |
+
export_to_glb(prediction, export_dir, **kwargs.get(export_format, {}))
|
| 55 |
+
elif export_format == "mini_npz":
|
| 56 |
+
export_to_mini_npz(prediction, export_dir)
|
| 57 |
+
elif export_format == "npz":
|
| 58 |
+
export_to_npz(prediction, export_dir)
|
| 59 |
+
elif export_format == "feat_vis":
|
| 60 |
+
export_to_feat_vis(prediction, export_dir, **kwargs.get(export_format, {}))
|
| 61 |
+
elif export_format == "depth_vis":
|
| 62 |
+
export_to_depth_vis(prediction, export_dir)
|
| 63 |
+
elif export_format == "gs_ply":
|
| 64 |
+
export_to_gs_ply(prediction, export_dir, **kwargs.get(export_format, {}))
|
| 65 |
+
elif export_format == "gs_video":
|
| 66 |
+
export_to_gs_video(prediction, export_dir, **kwargs.get(export_format, {}))
|
| 67 |
+
elif export_format == "colmap":
|
| 68 |
+
export_to_colmap(prediction, export_dir, **kwargs.get(export_format, {}))
|
| 69 |
+
else:
|
| 70 |
+
raise ValueError(f"Unsupported export format: {export_format}")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
__all__ = [
|
| 74 |
+
export,
|
| 75 |
+
]
|
multivue/cameras/source/src/depth_anything_3/utils/export/colmap.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import pycolmap
|
| 17 |
+
import cv2 as cv
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
from PIL import Image
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.specs import Prediction
|
| 23 |
+
from depth_anything_3.utils.logger import logger
|
| 24 |
+
|
| 25 |
+
from .glb import _depths_to_world_points_with_colors
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def export_to_colmap(
|
| 29 |
+
prediction: Prediction,
|
| 30 |
+
export_dir: str,
|
| 31 |
+
image_paths: list[str],
|
| 32 |
+
conf_thresh_percentile: float = 40.0,
|
| 33 |
+
process_res_method: str = "upper_bound_resize",
|
| 34 |
+
) -> None:
|
| 35 |
+
# 1. Data preparation
|
| 36 |
+
conf_thresh = np.percentile(prediction.conf, conf_thresh_percentile)
|
| 37 |
+
points, colors = _depths_to_world_points_with_colors(
|
| 38 |
+
prediction.depth,
|
| 39 |
+
prediction.intrinsics,
|
| 40 |
+
prediction.extrinsics, # w2c
|
| 41 |
+
prediction.processed_images,
|
| 42 |
+
prediction.conf,
|
| 43 |
+
conf_thresh,
|
| 44 |
+
)
|
| 45 |
+
num_points = len(points)
|
| 46 |
+
logger.info(f"Exporting to COLMAP with {num_points} points")
|
| 47 |
+
num_frames = len(prediction.processed_images)
|
| 48 |
+
h, w = prediction.processed_images.shape[1:3]
|
| 49 |
+
points_xyf = _create_xyf(num_frames, h, w)
|
| 50 |
+
points_xyf = points_xyf[prediction.conf >= conf_thresh]
|
| 51 |
+
|
| 52 |
+
# 2. Set Reconstruction
|
| 53 |
+
reconstruction = pycolmap.Reconstruction()
|
| 54 |
+
|
| 55 |
+
point3d_ids = []
|
| 56 |
+
for vidx in range(num_points):
|
| 57 |
+
point3d_id = reconstruction.add_point3D(points[vidx], pycolmap.Track(), colors[vidx])
|
| 58 |
+
point3d_ids.append(point3d_id)
|
| 59 |
+
|
| 60 |
+
for fidx in range(num_frames):
|
| 61 |
+
orig_w, orig_h = Image.open(image_paths[fidx]).size
|
| 62 |
+
|
| 63 |
+
intrinsic = prediction.intrinsics[fidx]
|
| 64 |
+
if process_res_method.endswith("resize"):
|
| 65 |
+
intrinsic[:1] *= orig_w / w
|
| 66 |
+
intrinsic[1:2] *= orig_h / h
|
| 67 |
+
elif process_res_method == "crop":
|
| 68 |
+
raise NotImplementedError("COLMAP export for crop method is not implemented")
|
| 69 |
+
else:
|
| 70 |
+
raise ValueError(f"Unknown process_res_method: {process_res_method}")
|
| 71 |
+
|
| 72 |
+
pycolmap_intri = np.array(
|
| 73 |
+
[intrinsic[0, 0], intrinsic[1, 1], intrinsic[0, 2], intrinsic[1, 2]]
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
extrinsic = prediction.extrinsics[fidx]
|
| 77 |
+
cam_from_world = pycolmap.Rigid3d(pycolmap.Rotation3d(extrinsic[:3, :3]), extrinsic[:3, 3])
|
| 78 |
+
|
| 79 |
+
# set and add camera
|
| 80 |
+
camera = pycolmap.Camera()
|
| 81 |
+
camera.camera_id = fidx + 1
|
| 82 |
+
camera.model = pycolmap.CameraModelId.PINHOLE
|
| 83 |
+
camera.width = orig_w
|
| 84 |
+
camera.height = orig_h
|
| 85 |
+
camera.params = pycolmap_intri
|
| 86 |
+
reconstruction.add_camera(camera)
|
| 87 |
+
|
| 88 |
+
# set and add rig (from camera)
|
| 89 |
+
rig = pycolmap.Rig()
|
| 90 |
+
rig.rig_id = camera.camera_id
|
| 91 |
+
rig.add_ref_sensor(camera.sensor_id)
|
| 92 |
+
reconstruction.add_rig(rig)
|
| 93 |
+
|
| 94 |
+
# set image
|
| 95 |
+
image = pycolmap.Image()
|
| 96 |
+
image.image_id = fidx + 1
|
| 97 |
+
image.camera_id = camera.camera_id
|
| 98 |
+
|
| 99 |
+
# set and add frame (from image)
|
| 100 |
+
frame = pycolmap.Frame()
|
| 101 |
+
frame.frame_id = image.image_id
|
| 102 |
+
frame.rig_id = camera.camera_id
|
| 103 |
+
frame.add_data_id(image.data_id)
|
| 104 |
+
frame.rig_from_world = cam_from_world
|
| 105 |
+
reconstruction.add_frame(frame)
|
| 106 |
+
|
| 107 |
+
# set point2d and update track
|
| 108 |
+
point2d_list = []
|
| 109 |
+
points_in_frame = points_xyf[:, 2].astype(np.int32) == fidx
|
| 110 |
+
for vidx in np.where(points_in_frame)[0]:
|
| 111 |
+
point2d = points_xyf[vidx][:2]
|
| 112 |
+
point2d[0] *= orig_w / w
|
| 113 |
+
point2d[1] *= orig_h / h
|
| 114 |
+
point3d_id = point3d_ids[vidx]
|
| 115 |
+
point2d_list.append(pycolmap.Point2D(point2d, point3d_id))
|
| 116 |
+
reconstruction.point3D(point3d_id).track.add_element(
|
| 117 |
+
image.image_id, len(point2d_list) - 1
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# set and add image
|
| 121 |
+
image.frame_id = image.image_id
|
| 122 |
+
image.name = os.path.basename(image_paths[fidx])
|
| 123 |
+
image.points2D = pycolmap.Point2DList(point2d_list)
|
| 124 |
+
reconstruction.add_image(image)
|
| 125 |
+
|
| 126 |
+
# 3. Export
|
| 127 |
+
reconstruction.write(export_dir)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _create_xyf(num_frames, height, width):
|
| 131 |
+
"""
|
| 132 |
+
Creates a grid of pixel coordinates and frame indices (fidx) for all frames.
|
| 133 |
+
"""
|
| 134 |
+
# Create coordinate grids for a single frame
|
| 135 |
+
y_grid, x_grid = np.indices((height, width), dtype=np.int32)
|
| 136 |
+
x_grid = x_grid[np.newaxis, :, :]
|
| 137 |
+
y_grid = y_grid[np.newaxis, :, :]
|
| 138 |
+
|
| 139 |
+
# Broadcast to all frames
|
| 140 |
+
x_coords = np.broadcast_to(x_grid, (num_frames, height, width))
|
| 141 |
+
y_coords = np.broadcast_to(y_grid, (num_frames, height, width))
|
| 142 |
+
|
| 143 |
+
# Create frame indices and broadcast
|
| 144 |
+
f_idx = np.arange(num_frames, dtype=np.int32)[:, np.newaxis, np.newaxis]
|
| 145 |
+
f_coords = np.broadcast_to(f_idx, (num_frames, height, width))
|
| 146 |
+
|
| 147 |
+
# Stack coordinates and frame indices
|
| 148 |
+
points_xyf = np.stack((x_coords, y_coords, f_coords), axis=-1)
|
| 149 |
+
|
| 150 |
+
return points_xyf
|
multivue/cameras/source/src/depth_anything_3/utils/export/depth_vis.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import imageio
|
| 17 |
+
import numpy as np
|
| 18 |
+
|
| 19 |
+
from depth_anything_3.specs import Prediction
|
| 20 |
+
from depth_anything_3.utils.visualize import visualize_depth
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def export_to_depth_vis(
|
| 24 |
+
prediction: Prediction,
|
| 25 |
+
export_dir: str,
|
| 26 |
+
):
|
| 27 |
+
# Use prediction.processed_images, which is already processed image data
|
| 28 |
+
if prediction.processed_images is None:
|
| 29 |
+
raise ValueError("prediction.processed_images is required but not available")
|
| 30 |
+
|
| 31 |
+
images_u8 = prediction.processed_images # (N,H,W,3) uint8
|
| 32 |
+
|
| 33 |
+
os.makedirs(os.path.join(export_dir, "depth_vis"), exist_ok=True)
|
| 34 |
+
for idx in range(prediction.depth.shape[0]):
|
| 35 |
+
depth_vis = visualize_depth(prediction.depth[idx])
|
| 36 |
+
image_vis = images_u8[idx]
|
| 37 |
+
depth_vis = depth_vis.astype(np.uint8)
|
| 38 |
+
image_vis = image_vis.astype(np.uint8)
|
| 39 |
+
vis_image = np.concatenate([image_vis, depth_vis], axis=1)
|
| 40 |
+
save_path = os.path.join(export_dir, f"depth_vis/{idx:04d}.jpg")
|
| 41 |
+
imageio.imwrite(save_path, vis_image, quality=95)
|
multivue/cameras/source/src/depth_anything_3/utils/export/feat_vis.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import subprocess
|
| 17 |
+
import cv2
|
| 18 |
+
import imageio
|
| 19 |
+
import numpy as np
|
| 20 |
+
from tqdm.auto import tqdm
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.utils.logger import logger
|
| 23 |
+
from depth_anything_3.utils.parallel_utils import async_call
|
| 24 |
+
from depth_anything_3.utils.pca_utils import PCARGBVisualizer
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@async_call
|
| 28 |
+
def export_to_feat_vis(
|
| 29 |
+
prediction,
|
| 30 |
+
export_dir,
|
| 31 |
+
fps=15,
|
| 32 |
+
):
|
| 33 |
+
"""Export feature visualization with PCA.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
prediction: Model prediction containing feature maps
|
| 37 |
+
export_dir: Directory to export results
|
| 38 |
+
fps: Frame rate for output video (default: 15)
|
| 39 |
+
"""
|
| 40 |
+
out_dir = os.path.join(export_dir, "feat_vis")
|
| 41 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 42 |
+
|
| 43 |
+
images = prediction.processed_images
|
| 44 |
+
for k, v in prediction.aux.items():
|
| 45 |
+
if not k.startswith("feat_layer_"):
|
| 46 |
+
continue
|
| 47 |
+
os.makedirs(os.path.join(out_dir, k), exist_ok=True)
|
| 48 |
+
viz = PCARGBVisualizer(basis_mode="fixed", percentile_mode="global", clip_percent=10.0)
|
| 49 |
+
viz.fit_reference(v)
|
| 50 |
+
feats_vis = viz.transform_video(v)
|
| 51 |
+
for idx in tqdm(range(len(feats_vis))):
|
| 52 |
+
img = images[idx]
|
| 53 |
+
feat_vis = (feats_vis[idx] * 255).astype(np.uint8)
|
| 54 |
+
feat_vis = cv2.resize(
|
| 55 |
+
feat_vis, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_NEAREST
|
| 56 |
+
)
|
| 57 |
+
save_path = os.path.join(out_dir, f"{k}/{idx:06d}.jpg")
|
| 58 |
+
save = np.concatenate([img, feat_vis], axis=1)
|
| 59 |
+
imageio.imwrite(save_path, save, quality=95)
|
| 60 |
+
cmd = [
|
| 61 |
+
"ffmpeg",
|
| 62 |
+
"-loglevel", "error",
|
| 63 |
+
"-hide_banner",
|
| 64 |
+
"-y",
|
| 65 |
+
"-framerate", str(fps),
|
| 66 |
+
"-start_number", "0",
|
| 67 |
+
"-i", os.path.join(out_dir, k, "%06d.jpg"),
|
| 68 |
+
"-c:v", "libx264",
|
| 69 |
+
"-pix_fmt", "yuv420p",
|
| 70 |
+
os.path.join(out_dir, f"{k}.mp4"),
|
| 71 |
+
]
|
| 72 |
+
try:
|
| 73 |
+
subprocess.run(cmd, shell=False, check=False)
|
| 74 |
+
except OSError as e:
|
| 75 |
+
logger.warn(f"Failed to run ffmpeg for feature visualization export: {e}")
|
multivue/cameras/source/src/depth_anything_3/utils/export/glb.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import numpy as np
|
| 20 |
+
import trimesh
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.specs import Prediction
|
| 23 |
+
from depth_anything_3.utils.logger import logger
|
| 24 |
+
|
| 25 |
+
from .depth_vis import export_to_depth_vis
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def set_sky_depth(prediction: Prediction, sky_mask: np.ndarray, sky_depth_def: float = 98.0):
|
| 29 |
+
non_sky_mask = ~sky_mask
|
| 30 |
+
valid_depth = prediction.depth[non_sky_mask]
|
| 31 |
+
if valid_depth.size > 0:
|
| 32 |
+
max_depth = np.percentile(valid_depth, sky_depth_def)
|
| 33 |
+
prediction.depth[sky_mask] = max_depth
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def get_conf_thresh(
|
| 37 |
+
prediction: Prediction,
|
| 38 |
+
sky_mask: np.ndarray,
|
| 39 |
+
conf_thresh: float,
|
| 40 |
+
conf_thresh_percentile: float = 10.0,
|
| 41 |
+
ensure_thresh_percentile: float = 90.0,
|
| 42 |
+
):
|
| 43 |
+
if sky_mask is not None and (~sky_mask).sum() > 10:
|
| 44 |
+
conf_pixels = prediction.conf[~sky_mask]
|
| 45 |
+
else:
|
| 46 |
+
conf_pixels = prediction.conf
|
| 47 |
+
lower = np.percentile(conf_pixels, conf_thresh_percentile)
|
| 48 |
+
upper = np.percentile(conf_pixels, ensure_thresh_percentile)
|
| 49 |
+
conf_thresh = min(max(conf_thresh, lower), upper)
|
| 50 |
+
return conf_thresh
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def export_to_glb(
|
| 54 |
+
prediction: Prediction,
|
| 55 |
+
export_dir: str,
|
| 56 |
+
num_max_points: int = 1_000_000,
|
| 57 |
+
conf_thresh: float = 1.05,
|
| 58 |
+
filter_black_bg: bool = False,
|
| 59 |
+
filter_white_bg: bool = False,
|
| 60 |
+
conf_thresh_percentile: float = 40.0,
|
| 61 |
+
ensure_thresh_percentile: float = 90.0,
|
| 62 |
+
sky_depth_def: float = 98.0,
|
| 63 |
+
show_cameras: bool = True,
|
| 64 |
+
camera_size: float = 0.03,
|
| 65 |
+
export_depth_vis: bool = True,
|
| 66 |
+
) -> str:
|
| 67 |
+
"""Generate a 3D point cloud and camera wireframes and export them as a ``.glb`` file.
|
| 68 |
+
|
| 69 |
+
The function builds a point cloud from the predicted depth maps, aligns it to the
|
| 70 |
+
first camera in glTF coordinates (X-right, Y-up, Z-backward), optionally draws
|
| 71 |
+
camera wireframes, and writes the result to ``scene.glb``. Auxiliary assets such as
|
| 72 |
+
depth visualizations can also be generated alongside the main export.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
prediction: Model prediction containing depth, confidence, intrinsics, extrinsics,
|
| 76 |
+
and pre-processed images.
|
| 77 |
+
export_dir: Output directory where the glTF assets will be written.
|
| 78 |
+
num_max_points: Maximum number of points retained after downsampling.
|
| 79 |
+
conf_thresh: Base confidence threshold used before percentile adjustments.
|
| 80 |
+
filter_black_bg: Mark near-black background pixels for removal during confidence filtering.
|
| 81 |
+
filter_white_bg: Mark near-white background pixels for removal during confidence filtering.
|
| 82 |
+
conf_thresh_percentile: Lower percentile used when adapting the confidence threshold.
|
| 83 |
+
ensure_thresh_percentile: Upper percentile clamp for the adaptive threshold.
|
| 84 |
+
sky_depth_def: Percentile used to fill sky pixels with plausible depth values.
|
| 85 |
+
show_cameras: Whether to render camera wireframes in the exported scene.
|
| 86 |
+
camera_size: Relative camera wireframe scale as a fraction of the scene diagonal.
|
| 87 |
+
export_depth_vis: Whether to export raster depth visualisations alongside the glTF.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Path to the exported ``scene.glb`` file.
|
| 91 |
+
"""
|
| 92 |
+
# 1) Use prediction.processed_images, which is already processed image data
|
| 93 |
+
assert (
|
| 94 |
+
prediction.processed_images is not None
|
| 95 |
+
), "Export to GLB: prediction.processed_images is required but not available"
|
| 96 |
+
assert (
|
| 97 |
+
prediction.depth is not None
|
| 98 |
+
), "Export to GLB: prediction.depth is required but not available"
|
| 99 |
+
assert (
|
| 100 |
+
prediction.intrinsics is not None
|
| 101 |
+
), "Export to GLB: prediction.intrinsics is required but not available"
|
| 102 |
+
assert (
|
| 103 |
+
prediction.extrinsics is not None
|
| 104 |
+
), "Export to GLB: prediction.extrinsics is required but not available"
|
| 105 |
+
assert (
|
| 106 |
+
prediction.conf is not None
|
| 107 |
+
), "Export to GLB: prediction.conf is required but not available"
|
| 108 |
+
logger.info(f"conf_thresh_percentile: {conf_thresh_percentile}")
|
| 109 |
+
logger.info(f"num max points: {num_max_points}")
|
| 110 |
+
logger.info(f"Exporting to GLB with num_max_points: {num_max_points}")
|
| 111 |
+
if prediction.processed_images is None:
|
| 112 |
+
raise ValueError("prediction.processed_images is required but not available")
|
| 113 |
+
|
| 114 |
+
images_u8 = prediction.processed_images # (N,H,W,3) uint8
|
| 115 |
+
|
| 116 |
+
# 2) Sky processing (if sky_mask is provided)
|
| 117 |
+
if getattr(prediction, "sky_mask", None) is not None:
|
| 118 |
+
set_sky_depth(prediction, prediction.sky_mask, sky_depth_def)
|
| 119 |
+
|
| 120 |
+
# 3) Confidence threshold (if no conf, then no filtering)
|
| 121 |
+
if filter_black_bg:
|
| 122 |
+
prediction.conf[(prediction.processed_images < 16).all(axis=-1)] = 1.0
|
| 123 |
+
if filter_white_bg:
|
| 124 |
+
prediction.conf[(prediction.processed_images >= 240).all(axis=-1)] = 1.0
|
| 125 |
+
conf_thr = get_conf_thresh(
|
| 126 |
+
prediction,
|
| 127 |
+
getattr(prediction, "sky_mask", None),
|
| 128 |
+
conf_thresh,
|
| 129 |
+
conf_thresh_percentile,
|
| 130 |
+
ensure_thresh_percentile,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# 4) Back-project to world coordinates and get colors (world frame)
|
| 134 |
+
points, colors = _depths_to_world_points_with_colors(
|
| 135 |
+
prediction.depth,
|
| 136 |
+
prediction.intrinsics,
|
| 137 |
+
prediction.extrinsics, # w2c
|
| 138 |
+
images_u8,
|
| 139 |
+
prediction.conf,
|
| 140 |
+
conf_thr,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# 5) Based on first camera orientation + glTF axis system, center by point cloud,
|
| 144 |
+
# construct alignment transform, and apply to point cloud
|
| 145 |
+
A = _compute_alignment_transform_first_cam_glTF_center_by_points(
|
| 146 |
+
prediction.extrinsics[0], points
|
| 147 |
+
) # (4,4)
|
| 148 |
+
|
| 149 |
+
if points.shape[0] > 0:
|
| 150 |
+
points = trimesh.transform_points(points, A)
|
| 151 |
+
|
| 152 |
+
# 6) Clean + downsample
|
| 153 |
+
points, colors = _filter_and_downsample(points, colors, num_max_points)
|
| 154 |
+
|
| 155 |
+
# 7) Assemble scene (add point cloud first)
|
| 156 |
+
scene = trimesh.Scene()
|
| 157 |
+
if scene.metadata is None:
|
| 158 |
+
scene.metadata = {}
|
| 159 |
+
scene.metadata["hf_alignment"] = A # For camera wireframes and external reuse
|
| 160 |
+
|
| 161 |
+
if points.shape[0] > 0:
|
| 162 |
+
pc = trimesh.points.PointCloud(vertices=points, colors=colors)
|
| 163 |
+
scene.add_geometry(pc)
|
| 164 |
+
|
| 165 |
+
# 8) Draw cameras (wireframe pyramids), using the same transform A
|
| 166 |
+
if show_cameras and prediction.intrinsics is not None and prediction.extrinsics is not None:
|
| 167 |
+
scene_scale = _estimate_scene_scale(points, fallback=1.0)
|
| 168 |
+
H, W = prediction.depth.shape[1:]
|
| 169 |
+
_add_cameras_to_scene(
|
| 170 |
+
scene=scene,
|
| 171 |
+
K=prediction.intrinsics,
|
| 172 |
+
ext_w2c=prediction.extrinsics,
|
| 173 |
+
image_sizes=[(H, W)] * prediction.depth.shape[0],
|
| 174 |
+
scale=scene_scale * camera_size,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# 9) Export
|
| 178 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 179 |
+
out_path = os.path.join(export_dir, "scene.glb")
|
| 180 |
+
scene.export(out_path)
|
| 181 |
+
|
| 182 |
+
if export_depth_vis:
|
| 183 |
+
export_to_depth_vis(prediction, export_dir)
|
| 184 |
+
depth_vis_thumbnail = os.path.join(export_dir, "depth_vis", "0000.jpg")
|
| 185 |
+
if os.path.isfile(depth_vis_thumbnail):
|
| 186 |
+
shutil.copy2(depth_vis_thumbnail, os.path.join(export_dir, "scene.jpg"))
|
| 187 |
+
else:
|
| 188 |
+
logger.warn(f"Depth visualization thumbnail not found at {depth_vis_thumbnail}")
|
| 189 |
+
return out_path
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# =========================
|
| 193 |
+
# utilities
|
| 194 |
+
# =========================
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _as_homogeneous44(ext: np.ndarray) -> np.ndarray:
|
| 198 |
+
"""
|
| 199 |
+
Accept (4,4) or (3,4) extrinsic parameters, return (4,4) homogeneous matrix.
|
| 200 |
+
"""
|
| 201 |
+
if ext.shape == (4, 4):
|
| 202 |
+
return ext
|
| 203 |
+
if ext.shape == (3, 4):
|
| 204 |
+
H = np.eye(4, dtype=ext.dtype)
|
| 205 |
+
H[:3, :4] = ext
|
| 206 |
+
return H
|
| 207 |
+
raise ValueError(f"extrinsic must be (4,4) or (3,4), got {ext.shape}")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _depths_to_world_points_with_colors(
|
| 211 |
+
depth: np.ndarray,
|
| 212 |
+
K: np.ndarray,
|
| 213 |
+
ext_w2c: np.ndarray,
|
| 214 |
+
images_u8: np.ndarray,
|
| 215 |
+
conf: np.ndarray | None,
|
| 216 |
+
conf_thr: float,
|
| 217 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 218 |
+
"""
|
| 219 |
+
For each frame, transform (u,v,1) through K^{-1} to get rays,
|
| 220 |
+
multiply by depth to camera frame, then use (w2c)^{-1} to transform to world frame.
|
| 221 |
+
Simultaneously extract colors.
|
| 222 |
+
"""
|
| 223 |
+
N, H, W = depth.shape
|
| 224 |
+
us, vs = np.meshgrid(np.arange(W), np.arange(H))
|
| 225 |
+
ones = np.ones_like(us)
|
| 226 |
+
pix = np.stack([us, vs, ones], axis=-1).reshape(-1, 3) # (H*W,3)
|
| 227 |
+
|
| 228 |
+
pts_all, col_all = [], []
|
| 229 |
+
|
| 230 |
+
for i in range(N):
|
| 231 |
+
d = depth[i] # (H,W)
|
| 232 |
+
valid = np.isfinite(d) & (d > 0)
|
| 233 |
+
if conf is not None:
|
| 234 |
+
valid &= conf[i] >= conf_thr
|
| 235 |
+
if not np.any(valid):
|
| 236 |
+
continue
|
| 237 |
+
|
| 238 |
+
d_flat = d.reshape(-1)
|
| 239 |
+
vidx = np.flatnonzero(valid.reshape(-1))
|
| 240 |
+
|
| 241 |
+
K_inv = np.linalg.inv(K[i]) # (3,3)
|
| 242 |
+
c2w = np.linalg.inv(_as_homogeneous44(ext_w2c[i])) # (4,4)
|
| 243 |
+
|
| 244 |
+
rays = K_inv @ pix[vidx].T # (3,M)
|
| 245 |
+
Xc = rays * d_flat[vidx][None, :] # (3,M)
|
| 246 |
+
Xc_h = np.vstack([Xc, np.ones((1, Xc.shape[1]))])
|
| 247 |
+
Xw = (c2w @ Xc_h)[:3].T.astype(np.float32) # (M,3)
|
| 248 |
+
|
| 249 |
+
cols = images_u8[i].reshape(-1, 3)[vidx].astype(np.uint8) # (M,3)
|
| 250 |
+
|
| 251 |
+
pts_all.append(Xw)
|
| 252 |
+
col_all.append(cols)
|
| 253 |
+
|
| 254 |
+
if len(pts_all) == 0:
|
| 255 |
+
return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.uint8)
|
| 256 |
+
|
| 257 |
+
return np.concatenate(pts_all, 0), np.concatenate(col_all, 0)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _filter_and_downsample(points: np.ndarray, colors: np.ndarray, num_max: int):
|
| 261 |
+
if points.shape[0] == 0:
|
| 262 |
+
return points, colors
|
| 263 |
+
finite = np.isfinite(points).all(axis=1)
|
| 264 |
+
points, colors = points[finite], colors[finite]
|
| 265 |
+
if points.shape[0] > num_max:
|
| 266 |
+
idx = np.random.choice(points.shape[0], num_max, replace=False)
|
| 267 |
+
points, colors = points[idx], colors[idx]
|
| 268 |
+
return points, colors
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _estimate_scene_scale(points: np.ndarray, fallback: float = 1.0) -> float:
|
| 272 |
+
if points.shape[0] < 2:
|
| 273 |
+
return fallback
|
| 274 |
+
lo = np.percentile(points, 5, axis=0)
|
| 275 |
+
hi = np.percentile(points, 95, axis=0)
|
| 276 |
+
diag = np.linalg.norm(hi - lo)
|
| 277 |
+
return float(diag if np.isfinite(diag) and diag > 0 else fallback)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _compute_alignment_transform_first_cam_glTF_center_by_points(
|
| 281 |
+
ext_w2c0: np.ndarray,
|
| 282 |
+
points_world: np.ndarray,
|
| 283 |
+
) -> np.ndarray:
|
| 284 |
+
"""Computes the transformation matrix to align the scene with glTF standards.
|
| 285 |
+
|
| 286 |
+
This function calculates a 4x4 homogeneous matrix that centers the scene's
|
| 287 |
+
point cloud and transforms its coordinate system from the computer vision (CV)
|
| 288 |
+
standard to the glTF standard.
|
| 289 |
+
|
| 290 |
+
The transformation process involves three main steps:
|
| 291 |
+
1. **Initial Alignment**: Orients the world coordinate system to match the
|
| 292 |
+
first camera's view (x-right, y-down, z-forward).
|
| 293 |
+
2. **Coordinate System Conversion**: Converts the CV camera frame to the
|
| 294 |
+
glTF frame (x-right, y-up, z-backward) by flipping the Y and Z axes.
|
| 295 |
+
3. **Centering**: Translates the entire scene so that the median of the
|
| 296 |
+
point cloud becomes the new origin (0,0,0).
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
A 4x4 homogeneous transformation matrix (torch.Tensor or np.ndarray)
|
| 300 |
+
that applies these transformations. A: X' = A @ [X;1]
|
| 301 |
+
"""
|
| 302 |
+
|
| 303 |
+
w2c0 = _as_homogeneous44(ext_w2c0).astype(np.float64)
|
| 304 |
+
|
| 305 |
+
# CV -> glTF axis transformation
|
| 306 |
+
M = np.eye(4, dtype=np.float64)
|
| 307 |
+
M[1, 1] = -1.0 # flip Y
|
| 308 |
+
M[2, 2] = -1.0 # flip Z
|
| 309 |
+
|
| 310 |
+
# Don't center first
|
| 311 |
+
A_no_center = M @ w2c0
|
| 312 |
+
|
| 313 |
+
# Calculate point cloud center in new coordinate system (use median to resist outliers)
|
| 314 |
+
if points_world.shape[0] > 0:
|
| 315 |
+
pts_tmp = trimesh.transform_points(points_world, A_no_center)
|
| 316 |
+
center = np.median(pts_tmp, axis=0)
|
| 317 |
+
else:
|
| 318 |
+
center = np.zeros(3, dtype=np.float64)
|
| 319 |
+
|
| 320 |
+
T_center = np.eye(4, dtype=np.float64)
|
| 321 |
+
T_center[:3, 3] = -center
|
| 322 |
+
|
| 323 |
+
A = T_center @ A_no_center
|
| 324 |
+
return A
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _add_cameras_to_scene(
|
| 328 |
+
scene: trimesh.Scene,
|
| 329 |
+
K: np.ndarray,
|
| 330 |
+
ext_w2c: np.ndarray,
|
| 331 |
+
image_sizes: list[tuple[int, int]],
|
| 332 |
+
scale: float,
|
| 333 |
+
) -> None:
|
| 334 |
+
"""Draws camera frustums to visualize their position and orientation.
|
| 335 |
+
|
| 336 |
+
This function renders each camera as a wireframe pyramid, originating from
|
| 337 |
+
the camera's center and extending to the corners of its imaging plane.
|
| 338 |
+
|
| 339 |
+
It reads the 'hf_alignment' metadata from the scene to ensure the
|
| 340 |
+
wireframes are correctly aligned with the 3D point cloud.
|
| 341 |
+
"""
|
| 342 |
+
N = K.shape[0]
|
| 343 |
+
if N == 0:
|
| 344 |
+
return
|
| 345 |
+
|
| 346 |
+
# Alignment matrix consistent with point cloud (use identity matrix if missing)
|
| 347 |
+
A = None
|
| 348 |
+
try:
|
| 349 |
+
A = scene.metadata.get("hf_alignment", None) if scene.metadata else None
|
| 350 |
+
except Exception:
|
| 351 |
+
A = None
|
| 352 |
+
if A is None:
|
| 353 |
+
A = np.eye(4, dtype=np.float64)
|
| 354 |
+
|
| 355 |
+
for i in range(N):
|
| 356 |
+
H, W = image_sizes[i]
|
| 357 |
+
segs = _camera_frustum_lines(K[i], ext_w2c[i], W, H, scale) # (8,2,3) world frame
|
| 358 |
+
# Apply unified transformation
|
| 359 |
+
segs = trimesh.transform_points(segs.reshape(-1, 3), A).reshape(-1, 2, 3)
|
| 360 |
+
path = trimesh.load_path(segs)
|
| 361 |
+
color = _index_color_rgb(i, N)
|
| 362 |
+
if hasattr(path, "colors"):
|
| 363 |
+
path.colors = np.tile(color, (len(path.entities), 1))
|
| 364 |
+
scene.add_geometry(path)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def _camera_frustum_lines(
|
| 368 |
+
K: np.ndarray, ext_w2c: np.ndarray, W: int, H: int, scale: float
|
| 369 |
+
) -> np.ndarray:
|
| 370 |
+
corners = np.array(
|
| 371 |
+
[
|
| 372 |
+
[0, 0, 1.0],
|
| 373 |
+
[W - 1, 0, 1.0],
|
| 374 |
+
[W - 1, H - 1, 1.0],
|
| 375 |
+
[0, H - 1, 1.0],
|
| 376 |
+
],
|
| 377 |
+
dtype=float,
|
| 378 |
+
) # (4,3)
|
| 379 |
+
|
| 380 |
+
K_inv = np.linalg.inv(K)
|
| 381 |
+
c2w = np.linalg.inv(_as_homogeneous44(ext_w2c))
|
| 382 |
+
|
| 383 |
+
# camera center in world
|
| 384 |
+
Cw = (c2w @ np.array([0, 0, 0, 1.0]))[:3]
|
| 385 |
+
|
| 386 |
+
# rays -> z=1 plane points (camera frame)
|
| 387 |
+
rays = (K_inv @ corners.T).T
|
| 388 |
+
z = rays[:, 2:3]
|
| 389 |
+
z[z == 0] = 1.0
|
| 390 |
+
plane_cam = (rays / z) * scale # (4,3)
|
| 391 |
+
|
| 392 |
+
# to world
|
| 393 |
+
plane_w = []
|
| 394 |
+
for p in plane_cam:
|
| 395 |
+
pw = (c2w @ np.array([p[0], p[1], p[2], 1.0]))[:3]
|
| 396 |
+
plane_w.append(pw)
|
| 397 |
+
plane_w = np.stack(plane_w, 0) # (4,3)
|
| 398 |
+
|
| 399 |
+
segs = []
|
| 400 |
+
# center to corners
|
| 401 |
+
for k in range(4):
|
| 402 |
+
segs.append(np.stack([Cw, plane_w[k]], 0))
|
| 403 |
+
# rectangle edges
|
| 404 |
+
order = [0, 1, 2, 3, 0]
|
| 405 |
+
for a, b in zip(order[:-1], order[1:]):
|
| 406 |
+
segs.append(np.stack([plane_w[a], plane_w[b]], 0))
|
| 407 |
+
|
| 408 |
+
return np.stack(segs, 0) # (8,2,3)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def _index_color_rgb(i: int, n: int) -> np.ndarray:
|
| 412 |
+
h = (i + 0.5) / max(n, 1)
|
| 413 |
+
s, v = 0.85, 0.95
|
| 414 |
+
r, g, b = _hsv_to_rgb(h, s, v)
|
| 415 |
+
return (np.array([r, g, b]) * 255).astype(np.uint8)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def _hsv_to_rgb(h: float, s: float, v: float) -> tuple[float, float, float]:
|
| 419 |
+
i = int(h * 6.0)
|
| 420 |
+
f = h * 6.0 - i
|
| 421 |
+
p = v * (1.0 - s)
|
| 422 |
+
q = v * (1.0 - f * s)
|
| 423 |
+
t = v * (1.0 - (1.0 - f) * s)
|
| 424 |
+
i = i % 6
|
| 425 |
+
if i == 0:
|
| 426 |
+
r, g, b = v, t, p
|
| 427 |
+
elif i == 1:
|
| 428 |
+
r, g, b = q, v, p
|
| 429 |
+
elif i == 2:
|
| 430 |
+
r, g, b = p, v, t
|
| 431 |
+
elif i == 3:
|
| 432 |
+
r, g, b = p, q, v
|
| 433 |
+
elif i == 4:
|
| 434 |
+
r, g, b = t, p, v
|
| 435 |
+
else:
|
| 436 |
+
r, g, b = v, p, q
|
| 437 |
+
return r, g, b
|
multivue/cameras/source/src/depth_anything_3/utils/export/gs.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
from typing import Literal, Optional
|
| 17 |
+
import moviepy.editor as mpy
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from depth_anything_3.model.utils.gs_renderer import run_renderer_in_chunk_w_trj_mode
|
| 21 |
+
from depth_anything_3.specs import Prediction
|
| 22 |
+
from depth_anything_3.utils.gsply_helpers import save_gaussian_ply
|
| 23 |
+
from depth_anything_3.utils.layout_helpers import hcat, vcat
|
| 24 |
+
from depth_anything_3.utils.visualize import vis_depth_map_tensor
|
| 25 |
+
|
| 26 |
+
VIDEO_QUALITY_MAP = {
|
| 27 |
+
"low": {"crf": "28", "preset": "veryfast"},
|
| 28 |
+
"medium": {"crf": "23", "preset": "medium"},
|
| 29 |
+
"high": {"crf": "18", "preset": "slow"},
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def export_to_gs_ply(
|
| 34 |
+
prediction: Prediction,
|
| 35 |
+
export_dir: str,
|
| 36 |
+
gs_views_interval: Optional[
|
| 37 |
+
int
|
| 38 |
+
] = 1, # export GS every N views, useful for extremely dense inputs
|
| 39 |
+
):
|
| 40 |
+
gs_world = prediction.gaussians
|
| 41 |
+
pred_depth = torch.from_numpy(prediction.depth).unsqueeze(-1).to(gs_world.means) # v h w 1
|
| 42 |
+
idx = 0
|
| 43 |
+
os.makedirs(os.path.join(export_dir, "gs_ply"), exist_ok=True)
|
| 44 |
+
save_path = os.path.join(export_dir, f"gs_ply/{idx:04d}.ply")
|
| 45 |
+
if gs_views_interval is None: # select around 12 views in total
|
| 46 |
+
gs_views_interval = max(pred_depth.shape[0] // 12, 1)
|
| 47 |
+
save_gaussian_ply(
|
| 48 |
+
gaussians=gs_world,
|
| 49 |
+
save_path=save_path,
|
| 50 |
+
ctx_depth=pred_depth,
|
| 51 |
+
shift_and_scale=False,
|
| 52 |
+
save_sh_dc_only=True,
|
| 53 |
+
gs_views_interval=gs_views_interval,
|
| 54 |
+
inv_opacity=True,
|
| 55 |
+
prune_by_depth_percent=0.9,
|
| 56 |
+
prune_border_gs=True,
|
| 57 |
+
match_3dgs_mcmc_dev=False,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def export_to_gs_video(
|
| 62 |
+
prediction: Prediction,
|
| 63 |
+
export_dir: str,
|
| 64 |
+
extrinsics: Optional[torch.Tensor] = None, # render views' world2cam, "b v 4 4"
|
| 65 |
+
intrinsics: Optional[torch.Tensor] = None, # render views' unnormed intrinsics, "b v 3 3"
|
| 66 |
+
out_image_hw: Optional[tuple[int, int]] = None, # render views' resolution, (h, w)
|
| 67 |
+
chunk_size: Optional[int] = 4,
|
| 68 |
+
trj_mode: Literal[
|
| 69 |
+
"original",
|
| 70 |
+
"smooth",
|
| 71 |
+
"interpolate",
|
| 72 |
+
"interpolate_smooth",
|
| 73 |
+
"wander",
|
| 74 |
+
"dolly_zoom",
|
| 75 |
+
"extend",
|
| 76 |
+
"wobble_inter",
|
| 77 |
+
] = "extend",
|
| 78 |
+
color_mode: Literal["RGB+D", "RGB+ED"] = "RGB+ED",
|
| 79 |
+
vis_depth: Optional[Literal["hcat", "vcat"]] = "hcat",
|
| 80 |
+
enable_tqdm: Optional[bool] = True,
|
| 81 |
+
output_name: Optional[str] = None,
|
| 82 |
+
video_quality: Literal["low", "medium", "high"] = "high",
|
| 83 |
+
) -> None:
|
| 84 |
+
gs_world = prediction.gaussians
|
| 85 |
+
# if target poses are not provided, render the (smooth/interpolate) input poses
|
| 86 |
+
if extrinsics is not None:
|
| 87 |
+
tgt_extrs = extrinsics
|
| 88 |
+
else:
|
| 89 |
+
tgt_extrs = torch.from_numpy(prediction.extrinsics).unsqueeze(0).to(gs_world.means)
|
| 90 |
+
if prediction.is_metric:
|
| 91 |
+
scale_factor = prediction.scale_factor
|
| 92 |
+
if scale_factor is not None:
|
| 93 |
+
tgt_extrs[:, :, :3, 3] /= scale_factor
|
| 94 |
+
tgt_intrs = (
|
| 95 |
+
intrinsics
|
| 96 |
+
if intrinsics is not None
|
| 97 |
+
else torch.from_numpy(prediction.intrinsics).unsqueeze(0).to(gs_world.means)
|
| 98 |
+
)
|
| 99 |
+
# if render resolution is not provided, render the input ones
|
| 100 |
+
if out_image_hw is not None:
|
| 101 |
+
H, W = out_image_hw
|
| 102 |
+
else:
|
| 103 |
+
H, W = prediction.depth.shape[-2:]
|
| 104 |
+
# if single views, render wander trj
|
| 105 |
+
if tgt_extrs.shape[1] <= 1:
|
| 106 |
+
trj_mode = "wander"
|
| 107 |
+
# trj_mode = "dolly_zoom"
|
| 108 |
+
|
| 109 |
+
color, depth = run_renderer_in_chunk_w_trj_mode(
|
| 110 |
+
gaussians=gs_world,
|
| 111 |
+
extrinsics=tgt_extrs,
|
| 112 |
+
intrinsics=tgt_intrs,
|
| 113 |
+
image_shape=(H, W),
|
| 114 |
+
chunk_size=chunk_size,
|
| 115 |
+
trj_mode=trj_mode,
|
| 116 |
+
use_sh=True,
|
| 117 |
+
color_mode=color_mode,
|
| 118 |
+
enable_tqdm=enable_tqdm,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# save as video
|
| 122 |
+
ffmpeg_params = [
|
| 123 |
+
"-crf",
|
| 124 |
+
VIDEO_QUALITY_MAP[video_quality]["crf"],
|
| 125 |
+
"-preset",
|
| 126 |
+
VIDEO_QUALITY_MAP[video_quality]["preset"],
|
| 127 |
+
"-pix_fmt",
|
| 128 |
+
"yuv420p",
|
| 129 |
+
] # best compatibility
|
| 130 |
+
|
| 131 |
+
os.makedirs(os.path.join(export_dir, "gs_video"), exist_ok=True)
|
| 132 |
+
for idx in range(color.shape[0]):
|
| 133 |
+
video_i = color[idx]
|
| 134 |
+
if vis_depth is not None:
|
| 135 |
+
depth_i = vis_depth_map_tensor(depth[0])
|
| 136 |
+
cat_fn = hcat if vis_depth == "hcat" else vcat
|
| 137 |
+
video_i = torch.stack([cat_fn(c, d) for c, d in zip(video_i, depth_i)])
|
| 138 |
+
frames = list(
|
| 139 |
+
(video_i.clamp(0, 1) * 255).byte().permute(0, 2, 3, 1).cpu().numpy()
|
| 140 |
+
) # T x H x W x C, uint8, numpy()
|
| 141 |
+
|
| 142 |
+
fps = 24
|
| 143 |
+
clip = mpy.ImageSequenceClip(frames, fps=fps)
|
| 144 |
+
output_name = f"{idx:04d}_{trj_mode}" if output_name is None else output_name
|
| 145 |
+
save_path = os.path.join(export_dir, f"gs_video/{output_name}.mp4")
|
| 146 |
+
# clip.write_videofile(save_path, codec="libx264", audio=False, bitrate="4000k")
|
| 147 |
+
clip.write_videofile(
|
| 148 |
+
save_path,
|
| 149 |
+
codec="libx264",
|
| 150 |
+
audio=False,
|
| 151 |
+
fps=fps,
|
| 152 |
+
ffmpeg_params=ffmpeg_params,
|
| 153 |
+
)
|
| 154 |
+
return
|
multivue/cameras/source/src/depth_anything_3/utils/export/npz.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
from depth_anything_3.specs import Prediction
|
| 19 |
+
from depth_anything_3.utils.parallel_utils import async_call
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@async_call
|
| 23 |
+
def export_to_npz(
|
| 24 |
+
prediction: Prediction,
|
| 25 |
+
export_dir: str,
|
| 26 |
+
):
|
| 27 |
+
output_file = os.path.join(export_dir, "exports", "npz", "results.npz")
|
| 28 |
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
| 29 |
+
|
| 30 |
+
# Use prediction.processed_images, which is already processed image data
|
| 31 |
+
if prediction.processed_images is None:
|
| 32 |
+
raise ValueError("prediction.processed_images is required but not available")
|
| 33 |
+
|
| 34 |
+
image = prediction.processed_images # (N,H,W,3) uint8
|
| 35 |
+
|
| 36 |
+
# Build save dict with only non-None values
|
| 37 |
+
save_dict = {
|
| 38 |
+
"image": image,
|
| 39 |
+
"depth": np.round(prediction.depth, 6),
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
if prediction.conf is not None:
|
| 43 |
+
save_dict["conf"] = np.round(prediction.conf, 2)
|
| 44 |
+
if prediction.extrinsics is not None:
|
| 45 |
+
save_dict["extrinsics"] = prediction.extrinsics
|
| 46 |
+
if prediction.intrinsics is not None:
|
| 47 |
+
save_dict["intrinsics"] = prediction.intrinsics
|
| 48 |
+
|
| 49 |
+
# aux = {k: np.round(v, 4) for k, v in prediction.aux.items()}
|
| 50 |
+
np.savez_compressed(output_file, **save_dict)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@async_call
|
| 54 |
+
def export_to_mini_npz(
|
| 55 |
+
prediction: Prediction,
|
| 56 |
+
export_dir: str,
|
| 57 |
+
):
|
| 58 |
+
output_file = os.path.join(export_dir, "exports", "mini_npz", "results.npz")
|
| 59 |
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
| 60 |
+
|
| 61 |
+
# Build save dict with only non-None values
|
| 62 |
+
save_dict = {
|
| 63 |
+
"depth": np.round(prediction.depth, 8),
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if prediction.conf is not None:
|
| 67 |
+
save_dict["conf"] = np.round(prediction.conf, 2)
|
| 68 |
+
if prediction.extrinsics is not None:
|
| 69 |
+
save_dict["extrinsics"] = prediction.extrinsics
|
| 70 |
+
if prediction.intrinsics is not None:
|
| 71 |
+
save_dict["intrinsics"] = prediction.intrinsics
|
| 72 |
+
|
| 73 |
+
np.savez_compressed(output_file, **save_dict)
|
multivue/cameras/source/src/depth_anything_3/utils/export/utils.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _denorm_and_to_uint8(image_tensor: torch.Tensor) -> np.ndarray:
|
| 20 |
+
"""Denormalize to [0,255] and output (N, H, W, 3) uint8."""
|
| 21 |
+
resnet_mean = torch.tensor(
|
| 22 |
+
[0.485, 0.456, 0.406], dtype=image_tensor.dtype, device=image_tensor.device
|
| 23 |
+
)
|
| 24 |
+
resnet_std = torch.tensor(
|
| 25 |
+
[0.229, 0.224, 0.225], dtype=image_tensor.dtype, device=image_tensor.device
|
| 26 |
+
)
|
| 27 |
+
img = image_tensor * resnet_std[None, :, None, None] + resnet_mean[None, :, None, None]
|
| 28 |
+
img = torch.clamp(img, 0.0, 1.0)
|
| 29 |
+
img = (img.permute(0, 2, 3, 1).cpu().numpy() * 255.0).round().astype(np.uint8) # (N,H,W,3)
|
| 30 |
+
return img
|
multivue/cameras/source/src/depth_anything_3/utils/geometry.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# flake8: noqa: F722
|
| 2 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
from types import SimpleNamespace
|
| 16 |
+
from typing import Optional
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from einops import einsum
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def as_homogeneous(ext):
|
| 24 |
+
"""
|
| 25 |
+
Accept (..., 3,4) or (..., 4,4) extrinsics, return (...,4,4) homogeneous matrix.
|
| 26 |
+
Supports torch.Tensor or np.ndarray.
|
| 27 |
+
"""
|
| 28 |
+
if isinstance(ext, torch.Tensor):
|
| 29 |
+
# If already in homogeneous form
|
| 30 |
+
if ext.shape[-2:] == (4, 4):
|
| 31 |
+
return ext
|
| 32 |
+
elif ext.shape[-2:] == (3, 4):
|
| 33 |
+
# Create a new homogeneous matrix
|
| 34 |
+
ones = torch.zeros_like(ext[..., :1, :4])
|
| 35 |
+
ones[..., 0, 3] = 1.0
|
| 36 |
+
return torch.cat([ext, ones], dim=-2)
|
| 37 |
+
else:
|
| 38 |
+
raise ValueError(f"Invalid shape for torch.Tensor: {ext.shape}")
|
| 39 |
+
|
| 40 |
+
elif isinstance(ext, np.ndarray):
|
| 41 |
+
if ext.shape[-2:] == (4, 4):
|
| 42 |
+
return ext
|
| 43 |
+
elif ext.shape[-2:] == (3, 4):
|
| 44 |
+
ones = np.zeros_like(ext[..., :1, :4])
|
| 45 |
+
ones[..., 0, 3] = 1.0
|
| 46 |
+
return np.concatenate([ext, ones], axis=-2)
|
| 47 |
+
else:
|
| 48 |
+
raise ValueError(f"Invalid shape for np.ndarray: {ext.shape}")
|
| 49 |
+
|
| 50 |
+
else:
|
| 51 |
+
raise TypeError("Input must be a torch.Tensor or np.ndarray.")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@torch.jit.script
|
| 55 |
+
def affine_inverse(A: torch.Tensor):
|
| 56 |
+
R = A[..., :3, :3] # ..., 3, 3
|
| 57 |
+
T = A[..., :3, 3:] # ..., 3, 1
|
| 58 |
+
P = A[..., 3:, :] # ..., 1, 4
|
| 59 |
+
return torch.cat([torch.cat([R.mT, -R.mT @ T], dim=-1), P], dim=-2)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def transpose_last_two_axes(arr):
|
| 63 |
+
"""
|
| 64 |
+
for np < 2
|
| 65 |
+
"""
|
| 66 |
+
if arr.ndim < 2:
|
| 67 |
+
return arr
|
| 68 |
+
axes = list(range(arr.ndim))
|
| 69 |
+
# swap the last two
|
| 70 |
+
axes[-2], axes[-1] = axes[-1], axes[-2]
|
| 71 |
+
return arr.transpose(axes)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def affine_inverse_np(A: np.ndarray):
|
| 75 |
+
R = A[..., :3, :3]
|
| 76 |
+
T = A[..., :3, 3:]
|
| 77 |
+
P = A[..., 3:, :]
|
| 78 |
+
return np.concatenate(
|
| 79 |
+
[
|
| 80 |
+
np.concatenate([transpose_last_two_axes(R), -transpose_last_two_axes(R) @ T], axis=-1),
|
| 81 |
+
P,
|
| 82 |
+
],
|
| 83 |
+
axis=-2,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def quat_to_mat(quaternions: torch.Tensor) -> torch.Tensor:
|
| 88 |
+
"""
|
| 89 |
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
| 90 |
+
|
| 91 |
+
Convert rotations given as quaternions to rotation matrices.
|
| 92 |
+
Args:
|
| 93 |
+
quaternions: quaternions with real part last,
|
| 94 |
+
as tensor of shape (..., 4).
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Rotation matrices as tensor of shape (..., 3, 3).
|
| 98 |
+
"""
|
| 99 |
+
i, j, k, r = torch.unbind(quaternions, -1)
|
| 100 |
+
# pyre-fixme[58]: `/` is not supported for operand types `float` and `Tensor`.
|
| 101 |
+
two_s = 2.0 / (quaternions * quaternions).sum(-1)
|
| 102 |
+
|
| 103 |
+
o = torch.stack(
|
| 104 |
+
(
|
| 105 |
+
1 - two_s * (j * j + k * k),
|
| 106 |
+
two_s * (i * j - k * r),
|
| 107 |
+
two_s * (i * k + j * r),
|
| 108 |
+
two_s * (i * j + k * r),
|
| 109 |
+
1 - two_s * (i * i + k * k),
|
| 110 |
+
two_s * (j * k - i * r),
|
| 111 |
+
two_s * (i * k - j * r),
|
| 112 |
+
two_s * (j * k + i * r),
|
| 113 |
+
1 - two_s * (i * i + j * j),
|
| 114 |
+
),
|
| 115 |
+
-1,
|
| 116 |
+
)
|
| 117 |
+
return o.reshape(quaternions.shape[:-1] + (3, 3))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def mat_to_quat(matrix: torch.Tensor) -> torch.Tensor:
|
| 121 |
+
"""
|
| 122 |
+
Convert rotations given as rotation matrices to quaternions.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
matrix: Rotation matrices as tensor of shape (..., 3, 3).
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
quaternions with real part last, as tensor of shape (..., 4).
|
| 129 |
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
| 130 |
+
"""
|
| 131 |
+
if matrix.size(-1) != 3 or matrix.size(-2) != 3:
|
| 132 |
+
raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
|
| 133 |
+
|
| 134 |
+
batch_dim = matrix.shape[:-2]
|
| 135 |
+
m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
|
| 136 |
+
matrix.reshape(batch_dim + (9,)), dim=-1
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
q_abs = _sqrt_positive_part(
|
| 140 |
+
torch.stack(
|
| 141 |
+
[
|
| 142 |
+
1.0 + m00 + m11 + m22,
|
| 143 |
+
1.0 + m00 - m11 - m22,
|
| 144 |
+
1.0 - m00 + m11 - m22,
|
| 145 |
+
1.0 - m00 - m11 + m22,
|
| 146 |
+
],
|
| 147 |
+
dim=-1,
|
| 148 |
+
)
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# we produce the desired quaternion multiplied by each of r, i, j, k
|
| 152 |
+
quat_by_rijk = torch.stack(
|
| 153 |
+
[
|
| 154 |
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
| 155 |
+
# `int`.
|
| 156 |
+
torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1),
|
| 157 |
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
| 158 |
+
# `int`.
|
| 159 |
+
torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1),
|
| 160 |
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
| 161 |
+
# `int`.
|
| 162 |
+
torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1),
|
| 163 |
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
| 164 |
+
# `int`.
|
| 165 |
+
torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1),
|
| 166 |
+
],
|
| 167 |
+
dim=-2,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# We floor here at 0.1 but the exact level is not important; if q_abs is small,
|
| 171 |
+
# the candidate won't be picked.
|
| 172 |
+
flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
|
| 173 |
+
quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
|
| 174 |
+
|
| 175 |
+
# if not for numerical problems, quat_candidates[i] should be same (up to a sign),
|
| 176 |
+
# forall i; we pick the best-conditioned one (with the largest denominator)
|
| 177 |
+
out = quat_candidates[F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :].reshape(
|
| 178 |
+
batch_dim + (4,)
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Convert from rijk to ijkr
|
| 182 |
+
out = out[..., [1, 2, 3, 0]]
|
| 183 |
+
|
| 184 |
+
out = standardize_quaternion(out)
|
| 185 |
+
|
| 186 |
+
return out
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor:
|
| 190 |
+
"""
|
| 191 |
+
Returns torch.sqrt(torch.max(0, x))
|
| 192 |
+
but with a zero subgradient where x is 0.
|
| 193 |
+
"""
|
| 194 |
+
ret = torch.zeros_like(x)
|
| 195 |
+
positive_mask = x > 0
|
| 196 |
+
if torch.is_grad_enabled():
|
| 197 |
+
ret[positive_mask] = torch.sqrt(x[positive_mask])
|
| 198 |
+
else:
|
| 199 |
+
ret = torch.where(positive_mask, torch.sqrt(x), ret)
|
| 200 |
+
return ret
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor:
|
| 204 |
+
"""
|
| 205 |
+
Convert a unit quaternion to a standard form: one in which the real
|
| 206 |
+
part is non negative.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
quaternions: Quaternions with real part last,
|
| 210 |
+
as tensor of shape (..., 4).
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
Standardized quaternions as tensor of shape (..., 4).
|
| 214 |
+
"""
|
| 215 |
+
return torch.where(quaternions[..., 3:4] < 0, -quaternions, quaternions)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def sample_image_grid(
|
| 219 |
+
shape: tuple[int, ...],
|
| 220 |
+
device: torch.device = torch.device("cpu"),
|
| 221 |
+
) -> tuple[
|
| 222 |
+
torch.Tensor, # float coordinates (xy indexing), "*shape dim"
|
| 223 |
+
torch.Tensor, # integer indices (ij indexing), "*shape dim"
|
| 224 |
+
]:
|
| 225 |
+
"""Get normalized (range 0 to 1) coordinates and integer indices for an image."""
|
| 226 |
+
|
| 227 |
+
# Each entry is a pixel-wise integer coordinate. In the 2D case, each entry is a
|
| 228 |
+
# (row, col) coordinate.
|
| 229 |
+
indices = [torch.arange(length, device=device) for length in shape]
|
| 230 |
+
stacked_indices = torch.stack(torch.meshgrid(*indices, indexing="ij"), dim=-1)
|
| 231 |
+
|
| 232 |
+
# Each entry is a floating-point coordinate in the range (0, 1). In the 2D case,
|
| 233 |
+
# each entry is an (x, y) coordinate.
|
| 234 |
+
coordinates = [(idx + 0.5) / length for idx, length in zip(indices, shape)]
|
| 235 |
+
coordinates = reversed(coordinates)
|
| 236 |
+
coordinates = torch.stack(torch.meshgrid(*coordinates, indexing="xy"), dim=-1)
|
| 237 |
+
|
| 238 |
+
return coordinates, stacked_indices
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def homogenize_points(points: torch.Tensor) -> torch.Tensor: # "*batch dim" # "*batch dim+1"
|
| 242 |
+
"""Convert batched points (xyz) to (xyz1)."""
|
| 243 |
+
return torch.cat([points, torch.ones_like(points[..., :1])], dim=-1)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def homogenize_vectors(vectors: torch.Tensor) -> torch.Tensor: # "*batch dim" # "*batch dim+1"
|
| 247 |
+
"""Convert batched vectors (xyz) to (xyz0)."""
|
| 248 |
+
return torch.cat([vectors, torch.zeros_like(vectors[..., :1])], dim=-1)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def transform_rigid(
|
| 252 |
+
homogeneous_coordinates: torch.Tensor, # "*#batch dim"
|
| 253 |
+
transformation: torch.Tensor, # "*#batch dim dim"
|
| 254 |
+
) -> torch.Tensor: # "*batch dim"
|
| 255 |
+
"""Apply a rigid-body transformation to points or vectors."""
|
| 256 |
+
return einsum(
|
| 257 |
+
transformation,
|
| 258 |
+
homogeneous_coordinates.to(transformation.dtype),
|
| 259 |
+
"... i j, ... j -> ... i",
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def transform_cam2world(
|
| 264 |
+
homogeneous_coordinates: torch.Tensor, # "*#batch dim"
|
| 265 |
+
extrinsics: torch.Tensor, # "*#batch dim dim"
|
| 266 |
+
) -> torch.Tensor: # "*batch dim"
|
| 267 |
+
"""Transform points from 3D camera coordinates to 3D world coordinates."""
|
| 268 |
+
return transform_rigid(homogeneous_coordinates, extrinsics)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def unproject(
|
| 272 |
+
coordinates: torch.Tensor, # "*#batch dim"
|
| 273 |
+
z: torch.Tensor, # "*#batch"
|
| 274 |
+
intrinsics: torch.Tensor, # "*#batch dim+1 dim+1"
|
| 275 |
+
) -> torch.Tensor: # "*batch dim+1"
|
| 276 |
+
"""Unproject 2D camera coordinates with the given Z values."""
|
| 277 |
+
|
| 278 |
+
# Apply the inverse intrinsics to the coordinates.
|
| 279 |
+
coordinates = homogenize_points(coordinates)
|
| 280 |
+
ray_directions = einsum(
|
| 281 |
+
intrinsics.float().inverse().to(intrinsics),
|
| 282 |
+
coordinates.to(intrinsics.dtype),
|
| 283 |
+
"... i j, ... j -> ... i",
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
# Apply the supplied depth values.
|
| 287 |
+
return ray_directions * z[..., None]
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def get_world_rays(
|
| 291 |
+
coordinates: torch.Tensor, # "*#batch dim"
|
| 292 |
+
extrinsics: torch.Tensor, # "*#batch dim+2 dim+2"
|
| 293 |
+
intrinsics: torch.Tensor, # "*#batch dim+1 dim+1"
|
| 294 |
+
) -> tuple[
|
| 295 |
+
torch.Tensor, # origins, "*batch dim+1"
|
| 296 |
+
torch.Tensor, # directions, "*batch dim+1"
|
| 297 |
+
]:
|
| 298 |
+
# Get camera-space ray directions.
|
| 299 |
+
directions = unproject(
|
| 300 |
+
coordinates,
|
| 301 |
+
torch.ones_like(coordinates[..., 0]),
|
| 302 |
+
intrinsics,
|
| 303 |
+
)
|
| 304 |
+
directions = directions / directions.norm(dim=-1, keepdim=True)
|
| 305 |
+
|
| 306 |
+
# Transform ray directions to world coordinates.
|
| 307 |
+
directions = homogenize_vectors(directions)
|
| 308 |
+
directions = transform_cam2world(directions, extrinsics)[..., :-1]
|
| 309 |
+
|
| 310 |
+
# Tile the ray origins to have the same shape as the ray directions.
|
| 311 |
+
origins = extrinsics[..., :-1, -1].broadcast_to(directions.shape)
|
| 312 |
+
|
| 313 |
+
return origins, directions
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def get_fov(intrinsics: torch.Tensor) -> torch.Tensor: # "batch 3 3" -> "batch 2"
|
| 317 |
+
intrinsics_inv = intrinsics.float().inverse().to(intrinsics)
|
| 318 |
+
|
| 319 |
+
def process_vector(vector):
|
| 320 |
+
vector = torch.tensor(vector, dtype=intrinsics.dtype, device=intrinsics.device)
|
| 321 |
+
vector = einsum(intrinsics_inv, vector, "b i j, j -> b i")
|
| 322 |
+
return vector / vector.norm(dim=-1, keepdim=True)
|
| 323 |
+
|
| 324 |
+
left = process_vector([0, 0.5, 1])
|
| 325 |
+
right = process_vector([1, 0.5, 1])
|
| 326 |
+
top = process_vector([0.5, 0, 1])
|
| 327 |
+
bottom = process_vector([0.5, 1, 1])
|
| 328 |
+
fov_x = (left * right).sum(dim=-1).acos()
|
| 329 |
+
fov_y = (top * bottom).sum(dim=-1).acos()
|
| 330 |
+
return torch.stack((fov_x, fov_y), dim=-1)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def map_pdf_to_opacity(
|
| 334 |
+
pdf: torch.Tensor, # " *batch"
|
| 335 |
+
global_step: int = 0,
|
| 336 |
+
opacity_mapping: Optional[dict] = None,
|
| 337 |
+
) -> torch.Tensor: # " *batch"
|
| 338 |
+
# https://www.desmos.com/calculator/opvwti3ba9
|
| 339 |
+
|
| 340 |
+
# Figure out the exponent.
|
| 341 |
+
if opacity_mapping is not None:
|
| 342 |
+
cfg = SimpleNamespace(**opacity_mapping)
|
| 343 |
+
x = cfg.initial + min(global_step / cfg.warm_up, 1) * (cfg.final - cfg.initial)
|
| 344 |
+
else:
|
| 345 |
+
x = 0.0
|
| 346 |
+
exponent = 2**x
|
| 347 |
+
|
| 348 |
+
# Map the probability density to an opacity.
|
| 349 |
+
return 0.5 * (1 - (1 - pdf) ** exponent + pdf ** (1 / exponent))
|
| 350 |
+
|
| 351 |
+
def normalize_homogenous_points(points):
|
| 352 |
+
"""Normalize the point vectors"""
|
| 353 |
+
return points / points[..., -1:]
|
| 354 |
+
|
| 355 |
+
def inverse_intrinsic_matrix(ixts):
|
| 356 |
+
""" """
|
| 357 |
+
return torch.inverse(ixts)
|
| 358 |
+
|
| 359 |
+
def pixel_space_to_camera_space(pixel_space_points, depth, intrinsics):
|
| 360 |
+
"""
|
| 361 |
+
Convert pixel space points to camera space points.
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
pixel_space_points (torch.Tensor): Pixel space points with shape (h, w, 2)
|
| 365 |
+
depth (torch.Tensor): Depth map with shape (b, v, h, w, 1)
|
| 366 |
+
intrinsics (torch.Tensor): Camera intrinsics with shape (b, v, 3, 3)
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
torch.Tensor: Camera space points with shape (b, v, h, w, 3).
|
| 370 |
+
"""
|
| 371 |
+
pixel_space_points = homogenize_points(pixel_space_points)
|
| 372 |
+
# camera_space_points = torch.einsum(
|
| 373 |
+
# "b v i j , h w j -> b v h w i", intrinsics.inverse(), pixel_space_points
|
| 374 |
+
# )
|
| 375 |
+
camera_space_points = torch.einsum(
|
| 376 |
+
"b v i j , h w j -> b v h w i", inverse_intrinsic_matrix(intrinsics), pixel_space_points
|
| 377 |
+
)
|
| 378 |
+
camera_space_points = camera_space_points * depth
|
| 379 |
+
return camera_space_points
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def camera_space_to_world_space(camera_space_points, c2w):
|
| 383 |
+
"""
|
| 384 |
+
Convert camera space points to world space points.
|
| 385 |
+
|
| 386 |
+
Args:
|
| 387 |
+
camera_space_points (torch.Tensor): Camera space points with shape (b, v, h, w, 3)
|
| 388 |
+
c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v, 4, 4)
|
| 389 |
+
|
| 390 |
+
Returns:
|
| 391 |
+
torch.Tensor: World space points with shape (b, v, h, w, 3).
|
| 392 |
+
"""
|
| 393 |
+
camera_space_points = homogenize_points(camera_space_points)
|
| 394 |
+
world_space_points = torch.einsum("b v i j , b v h w j -> b v h w i", c2w, camera_space_points)
|
| 395 |
+
return world_space_points[..., :3]
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def camera_space_to_pixel_space(camera_space_points, intrinsics):
|
| 399 |
+
"""
|
| 400 |
+
Convert camera space points to pixel space points.
|
| 401 |
+
|
| 402 |
+
Args:
|
| 403 |
+
camera_space_points (torch.Tensor): Camera space points with shape (b, v1, v2, h, w, 3)
|
| 404 |
+
c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v2, 3, 3)
|
| 405 |
+
|
| 406 |
+
Returns:
|
| 407 |
+
torch.Tensor: World space points with shape (b, v1, v2, h, w, 2).
|
| 408 |
+
"""
|
| 409 |
+
camera_space_points = normalize_homogenous_points(camera_space_points)
|
| 410 |
+
pixel_space_points = torch.einsum(
|
| 411 |
+
"b u i j , b v u h w j -> b v u h w i", intrinsics, camera_space_points
|
| 412 |
+
)
|
| 413 |
+
return pixel_space_points[..., :2]
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def world_space_to_camera_space(world_space_points, c2w):
|
| 417 |
+
"""
|
| 418 |
+
Convert world space points to pixel space points.
|
| 419 |
+
|
| 420 |
+
Args:
|
| 421 |
+
world_space_points (torch.Tensor): World space points with shape (b, v1, h, w, 3)
|
| 422 |
+
c2w (torch.Tensor): Camera to world extrinsics matrix with shape (b, v2, 4, 4)
|
| 423 |
+
|
| 424 |
+
Returns:
|
| 425 |
+
torch.Tensor: Camera space points with shape (b, v1, v2, h, w, 3).
|
| 426 |
+
"""
|
| 427 |
+
world_space_points = homogenize_points(world_space_points)
|
| 428 |
+
camera_space_points = torch.einsum(
|
| 429 |
+
"b u i j , b v h w j -> b v u h w i", c2w.inverse(), world_space_points
|
| 430 |
+
)
|
| 431 |
+
return camera_space_points[..., :3]
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def unproject_depth(
|
| 435 |
+
depth, intrinsics, c2w=None, ixt_normalized=False, num_patches_x=None, num_patches_y=None
|
| 436 |
+
):
|
| 437 |
+
"""
|
| 438 |
+
Turn the depth map into a 3D point cloud in world space
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
depth: (b, v, h, w, 1)
|
| 442 |
+
intrinsics: (b, v, 3, 3)
|
| 443 |
+
c2w: (b, v, 4, 4)
|
| 444 |
+
|
| 445 |
+
Returns:
|
| 446 |
+
torch.Tensor: World space points with shape (b, v, h, w, 3).
|
| 447 |
+
"""
|
| 448 |
+
if c2w is None:
|
| 449 |
+
c2w = torch.eye(4, device=depth.device, dtype=depth.dtype)
|
| 450 |
+
c2w = c2w[None, None].repeat(depth.shape[0], depth.shape[1], 1, 1)
|
| 451 |
+
|
| 452 |
+
if not ixt_normalized:
|
| 453 |
+
# Compute indices of pixels
|
| 454 |
+
h, w = depth.shape[-3], depth.shape[-2]
|
| 455 |
+
x_grid, y_grid = torch.meshgrid(
|
| 456 |
+
torch.arange(w, device=depth.device, dtype=depth.dtype),
|
| 457 |
+
torch.arange(h, device=depth.device, dtype=depth.dtype),
|
| 458 |
+
indexing="xy",
|
| 459 |
+
) # (h, w), (h, w)
|
| 460 |
+
else:
|
| 461 |
+
# ixt_normalized: h=w=2.0. cx, cy, fx, fy are normalized according to h=w=2.0
|
| 462 |
+
assert num_patches_x is not None and num_patches_y is not None
|
| 463 |
+
dx = 1 / num_patches_x
|
| 464 |
+
dy = 1 / num_patches_y
|
| 465 |
+
max_y = 1 - dy
|
| 466 |
+
min_y = -max_y
|
| 467 |
+
max_x = 1 - dx
|
| 468 |
+
min_x = -max_x
|
| 469 |
+
|
| 470 |
+
grid_shift = 1.0
|
| 471 |
+
y_grid, x_grid = torch.meshgrid(
|
| 472 |
+
torch.linspace(
|
| 473 |
+
min_y + grid_shift,
|
| 474 |
+
max_y + grid_shift,
|
| 475 |
+
num_patches_y,
|
| 476 |
+
dtype=torch.float32,
|
| 477 |
+
device=depth.device,
|
| 478 |
+
),
|
| 479 |
+
torch.linspace(
|
| 480 |
+
min_x + grid_shift,
|
| 481 |
+
max_x + grid_shift,
|
| 482 |
+
num_patches_x,
|
| 483 |
+
dtype=torch.float32,
|
| 484 |
+
device=depth.device,
|
| 485 |
+
),
|
| 486 |
+
indexing="ij",
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Compute coordinates of pixels in camera space
|
| 490 |
+
pixel_space_points = torch.stack((x_grid, y_grid), dim=-1) # (..., h, w, 2)
|
| 491 |
+
camera_points = pixel_space_to_camera_space(
|
| 492 |
+
pixel_space_points, depth, intrinsics
|
| 493 |
+
) # (..., h, w, 3)
|
| 494 |
+
|
| 495 |
+
# Convert points to world space
|
| 496 |
+
world_points = camera_space_to_world_space(camera_points, c2w) # (..., h, w, 3)
|
| 497 |
+
|
| 498 |
+
return world_points
|
multivue/cameras/source/src/depth_anything_3/utils/gsply_helpers.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Optional
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
from einops import rearrange, repeat
|
| 19 |
+
from plyfile import PlyData, PlyElement
|
| 20 |
+
from torch import Tensor
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.specs import Gaussians
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def construct_list_of_attributes(num_rest: int) -> list[str]:
|
| 26 |
+
attributes = ["x", "y", "z", "nx", "ny", "nz"]
|
| 27 |
+
for i in range(3):
|
| 28 |
+
attributes.append(f"f_dc_{i}")
|
| 29 |
+
for i in range(num_rest):
|
| 30 |
+
attributes.append(f"f_rest_{i}")
|
| 31 |
+
attributes.append("opacity")
|
| 32 |
+
for i in range(3):
|
| 33 |
+
attributes.append(f"scale_{i}")
|
| 34 |
+
for i in range(4):
|
| 35 |
+
attributes.append(f"rot_{i}")
|
| 36 |
+
return attributes
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def export_ply(
|
| 40 |
+
means: Tensor, # "gaussian 3"
|
| 41 |
+
scales: Tensor, # "gaussian 3"
|
| 42 |
+
rotations: Tensor, # "gaussian 4"
|
| 43 |
+
harmonics: Tensor, # "gaussian 3 d_sh"
|
| 44 |
+
opacities: Tensor, # "gaussian"
|
| 45 |
+
path: Path,
|
| 46 |
+
shift_and_scale: bool = False,
|
| 47 |
+
save_sh_dc_only: bool = True,
|
| 48 |
+
match_3dgs_mcmc_dev: Optional[bool] = False,
|
| 49 |
+
):
|
| 50 |
+
if shift_and_scale:
|
| 51 |
+
# Shift the scene so that the median Gaussian is at the origin.
|
| 52 |
+
means = means - means.median(dim=0).values
|
| 53 |
+
|
| 54 |
+
# Rescale the scene so that most Gaussians are within range [-1, 1].
|
| 55 |
+
scale_factor = means.abs().quantile(0.95, dim=0).max()
|
| 56 |
+
means = means / scale_factor
|
| 57 |
+
scales = scales / scale_factor
|
| 58 |
+
|
| 59 |
+
rotations = rotations.detach().cpu().numpy()
|
| 60 |
+
|
| 61 |
+
# Since current model use SH_degree = 4,
|
| 62 |
+
# which require large memory to store, we can only save the DC band to save memory.
|
| 63 |
+
f_dc = harmonics[..., 0]
|
| 64 |
+
f_rest = harmonics[..., 1:].flatten(start_dim=1)
|
| 65 |
+
|
| 66 |
+
if match_3dgs_mcmc_dev:
|
| 67 |
+
sh_degree = 3
|
| 68 |
+
n_rest = 3 * (sh_degree + 1) ** 2 - 3
|
| 69 |
+
f_rest = repeat(
|
| 70 |
+
torch.zeros_like(harmonics[..., :1]), "... i -> ... (n i)", n=(n_rest // 3)
|
| 71 |
+
).flatten(start_dim=1)
|
| 72 |
+
dtype_full = [
|
| 73 |
+
(attribute, "f4")
|
| 74 |
+
for attribute in construct_list_of_attributes(num_rest=n_rest)
|
| 75 |
+
if attribute not in ("nx", "ny", "nz")
|
| 76 |
+
]
|
| 77 |
+
else:
|
| 78 |
+
dtype_full = [
|
| 79 |
+
(attribute, "f4")
|
| 80 |
+
for attribute in construct_list_of_attributes(
|
| 81 |
+
0 if save_sh_dc_only else f_rest.shape[1]
|
| 82 |
+
)
|
| 83 |
+
]
|
| 84 |
+
elements = np.empty(means.shape[0], dtype=dtype_full)
|
| 85 |
+
attributes = [
|
| 86 |
+
means.detach().cpu().numpy(),
|
| 87 |
+
torch.zeros_like(means).detach().cpu().numpy(),
|
| 88 |
+
f_dc.detach().cpu().contiguous().numpy(),
|
| 89 |
+
f_rest.detach().cpu().contiguous().numpy(),
|
| 90 |
+
opacities[..., None].detach().cpu().numpy(),
|
| 91 |
+
scales.log().detach().cpu().numpy(),
|
| 92 |
+
rotations,
|
| 93 |
+
]
|
| 94 |
+
if match_3dgs_mcmc_dev:
|
| 95 |
+
attributes.pop(1) # dummy normal is not needed
|
| 96 |
+
elif save_sh_dc_only:
|
| 97 |
+
attributes.pop(3) # remove f_rest from attributes
|
| 98 |
+
|
| 99 |
+
attributes = np.concatenate(attributes, axis=1)
|
| 100 |
+
elements[:] = list(map(tuple, attributes))
|
| 101 |
+
path.parent.mkdir(exist_ok=True, parents=True)
|
| 102 |
+
PlyData([PlyElement.describe(elements, "vertex")]).write(path)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def inverse_sigmoid(x):
|
| 106 |
+
return torch.log(x / (1 - x))
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def save_gaussian_ply(
|
| 110 |
+
gaussians: Gaussians,
|
| 111 |
+
save_path: str,
|
| 112 |
+
ctx_depth: torch.Tensor, # depth of input views; for getting shape and filtering, "v h w 1"
|
| 113 |
+
shift_and_scale: bool = False,
|
| 114 |
+
save_sh_dc_only: bool = True,
|
| 115 |
+
gs_views_interval: int = 1,
|
| 116 |
+
inv_opacity: Optional[bool] = True,
|
| 117 |
+
prune_by_depth_percent: Optional[float] = 1.0,
|
| 118 |
+
prune_border_gs: Optional[bool] = True,
|
| 119 |
+
match_3dgs_mcmc_dev: Optional[bool] = False,
|
| 120 |
+
):
|
| 121 |
+
b = gaussians.means.shape[0]
|
| 122 |
+
assert b == 1, "must set batch_size=1 when exporting 3D gaussians"
|
| 123 |
+
src_v, out_h, out_w, _ = ctx_depth.shape
|
| 124 |
+
|
| 125 |
+
# extract gs params
|
| 126 |
+
world_means = gaussians.means
|
| 127 |
+
world_shs = gaussians.harmonics
|
| 128 |
+
world_rotations = gaussians.rotations
|
| 129 |
+
gs_scales = gaussians.scales
|
| 130 |
+
gs_opacities = inverse_sigmoid(gaussians.opacities) if inv_opacity else gaussians.opacities
|
| 131 |
+
|
| 132 |
+
# Create a mask to filter the Gaussians.
|
| 133 |
+
|
| 134 |
+
# TODO: prune the sky region here
|
| 135 |
+
|
| 136 |
+
# throw away Gaussians at the borders, since they're generally of lower quality.
|
| 137 |
+
if prune_border_gs:
|
| 138 |
+
mask = torch.zeros_like(ctx_depth, dtype=torch.bool)
|
| 139 |
+
gstrim_h = int(8 / 256 * out_h)
|
| 140 |
+
gstrim_w = int(8 / 256 * out_w)
|
| 141 |
+
mask[:, gstrim_h:-gstrim_h, gstrim_w:-gstrim_w, :] = 1
|
| 142 |
+
else:
|
| 143 |
+
mask = torch.ones_like(ctx_depth, dtype=torch.bool)
|
| 144 |
+
|
| 145 |
+
# trim the far away point based on depth;
|
| 146 |
+
if prune_by_depth_percent is not None and prune_by_depth_percent < 1:
|
| 147 |
+
in_depths = ctx_depth
|
| 148 |
+
d_percentile = torch.quantile(
|
| 149 |
+
in_depths.view(in_depths.shape[0], -1), q=prune_by_depth_percent, dim=1
|
| 150 |
+
).view(-1, 1, 1)
|
| 151 |
+
d_mask = (in_depths[..., 0] <= d_percentile).unsqueeze(-1)
|
| 152 |
+
mask = mask & d_mask
|
| 153 |
+
mask = mask.squeeze(-1) # v h w
|
| 154 |
+
|
| 155 |
+
# helper fn, must place after mask
|
| 156 |
+
def trim_select_reshape(element):
|
| 157 |
+
selected_element = rearrange(
|
| 158 |
+
element[0], "(v h w) ... -> v h w ...", v=src_v, h=out_h, w=out_w
|
| 159 |
+
)
|
| 160 |
+
selected_element = selected_element[::gs_views_interval][mask[::gs_views_interval]]
|
| 161 |
+
return selected_element
|
| 162 |
+
|
| 163 |
+
export_ply(
|
| 164 |
+
means=trim_select_reshape(world_means),
|
| 165 |
+
scales=trim_select_reshape(gs_scales),
|
| 166 |
+
rotations=trim_select_reshape(world_rotations),
|
| 167 |
+
harmonics=trim_select_reshape(world_shs),
|
| 168 |
+
opacities=trim_select_reshape(gs_opacities),
|
| 169 |
+
path=Path(save_path),
|
| 170 |
+
shift_and_scale=shift_and_scale,
|
| 171 |
+
save_sh_dc_only=save_sh_dc_only,
|
| 172 |
+
match_3dgs_mcmc_dev=match_3dgs_mcmc_dev,
|
| 173 |
+
)
|
multivue/cameras/source/src/depth_anything_3/utils/io/__pycache__/input_processor.cpython-312.pyc
ADDED
|
Binary file (21.8 kB). View file
|
|
|
multivue/cameras/source/src/depth_anything_3/utils/io/input_processor.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Input processor for Depth Anything 3 (parallelized).
|
| 17 |
+
|
| 18 |
+
This version removes the square center-crop step for "*crop" methods (same as your note).
|
| 19 |
+
In addition, it parallelizes per-image preprocessing using the provided `parallel_execution`.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
from typing import Sequence
|
| 25 |
+
import cv2
|
| 26 |
+
import numpy as np
|
| 27 |
+
import torch
|
| 28 |
+
import torchvision.transforms as T
|
| 29 |
+
from PIL import Image
|
| 30 |
+
|
| 31 |
+
from depth_anything_3.utils.logger import logger
|
| 32 |
+
from depth_anything_3.utils.parallel_utils import parallel_execution
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class InputProcessor:
|
| 36 |
+
"""Prepares a batch of images for model inference.
|
| 37 |
+
This processor converts a list of image file paths into a single, model-ready
|
| 38 |
+
tensor. The processing pipeline is executed in parallel across multiple workers
|
| 39 |
+
for efficiency.
|
| 40 |
+
|
| 41 |
+
Pipeline:
|
| 42 |
+
1) Load image and convert to RGB
|
| 43 |
+
2) Boundary resize (upper/lower bound, preserving aspect ratio)
|
| 44 |
+
3) Enforce divisibility by PATCH_SIZE:
|
| 45 |
+
- "*resize" methods: each dimension is rounded to nearest multiple
|
| 46 |
+
(may up/downscale a few px)
|
| 47 |
+
- "*crop" methods: each dimension is floored to nearest multiple via center crop
|
| 48 |
+
4) Convert to tensor and apply ImageNet normalization
|
| 49 |
+
5) Stack into (1, N, 3, H, W)
|
| 50 |
+
|
| 51 |
+
Parallelization:
|
| 52 |
+
- Each image is processed independently in a worker.
|
| 53 |
+
- Order of outputs matches the input order.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
NORMALIZE = T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| 57 |
+
PATCH_SIZE = 14
|
| 58 |
+
|
| 59 |
+
def __init__(self):
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
# -----------------------------
|
| 63 |
+
# Public API
|
| 64 |
+
# -----------------------------
|
| 65 |
+
def __call__(
|
| 66 |
+
self,
|
| 67 |
+
image: list[np.ndarray | Image.Image | str],
|
| 68 |
+
extrinsics: np.ndarray | None = None,
|
| 69 |
+
intrinsics: np.ndarray | None = None,
|
| 70 |
+
process_res: int = 504,
|
| 71 |
+
process_res_method: str = "upper_bound_resize",
|
| 72 |
+
*,
|
| 73 |
+
num_workers: int = 8,
|
| 74 |
+
print_progress: bool = False,
|
| 75 |
+
sequential: bool | None = None,
|
| 76 |
+
desc: str | None = "Preprocess",
|
| 77 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
| 78 |
+
"""
|
| 79 |
+
Returns:
|
| 80 |
+
(tensor, extrinsics_list, intrinsics_list)
|
| 81 |
+
tensor shape: (1, N, 3, H, W)
|
| 82 |
+
"""
|
| 83 |
+
sequential = self._resolve_sequential(sequential, num_workers)
|
| 84 |
+
exts_list, ixts_list = self._validate_and_pack_meta(image, extrinsics, intrinsics)
|
| 85 |
+
|
| 86 |
+
results = self._run_parallel(
|
| 87 |
+
image=image,
|
| 88 |
+
exts_list=exts_list,
|
| 89 |
+
ixts_list=ixts_list,
|
| 90 |
+
process_res=process_res,
|
| 91 |
+
process_res_method=process_res_method,
|
| 92 |
+
num_workers=num_workers,
|
| 93 |
+
print_progress=print_progress,
|
| 94 |
+
sequential=sequential,
|
| 95 |
+
desc=desc,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
proc_imgs, out_sizes, out_ixts, out_exts = self._unpack_results(results)
|
| 99 |
+
proc_imgs, out_sizes, out_ixts = self._unify_batch_shapes(proc_imgs, out_sizes, out_ixts)
|
| 100 |
+
|
| 101 |
+
batch_tensor = self._stack_batch(proc_imgs)
|
| 102 |
+
out_exts = (
|
| 103 |
+
torch.from_numpy(np.asarray(out_exts)).float()
|
| 104 |
+
if out_exts is not None and out_exts[0] is not None
|
| 105 |
+
else None
|
| 106 |
+
)
|
| 107 |
+
out_ixts = (
|
| 108 |
+
torch.from_numpy(np.asarray(out_ixts)).float()
|
| 109 |
+
if out_ixts is not None and out_ixts[0] is not None
|
| 110 |
+
else None
|
| 111 |
+
)
|
| 112 |
+
return (batch_tensor, out_exts, out_ixts)
|
| 113 |
+
|
| 114 |
+
# -----------------------------
|
| 115 |
+
# __call__ helpers
|
| 116 |
+
# -----------------------------
|
| 117 |
+
def _resolve_sequential(self, sequential: bool | None, num_workers: int) -> bool:
|
| 118 |
+
return (num_workers <= 1) if sequential is None else sequential
|
| 119 |
+
|
| 120 |
+
def _validate_and_pack_meta(
|
| 121 |
+
self,
|
| 122 |
+
images: list[np.ndarray | Image.Image | str],
|
| 123 |
+
extrinsics: np.ndarray | None,
|
| 124 |
+
intrinsics: np.ndarray | None,
|
| 125 |
+
) -> tuple[list[np.ndarray | None] | None, list[np.ndarray | None] | None]:
|
| 126 |
+
if extrinsics is not None and len(extrinsics) != len(images):
|
| 127 |
+
raise ValueError("Length of extrinsics must match images when provided.")
|
| 128 |
+
if intrinsics is not None and len(intrinsics) != len(images):
|
| 129 |
+
raise ValueError("Length of intrinsics must match images when provided.")
|
| 130 |
+
exts_list = [e for e in extrinsics] if extrinsics is not None else None
|
| 131 |
+
ixts_list = [k for k in intrinsics] if intrinsics is not None else None
|
| 132 |
+
return exts_list, ixts_list
|
| 133 |
+
|
| 134 |
+
def _run_parallel(
|
| 135 |
+
self,
|
| 136 |
+
*,
|
| 137 |
+
image: list[np.ndarray | Image.Image | str],
|
| 138 |
+
exts_list: list[np.ndarray | None] | None,
|
| 139 |
+
ixts_list: list[np.ndarray | None] | None,
|
| 140 |
+
process_res: int,
|
| 141 |
+
process_res_method: str,
|
| 142 |
+
num_workers: int,
|
| 143 |
+
print_progress: bool,
|
| 144 |
+
sequential: bool,
|
| 145 |
+
desc: str | None,
|
| 146 |
+
):
|
| 147 |
+
results = parallel_execution(
|
| 148 |
+
image,
|
| 149 |
+
exts_list,
|
| 150 |
+
ixts_list,
|
| 151 |
+
action=self._process_one, # (img, extrinsic, intrinsic, ...)
|
| 152 |
+
num_processes=num_workers,
|
| 153 |
+
print_progress=print_progress,
|
| 154 |
+
sequential=sequential,
|
| 155 |
+
desc=desc,
|
| 156 |
+
process_res=process_res,
|
| 157 |
+
process_res_method=process_res_method,
|
| 158 |
+
)
|
| 159 |
+
if not results:
|
| 160 |
+
raise RuntimeError(
|
| 161 |
+
"No preprocessing results returned. Check inputs and parallel_execution."
|
| 162 |
+
)
|
| 163 |
+
return results
|
| 164 |
+
|
| 165 |
+
def _unpack_results(self, results):
|
| 166 |
+
"""
|
| 167 |
+
results: List[Tuple[torch.Tensor, Tuple[H, W], Optional[np.ndarray], Optional[np.ndarray]]]
|
| 168 |
+
-> processed_images, out_sizes, out_intrinsics, out_extrinsics
|
| 169 |
+
"""
|
| 170 |
+
try:
|
| 171 |
+
processed_images, out_sizes, out_intrinsics, out_extrinsics = zip(*results)
|
| 172 |
+
except Exception as e:
|
| 173 |
+
raise RuntimeError(
|
| 174 |
+
"Unexpected results structure from parallel_execution: "
|
| 175 |
+
f"{type(results)} / sample: {results[0]}"
|
| 176 |
+
) from e
|
| 177 |
+
|
| 178 |
+
return list(processed_images), list(out_sizes), list(out_intrinsics), list(out_extrinsics)
|
| 179 |
+
|
| 180 |
+
def _unify_batch_shapes(
|
| 181 |
+
self,
|
| 182 |
+
processed_images: list[torch.Tensor],
|
| 183 |
+
out_sizes: list[tuple[int, int]],
|
| 184 |
+
out_intrinsics: list[np.ndarray | None],
|
| 185 |
+
) -> tuple[list[torch.Tensor], list[tuple[int, int]], list[np.ndarray | None]]:
|
| 186 |
+
"""Center-crop all tensors to the smallest H, W; adjust intrinsics' cx, cy accordingly."""
|
| 187 |
+
if len(set(out_sizes)) <= 1:
|
| 188 |
+
return processed_images, out_sizes, out_intrinsics
|
| 189 |
+
|
| 190 |
+
min_h = min(h for h, _ in out_sizes)
|
| 191 |
+
min_w = min(w for _, w in out_sizes)
|
| 192 |
+
logger.warn(
|
| 193 |
+
f"Images in batch have different sizes {out_sizes}; "
|
| 194 |
+
f"center-cropping all to smallest ({min_h},{min_w})"
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
center_crop = T.CenterCrop((min_h, min_w))
|
| 198 |
+
new_imgs, new_sizes, new_ixts = [], [], []
|
| 199 |
+
for img_t, (H, W), K in zip(processed_images, out_sizes, out_intrinsics):
|
| 200 |
+
crop_top = max(0, (H - min_h) // 2)
|
| 201 |
+
crop_left = max(0, (W - min_w) // 2)
|
| 202 |
+
new_imgs.append(center_crop(img_t))
|
| 203 |
+
new_sizes.append((min_h, min_w))
|
| 204 |
+
if K is None:
|
| 205 |
+
new_ixts.append(None)
|
| 206 |
+
else:
|
| 207 |
+
K_adj = K.copy()
|
| 208 |
+
K_adj[0, 2] -= crop_left
|
| 209 |
+
K_adj[1, 2] -= crop_top
|
| 210 |
+
new_ixts.append(K_adj)
|
| 211 |
+
return new_imgs, new_sizes, new_ixts
|
| 212 |
+
|
| 213 |
+
def _stack_batch(self, processed_images: list[torch.Tensor]) -> torch.Tensor:
|
| 214 |
+
return torch.stack(processed_images)
|
| 215 |
+
|
| 216 |
+
# -----------------------------
|
| 217 |
+
# Per-item worker
|
| 218 |
+
# -----------------------------
|
| 219 |
+
def _process_one(
|
| 220 |
+
self,
|
| 221 |
+
img: np.ndarray | Image.Image | str,
|
| 222 |
+
extrinsic: np.ndarray | None = None,
|
| 223 |
+
intrinsic: np.ndarray | None = None,
|
| 224 |
+
*,
|
| 225 |
+
process_res: int,
|
| 226 |
+
process_res_method: str,
|
| 227 |
+
) -> tuple[torch.Tensor, tuple[int, int], np.ndarray | None, np.ndarray | None]:
|
| 228 |
+
# Load & remember original size
|
| 229 |
+
pil_img = self._load_image(img)
|
| 230 |
+
orig_w, orig_h = pil_img.size
|
| 231 |
+
|
| 232 |
+
# Boundary resize
|
| 233 |
+
pil_img = self._resize_image(pil_img, process_res, process_res_method)
|
| 234 |
+
w, h = pil_img.size
|
| 235 |
+
intrinsic = self._resize_ixt(intrinsic, orig_w, orig_h, w, h)
|
| 236 |
+
|
| 237 |
+
# Enforce divisibility by PATCH_SIZE
|
| 238 |
+
if process_res_method.endswith("resize"):
|
| 239 |
+
pil_img = self._make_divisible_by_resize(pil_img, self.PATCH_SIZE)
|
| 240 |
+
new_w, new_h = pil_img.size
|
| 241 |
+
intrinsic = self._resize_ixt(intrinsic, w, h, new_w, new_h)
|
| 242 |
+
w, h = new_w, new_h
|
| 243 |
+
elif process_res_method.endswith("crop"):
|
| 244 |
+
pil_img = self._make_divisible_by_crop(pil_img, self.PATCH_SIZE)
|
| 245 |
+
new_w, new_h = pil_img.size
|
| 246 |
+
intrinsic = self._crop_ixt(intrinsic, w, h, new_w, new_h)
|
| 247 |
+
w, h = new_w, new_h
|
| 248 |
+
else:
|
| 249 |
+
raise ValueError(f"Unsupported process_res_method: {process_res_method}")
|
| 250 |
+
|
| 251 |
+
# Convert to tensor & normalize
|
| 252 |
+
img_tensor = self._normalize_image(pil_img)
|
| 253 |
+
_, H, W = img_tensor.shape
|
| 254 |
+
assert (W, H) == (w, h), "Tensor size mismatch with PIL image size after processing."
|
| 255 |
+
|
| 256 |
+
# Return: (img_tensor, (H, W), intrinsic, extrinsic)
|
| 257 |
+
return img_tensor, (H, W), intrinsic, extrinsic
|
| 258 |
+
|
| 259 |
+
# -----------------------------
|
| 260 |
+
# Intrinsics transforms
|
| 261 |
+
# -----------------------------
|
| 262 |
+
def _resize_ixt(
|
| 263 |
+
self,
|
| 264 |
+
intrinsic: np.ndarray | None,
|
| 265 |
+
orig_w: int,
|
| 266 |
+
orig_h: int,
|
| 267 |
+
w: int,
|
| 268 |
+
h: int,
|
| 269 |
+
) -> np.ndarray | None:
|
| 270 |
+
if intrinsic is None:
|
| 271 |
+
return None
|
| 272 |
+
K = intrinsic.copy()
|
| 273 |
+
# scale fx, cx by w ratio; fy, cy by h ratio
|
| 274 |
+
K[:1] *= w / float(orig_w)
|
| 275 |
+
K[1:2] *= h / float(orig_h)
|
| 276 |
+
return K
|
| 277 |
+
|
| 278 |
+
def _crop_ixt(
|
| 279 |
+
self,
|
| 280 |
+
intrinsic: np.ndarray | None,
|
| 281 |
+
orig_w: int,
|
| 282 |
+
orig_h: int,
|
| 283 |
+
w: int,
|
| 284 |
+
h: int,
|
| 285 |
+
) -> np.ndarray | None:
|
| 286 |
+
if intrinsic is None:
|
| 287 |
+
return None
|
| 288 |
+
K = intrinsic.copy()
|
| 289 |
+
crop_h = (orig_h - h) // 2
|
| 290 |
+
crop_w = (orig_w - w) // 2
|
| 291 |
+
K[0, 2] -= crop_w
|
| 292 |
+
K[1, 2] -= crop_h
|
| 293 |
+
return K
|
| 294 |
+
|
| 295 |
+
# -----------------------------
|
| 296 |
+
# I/O & normalization
|
| 297 |
+
# -----------------------------
|
| 298 |
+
def _load_image(self, img: np.ndarray | Image.Image | str) -> Image.Image:
|
| 299 |
+
if isinstance(img, str):
|
| 300 |
+
return Image.open(img).convert("RGB")
|
| 301 |
+
elif isinstance(img, np.ndarray):
|
| 302 |
+
# Assume HxWxC uint8/RGB
|
| 303 |
+
return Image.fromarray(img).convert("RGB")
|
| 304 |
+
elif isinstance(img, Image.Image):
|
| 305 |
+
return img.convert("RGB")
|
| 306 |
+
else:
|
| 307 |
+
raise ValueError(f"Unsupported image type: {type(img)}")
|
| 308 |
+
|
| 309 |
+
def _normalize_image(self, img: Image.Image) -> torch.Tensor:
|
| 310 |
+
img_tensor = T.ToTensor()(img)
|
| 311 |
+
return self.NORMALIZE(img_tensor)
|
| 312 |
+
|
| 313 |
+
# -----------------------------
|
| 314 |
+
# Boundary resizing
|
| 315 |
+
# -----------------------------
|
| 316 |
+
def _resize_image(self, img: Image.Image, target_size: int, method: str) -> Image.Image:
|
| 317 |
+
if method in ("upper_bound_resize", "upper_bound_crop"):
|
| 318 |
+
return self._resize_longest_side(img, target_size)
|
| 319 |
+
elif method in ("lower_bound_resize", "lower_bound_crop"):
|
| 320 |
+
return self._resize_shortest_side(img, target_size)
|
| 321 |
+
else:
|
| 322 |
+
raise ValueError(f"Unsupported resize method: {method}")
|
| 323 |
+
|
| 324 |
+
def _resize_longest_side(self, img: Image.Image, target_size: int) -> Image.Image:
|
| 325 |
+
w, h = img.size
|
| 326 |
+
longest = max(w, h)
|
| 327 |
+
if longest == target_size:
|
| 328 |
+
return img
|
| 329 |
+
scale = target_size / float(longest)
|
| 330 |
+
new_w = max(1, int(round(w * scale)))
|
| 331 |
+
new_h = max(1, int(round(h * scale)))
|
| 332 |
+
interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
|
| 333 |
+
arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation)
|
| 334 |
+
return Image.fromarray(arr)
|
| 335 |
+
|
| 336 |
+
def _resize_shortest_side(self, img: Image.Image, target_size: int) -> Image.Image:
|
| 337 |
+
w, h = img.size
|
| 338 |
+
shortest = min(w, h)
|
| 339 |
+
if shortest == target_size:
|
| 340 |
+
return img
|
| 341 |
+
scale = target_size / float(shortest)
|
| 342 |
+
new_w = max(1, int(round(w * scale)))
|
| 343 |
+
new_h = max(1, int(round(h * scale)))
|
| 344 |
+
interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
|
| 345 |
+
arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation)
|
| 346 |
+
return Image.fromarray(arr)
|
| 347 |
+
|
| 348 |
+
# -----------------------------
|
| 349 |
+
# Make divisible by PATCH_SIZE
|
| 350 |
+
# -----------------------------
|
| 351 |
+
def _make_divisible_by_crop(self, img: Image.Image, patch: int) -> Image.Image:
|
| 352 |
+
"""
|
| 353 |
+
Floor each dimension to the nearest multiple of PATCH_SIZE via center crop.
|
| 354 |
+
Example: 504x377 -> 504x364
|
| 355 |
+
"""
|
| 356 |
+
w, h = img.size
|
| 357 |
+
new_w = (w // patch) * patch
|
| 358 |
+
new_h = (h // patch) * patch
|
| 359 |
+
if new_w == w and new_h == h:
|
| 360 |
+
return img
|
| 361 |
+
left = (w - new_w) // 2
|
| 362 |
+
top = (h - new_h) // 2
|
| 363 |
+
return img.crop((left, top, left + new_w, top + new_h))
|
| 364 |
+
|
| 365 |
+
def _make_divisible_by_resize(self, img: Image.Image, patch: int) -> Image.Image:
|
| 366 |
+
"""
|
| 367 |
+
Round each dimension to nearest multiple of PATCH_SIZE via small resize.
|
| 368 |
+
"""
|
| 369 |
+
w, h = img.size
|
| 370 |
+
|
| 371 |
+
def nearest_multiple(x: int, p: int) -> int:
|
| 372 |
+
down = (x // p) * p
|
| 373 |
+
up = down + p
|
| 374 |
+
return up if abs(up - x) <= abs(x - down) else down
|
| 375 |
+
|
| 376 |
+
new_w = max(1, nearest_multiple(w, patch))
|
| 377 |
+
new_h = max(1, nearest_multiple(h, patch))
|
| 378 |
+
if new_w == w and new_h == h:
|
| 379 |
+
return img
|
| 380 |
+
upscale = (new_w > w) or (new_h > h)
|
| 381 |
+
interpolation = cv2.INTER_CUBIC if upscale else cv2.INTER_AREA
|
| 382 |
+
arr = cv2.resize(np.asarray(img), (new_w, new_h), interpolation=interpolation)
|
| 383 |
+
return Image.fromarray(arr)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# Backward compatibility alias
|
| 387 |
+
InputAdapter = InputProcessor
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
# ===========================
|
| 391 |
+
# Minimal test runner (parallel execution)
|
| 392 |
+
# ===========================
|
| 393 |
+
if __name__ == "__main__":
|
| 394 |
+
"""
|
| 395 |
+
Minimal test suite:
|
| 396 |
+
- Creates pairs of images so batch shapes match.
|
| 397 |
+
- Tests all four process_res_methods.
|
| 398 |
+
- Prints fx fy cx cy IN->OUT per image.
|
| 399 |
+
- Includes cases with K/E provided and with None.
|
| 400 |
+
"""
|
| 401 |
+
|
| 402 |
+
def fmt_k_line(K: np.ndarray | None) -> str:
|
| 403 |
+
if K is None:
|
| 404 |
+
return "None"
|
| 405 |
+
fx, fy, cx, cy = float(K[0, 0]), float(K[1, 1]), float(K[0, 2]), float(K[1, 2])
|
| 406 |
+
return f"fx={fx:.3f} fy={fy:.3f} cx={cx:.3f} cy={cy:.3f}"
|
| 407 |
+
|
| 408 |
+
def show_result(
|
| 409 |
+
tag: str,
|
| 410 |
+
tensor: torch.Tensor,
|
| 411 |
+
Ks_in: Sequence[np.ndarray | None] | None = None,
|
| 412 |
+
Ks_out: Sequence[np.ndarray | None] | None = None,
|
| 413 |
+
):
|
| 414 |
+
B, N, C, H, W = tensor.shape
|
| 415 |
+
print(f"[{tag}] shape={tuple(tensor.shape)} HxW=({H},{W}) div14=({H%14==0},{W%14==0})")
|
| 416 |
+
assert H % 14 == 0 and W % 14 == 0, f"{tag}: output size not divisible by 14!"
|
| 417 |
+
if Ks_in is not None or Ks_out is not None:
|
| 418 |
+
Ks_in = Ks_in or [None] * N
|
| 419 |
+
Ks_out = Ks_out or [None] * N
|
| 420 |
+
for i in range(N):
|
| 421 |
+
print(f" K[{i}]: {fmt_k_line(Ks_in[i])} -> {fmt_k_line(Ks_out[i])}")
|
| 422 |
+
|
| 423 |
+
proc = InputProcessor()
|
| 424 |
+
process_res = 504
|
| 425 |
+
methods = ["upper_bound_resize", "upper_bound_crop", "lower_bound_resize", "lower_bound_crop"]
|
| 426 |
+
|
| 427 |
+
# Example sizes (two orientations)
|
| 428 |
+
small_sizes = [(680, 1208), (1208, 680)]
|
| 429 |
+
large_sizes = [(1208, 680), (680, 1208)]
|
| 430 |
+
|
| 431 |
+
def make_K(w, h, fx=1200.0, fy=1100.0):
|
| 432 |
+
cx, cy = w / 2.0, h / 2.0
|
| 433 |
+
K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32)
|
| 434 |
+
return K
|
| 435 |
+
|
| 436 |
+
def run_suite(suite_name: str, sizes: list[tuple[int, int]]):
|
| 437 |
+
print(f"\n===== {suite_name} =====")
|
| 438 |
+
for w, h in sizes:
|
| 439 |
+
img = Image.new("RGB", (w, h), color=(123, 222, 100))
|
| 440 |
+
batch_imgs = [img, img]
|
| 441 |
+
|
| 442 |
+
# intrinsics / extrinsics examples
|
| 443 |
+
Ks_in = [make_K(w, h), make_K(w, h)]
|
| 444 |
+
Es_in = [np.eye(4, dtype=np.float32), np.eye(4, dtype=np.float32)]
|
| 445 |
+
|
| 446 |
+
for m in methods:
|
| 447 |
+
tensor, Es_out, Ks_out = proc(
|
| 448 |
+
image=batch_imgs,
|
| 449 |
+
process_res=process_res,
|
| 450 |
+
process_res_method=m,
|
| 451 |
+
num_workers=8,
|
| 452 |
+
print_progress=False,
|
| 453 |
+
intrinsics=Ks_in, # test with non-None
|
| 454 |
+
extrinsics=Es_in,
|
| 455 |
+
)
|
| 456 |
+
show_result(f"{suite_name} size=({w},{h}) | {m}", tensor, Ks_in, Ks_out)
|
| 457 |
+
|
| 458 |
+
# Also test None path
|
| 459 |
+
tensor2, Es_out2, Ks_out2 = proc(
|
| 460 |
+
image=batch_imgs,
|
| 461 |
+
process_res=process_res,
|
| 462 |
+
process_res_method="upper_bound_resize",
|
| 463 |
+
num_workers=8,
|
| 464 |
+
intrinsics=None,
|
| 465 |
+
extrinsics=None,
|
| 466 |
+
)
|
| 467 |
+
show_result(
|
| 468 |
+
f"{suite_name} size=({w},{h}) | upper_bound_resize | no K/E",
|
| 469 |
+
tensor2,
|
| 470 |
+
None,
|
| 471 |
+
Ks_out2,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
run_suite("SMALL", small_sizes)
|
| 475 |
+
run_suite("LARGE", large_sizes)
|
| 476 |
+
|
| 477 |
+
# Extra sanity for 504x376
|
| 478 |
+
print("\n===== EXTRA sanity for 504x376 =====")
|
| 479 |
+
img_example = Image.new("RGB", (504, 376), color=(10, 20, 30))
|
| 480 |
+
Ks_in_extra = [make_K(504, 376, fx=900.0, fy=900.0), make_K(504, 376, fx=900.0, fy=900.0)]
|
| 481 |
+
|
| 482 |
+
out_r, _, Ks_out_r = proc(
|
| 483 |
+
image=[img_example, img_example],
|
| 484 |
+
process_res=504,
|
| 485 |
+
process_res_method="upper_bound_resize",
|
| 486 |
+
num_workers=8,
|
| 487 |
+
intrinsics=Ks_in_extra,
|
| 488 |
+
)
|
| 489 |
+
out_c, _, Ks_out_c = proc(
|
| 490 |
+
image=[img_example, img_example],
|
| 491 |
+
process_res=504,
|
| 492 |
+
process_res_method="upper_bound_crop",
|
| 493 |
+
num_workers=8,
|
| 494 |
+
intrinsics=Ks_in_extra,
|
| 495 |
+
)
|
| 496 |
+
_, _, _, Hr, Wr = out_r.shape
|
| 497 |
+
_, _, _, Hc, Wc = out_c.shape
|
| 498 |
+
print(f"upper_bound_resize -> ({Hr},{Wr}) (rounded to nearest multiple of 14)")
|
| 499 |
+
show_result("Ks after upper_bound_resize", out_r, Ks_in_extra, Ks_out_r)
|
| 500 |
+
print(f"upper_bound_crop -> ({Hc},{Wc}) (floored to multiple of 14)")
|
| 501 |
+
show_result("Ks after upper_bound_crop", out_c, Ks_in_extra, Ks_out_c)
|
multivue/cameras/source/src/depth_anything_3/utils/io/output_processor.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Output processor for Depth Anything 3.
|
| 17 |
+
|
| 18 |
+
This module handles model output processing, including tensor-to-numpy conversion,
|
| 19 |
+
batch dimension removal, and Prediction object creation.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import torch
|
| 26 |
+
from addict import Dict as AddictDict
|
| 27 |
+
|
| 28 |
+
from depth_anything_3.specs import Prediction
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class OutputProcessor:
|
| 32 |
+
"""
|
| 33 |
+
Output processor for converting model outputs to Prediction objects.
|
| 34 |
+
|
| 35 |
+
Handles tensor-to-numpy conversion, batch dimension removal,
|
| 36 |
+
and creates structured Prediction objects with proper data types.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self) -> None:
|
| 40 |
+
"""Initialize the output processor."""
|
| 41 |
+
|
| 42 |
+
def __call__(self, model_output: dict[str, torch.Tensor]) -> Prediction:
|
| 43 |
+
"""
|
| 44 |
+
Convert model output to Prediction object.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
model_output: Model output dictionary containing depth, conf, extrinsics, intrinsics
|
| 48 |
+
Expected shapes: depth (B, N, 1, H, W), conf (B, N, 1, H, W),
|
| 49 |
+
extrinsics (B, N, 4, 4), intrinsics (B, N, 3, 3)
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
Prediction: Object containing depth estimation results with shapes:
|
| 53 |
+
depth (N, H, W), conf (N, H, W), extrinsics (N, 4, 4), intrinsics (N, 3, 3)
|
| 54 |
+
"""
|
| 55 |
+
# Extract data from batch dimension (B=1, N=number of images)
|
| 56 |
+
depth = self._extract_depth(model_output)
|
| 57 |
+
conf = self._extract_conf(model_output)
|
| 58 |
+
extrinsics = self._extract_extrinsics(model_output)
|
| 59 |
+
intrinsics = self._extract_intrinsics(model_output)
|
| 60 |
+
sky = self._extract_sky(model_output)
|
| 61 |
+
aux = self._extract_aux(model_output)
|
| 62 |
+
gaussians = model_output.get("gaussians", None)
|
| 63 |
+
scale_factor = model_output.get("scale_factor", None)
|
| 64 |
+
|
| 65 |
+
return Prediction(
|
| 66 |
+
depth=depth,
|
| 67 |
+
sky=sky,
|
| 68 |
+
conf=conf,
|
| 69 |
+
extrinsics=extrinsics,
|
| 70 |
+
intrinsics=intrinsics,
|
| 71 |
+
is_metric=getattr(model_output, "is_metric", 0),
|
| 72 |
+
gaussians=gaussians,
|
| 73 |
+
aux=aux,
|
| 74 |
+
scale_factor=scale_factor,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
def _extract_depth(self, model_output: dict[str, torch.Tensor]) -> np.ndarray:
|
| 78 |
+
"""
|
| 79 |
+
Extract depth tensor from model output and convert to numpy.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
model_output: Model output dictionary
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Depth array with shape (N, H, W)
|
| 86 |
+
"""
|
| 87 |
+
depth = model_output["depth"].squeeze(0).squeeze(-1).cpu().numpy() # (N, H, W)
|
| 88 |
+
return depth
|
| 89 |
+
|
| 90 |
+
def _extract_conf(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None:
|
| 91 |
+
"""
|
| 92 |
+
Extract confidence tensor from model output and convert to numpy.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
model_output: Model output dictionary
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
Confidence array with shape (N, H, W) or None
|
| 99 |
+
"""
|
| 100 |
+
conf = model_output.get("depth_conf", None)
|
| 101 |
+
if conf is not None:
|
| 102 |
+
conf = conf.squeeze(0).cpu().numpy() # (N, H, W)
|
| 103 |
+
return conf
|
| 104 |
+
|
| 105 |
+
def _extract_extrinsics(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None:
|
| 106 |
+
"""
|
| 107 |
+
Extract extrinsics tensor from model output and convert to numpy.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
model_output: Model output dictionary
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
Extrinsics array with shape (N, 4, 4) or None
|
| 114 |
+
"""
|
| 115 |
+
extrinsics = model_output.get("extrinsics", None)
|
| 116 |
+
if extrinsics is not None:
|
| 117 |
+
extrinsics = extrinsics.squeeze(0).cpu().numpy() # (N, 4, 4)
|
| 118 |
+
return extrinsics
|
| 119 |
+
|
| 120 |
+
def _extract_intrinsics(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None:
|
| 121 |
+
"""
|
| 122 |
+
Extract intrinsics tensor from model output and convert to numpy.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
model_output: Model output dictionary
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Intrinsics array with shape (N, 3, 3) or None
|
| 129 |
+
"""
|
| 130 |
+
intrinsics = model_output.get("intrinsics", None)
|
| 131 |
+
if intrinsics is not None:
|
| 132 |
+
intrinsics = intrinsics.squeeze(0).cpu().numpy() # (N, 3, 3)
|
| 133 |
+
return intrinsics
|
| 134 |
+
|
| 135 |
+
def _extract_sky(self, model_output: dict[str, torch.Tensor]) -> np.ndarray | None:
|
| 136 |
+
"""
|
| 137 |
+
Extract sky tensor from model output and convert to numpy.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
model_output: Model output dictionary
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
Sky mask array with shape (N, H, W) or None
|
| 144 |
+
"""
|
| 145 |
+
sky = model_output.get("sky", None)
|
| 146 |
+
if sky is not None:
|
| 147 |
+
sky = sky.squeeze(0).cpu().numpy() >= 0.5 # (N, H, W)
|
| 148 |
+
return sky
|
| 149 |
+
|
| 150 |
+
def _extract_aux(self, model_output: dict[str, torch.Tensor]) -> AddictDict:
|
| 151 |
+
"""
|
| 152 |
+
Extract auxiliary data from model output and convert to numpy.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
model_output: Model output dictionary
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Dictionary containing auxiliary data
|
| 159 |
+
"""
|
| 160 |
+
aux = model_output.get("aux", None)
|
| 161 |
+
ret = AddictDict()
|
| 162 |
+
if aux is not None:
|
| 163 |
+
for k in aux.keys():
|
| 164 |
+
if isinstance(aux[k], torch.Tensor):
|
| 165 |
+
ret[k] = aux[k].squeeze(0).cpu().numpy()
|
| 166 |
+
else:
|
| 167 |
+
ret[k] = aux[k]
|
| 168 |
+
return ret
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# Backward compatibility alias
|
| 172 |
+
OutputAdapter = OutputProcessor
|
multivue/cameras/source/src/depth_anything_3/utils/layout_helpers.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""This file contains useful layout utilities for images. They are:
|
| 16 |
+
|
| 17 |
+
- add_border: Add a border to an image.
|
| 18 |
+
- cat/hcat/vcat: Join images by arranging them in a line. If the images have different
|
| 19 |
+
sizes, they are aligned as specified (start, end, center). Allows you to specify a gap
|
| 20 |
+
between images.
|
| 21 |
+
|
| 22 |
+
Images are assumed to be float32 tensors with shape (channel, height, width).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from typing import Any, Generator, Iterable, Literal, Union
|
| 26 |
+
import torch
|
| 27 |
+
from torch import Tensor
|
| 28 |
+
|
| 29 |
+
Alignment = Literal["start", "center", "end"]
|
| 30 |
+
Axis = Literal["horizontal", "vertical"]
|
| 31 |
+
Color = Union[
|
| 32 |
+
int,
|
| 33 |
+
float,
|
| 34 |
+
Iterable[int],
|
| 35 |
+
Iterable[float],
|
| 36 |
+
Tensor,
|
| 37 |
+
Tensor,
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _sanitize_color(color: Color) -> Tensor: # "#channel"
|
| 42 |
+
# Convert tensor to list (or individual item).
|
| 43 |
+
if isinstance(color, torch.Tensor):
|
| 44 |
+
color = color.tolist()
|
| 45 |
+
|
| 46 |
+
# Turn iterators and individual items into lists.
|
| 47 |
+
if isinstance(color, Iterable):
|
| 48 |
+
color = list(color)
|
| 49 |
+
else:
|
| 50 |
+
color = [color]
|
| 51 |
+
|
| 52 |
+
return torch.tensor(color, dtype=torch.float32)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _intersperse(iterable: Iterable, delimiter: Any) -> Generator[Any, None, None]:
|
| 56 |
+
it = iter(iterable)
|
| 57 |
+
yield next(it)
|
| 58 |
+
for item in it:
|
| 59 |
+
yield delimiter
|
| 60 |
+
yield item
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _get_main_dim(main_axis: Axis) -> int:
|
| 64 |
+
return {
|
| 65 |
+
"horizontal": 2,
|
| 66 |
+
"vertical": 1,
|
| 67 |
+
}[main_axis]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _get_cross_dim(main_axis: Axis) -> int:
|
| 71 |
+
return {
|
| 72 |
+
"horizontal": 1,
|
| 73 |
+
"vertical": 2,
|
| 74 |
+
}[main_axis]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _compute_offset(base: int, overlay: int, align: Alignment) -> slice:
|
| 78 |
+
assert base >= overlay
|
| 79 |
+
offset = {
|
| 80 |
+
"start": 0,
|
| 81 |
+
"center": (base - overlay) // 2,
|
| 82 |
+
"end": base - overlay,
|
| 83 |
+
}[align]
|
| 84 |
+
return slice(offset, offset + overlay)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def overlay(
|
| 88 |
+
base: Tensor, # "channel base_height base_width"
|
| 89 |
+
overlay: Tensor, # "channel overlay_height overlay_width"
|
| 90 |
+
main_axis: Axis,
|
| 91 |
+
main_axis_alignment: Alignment,
|
| 92 |
+
cross_axis_alignment: Alignment,
|
| 93 |
+
) -> Tensor: # "channel base_height base_width"
|
| 94 |
+
# The overlay must be smaller than the base.
|
| 95 |
+
_, base_height, base_width = base.shape
|
| 96 |
+
_, overlay_height, overlay_width = overlay.shape
|
| 97 |
+
assert base_height >= overlay_height and base_width >= overlay_width
|
| 98 |
+
|
| 99 |
+
# Compute spacing on the main dimension.
|
| 100 |
+
main_dim = _get_main_dim(main_axis)
|
| 101 |
+
main_slice = _compute_offset(
|
| 102 |
+
base.shape[main_dim], overlay.shape[main_dim], main_axis_alignment
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Compute spacing on the cross dimension.
|
| 106 |
+
cross_dim = _get_cross_dim(main_axis)
|
| 107 |
+
cross_slice = _compute_offset(
|
| 108 |
+
base.shape[cross_dim], overlay.shape[cross_dim], cross_axis_alignment
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Combine the slices and paste the overlay onto the base accordingly.
|
| 112 |
+
selector = [..., None, None]
|
| 113 |
+
selector[main_dim] = main_slice
|
| 114 |
+
selector[cross_dim] = cross_slice
|
| 115 |
+
result = base.clone()
|
| 116 |
+
result[selector] = overlay
|
| 117 |
+
return result
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def cat(
|
| 121 |
+
main_axis: Axis,
|
| 122 |
+
*images: Iterable[Tensor], # "channel _ _"
|
| 123 |
+
align: Alignment = "center",
|
| 124 |
+
gap: int = 8,
|
| 125 |
+
gap_color: Color = 1,
|
| 126 |
+
) -> Tensor: # "channel height width"
|
| 127 |
+
"""Arrange images in a line. The interface resembles a CSS div with flexbox."""
|
| 128 |
+
device = images[0].device
|
| 129 |
+
gap_color = _sanitize_color(gap_color).to(device)
|
| 130 |
+
|
| 131 |
+
# Find the maximum image side length in the cross axis dimension.
|
| 132 |
+
cross_dim = _get_cross_dim(main_axis)
|
| 133 |
+
cross_axis_length = max(image.shape[cross_dim] for image in images)
|
| 134 |
+
|
| 135 |
+
# Pad the images.
|
| 136 |
+
padded_images = []
|
| 137 |
+
for image in images:
|
| 138 |
+
# Create an empty image with the correct size.
|
| 139 |
+
padded_shape = list(image.shape)
|
| 140 |
+
padded_shape[cross_dim] = cross_axis_length
|
| 141 |
+
base = torch.ones(padded_shape, dtype=torch.float32, device=device)
|
| 142 |
+
base = base * gap_color[:, None, None]
|
| 143 |
+
padded_images.append(overlay(base, image, main_axis, "start", align))
|
| 144 |
+
|
| 145 |
+
# Intersperse separators if necessary.
|
| 146 |
+
if gap > 0:
|
| 147 |
+
# Generate a separator.
|
| 148 |
+
c, _, _ = images[0].shape
|
| 149 |
+
separator_size = [gap, gap]
|
| 150 |
+
separator_size[cross_dim - 1] = cross_axis_length
|
| 151 |
+
separator = torch.ones((c, *separator_size), dtype=torch.float32, device=device)
|
| 152 |
+
separator = separator * gap_color[:, None, None]
|
| 153 |
+
|
| 154 |
+
# Intersperse the separator between the images.
|
| 155 |
+
padded_images = list(_intersperse(padded_images, separator))
|
| 156 |
+
|
| 157 |
+
return torch.cat(padded_images, dim=_get_main_dim(main_axis))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def hcat(
|
| 161 |
+
*images: Iterable[Tensor], # "channel _ _"
|
| 162 |
+
align: Literal["start", "center", "end", "top", "bottom"] = "start",
|
| 163 |
+
gap: int = 8,
|
| 164 |
+
gap_color: Color = 1,
|
| 165 |
+
):
|
| 166 |
+
"""Shorthand for a horizontal linear concatenation."""
|
| 167 |
+
return cat(
|
| 168 |
+
"horizontal",
|
| 169 |
+
*images,
|
| 170 |
+
align={
|
| 171 |
+
"start": "start",
|
| 172 |
+
"center": "center",
|
| 173 |
+
"end": "end",
|
| 174 |
+
"top": "start",
|
| 175 |
+
"bottom": "end",
|
| 176 |
+
}[align],
|
| 177 |
+
gap=gap,
|
| 178 |
+
gap_color=gap_color,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def vcat(
|
| 183 |
+
*images: Iterable[Tensor], # "channel _ _"
|
| 184 |
+
align: Literal["start", "center", "end", "left", "right"] = "start",
|
| 185 |
+
gap: int = 8,
|
| 186 |
+
gap_color: Color = 1,
|
| 187 |
+
):
|
| 188 |
+
"""Shorthand for a horizontal linear concatenation."""
|
| 189 |
+
return cat(
|
| 190 |
+
"vertical",
|
| 191 |
+
*images,
|
| 192 |
+
align={
|
| 193 |
+
"start": "start",
|
| 194 |
+
"center": "center",
|
| 195 |
+
"end": "end",
|
| 196 |
+
"left": "start",
|
| 197 |
+
"right": "end",
|
| 198 |
+
}[align],
|
| 199 |
+
gap=gap,
|
| 200 |
+
gap_color=gap_color,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def add_border(
|
| 205 |
+
image: Tensor, # "channel height width"
|
| 206 |
+
border: int = 8,
|
| 207 |
+
color: Color = 1,
|
| 208 |
+
) -> Tensor: # "channel new_height new_width"
|
| 209 |
+
color = _sanitize_color(color).to(image)
|
| 210 |
+
c, h, w = image.shape
|
| 211 |
+
result = torch.empty(
|
| 212 |
+
(c, h + 2 * border, w + 2 * border), dtype=torch.float32, device=image.device
|
| 213 |
+
)
|
| 214 |
+
result[:] = color[:, None, None]
|
| 215 |
+
result[:, border : h + border, border : w + border] = image
|
| 216 |
+
return result
|
multivue/cameras/source/src/depth_anything_3/utils/logger.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Color:
|
| 20 |
+
RED = "\033[91m"
|
| 21 |
+
YELLOW = "\033[93m"
|
| 22 |
+
WHITE = "\033[97m"
|
| 23 |
+
GREEN = "\033[92m"
|
| 24 |
+
RESET = "\033[0m"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
LOG_LEVELS = {"ERROR": 0, "WARN": 1, "INFO": 2, "DEBUG": 3}
|
| 28 |
+
|
| 29 |
+
COLOR_MAP = {"ERROR": Color.RED, "WARN": Color.YELLOW, "INFO": Color.WHITE, "DEBUG": Color.GREEN}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_env_log_level():
|
| 33 |
+
level = os.environ.get("DA3_LOG_LEVEL", "INFO").upper()
|
| 34 |
+
return LOG_LEVELS.get(level, LOG_LEVELS["INFO"])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Logger:
|
| 38 |
+
def __init__(self):
|
| 39 |
+
self.level = get_env_log_level()
|
| 40 |
+
|
| 41 |
+
def log(self, level_str, *args, **kwargs):
|
| 42 |
+
level_key = level_str.split(":")[0].strip()
|
| 43 |
+
level_val = LOG_LEVELS.get(level_key)
|
| 44 |
+
if level_val is None:
|
| 45 |
+
raise ValueError(f"Unknown log level: {level_str}")
|
| 46 |
+
if self.level >= level_val:
|
| 47 |
+
color = COLOR_MAP[level_key]
|
| 48 |
+
msg = " ".join(str(arg) for arg in args)
|
| 49 |
+
|
| 50 |
+
# Align log level output in square brackets
|
| 51 |
+
# ERROR and DEBUG are 5 characters, INFO and WARN have an extra space for alignment
|
| 52 |
+
tag = level_key
|
| 53 |
+
if tag in ("INFO", "WARN"):
|
| 54 |
+
tag += " "
|
| 55 |
+
print(
|
| 56 |
+
f"{color}[{tag}] {msg}{Color.RESET}",
|
| 57 |
+
file=sys.stderr if level_key == "ERROR" else sys.stdout,
|
| 58 |
+
**kwargs,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
def error(self, *args, **kwargs):
|
| 62 |
+
self.log("ERROR:", *args, **kwargs)
|
| 63 |
+
|
| 64 |
+
def warn(self, *args, **kwargs):
|
| 65 |
+
self.log("WARN:", *args, **kwargs)
|
| 66 |
+
|
| 67 |
+
def info(self, *args, **kwargs):
|
| 68 |
+
self.log("INFO:", *args, **kwargs)
|
| 69 |
+
|
| 70 |
+
def debug(self, *args, **kwargs):
|
| 71 |
+
self.log("DEBUG:", *args, **kwargs)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
logger = Logger()
|
| 75 |
+
|
| 76 |
+
__all__ = ["logger"]
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
logger.info("This is an info message")
|
| 80 |
+
logger.warn("This is a warning message")
|
| 81 |
+
logger.error("This is an error message")
|
| 82 |
+
logger.debug("This is a debug message")
|
multivue/cameras/source/src/depth_anything_3/utils/memory.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GPU memory utility helpers.
|
| 3 |
+
|
| 4 |
+
Shared cleanup and memory checking logic used by both the backend API and
|
| 5 |
+
the Gradio UI to keep memory-management behavior consistent.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import gc
|
| 10 |
+
|
| 11 |
+
from typing import Any, Dict, Optional
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def get_gpu_memory_info() -> Optional[Dict[str, Any]]:
|
| 17 |
+
"""Return a snapshot of current GPU memory usage or None if CUDA not available.
|
| 18 |
+
|
| 19 |
+
Keys in returned dict: total_gb, allocated_gb, reserved_gb, free_gb, utilization
|
| 20 |
+
"""
|
| 21 |
+
if not torch.cuda.is_available():
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
device = torch.cuda.current_device()
|
| 26 |
+
total_memory = torch.cuda.get_device_properties(device).total_memory
|
| 27 |
+
allocated_memory = torch.cuda.memory_allocated(device)
|
| 28 |
+
reserved_memory = torch.cuda.memory_reserved(device)
|
| 29 |
+
free_memory = total_memory - reserved_memory
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
"total_gb": total_memory / 1024 ** 3,
|
| 33 |
+
"allocated_gb": allocated_memory / 1024 ** 3,
|
| 34 |
+
"reserved_gb": reserved_memory / 1024 ** 3,
|
| 35 |
+
"free_gb": free_memory / 1024 ** 3,
|
| 36 |
+
"utilization": (reserved_memory / total_memory) * 100,
|
| 37 |
+
}
|
| 38 |
+
except Exception:
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def cleanup_cuda_memory() -> None:
|
| 43 |
+
"""Perform a robust GPU cleanup sequence.
|
| 44 |
+
|
| 45 |
+
This includes synchronizing, emptying caches, collecting IPC handles and
|
| 46 |
+
running the Python garbage collector. Use this instead of a raw
|
| 47 |
+
``torch.cuda.empty_cache()`` where you need reliable freeing of GPU memory
|
| 48 |
+
between model loads or in error handling paths.
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
if torch.cuda.is_available():
|
| 52 |
+
mem_before = get_gpu_memory_info()
|
| 53 |
+
|
| 54 |
+
torch.cuda.synchronize()
|
| 55 |
+
torch.cuda.empty_cache()
|
| 56 |
+
# Collect cross-process cuda resources
|
| 57 |
+
try:
|
| 58 |
+
torch.cuda.ipc_collect()
|
| 59 |
+
except Exception:
|
| 60 |
+
# Older PyTorch versions or non-cuda devices may not support
|
| 61 |
+
# ipc_collect (no-op if not available)
|
| 62 |
+
pass
|
| 63 |
+
gc.collect()
|
| 64 |
+
|
| 65 |
+
mem_after = get_gpu_memory_info()
|
| 66 |
+
if mem_before and mem_after:
|
| 67 |
+
freed = mem_before["reserved_gb"] - mem_after["reserved_gb"]
|
| 68 |
+
print(
|
| 69 |
+
f"CUDA cleanup: freed {freed:.2f}GB, "
|
| 70 |
+
f"available: {mem_after['free_gb']:.2f}GB/{mem_after['total_gb']:.2f}GB"
|
| 71 |
+
)
|
| 72 |
+
else:
|
| 73 |
+
print("CUDA memory cleanup completed")
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"Warning: CUDA cleanup failed: {e}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def check_memory_availability(required_gb: float = 2.0) -> tuple[bool, str]:
|
| 79 |
+
"""Return whether at least ``required_gb`` seems available on the current GPU.
|
| 80 |
+
|
| 81 |
+
The returned tuple is (is_available, message) with a human-friendly message.
|
| 82 |
+
"""
|
| 83 |
+
try:
|
| 84 |
+
if not torch.cuda.is_available():
|
| 85 |
+
return False, "CUDA is not available"
|
| 86 |
+
|
| 87 |
+
mem_info = get_gpu_memory_info()
|
| 88 |
+
if mem_info is None:
|
| 89 |
+
return True, "Cannot check memory, proceeding anyway"
|
| 90 |
+
|
| 91 |
+
if mem_info["free_gb"] < required_gb:
|
| 92 |
+
return (
|
| 93 |
+
False,
|
| 94 |
+
(
|
| 95 |
+
f"Insufficient GPU memory: {mem_info['free_gb']:.2f}GB available, "
|
| 96 |
+
f"{required_gb:.2f}GB required. Total: {mem_info['total_gb']:.2f}GB, "
|
| 97 |
+
f"Used: {mem_info['reserved_gb']:.2f}GB ({mem_info['utilization']:.1f}%)"
|
| 98 |
+
),
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
True,
|
| 103 |
+
(
|
| 104 |
+
f"Memory check passed: {mem_info['free_gb']:.2f}GB available, "
|
| 105 |
+
f"{required_gb:.2f}GB required"
|
| 106 |
+
),
|
| 107 |
+
)
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return True, f"Memory check failed: {e}, proceeding anyway"
|
| 110 |
+
def estimate_memory_requirement(num_images: int, process_res: int) -> float:
|
| 111 |
+
"""Heuristic estimate for memory usage (GB) based on image count and resolution.
|
| 112 |
+
|
| 113 |
+
This mirrors the simple policy used by the backend service so other code
|
| 114 |
+
(e.g., Gradio UI) can make consistent decisions when checking available
|
| 115 |
+
memory before loading a model or running inference.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
num_images: Number of images to process.
|
| 119 |
+
process_res: Processing resolution.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Estimated memory requirement in GB.
|
| 123 |
+
"""
|
| 124 |
+
base_memory = 2.0
|
| 125 |
+
per_image_memory = (process_res / 504) ** 2 * 0.5
|
| 126 |
+
total_memory = base_memory + (num_images * per_image_memory * 0.1)
|
| 127 |
+
return total_memory
|
multivue/cameras/source/src/depth_anything_3/utils/model_loading.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Model loading and state dict conversion utilities.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from typing import Dict, Tuple
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from depth_anything_3.utils.logger import logger
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def convert_general_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 26 |
+
"""
|
| 27 |
+
Convert general model state dict to match current model architecture.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
state_dict: Original state dictionary
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Converted state dictionary
|
| 34 |
+
"""
|
| 35 |
+
# Replace module prefixes
|
| 36 |
+
state_dict = {k.replace("module.", "model."): v for k, v in state_dict.items()}
|
| 37 |
+
state_dict = {k.replace(".net.", ".backbone."): v for k, v in state_dict.items()}
|
| 38 |
+
|
| 39 |
+
# Remove camera token if present
|
| 40 |
+
if "model.backbone.pretrained.camera_token" in state_dict:
|
| 41 |
+
del state_dict["model.backbone.pretrained.camera_token"]
|
| 42 |
+
|
| 43 |
+
# Replace camera token naming
|
| 44 |
+
state_dict = {
|
| 45 |
+
k.replace(".camera_token_extra", ".camera_token"): v for k, v in state_dict.items()
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Replace head naming
|
| 49 |
+
state_dict = {
|
| 50 |
+
k.replace("model.all_heads.camera_cond_head", "model.cam_enc"): v
|
| 51 |
+
for k, v in state_dict.items()
|
| 52 |
+
}
|
| 53 |
+
state_dict = {
|
| 54 |
+
k.replace("model.all_heads.camera_head", "model.cam_dec"): v for k, v in state_dict.items()
|
| 55 |
+
}
|
| 56 |
+
state_dict = {k.replace(".more_mlps.", ".backbone."): v for k, v in state_dict.items()}
|
| 57 |
+
state_dict = {k.replace(".fc_rot.", ".fc_qvec."): v for k, v in state_dict.items()}
|
| 58 |
+
state_dict = {
|
| 59 |
+
k.replace("model.all_heads.head", "model.head"): v for k, v in state_dict.items()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# Replace output naming
|
| 63 |
+
state_dict = {
|
| 64 |
+
k.replace("output_conv2_additional.sky_mask", "sky_output_conv2"): v
|
| 65 |
+
for k, v in state_dict.items()
|
| 66 |
+
}
|
| 67 |
+
state_dict = {k.replace("_ray.", "_aux."): v for k, v in state_dict.items()}
|
| 68 |
+
|
| 69 |
+
# Update GS-DPT head naming and value
|
| 70 |
+
state_dict = {k.replace("gaussian_param_head.", "gs_head."): v for k, v in state_dict.items()}
|
| 71 |
+
|
| 72 |
+
return state_dict
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def convert_metric_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 76 |
+
"""
|
| 77 |
+
Convert metric model state dict to match current model architecture.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
state_dict: Original metric state dictionary
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
Converted state dictionary
|
| 84 |
+
"""
|
| 85 |
+
# Add module prefix for metric models
|
| 86 |
+
state_dict = {"module." + k: v for k, v in state_dict.items()}
|
| 87 |
+
return convert_general_state_dict(state_dict)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def load_pretrained_weights(model, model_path: str, is_metric: bool = False) -> Tuple[list, list]:
|
| 91 |
+
"""
|
| 92 |
+
Load pretrained weights for a single model.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
model: Model instance to load weights into
|
| 96 |
+
model_path: Path to the pretrained weights
|
| 97 |
+
is_metric: Whether this is a metric model
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Tuple of (missed_keys, unexpected_keys)
|
| 101 |
+
"""
|
| 102 |
+
state_dict = torch.load(model_path, map_location="cpu")
|
| 103 |
+
|
| 104 |
+
if is_metric:
|
| 105 |
+
state_dict = convert_metric_state_dict(state_dict)
|
| 106 |
+
else:
|
| 107 |
+
state_dict = convert_general_state_dict(state_dict)
|
| 108 |
+
|
| 109 |
+
missed, unexpected = model.load_state_dict(state_dict, strict=False)
|
| 110 |
+
logger.info("Missed keys:", missed)
|
| 111 |
+
logger.info("Unexpected keys:", unexpected)
|
| 112 |
+
|
| 113 |
+
return missed, unexpected
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def load_pretrained_nested_weights(
|
| 117 |
+
model, main_model_path: str, metric_model_path: str
|
| 118 |
+
) -> Tuple[list, list]:
|
| 119 |
+
"""
|
| 120 |
+
Load pretrained weights for a nested model with both main and metric branches.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
model: Nested model instance
|
| 124 |
+
main_model_path: Path to main model weights
|
| 125 |
+
metric_model_path: Path to metric model weights
|
| 126 |
+
|
| 127 |
+
Returns:
|
| 128 |
+
Tuple of (missed_keys, unexpected_keys)
|
| 129 |
+
"""
|
| 130 |
+
# Load main model weights
|
| 131 |
+
state_dict0 = torch.load(main_model_path, map_location="cpu")
|
| 132 |
+
state_dict0 = convert_general_state_dict(state_dict0)
|
| 133 |
+
state_dict0 = {k.replace("model.", "model.da3."): v for k, v in state_dict0.items()}
|
| 134 |
+
|
| 135 |
+
# Load metric model weights
|
| 136 |
+
state_dict1 = torch.load(metric_model_path, map_location="cpu")
|
| 137 |
+
state_dict1 = convert_metric_state_dict(state_dict1)
|
| 138 |
+
state_dict1 = {k.replace("model.", "model.da3_metric."): v for k, v in state_dict1.items()}
|
| 139 |
+
|
| 140 |
+
# Combine state dictionaries
|
| 141 |
+
combined_state_dict = state_dict0.copy()
|
| 142 |
+
combined_state_dict.update(state_dict1)
|
| 143 |
+
|
| 144 |
+
missed, unexpected = model.load_state_dict(combined_state_dict, strict=False)
|
| 145 |
+
|
| 146 |
+
print("Missed keys:", missed)
|
| 147 |
+
print("Unexpected keys:", unexpected)
|
| 148 |
+
|
| 149 |
+
return missed, unexpected
|
multivue/cameras/source/src/depth_anything_3/utils/parallel_utils.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import asyncio
|
| 16 |
+
import os
|
| 17 |
+
from functools import wraps
|
| 18 |
+
from multiprocessing.pool import ThreadPool
|
| 19 |
+
from threading import Thread
|
| 20 |
+
from typing import Callable, Dict, List
|
| 21 |
+
import imageio
|
| 22 |
+
from tqdm import tqdm
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def async_call_func(func):
|
| 26 |
+
@wraps(func)
|
| 27 |
+
async def wrapper(*args, **kwargs):
|
| 28 |
+
loop = asyncio.get_event_loop()
|
| 29 |
+
# Use run_in_executor to run the blocking function in a separate thread
|
| 30 |
+
return await loop.run_in_executor(None, func, *args, **kwargs)
|
| 31 |
+
|
| 32 |
+
return wrapper
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
slice_func = lambda chunk_index, chunk_dim, chunk_size: [slice(None)] * chunk_dim + [
|
| 36 |
+
slice(chunk_index, chunk_index + chunk_size)
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def async_call(fn):
|
| 41 |
+
def wrapper(*args, **kwargs):
|
| 42 |
+
Thread(target=fn, args=args, kwargs=kwargs).start()
|
| 43 |
+
|
| 44 |
+
return wrapper
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _save_image_impl(save_img, save_path):
|
| 48 |
+
"""Common implementation for saving images synchronously or asynchronously"""
|
| 49 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
| 50 |
+
imageio.imwrite(save_path, save_img)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@async_call
|
| 54 |
+
def save_image_async(save_img, save_path):
|
| 55 |
+
"""Save image asynchronously"""
|
| 56 |
+
_save_image_impl(save_img, save_path)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def save_image(save_img, save_path):
|
| 60 |
+
"""Save image synchronously"""
|
| 61 |
+
_save_image_impl(save_img, save_path)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def parallel_execution(
|
| 65 |
+
*args,
|
| 66 |
+
action: Callable,
|
| 67 |
+
num_processes=32,
|
| 68 |
+
print_progress=False,
|
| 69 |
+
sequential=False,
|
| 70 |
+
async_return=False,
|
| 71 |
+
desc=None,
|
| 72 |
+
**kwargs,
|
| 73 |
+
):
|
| 74 |
+
# Partially copy from EasyVolumetricVideo (parallel_execution)
|
| 75 |
+
# NOTE: we expect first arg / or kwargs to be distributed
|
| 76 |
+
# NOTE: print_progress arg is reserved.
|
| 77 |
+
# `*args` packs all positional arguments passed to the function into a tuple
|
| 78 |
+
args = list(args)
|
| 79 |
+
|
| 80 |
+
def get_length(args: List, kwargs: Dict):
|
| 81 |
+
for a in args:
|
| 82 |
+
if isinstance(a, list):
|
| 83 |
+
return len(a)
|
| 84 |
+
for v in kwargs.values():
|
| 85 |
+
if isinstance(v, list):
|
| 86 |
+
return len(v)
|
| 87 |
+
raise NotImplementedError
|
| 88 |
+
|
| 89 |
+
def get_action_args(length: int, args: List, kwargs: Dict, i: int):
|
| 90 |
+
action_args = [
|
| 91 |
+
(arg[i] if isinstance(arg, list) and len(arg) == length else arg) for arg in args
|
| 92 |
+
]
|
| 93 |
+
# TODO: Support all types of iterable
|
| 94 |
+
action_kwargs = {
|
| 95 |
+
key: (
|
| 96 |
+
kwargs[key][i]
|
| 97 |
+
if isinstance(kwargs[key], list) and len(kwargs[key]) == length
|
| 98 |
+
else kwargs[key]
|
| 99 |
+
)
|
| 100 |
+
for key in kwargs
|
| 101 |
+
}
|
| 102 |
+
return action_args, action_kwargs
|
| 103 |
+
|
| 104 |
+
if not sequential:
|
| 105 |
+
# Create ThreadPool
|
| 106 |
+
pool = ThreadPool(processes=num_processes)
|
| 107 |
+
|
| 108 |
+
# Spawn threads
|
| 109 |
+
results = []
|
| 110 |
+
asyncs = []
|
| 111 |
+
length = get_length(args, kwargs)
|
| 112 |
+
for i in range(length):
|
| 113 |
+
action_args, action_kwargs = get_action_args(length, args, kwargs, i)
|
| 114 |
+
async_result = pool.apply_async(action, action_args, action_kwargs)
|
| 115 |
+
asyncs.append(async_result)
|
| 116 |
+
|
| 117 |
+
# Join threads and get return values
|
| 118 |
+
if not async_return:
|
| 119 |
+
for async_result in tqdm(asyncs, desc=desc, disable=not print_progress):
|
| 120 |
+
results.append(async_result.get()) # will sync the corresponding thread
|
| 121 |
+
pool.close()
|
| 122 |
+
pool.join()
|
| 123 |
+
return results
|
| 124 |
+
else:
|
| 125 |
+
return pool
|
| 126 |
+
else:
|
| 127 |
+
results = []
|
| 128 |
+
length = get_length(args, kwargs)
|
| 129 |
+
for i in tqdm(range(length), desc=desc, disable=not print_progress):
|
| 130 |
+
action_args, action_kwargs = get_action_args(length, args, kwargs, i)
|
| 131 |
+
async_result = action(*action_args, **action_kwargs)
|
| 132 |
+
results.append(async_result)
|
| 133 |
+
return results
|
multivue/cameras/source/src/depth_anything_3/utils/pca_utils.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
PCA utilities for feature visualization and dimensionality reduction (video-friendly).
|
| 17 |
+
- Support frame-by-frame: transform_frame / transform_video
|
| 18 |
+
- Support one-time global PCA fitting and reuse (mean, V3) for stable colors
|
| 19 |
+
- Support Procrustes alignment (solving principal component order/sign/rotation jumps)
|
| 20 |
+
- Support global fixed or temporal EMA for percentiles (time dimension only, no spatial)
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def pca_to_rgb_4d_bf16_percentile(
|
| 28 |
+
x_np: np.ndarray,
|
| 29 |
+
device=None,
|
| 30 |
+
q_oversample: int = 6,
|
| 31 |
+
clip_percent: float = 10.0, # Percentage to clip from top and bottom (0~49.9)
|
| 32 |
+
return_uint8: bool = False,
|
| 33 |
+
enable_autocast_bf16: bool = True,
|
| 34 |
+
):
|
| 35 |
+
"""
|
| 36 |
+
Reduce numpy array of shape (49, 27, 36, 3072) to 3D via PCA and visualize as (49, 27, 36, 3).
|
| 37 |
+
- PCA uses torch.pca_lowrank (randomized SVD), defaults to GPU.
|
| 38 |
+
- Uses CUDA bf16 autocast in computation (if available),
|
| 39 |
+
then per-channel percentile clipping and normalization.
|
| 40 |
+
- Default removes 5% outliers from top and bottom (adjustable via clip_percent) to
|
| 41 |
+
improve visualization contrast.
|
| 42 |
+
|
| 43 |
+
Parameters
|
| 44 |
+
----------
|
| 45 |
+
x_np : np.ndarray
|
| 46 |
+
Shape must be (49, 27, 36, 3072). dtype recommended float32/float64.
|
| 47 |
+
device : str | None
|
| 48 |
+
Specify 'cuda' or 'cpu'. Auto-select if None (prefer cuda).
|
| 49 |
+
q_oversample : int
|
| 50 |
+
Oversampling q for pca_lowrank, must be >= 3.
|
| 51 |
+
Slightly larger than target dim (3) is more stable, default 6.
|
| 52 |
+
clip_percent : float
|
| 53 |
+
Percentage to clip from top and bottom (0~49.9),
|
| 54 |
+
e.g. 5.0 means clip lowest 5% and highest 5% per channel.
|
| 55 |
+
return_uint8 : bool
|
| 56 |
+
True returns uint8(0~255), otherwise returns float32(0~1).
|
| 57 |
+
enable_autocast_bf16 : bool
|
| 58 |
+
Enable bf16 autocast on CUDA.
|
| 59 |
+
|
| 60 |
+
Returns
|
| 61 |
+
-------
|
| 62 |
+
np.ndarray
|
| 63 |
+
Array of shape (49, 27, 36, 3), float32[0,1] or uint8[0,255].
|
| 64 |
+
"""
|
| 65 |
+
assert (
|
| 66 |
+
x_np.ndim == 4
|
| 67 |
+
) # and x_np.shape[-1] == 3072, f"expect (49,27,36,3072), got {x_np.shape}"
|
| 68 |
+
B1, B2, B3, D = x_np.shape
|
| 69 |
+
N = B1 * B2 * B3
|
| 70 |
+
|
| 71 |
+
# Device selection
|
| 72 |
+
if device is None:
|
| 73 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 74 |
+
|
| 75 |
+
# Convert input to torch, unified float32
|
| 76 |
+
X = torch.from_numpy(x_np.reshape(N, D)).to(device=device, dtype=torch.float32)
|
| 77 |
+
|
| 78 |
+
# Parameter and safety checks
|
| 79 |
+
k = 3
|
| 80 |
+
q = max(int(q_oversample), k)
|
| 81 |
+
clip_percent = float(clip_percent)
|
| 82 |
+
if not (0.0 <= clip_percent < 50.0):
|
| 83 |
+
raise ValueError(
|
| 84 |
+
"clip_percent must be in [0, 50), e.g. 5.0 means clip 5% from top and bottom"
|
| 85 |
+
)
|
| 86 |
+
low = clip_percent / 100.0
|
| 87 |
+
high = 1.0 - low
|
| 88 |
+
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
# Zero mean
|
| 91 |
+
mean = X.mean(dim=0, keepdim=True)
|
| 92 |
+
Xc = X - mean
|
| 93 |
+
|
| 94 |
+
# Main computation: PCA + projection, try to use bf16
|
| 95 |
+
# (auto-fallback if operator not supported)
|
| 96 |
+
device.startswith("cuda") and enable_autocast_bf16
|
| 97 |
+
U, S, V = torch.pca_lowrank(Xc, q=q, center=False) # V: (D, q)
|
| 98 |
+
V3 = V[:, :k] # (3072, 3)
|
| 99 |
+
PCs = Xc @ V3 # (N, 3)
|
| 100 |
+
|
| 101 |
+
# === Per-channel percentile clipping and normalization to [0,1] ===
|
| 102 |
+
# Vectorized one-time calculation of low/high percentiles for each channel
|
| 103 |
+
qs = torch.tensor([low, high], device=PCs.device, dtype=PCs.dtype)
|
| 104 |
+
qvals = torch.quantile(PCs, q=qs, dim=0) # Shape (2, 3)
|
| 105 |
+
lo = qvals[0] # (3,)
|
| 106 |
+
hi = qvals[1] # (3,)
|
| 107 |
+
|
| 108 |
+
# Avoid degenerate case where hi==lo
|
| 109 |
+
denom = torch.clamp(hi - lo, min=1e-8)
|
| 110 |
+
|
| 111 |
+
# Broadcast clipping + normalization
|
| 112 |
+
PCs = torch.clamp(PCs, lo, hi)
|
| 113 |
+
PCs = (PCs - lo) / denom # (N, 3) in [0,1]
|
| 114 |
+
|
| 115 |
+
# Restore 4D
|
| 116 |
+
PCs = PCs.reshape(B1, B2, B3, k)
|
| 117 |
+
|
| 118 |
+
# Output
|
| 119 |
+
if return_uint8:
|
| 120 |
+
out = (PCs * 255.0).round().clamp(0, 255).to(torch.uint8).cpu().numpy()
|
| 121 |
+
else:
|
| 122 |
+
out = PCs.clamp(0, 1).to(torch.float32).cpu().numpy()
|
| 123 |
+
|
| 124 |
+
return out
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class PCARGBVisualizer:
|
| 128 |
+
"""
|
| 129 |
+
Stable PCA→RGB for video features shaped (T, H, W, D) or a single frame (H, W, D).
|
| 130 |
+
- Global mean/V3 reference for stable colors
|
| 131 |
+
- Per-frame PCA with Procrustes alignment to V3_ref (basis_mode='procrustes')
|
| 132 |
+
- Percentile normalization with global or EMA stats (time-only, no spatial smoothing)
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
def __init__(
|
| 136 |
+
self,
|
| 137 |
+
device=None,
|
| 138 |
+
q_oversample: int = 16,
|
| 139 |
+
clip_percent: float = 10.0,
|
| 140 |
+
return_uint8: bool = False,
|
| 141 |
+
enable_autocast_bf16: bool = True,
|
| 142 |
+
basis_mode: str = "procrustes", # 'fixed' | 'procrustes'
|
| 143 |
+
percentile_mode: str = "ema", # 'global' | 'ema'
|
| 144 |
+
ema_alpha: float = 0.1,
|
| 145 |
+
denom_eps: float = 1e-4,
|
| 146 |
+
):
|
| 147 |
+
assert 0.0 <= clip_percent < 50.0
|
| 148 |
+
assert basis_mode in ("fixed", "procrustes")
|
| 149 |
+
assert percentile_mode in ("global", "ema")
|
| 150 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 151 |
+
self.q = max(int(q_oversample), 6)
|
| 152 |
+
self.clip_percent = float(clip_percent)
|
| 153 |
+
self.return_uint8 = return_uint8
|
| 154 |
+
self.enable_autocast_bf16 = enable_autocast_bf16
|
| 155 |
+
self.basis_mode = basis_mode
|
| 156 |
+
self.percentile_mode = percentile_mode
|
| 157 |
+
self.ema_alpha = float(ema_alpha)
|
| 158 |
+
self.denom_eps = float(denom_eps)
|
| 159 |
+
|
| 160 |
+
# reference state
|
| 161 |
+
self.mean_ref = None # (1, D)
|
| 162 |
+
self.V3_ref = None # (D, 3)
|
| 163 |
+
self.lo_ref = None # (3,)
|
| 164 |
+
self.hi_ref = None # (3,)
|
| 165 |
+
|
| 166 |
+
@torch.no_grad()
|
| 167 |
+
def fit_reference(self, frames):
|
| 168 |
+
"""
|
| 169 |
+
Fit global mean/V3 and initialize percentiles from a reference set.
|
| 170 |
+
frames: ndarray (T,H,W,D) or list of (H,W,D)
|
| 171 |
+
"""
|
| 172 |
+
if isinstance(frames, np.ndarray):
|
| 173 |
+
if frames.ndim != 4:
|
| 174 |
+
raise ValueError("fit_reference expects (T,H,W,D) ndarray.")
|
| 175 |
+
T, H, W, D = frames.shape
|
| 176 |
+
X = torch.from_numpy(frames.reshape(T * H * W, D))
|
| 177 |
+
else: # list of (H,W,D)
|
| 178 |
+
xs = [torch.from_numpy(x.reshape(-1, x.shape[-1])) for x in frames]
|
| 179 |
+
D = xs[0].shape[-1]
|
| 180 |
+
X = torch.cat(xs, dim=0)
|
| 181 |
+
|
| 182 |
+
X = X.to(self.device, dtype=torch.float32)
|
| 183 |
+
X = torch.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
| 184 |
+
|
| 185 |
+
mean = X.mean(0, keepdim=True)
|
| 186 |
+
Xc = X - mean
|
| 187 |
+
|
| 188 |
+
U, S, V = torch.pca_lowrank(Xc, q=max(self.q, 8), center=False)
|
| 189 |
+
V3 = V[:, :3] # (D,3)
|
| 190 |
+
|
| 191 |
+
PCs = Xc @ V3
|
| 192 |
+
low = self.clip_percent / 100.0
|
| 193 |
+
high = 1.0 - low
|
| 194 |
+
qs = torch.tensor([low, high], device=PCs.device, dtype=PCs.dtype)
|
| 195 |
+
qvals = torch.quantile(PCs, q=qs, dim=0)
|
| 196 |
+
lo, hi = qvals[0], qvals[1]
|
| 197 |
+
|
| 198 |
+
self.mean_ref = mean
|
| 199 |
+
self.V3_ref = V3
|
| 200 |
+
if self.percentile_mode == "global":
|
| 201 |
+
self.lo_ref, self.hi_ref = lo, hi
|
| 202 |
+
else:
|
| 203 |
+
self.lo_ref = lo.clone()
|
| 204 |
+
self.hi_ref = hi.clone()
|
| 205 |
+
|
| 206 |
+
@torch.no_grad()
|
| 207 |
+
def _project_with_stable_colors(self, X: torch.Tensor) -> torch.Tensor:
|
| 208 |
+
"""
|
| 209 |
+
X: (N,D) where N = H*W
|
| 210 |
+
Returns PCs_raw: (N,3) using stable basis (fixed or Procrustes-aligned)
|
| 211 |
+
"""
|
| 212 |
+
assert self.mean_ref is not None and self.V3_ref is not None, "Call fit_reference() first."
|
| 213 |
+
X = torch.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
| 214 |
+
Xc = X - self.mean_ref
|
| 215 |
+
|
| 216 |
+
if self.basis_mode == "fixed":
|
| 217 |
+
V3_used = self.V3_ref
|
| 218 |
+
else:
|
| 219 |
+
U, S, V = torch.pca_lowrank(Xc, q=max(self.q, 6), center=False)
|
| 220 |
+
V3 = V[:, :3] # (D,3)
|
| 221 |
+
M = V3.T @ self.V3_ref
|
| 222 |
+
Uo, So, Vh = torch.linalg.svd(M)
|
| 223 |
+
R = Uo @ Vh
|
| 224 |
+
V3_used = V3 @ R
|
| 225 |
+
# Optional polarity fix via anchor
|
| 226 |
+
a = self.V3_ref.mean(0, keepdim=True)
|
| 227 |
+
sign = torch.sign((V3_used * a).sum(0, keepdim=True)).clamp(min=-1)
|
| 228 |
+
V3_used = V3_used * sign
|
| 229 |
+
|
| 230 |
+
return Xc @ V3_used
|
| 231 |
+
|
| 232 |
+
@torch.no_grad()
|
| 233 |
+
def _normalize_rgb(self, PCs_raw: torch.Tensor) -> torch.Tensor:
|
| 234 |
+
assert self.lo_ref is not None and self.hi_ref is not None
|
| 235 |
+
if self.percentile_mode == "global":
|
| 236 |
+
lo, hi = self.lo_ref, self.hi_ref
|
| 237 |
+
else:
|
| 238 |
+
low = self.clip_percent / 100.0
|
| 239 |
+
high = 1.0 - low
|
| 240 |
+
qs = torch.tensor([low, high], device=PCs_raw.device, dtype=PCs_raw.dtype)
|
| 241 |
+
qvals = torch.quantile(PCs_raw, q=qs, dim=0)
|
| 242 |
+
lo_now, hi_now = qvals[0], qvals[1]
|
| 243 |
+
a = self.ema_alpha
|
| 244 |
+
self.lo_ref = (1 - a) * self.lo_ref + a * lo_now
|
| 245 |
+
self.hi_ref = (1 - a) * self.hi_ref + a * hi_now
|
| 246 |
+
lo, hi = self.lo_ref, self.hi_ref
|
| 247 |
+
|
| 248 |
+
denom = torch.clamp(hi - lo, min=self.denom_eps)
|
| 249 |
+
PCs = torch.clamp(PCs_raw, lo, hi)
|
| 250 |
+
PCs = (PCs - lo) / denom
|
| 251 |
+
return PCs.clamp_(0, 1)
|
| 252 |
+
|
| 253 |
+
@torch.no_grad()
|
| 254 |
+
def transform_frame(self, frame: np.ndarray) -> np.ndarray:
|
| 255 |
+
"""
|
| 256 |
+
frame: (H,W,D) -> (H,W,3)
|
| 257 |
+
"""
|
| 258 |
+
if frame.ndim != 3:
|
| 259 |
+
raise ValueError("transform_frame expects (H,W,D).")
|
| 260 |
+
H, W, D = frame.shape
|
| 261 |
+
X = torch.from_numpy(frame.reshape(H * W, D)).to(self.device, dtype=torch.float32)
|
| 262 |
+
PCs_raw = self._project_with_stable_colors(X)
|
| 263 |
+
PCs = self._normalize_rgb(PCs_raw).reshape(H, W, 3)
|
| 264 |
+
if self.return_uint8:
|
| 265 |
+
return (PCs * 255.0).round().clamp(0, 255).to(torch.uint8).cpu().numpy()
|
| 266 |
+
return PCs.to(torch.float32).cpu().numpy()
|
| 267 |
+
|
| 268 |
+
@torch.no_grad()
|
| 269 |
+
def transform_video(self, frames) -> np.ndarray:
|
| 270 |
+
"""
|
| 271 |
+
frames: (T,H,W,D) or list of (H,W,D)
|
| 272 |
+
returns: (T,H,W,3)
|
| 273 |
+
"""
|
| 274 |
+
outs = []
|
| 275 |
+
if isinstance(frames, np.ndarray):
|
| 276 |
+
if frames.ndim != 4:
|
| 277 |
+
raise ValueError("transform_video expects (T,H,W,D).")
|
| 278 |
+
T, H, W, D = frames.shape
|
| 279 |
+
for t in range(T):
|
| 280 |
+
outs.append(self.transform_frame(frames[t]))
|
| 281 |
+
else:
|
| 282 |
+
for f in frames:
|
| 283 |
+
outs.append(self.transform_frame(f))
|
| 284 |
+
return np.stack(outs, axis=0)
|
multivue/cameras/source/src/depth_anything_3/utils/pose_align.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import List
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
from evo.core.trajectory import PosePath3D
|
| 19 |
+
|
| 20 |
+
from depth_anything_3.utils.geometry import affine_inverse, affine_inverse_np
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def batch_apply_alignment_to_enc(
|
| 24 |
+
rots: torch.Tensor, trans: torch.Tensor, scales: torch.Tensor, enc_list: List[torch.Tensor]
|
| 25 |
+
):
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def batch_apply_alignment_to_ext(
|
| 30 |
+
rots: torch.Tensor, trans: torch.Tensor, scales: torch.Tensor, ext: torch.Tensor
|
| 31 |
+
):
|
| 32 |
+
device, _ = ext.device, ext.dtype
|
| 33 |
+
if ext.shape[-2:] == (3, 4):
|
| 34 |
+
pad = torch.zeros((*ext.shape[:-2], 4, 4), dtype=ext.dtype, device=device)
|
| 35 |
+
pad[..., :3, :4] = ext
|
| 36 |
+
pad[..., 3, 3] = 1.0
|
| 37 |
+
ext = pad
|
| 38 |
+
pose_est = affine_inverse(ext)
|
| 39 |
+
pose_new_align_rot = rots[:, None] @ pose_est[..., :3, :3]
|
| 40 |
+
pose_new_align_trans = (
|
| 41 |
+
scales[:, None, None] * (rots[:, None] @ pose_est[..., :3, 3:])[..., 0] + trans[:, None]
|
| 42 |
+
)
|
| 43 |
+
pose_new_align = torch.zeros_like(ext)
|
| 44 |
+
pose_new_align[..., :3, :3] = pose_new_align_rot
|
| 45 |
+
pose_new_align[..., :3, 3] = pose_new_align_trans
|
| 46 |
+
pose_new_align[..., 3, 3] = 1.0
|
| 47 |
+
return affine_inverse(pose_new_align)[:, :3]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def batch_align_poses_umeyama(ext_ref: torch.Tensor, ext_est: torch.Tensor):
|
| 51 |
+
device, dtype = ext_ref.device, ext_ref.dtype
|
| 52 |
+
assert ext_ref.dtype in [torch.float32, torch.float64]
|
| 53 |
+
assert ext_est.dtype in [torch.float32, torch.float64]
|
| 54 |
+
assert ext_ref.requires_grad is False
|
| 55 |
+
assert ext_est.requires_grad is False
|
| 56 |
+
rots, trans, scales = [], [], []
|
| 57 |
+
for b in range(ext_ref.shape[0]):
|
| 58 |
+
r, t, s = align_poses_umeyama(ext_ref[b].cpu().numpy(), ext_est[b].cpu().numpy())
|
| 59 |
+
rots.append(torch.from_numpy(r).to(device=device, dtype=dtype))
|
| 60 |
+
trans.append(torch.from_numpy(t).to(device=device, dtype=dtype))
|
| 61 |
+
scales.append(torch.tensor(s, device=device, dtype=dtype))
|
| 62 |
+
return torch.stack(rots), torch.stack(trans), torch.stack(scales)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Dependencies: affine_inverse_np, PosePath3D (maintain consistency with your existing project)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _to44(ext):
|
| 69 |
+
if ext.shape[1] == 3:
|
| 70 |
+
out = np.eye(4)[None].repeat(len(ext), 0)
|
| 71 |
+
out[:, :3, :4] = ext
|
| 72 |
+
return out
|
| 73 |
+
return ext
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _poses_from_ext(ext_ref, ext_est):
|
| 77 |
+
ext_ref = _to44(ext_ref)
|
| 78 |
+
ext_est = _to44(ext_est)
|
| 79 |
+
pose_ref = affine_inverse_np(ext_ref)
|
| 80 |
+
pose_est = affine_inverse_np(ext_est)
|
| 81 |
+
return pose_ref, pose_est
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _umeyama_sim3_from_paths(pose_ref, pose_est):
|
| 85 |
+
path_ref = PosePath3D(poses_se3=pose_ref.copy())
|
| 86 |
+
path_est = PosePath3D(poses_se3=pose_est.copy())
|
| 87 |
+
r, t, s = path_est.align(path_ref, correct_scale=True)
|
| 88 |
+
pose_est_aligned = np.stack(path_est.poses_se3)
|
| 89 |
+
return r, t, s, pose_est_aligned
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _apply_sim3_to_poses(poses, r, t, s):
|
| 93 |
+
out = poses.copy()
|
| 94 |
+
Ri = poses[:, :3, :3]
|
| 95 |
+
ti = poses[:, :3, 3]
|
| 96 |
+
out[:, :3, :3] = r @ Ri
|
| 97 |
+
out[:, :3, 3] = (r @ (s * ti.T)).T + t
|
| 98 |
+
return out
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _median_nn_thresh(pose_ref, pose_est_aligned):
|
| 102 |
+
P_ref = pose_ref[:, :3, 3]
|
| 103 |
+
P_est = pose_est_aligned[:, :3, 3]
|
| 104 |
+
dists = []
|
| 105 |
+
for p in P_est:
|
| 106 |
+
dd = np.linalg.norm(P_ref - p[None, :], axis=1)
|
| 107 |
+
dists.append(dd.min())
|
| 108 |
+
return float(np.median(dists)) if dists else 0.0
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _ransac_align_sim3(
|
| 112 |
+
pose_ref, pose_est, sub_n=None, inlier_thresh=None, max_iters=10, random_state=None
|
| 113 |
+
):
|
| 114 |
+
rng = np.random.default_rng(random_state)
|
| 115 |
+
N = pose_ref.shape[0]
|
| 116 |
+
idx_all = np.arange(N)
|
| 117 |
+
if sub_n is None:
|
| 118 |
+
sub_n = max(3, (N + 1) // 2)
|
| 119 |
+
else:
|
| 120 |
+
sub_n = max(3, min(sub_n, N))
|
| 121 |
+
|
| 122 |
+
# Pre-alignment + default threshold
|
| 123 |
+
r0, t0, s0, pose_est0 = _umeyama_sim3_from_paths(pose_ref, pose_est)
|
| 124 |
+
if inlier_thresh is None:
|
| 125 |
+
inlier_thresh = _median_nn_thresh(pose_ref, pose_est0)
|
| 126 |
+
|
| 127 |
+
P_ref_all = pose_ref[:, :3, 3]
|
| 128 |
+
|
| 129 |
+
best_model = (r0, t0, s0)
|
| 130 |
+
best_inliers = None
|
| 131 |
+
best_score = (-1, np.inf) # (num_inliers, mean_err)
|
| 132 |
+
|
| 133 |
+
for _ in range(max_iters):
|
| 134 |
+
sample = rng.choice(idx_all, size=sub_n, replace=False)
|
| 135 |
+
try:
|
| 136 |
+
r, t, s, _ = _umeyama_sim3_from_paths(pose_ref[sample], pose_est[sample])
|
| 137 |
+
except Exception:
|
| 138 |
+
continue
|
| 139 |
+
pose_h = _apply_sim3_to_poses(pose_est, r, t, s)
|
| 140 |
+
P_h = pose_h[:, :3, 3]
|
| 141 |
+
errs = np.linalg.norm(P_h - P_ref_all, axis=1) # Match by same index
|
| 142 |
+
inliers = errs <= inlier_thresh
|
| 143 |
+
k = int(inliers.sum())
|
| 144 |
+
mean_err = float(errs[inliers].mean()) if k > 0 else np.inf
|
| 145 |
+
if (k > best_score[0]) or (k == best_score[0] and mean_err < best_score[1]):
|
| 146 |
+
best_score = (k, mean_err)
|
| 147 |
+
best_model = (r, t, s)
|
| 148 |
+
best_inliers = inliers
|
| 149 |
+
|
| 150 |
+
# Fit again with best inliers
|
| 151 |
+
if best_inliers is not None and best_inliers.sum() >= 3:
|
| 152 |
+
r, t, s, _ = _umeyama_sim3_from_paths(pose_ref[best_inliers], pose_est[best_inliers])
|
| 153 |
+
else:
|
| 154 |
+
r, t, s = best_model
|
| 155 |
+
return r, t, s
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def align_poses_umeyama(
|
| 159 |
+
ext_ref: np.ndarray,
|
| 160 |
+
ext_est: np.ndarray,
|
| 161 |
+
return_aligned=False,
|
| 162 |
+
ransac=False,
|
| 163 |
+
sub_n=None,
|
| 164 |
+
inlier_thresh=None,
|
| 165 |
+
ransac_max_iters=10,
|
| 166 |
+
random_state=None,
|
| 167 |
+
):
|
| 168 |
+
"""
|
| 169 |
+
Align estimated trajectory to reference using Umeyama Sim(3).
|
| 170 |
+
Default no RANSAC; if ransac=True, use RANSAC (max iterations default 10).
|
| 171 |
+
- sub_n defaults to half the number of frames (rounded up, at least 3)
|
| 172 |
+
- inlier_thresh defaults to median of "distance from each estimated pose to
|
| 173 |
+
nearest reference pose after pre-alignment"
|
| 174 |
+
Returns rotation (3x3), translation (3,), scale; optionally returns aligned extrinsics (4x4).
|
| 175 |
+
"""
|
| 176 |
+
pose_ref, pose_est = _poses_from_ext(ext_ref, ext_est)
|
| 177 |
+
|
| 178 |
+
if not ransac:
|
| 179 |
+
r, t, s, pose_est_aligned = _umeyama_sim3_from_paths(pose_ref, pose_est)
|
| 180 |
+
else:
|
| 181 |
+
r, t, s = _ransac_align_sim3(
|
| 182 |
+
pose_ref,
|
| 183 |
+
pose_est,
|
| 184 |
+
sub_n=sub_n,
|
| 185 |
+
inlier_thresh=inlier_thresh,
|
| 186 |
+
max_iters=ransac_max_iters,
|
| 187 |
+
random_state=random_state,
|
| 188 |
+
)
|
| 189 |
+
pose_est_aligned = _apply_sim3_to_poses(pose_est, r, t, s)
|
| 190 |
+
|
| 191 |
+
if return_aligned:
|
| 192 |
+
ext_est_aligned = affine_inverse_np(pose_est_aligned)
|
| 193 |
+
return r, t, s, ext_est_aligned
|
| 194 |
+
return r, t, s
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# def align_poses_umeyama(ext_ref: np.ndarray, ext_est: np.ndarray, return_aligned=False):
|
| 198 |
+
# """
|
| 199 |
+
# Align estimated trajectory to reference trajectory using Umeyama Sim(3)
|
| 200 |
+
# alignment (via evo PosePath3D). # noqa
|
| 201 |
+
# Returns rotation, translation, and scale.
|
| 202 |
+
# """
|
| 203 |
+
# # If input extrinsics are 3x4, convert to 4x4 by padding
|
| 204 |
+
# if ext_ref.shape[1] == 3:
|
| 205 |
+
# ext_ref_ = np.eye(4)[None].repeat(len(ext_ref), 0)
|
| 206 |
+
# ext_ref_[:, :3] = ext_ref
|
| 207 |
+
# ext_ref = ext_ref_
|
| 208 |
+
# if ext_est.shape[1] == 3:
|
| 209 |
+
# ext_est_ = np.eye(4)[None].repeat(len(ext_est), 0)
|
| 210 |
+
# ext_est_[:, :3] = ext_est
|
| 211 |
+
# ext_est = ext_est_
|
| 212 |
+
|
| 213 |
+
# # Convert to camera poses (inverse extrinsics)
|
| 214 |
+
# pose_ref = affine_inverse_np(ext_ref)
|
| 215 |
+
# pose_est = affine_inverse_np(ext_est)
|
| 216 |
+
|
| 217 |
+
# # Create evo PosePath3D objects
|
| 218 |
+
# path_ref = PosePath3D(poses_se3=pose_ref)
|
| 219 |
+
# path_est = PosePath3D(poses_se3=pose_est)
|
| 220 |
+
# r, t, s = path_est.align(path_ref, correct_scale=True)
|
| 221 |
+
# if return_aligned:
|
| 222 |
+
# return r, t, s, affine_inverse_np(np.stack(path_est.poses_se3))
|
| 223 |
+
# else:
|
| 224 |
+
# return r, t, s
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def apply_umeyama_alignment_to_ext(
|
| 228 |
+
rot: np.ndarray, # (3,3)
|
| 229 |
+
trans: np.ndarray, # (3,) or (1,3)
|
| 230 |
+
scale: float,
|
| 231 |
+
ext_est: np.ndarray, # (...,4,4) or (...,3,4)
|
| 232 |
+
) -> np.ndarray:
|
| 233 |
+
"""
|
| 234 |
+
Apply Sim(3) (R, t, s) to a batch of world-to-camera extrinsics ext_est.
|
| 235 |
+
Returns the aligned extrinsics, with the same shape as input.
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
# Allow 3x4 extrinsics: pad to 4x4
|
| 239 |
+
if ext_est.shape[-2:] == (3, 4):
|
| 240 |
+
pad = np.zeros((*ext_est.shape[:-2], 4, 4), dtype=ext_est.dtype)
|
| 241 |
+
pad[..., :3, :4] = ext_est
|
| 242 |
+
pad[..., 3, 3] = 1.0
|
| 243 |
+
ext_est = pad
|
| 244 |
+
|
| 245 |
+
# Convert world-to-camera to camera-to-world
|
| 246 |
+
pose_est = affine_inverse_np(ext_est) # (...,4,4)
|
| 247 |
+
R_e = pose_est[..., :3, :3] # (...,3,3)
|
| 248 |
+
t_e = pose_est[..., :3, 3] # (...,3)
|
| 249 |
+
|
| 250 |
+
# Apply Sim(3) transformation
|
| 251 |
+
R_a = np.einsum("ij,...jk->...ik", rot, R_e) # (...,3,3)
|
| 252 |
+
t_a = scale * np.einsum("ij,...j->...i", rot, t_e) + trans # (...,3)
|
| 253 |
+
|
| 254 |
+
# Assemble the transformed pose
|
| 255 |
+
pose_a = np.zeros_like(pose_est)
|
| 256 |
+
pose_a[..., :3, :3] = R_a
|
| 257 |
+
pose_a[..., :3, 3] = t_a
|
| 258 |
+
pose_a[..., 3, 3] = 1.0
|
| 259 |
+
|
| 260 |
+
# Convert back to world-to-camera
|
| 261 |
+
return affine_inverse_np(pose_a)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def transform_points_sim3(points, rot, trans, scale, inverse=False):
|
| 265 |
+
"""
|
| 266 |
+
Sim(3) transform point cloud
|
| 267 |
+
points: (N, 3)
|
| 268 |
+
rot: (3, 3)
|
| 269 |
+
trans: (3,) or (1, 3)
|
| 270 |
+
scale: float
|
| 271 |
+
inverse: Whether to do inverse transform (ref->est)
|
| 272 |
+
Returns: (N, 3)
|
| 273 |
+
"""
|
| 274 |
+
if not inverse:
|
| 275 |
+
# Forward: est -> ref
|
| 276 |
+
return scale * (points @ rot.T) + trans
|
| 277 |
+
else:
|
| 278 |
+
# Inverse: ref -> est
|
| 279 |
+
return ((points - trans) @ rot) / scale
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _rand_rot():
|
| 283 |
+
u1, u2, u3 = np.random.rand(3)
|
| 284 |
+
q = np.array(
|
| 285 |
+
[
|
| 286 |
+
np.sqrt(1 - u1) * np.sin(2 * np.math.pi * u2),
|
| 287 |
+
np.sqrt(1 - u1) * np.cos(2 * np.math.pi * u2),
|
| 288 |
+
np.sqrt(u1) * np.sin(2 * np.math.pi * u3),
|
| 289 |
+
np.sqrt(u1) * np.cos(2 * np.math.pi * u3),
|
| 290 |
+
]
|
| 291 |
+
)
|
| 292 |
+
w, x, y, z = q
|
| 293 |
+
return np.array(
|
| 294 |
+
[
|
| 295 |
+
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
|
| 296 |
+
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
|
| 297 |
+
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
|
| 298 |
+
]
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def _rand_pose():
|
| 303 |
+
R, t = _rand_rot(), np.random.randn(3)
|
| 304 |
+
P = np.eye(4)
|
| 305 |
+
P[:3, :3] = R
|
| 306 |
+
P[:3, 3] = t
|
| 307 |
+
return P
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
if __name__ == "__main__":
|
| 311 |
+
np.random.seed(42)
|
| 312 |
+
# 1. Randomly generate reference trajectory and Sim(3)
|
| 313 |
+
N = 8
|
| 314 |
+
pose_ref = np.stack([_rand_pose() for _ in range(N)]) # (N,4,4) cam→world
|
| 315 |
+
rot_gt = _rand_rot()
|
| 316 |
+
scale_gt = 2.3
|
| 317 |
+
trans_gt = np.random.randn(3)
|
| 318 |
+
# 2. Generate estimated trajectory (apply Sim(3))
|
| 319 |
+
pose_est = np.zeros_like(pose_ref)
|
| 320 |
+
for i in range(N):
|
| 321 |
+
R = pose_ref[i][:3, :3]
|
| 322 |
+
t = pose_ref[i][:3, 3]
|
| 323 |
+
pose_est[i][:3, :3] = rot_gt @ R
|
| 324 |
+
pose_est[i][:3, 3] = scale_gt * (rot_gt @ t) + trans_gt
|
| 325 |
+
pose_est[i][3, 3] = 1.0
|
| 326 |
+
# 3. Get extrinsics (world->cam)
|
| 327 |
+
ext_ref = affine_inverse_np(pose_ref)
|
| 328 |
+
ext_est = affine_inverse_np(pose_est)
|
| 329 |
+
# 4. Use umeyama alignment, estimate Sim(3)
|
| 330 |
+
r_est, t_est, s_est = align_poses_umeyama(ext_ref, ext_est)
|
| 331 |
+
print("GT scale:", scale_gt, "Estimated:", s_est)
|
| 332 |
+
print("GT trans:", trans_gt, "Estimated:", t_est)
|
| 333 |
+
print("GT rot:\n", rot_gt, "\nEstimated:\n", r_est)
|
| 334 |
+
# 5. Random point cloud, in ref frame
|
| 335 |
+
num_points = 100
|
| 336 |
+
points_ref = np.random.randn(num_points, 3)
|
| 337 |
+
# 6. Use GT Sim(3) inverse transform to est frame
|
| 338 |
+
points_est = transform_points_sim3(points_ref, rot_gt, trans_gt, scale_gt, inverse=True)
|
| 339 |
+
# 7. Use estimated Sim(3) forward transform back to ref frame
|
| 340 |
+
points_ref_recovered = transform_points_sim3(points_est, r_est, t_est, s_est, inverse=False)
|
| 341 |
+
# 8. Check error
|
| 342 |
+
err = np.abs(points_ref_recovered - points_ref)
|
| 343 |
+
print("Point cloud sim3 transform error (mean abs):", err.mean())
|
| 344 |
+
print("Point cloud sim3 transform error (max abs):", err.max())
|
| 345 |
+
assert err.mean() < 1e-6, "Mean sim3 transform error too large!"
|
| 346 |
+
assert err.max() < 1e-5, "Max sim3 transform error too large!"
|
| 347 |
+
print("Sim(3) point cloud transform & alignment test passed!")
|