diff --git a/README.md b/README.md index e01ce54..1e01529 100755 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ We **refactored the original code following the standard Python package structur Initialization methods: - [x] DUST3R (same method used in [InstantSplat](https://github.com/NVlabs/InstantSplat)) +- [x] TTT3R (via a dedicated bridge env to avoid namespace conflicts with DUSt3R) - [x] MAST3R (same method used in [Splatt3R](https://github.com/btsmart/splatt3r)) - [x] COLMAP Sparse reconstruct (same method used in [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting)) - [x] COLMAP Dense reconstruct (use `patch_match_stereo`, `stereo_fusion`, `poisson_mesher` and `delaunay_mesher` in COLMAP to reconstruct dense point cloud for initialization) @@ -31,6 +32,16 @@ pip install --upgrade Pillow hydra-core omegaconf # deps for vggt pip install --upgrade git+https://github.com/jytime/LightGlue.git#egg=lightglue # deps for vggt ``` +Install `TTT3R` in a separate conda environment so its bundled `dust3r` package does not collide with InstantSplat's DUSt3R stack: +```shell +git clone https://github.com/Inception3D/TTT3R.git submodules/ttt3r +conda create -n instantsplat-ttt3r python=3.11 cmake=3.14.0 -y +conda run -n instantsplat-ttt3r conda install -y pytorch torchvision pytorch-cuda=12.1 -c pytorch -c nvidia +conda run -n instantsplat-ttt3r pip install -r submodules/ttt3r/requirements.txt +conda run -n instantsplat-ttt3r conda install -y "llvm-openmp<16" +conda run -n instantsplat-ttt3r bash -lc "cd submodules/ttt3r/src/croco/models/curope && python setup.py build_ext --inplace" +``` + (Optional) Install `xformers` for faster Depth-Anything V2 inference: ```shell pip install xformers @@ -75,6 +86,7 @@ wget -P checkpoints/ https://huggingface.co/depth-anything/Depth-Anything-V2-Lar wget -P checkpoints/ https://huggingface.co/facebook/VGGT-1B-Commercial/resolve/main/vggt_1B_commercial.pt --header="Authorization: Bearer $HF_TOKEN" wget -P checkpoints/ https://download.europe.naverlabs.com/ComputerVision/MUSt3R/MUSt3R_512.pth wget -P checkpoints/ https://download.europe.naverlabs.com/ComputerVision/Pow3R/Pow3R_ViTLarge_BaseDecoder_512_linear.pth +wget -P submodules/ttt3r/src/ https://drive.google.com/uc?id=1Asz-ZB3FfpzZYwunhQvNPZEUA8XUNAYD ``` Configs for `map-anything`: @@ -102,6 +114,12 @@ python -m instantsplat.initialize -d data/sora/santorini/3_views -i vggt --with_ python -m instantsplat.train -s data/sora/santorini/3_views -d output/sora/santorini/3_views -i 1000 --init mapanything --with_depth_anything ``` +TTT3R initialization example: +```shell +python -m instantsplat.initialize -d data/sora/santorini/3_views -i ttt3r +python -m instantsplat.train -s data/sora/santorini/3_views -d output/sora/santorini/3_views -i 1000 --init ttt3r +``` + Depth format note: - `Depth-Anything V2` saves inverse depth (`1 / depth`), which matches the default depth supervision used by 3DGS. - The native depth saved by `mapanything`, `mapanything-external`, and `vggt` is regular depth, not inverse depth. diff --git a/instantsplat/initialize.py b/instantsplat/initialize.py index cea6eac..0ab1c74 100644 --- a/instantsplat/initialize.py +++ b/instantsplat/initialize.py @@ -6,6 +6,7 @@ default_image_folder = { "dust3r": "images", + "ttt3r": "images", "mast3r": "images", "mapanything": "images", "mapanything-external": "images", @@ -26,6 +27,8 @@ def convert_image_path(image_path): return os.path.join(os.path.dirname(os.path. match initializer: case "dust3r": constructor = Dust3rInitializer + case "ttt3r": + constructor = Ttt3rInitializer case "mast3r": constructor = Mast3rInitializer case "vggt": diff --git a/instantsplat/initializer/__init__.py b/instantsplat/initializer/__init__.py index bcfee8b..723ad9c 100644 --- a/instantsplat/initializer/__init__.py +++ b/instantsplat/initializer/__init__.py @@ -1,6 +1,7 @@ from .abc import AbstractInitializer, InitializingCamera, InitializedPointCloud from .dataset import InitializedCameraDataset, TrainableCameraDataset, TrainableInitializedCameraDataset from .dust3r import Dust3rInitializer, Mast3rInitializer +from .ttt3r import Ttt3rInitializer from .vggt import VGGTInitializer, VGGTColmapSparseInitializer, VGGTColmapDenseInitializer from .mapanything import MapAnythingInitializer, MapAnythingExternalInitializer from .colmap import ColmapSparseInitializer, ColmapDenseInitializer diff --git a/instantsplat/initializer/ttt3r/__init__.py b/instantsplat/initializer/ttt3r/__init__.py new file mode 100644 index 0000000..3b0cbce --- /dev/null +++ b/instantsplat/initializer/ttt3r/__init__.py @@ -0,0 +1 @@ +from .ttt3r import Ttt3rInitializer diff --git a/instantsplat/initializer/ttt3r/blocks.py b/instantsplat/initializer/ttt3r/blocks.py new file mode 100644 index 0000000..7ce79dc --- /dev/null +++ b/instantsplat/initializer/ttt3r/blocks.py @@ -0,0 +1,579 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch +import torch.nn as nn + +from itertools import repeat +import collections.abc +from torch.nn.functional import scaled_dot_product_attention +from functools import partial + + +def _ntuple(n): + def parse(x): + if isinstance(x, collections.abc.Iterable) and not isinstance(x, str): + return x + return tuple(repeat(x, n)) + + return parse + + +to_2tuple = _ntuple(2) + + +def drop_path( + x, drop_prob: float = 0.0, training: bool = False, scale_by_keep: bool = True +): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + if drop_prob == 0.0 or not training: + return x + keep_prob = 1 - drop_prob + shape = (x.shape[0],) + (1,) * ( + x.ndim - 1 + ) # work with diff dim tensors, not just 2D ConvNets + random_tensor = x.new_empty(shape).bernoulli_(keep_prob) + if keep_prob > 0.0 and scale_by_keep: + random_tensor.div_(keep_prob) + return x * random_tensor + + +class DropPath(nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, drop_prob: float = 0.0, scale_by_keep: bool = True): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + self.scale_by_keep = scale_by_keep + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training, self.scale_by_keep) + + def extra_repr(self): + return f"drop_prob={round(self.drop_prob,3):0.3f}" + + +class Mlp(nn.Module): + """MLP as used in Vision Transformer, MLP-Mixer and related networks""" + + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + bias=True, + drop=0.0, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + bias = to_2tuple(bias) + drop_probs = to_2tuple(drop) + + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias[0]) + self.act = act_layer() + self.drop1 = nn.Dropout(drop_probs[0]) + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias[1]) + self.drop2 = nn.Dropout(drop_probs[1]) + + def forward(self, x): + return self.drop2(self.fc2(self.drop1(self.act(self.fc1(x))))) + + +class Attention(nn.Module): + + def __init__( + self, dim, rope=None, num_heads=8, qkv_bias=False, attn_drop=0.0, proj_drop=0.0 + ): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + self.rope = rope.float() if rope is not None else None + + @staticmethod + def _apply_rope(rope, tokens, positions): + if positions is None: + return tokens + valid = (positions >= 0).all(dim=-1) + if bool(valid.all()): + return rope(tokens, positions) + + safe_positions = positions.clamp_min(0) + rotated = rope(tokens, safe_positions) + valid = valid[:, None, :, None] + return torch.where(valid, rotated, tokens) + + def forward(self, x, xpos, return_attn=False): + B, N, C = x.shape + + qkv = ( + self.qkv(x) + .reshape(B, N, 3, self.num_heads, C // self.num_heads) + .transpose(1, 3) + ) + q, k, v = [qkv[:, :, i] for i in range(3)] + + q_type = q.dtype + k_type = k.dtype + if self.rope is not None: + q = q.float() + k = k.float() + with torch.autocast(device_type="cuda", enabled=False): + q = self._apply_rope(self.rope, q, xpos) + k = self._apply_rope(self.rope, k, xpos) + q = q.to(q_type) + k = k.to(k_type) + + if return_attn: + # original attention + attn = (q @ k.transpose(-2, -1)) * self.scale # [B, num_heads, Nq, Nk] [1, 16, 768, 1 + 576] + attn_before_softmax = attn.detach().clone() + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + x = (attn @ v).transpose(1, 2).reshape(B, N, C) # [B, N, C] [1, 768, 768] + + x = self.proj(x) + x = self.proj_drop(x) + return x, attn_before_softmax + else: + x = ( + scaled_dot_product_attention( + query=q, key=k, value=v, dropout_p=self.attn_drop.p, scale=self.scale + ) + .transpose(1, 2) + .reshape(B, N, C) + ) + + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class Block(nn.Module): + + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + rope=None, + ): + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + ) + + def forward(self, x, xpos): + x = x + self.drop_path(self.attn(self.norm1(x), xpos)) + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + + +class CrossAttention(nn.Module): + + def __init__( + self, dim, rope=None, num_heads=8, qkv_bias=False, attn_drop=0.0, proj_drop=0.0 + ): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + + self.projq = nn.Linear(dim, dim, bias=qkv_bias) + self.projk = nn.Linear(dim, dim, bias=qkv_bias) + self.projv = nn.Linear(dim, dim, bias=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + + self.rope = rope.float() if rope is not None else None + + def forward(self, query, key, value, qpos, kpos, return_attn=False): + B, Nq, C = query.shape + Nk = key.shape[1] + Nv = value.shape[1] + + q = ( + self.projq(query) # [B, Nq, C] [1, 768, 768] + .reshape(B, Nq, self.num_heads, C // self.num_heads) + .permute(0, 2, 1, 3) + ) # [B, num_heads, Nq, C//num_heads] [1, 16, 768, 48] + k = ( + self.projk(key) # [B, Nk, C] [1, 1 + 576, 768] + .reshape(B, Nk, self.num_heads, C // self.num_heads) + .permute(0, 2, 1, 3) + ) # [B, num_heads, Nk, C//num_heads] [1, 16, 1 + 576, 48] + v = ( + self.projv(value) # [B, Nv, C] [1, 1 + 576, 768] + .reshape(B, Nv, self.num_heads, C // self.num_heads) + .permute(0, 2, 1, 3) + ) # [B, num_heads, Nv, C//num_heads] [1, 16, 1 + 576, 48] + + q_type = q.dtype + k_type = k.dtype + if self.rope is not None: + if qpos is not None: + q = q.float() + with torch.autocast(device_type="cuda", enabled=False): + q = Attention._apply_rope(self.rope, q, qpos) + q = q.to(q_type) # [B, num_heads, Nq, C//num_heads] [1, 16, 768, 48] + + if kpos is not None: + k = k.float() + with torch.autocast(device_type="cuda", enabled=False): + k = Attention._apply_rope(self.rope, k, kpos) + k = k.to(k_type) # [B, num_heads, Nk, C//num_heads] [1, 16, 1 + 576, 48] + + if return_attn: + # original attention + attn = (q @ k.transpose(-2, -1)) * self.scale # [B, num_heads, Nq, Nk] [1, 16, 768, 1 + 576] + attn_before_softmax = attn.detach().clone() + + attn = attn.softmax(dim=-1) + attn = self.attn_drop(attn) + x = (attn @ v).transpose(1, 2).reshape(B, Nq, C) # [B, Nq, C] [1, 768, 768] + + x = self.proj(x) + x = self.proj_drop(x) + + return x, attn_before_softmax + else: + x = ( + scaled_dot_product_attention( + query=q, key=k, value=v, dropout_p=self.attn_drop.p, scale=self.scale + ) + .transpose(1, 2) + .reshape(B, Nq, C) + ) # [B, Nq, C] [1, 768, 768] + + x = self.proj(x) + x = self.proj_drop(x) + + return x + + +class DecoderBlock(nn.Module): + + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + norm_mem=True, + rope=None, + ): + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.cross_attn = CrossAttention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.norm2 = norm_layer(dim) + self.norm3 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + ) + self.norm_y = norm_layer(dim) if norm_mem else nn.Identity() + + def forward(self, x, y, xpos, ypos, return_attn=False): + if return_attn: + self_attn_output, self_attn = self.attn(self.norm1(x), xpos, return_attn=True) + x = x + self.drop_path(self_attn_output) + y_ = self.norm_y(y) + cross_attn_output, cross_attn = self.cross_attn(self.norm2(x), y_, y_, xpos, ypos, return_attn=True) + x = x + self.drop_path(cross_attn_output) + x = x + self.drop_path(self.mlp(self.norm3(x))) + return x, y, self_attn, cross_attn + else: + x = x + self.drop_path(self.attn(self.norm1(x), xpos)) + y_ = self.norm_y(y) + x = x + self.drop_path(self.cross_attn(self.norm2(x), y_, y_, xpos, ypos)) + x = x + self.drop_path(self.mlp(self.norm3(x))) + return x, y, None, None + +class CustomDecoderBlock(nn.Module): + + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + norm_mem=True, + rope=None, + ): + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.cross_attn = CrossAttention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.norm2 = norm_layer(dim) + self.norm3 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + ) + self.norm_y = norm_layer(dim) if norm_mem else nn.Identity() + self.norm_z = norm_layer(dim) if norm_mem else nn.Identity() + + def forward(self, x, y, z, xpos, ypos): + x = x + self.drop_path(self.attn(self.norm1(x), xpos)) + y_ = self.norm_y(y) + z_ = self.norm_z(z) + x = x + self.drop_path(self.cross_attn(self.norm2(x), y_, z_, xpos, ypos)) + x = x + self.drop_path(self.mlp(self.norm3(x))) + return x, y + + +class ModLN(nn.Module): + """ + Modulation with adaLN. + + References: + DiT: https://github.com/facebookresearch/DiT/blob/main/models.py#L101 + """ + + def __init__(self, inner_dim: int, mod_dim: int, eps: float): + super().__init__() + self.norm = nn.LayerNorm(inner_dim, eps=eps) + self.mlp = nn.Sequential( + nn.SiLU(), + nn.Linear(mod_dim, inner_dim * 2), + ) + + @staticmethod + def modulate(x, shift, scale): + + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + + def forward(self, x: torch.Tensor, mod: torch.Tensor) -> torch.Tensor: + shift, scale = self.mlp(mod).chunk(2, dim=-1) # [N, D] + return self.modulate(self.norm(x), shift, scale) # [N, L, D] + + +class ConditionModulationBlock(nn.Module): + + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=partial(ModLN, eps=1e-6), + rope=None, + ): + super().__init__() + self.norm1 = norm_layer(dim, dim) + self.attn = Attention( + dim, + rope=rope, + num_heads=num_heads, + qkv_bias=qkv_bias, + attn_drop=attn_drop, + proj_drop=drop, + ) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.norm2 = norm_layer(dim, dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + ) + + def forward(self, x, mod, xpos): + x = x + self.drop_path(self.attn(self.norm1(x, mod), xpos)) + x = x + self.drop_path(self.mlp(self.norm2(x, mod))) + return x + + +class PositionGetter(object): + """return positions of patches""" + + def __init__(self): + self.cache_positions = {} + + def __call__(self, b, h, w, device): + if not (h, w) in self.cache_positions: + x = torch.arange(w, device=device) + y = torch.arange(h, device=device) + self.cache_positions[h, w] = torch.cartesian_prod(y, x) # (h, w, 2) + pos = self.cache_positions[h, w].view(1, h * w, 2).expand(b, -1, 2).clone() + return pos + + +class PatchEmbed(nn.Module): + """just adding _init_weights + position getter compared to timm.models.layers.patch_embed.PatchEmbed""" + + def __init__( + self, + img_size=224, + patch_size=16, + in_chans=3, + embed_dim=768, + norm_layer=None, + flatten=True, + ): + super().__init__() + img_size = to_2tuple(img_size) + patch_size = to_2tuple(patch_size) + self.img_size = img_size + self.patch_size = patch_size + self.grid_size = (img_size[0] // patch_size[0], img_size[1] // patch_size[1]) + self.num_patches = self.grid_size[0] * self.grid_size[1] + self.flatten = flatten + + self.proj = nn.Conv2d( + in_chans, embed_dim, kernel_size=patch_size, stride=patch_size + ) + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + self.position_getter = PositionGetter() + + def forward(self, x): + B, C, H, W = x.shape + torch._assert( + H == self.img_size[0], + f"Input image height ({H}) doesn't match model ({self.img_size[0]}).", + ) + torch._assert( + W == self.img_size[1], + f"Input image width ({W}) doesn't match model ({self.img_size[1]}).", + ) + x = self.proj(x) + pos = self.position_getter(B, x.size(2), x.size(3), x.device) + if self.flatten: + x = x.flatten(2).transpose(1, 2) # BCHW -> BNC + x = self.norm(x) + return x, pos + + def _init_weights(self): + w = self.proj.weight.data + torch.nn.init.xavier_uniform_(w.view([w.shape[0], -1])) + + +if __name__ == "__main__": + import os + import sys + + sys.path.append(os.path.dirname(os.path.dirname(__file__))) + import dust3r.utils.path_to_croco + from models.pos_embed import get_2d_sincos_pos_embed, RoPE2D + from functools import partial + from torch.utils.checkpoint import checkpoint + + torch.manual_seed(0) + + enc_blocks_ray_map = ( + nn.ModuleList( + [ + Block( + 768, + 16, + 4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + rope=RoPE2D(100), + ) + for _ in range(2) + ] + ) + .cuda() + .train() + ) + + x = torch.randn(2, 196, 768, requires_grad=True).cuda() + xpos = torch.arange(0, 196).unsqueeze(0).unsqueeze(-1).repeat(2, 1, 2).cuda().long() + enc_blocks_ray_map.zero_grad() + for blk in enc_blocks_ray_map: + + x = checkpoint(blk, x, xpos) + enc_blocks_ray_map.zero_grad() + x.sum().backward() + + grad_not_checkpointed = {} + for name, param in enc_blocks_ray_map.named_parameters(): + grad_not_checkpointed[name] = param.grad.data.clone() + print(name, grad_not_checkpointed[name]) + break diff --git a/instantsplat/initializer/ttt3r/bootstrap.py b/instantsplat/initializer/ttt3r/bootstrap.py new file mode 100644 index 0000000..36035a7 --- /dev/null +++ b/instantsplat/initializer/ttt3r/bootstrap.py @@ -0,0 +1,15 @@ +import sys +from pathlib import Path + + +def ensure_runtime_paths() -> Path: + root = Path(__file__).resolve().parents[3] + dust3r_root = root / "submodules" / "dust3r" + croco_root = dust3r_root / "croco" + + for path in (dust3r_root, croco_root): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + + return root diff --git a/instantsplat/initializer/ttt3r/camera.py b/instantsplat/initializer/ttt3r/camera.py new file mode 100644 index 0000000..50d9b9b --- /dev/null +++ b/instantsplat/initializer/ttt3r/camera.py @@ -0,0 +1,466 @@ +from typing import Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .bootstrap import ensure_runtime_paths +from .heads_postprocess import postprocess_pose + +ensure_runtime_paths() + +from croco.models.blocks import Mlp + +inf = float("inf") + + +class PoseDecoder(nn.Module): + def __init__( + self, + hidden_size=768, + mlp_ratio=4, + pose_encoding_type="absT_quaR", + ): + super().__init__() + + self.pose_encoding_type = pose_encoding_type + if self.pose_encoding_type == "absT_quaR": + self.target_dim = 7 + + self.mlp = Mlp( + in_features=hidden_size, + hidden_features=int(hidden_size * mlp_ratio), + out_features=self.target_dim, + drop=0, + ) + + def forward( + self, + pose_feat, + ): + """ + pose_feat: BxC + preliminary_cameras: cameras in opencv coordinate. + """ + + pred_cameras = self.mlp(pose_feat) # Bx7, 3 for absT, 4 for quaR + return pred_cameras + + +class PoseEncoder(nn.Module): + def __init__( + self, + hidden_size=768, + mlp_ratio=4, + pose_mode=("exp", -inf, inf), + pose_encoding_type="absT_quaR", + ): + super().__init__() + self.pose_encoding_type = pose_encoding_type + self.pose_mode = pose_mode + + if self.pose_encoding_type == "absT_quaR": + self.target_dim = 7 + + self.embed_pose = PoseEmbedding( + target_dim=self.target_dim, + out_dim=hidden_size, + n_harmonic_functions=10, + append_input=True, + ) + self.pose_encoder = Mlp( + in_features=self.embed_pose.out_dim, + hidden_features=int(hidden_size * mlp_ratio), + out_features=hidden_size, + drop=0, + ) + + def forward(self, camera): + pose_enc = camera_to_pose_encoding( + camera, + pose_encoding_type=self.pose_encoding_type, + ).to(camera.dtype) + pose_enc = postprocess_pose(pose_enc, self.pose_mode, inverse=True) + pose_feat = self.embed_pose(pose_enc) + pose_feat = self.pose_encoder(pose_feat) + return pose_feat + + +class HarmonicEmbedding(torch.nn.Module): + def __init__( + self, + n_harmonic_functions: int = 6, + omega_0: float = 1.0, + logspace: bool = True, + append_input: bool = True, + ) -> None: + """ + The harmonic embedding layer supports the classical + Nerf positional encoding described in + `NeRF `_ + and the integrated position encoding in + `MIP-NeRF `_. + + During the inference you can provide the extra argument `diag_cov`. + + If `diag_cov is None`, it converts + rays parametrized with a `ray_bundle` to 3D points by + extending each ray according to the corresponding length. + Then it converts each feature + (i.e. vector along the last dimension) in `x` + into a series of harmonic features `embedding`, + where for each i in range(dim) the following are present + in embedding[...]:: + + [ + sin(f_1*x[..., i]), + sin(f_2*x[..., i]), + ... + sin(f_N * x[..., i]), + cos(f_1*x[..., i]), + cos(f_2*x[..., i]), + ... + cos(f_N * x[..., i]), + x[..., i], # only present if append_input is True. + ] + + where N corresponds to `n_harmonic_functions-1`, and f_i is a scalar + denoting the i-th frequency of the harmonic embedding. + + + If `diag_cov is not None`, it approximates + conical frustums following a ray bundle as gaussians, + defined by x, the means of the gaussians and diag_cov, + the diagonal covariances. + Then it converts each gaussian + into a series of harmonic features `embedding`, + where for each i in range(dim) the following are present + in embedding[...]:: + + [ + sin(f_1*x[..., i]) * exp(0.5 * f_1**2 * diag_cov[..., i,]), + sin(f_2*x[..., i]) * exp(0.5 * f_2**2 * diag_cov[..., i,]), + ... + sin(f_N * x[..., i]) * exp(0.5 * f_N**2 * diag_cov[..., i,]), + cos(f_1*x[..., i]) * exp(0.5 * f_1**2 * diag_cov[..., i,]), + cos(f_2*x[..., i]) * exp(0.5 * f_2**2 * diag_cov[..., i,]),, + ... + cos(f_N * x[..., i]) * exp(0.5 * f_N**2 * diag_cov[..., i,]), + x[..., i], # only present if append_input is True. + ] + + where N equals `n_harmonic_functions-1`, and f_i is a scalar + denoting the i-th frequency of the harmonic embedding. + + If `logspace==True`, the frequencies `[f_1, ..., f_N]` are + powers of 2: + `f_1, ..., f_N = 2**torch.arange(n_harmonic_functions)` + + If `logspace==False`, frequencies are linearly spaced between + `1.0` and `2**(n_harmonic_functions-1)`: + `f_1, ..., f_N = torch.linspace( + 1.0, 2**(n_harmonic_functions-1), n_harmonic_functions + )` + + Note that `x` is also premultiplied by the base frequency `omega_0` + before evaluating the harmonic functions. + + Args: + n_harmonic_functions: int, number of harmonic + features + omega_0: float, base frequency + logspace: bool, Whether to space the frequencies in + logspace or linear space + append_input: bool, whether to concat the original + input to the harmonic embedding. If true the + output is of the form (embed.sin(), embed.cos(), x) + """ + super().__init__() + + if logspace: + frequencies = 2.0 ** torch.arange(n_harmonic_functions, dtype=torch.float32) + else: + frequencies = torch.linspace( + 1.0, + 2.0 ** (n_harmonic_functions - 1), + n_harmonic_functions, + dtype=torch.float32, + ) + + self.register_buffer("_frequencies", frequencies * omega_0, persistent=False) + self.register_buffer( + "_zero_half_pi", + torch.tensor([0.0, 0.5 * torch.pi]), + persistent=False, + ) + self.append_input = append_input + + def forward( + self, x: torch.Tensor, diag_cov: Optional[torch.Tensor] = None, **kwargs + ) -> torch.Tensor: + """ + Args: + x: tensor of shape [..., dim] + diag_cov: An optional tensor of shape `(..., dim)` + representing the diagonal covariance matrices of our Gaussians, joined with x + as means of the Gaussians. + + Returns: + embedding: a harmonic embedding of `x` of shape + [..., (n_harmonic_functions * 2 + int(append_input)) * num_points_per_ray] + """ + + embed = x[..., None] * self._frequencies + + embed = embed[..., None, :, :] + self._zero_half_pi[..., None, None] + + embed = embed.sin() + if diag_cov is not None: + x_var = diag_cov[..., None] * torch.pow(self._frequencies, 2) + exp_var = torch.exp(-0.5 * x_var) + + embed = embed * exp_var[..., None, :, :] + + embed = embed.reshape(*x.shape[:-1], -1) + + if self.append_input: + return torch.cat([embed, x], dim=-1) + return embed + + @staticmethod + def get_output_dim_static( + input_dims: int, n_harmonic_functions: int, append_input: bool + ) -> int: + """ + Utility to help predict the shape of the output of `forward`. + + Args: + input_dims: length of the last dimension of the input tensor + n_harmonic_functions: number of embedding frequencies + append_input: whether or not to concat the original + input to the harmonic embedding + Returns: + int: the length of the last dimension of the output tensor + """ + return input_dims * (2 * n_harmonic_functions + int(append_input)) + + def get_output_dim(self, input_dims: int = 3) -> int: + """ + Same as above. The default for input_dims is 3 for 3D applications + which use harmonic embedding for positional encoding, + so the input might be xyz. + """ + return self.get_output_dim_static( + input_dims, len(self._frequencies), self.append_input + ) + + +class PoseEmbedding(nn.Module): + def __init__(self, target_dim, out_dim, n_harmonic_functions=10, append_input=True): + super().__init__() + + self._emb_pose = HarmonicEmbedding( + n_harmonic_functions=n_harmonic_functions, append_input=append_input + ) + + self.out_dim = self._emb_pose.get_output_dim(target_dim) + + def forward(self, pose_encoding): + e_pose_encoding = self._emb_pose(pose_encoding) + return e_pose_encoding + + +def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor: + """ + Returns torch.sqrt(torch.max(0, x)) + but with a zero subgradient where x is 0. + """ + ret = torch.zeros_like(x) + positive_mask = x > 0 + ret[positive_mask] = torch.sqrt(x[positive_mask]) + return ret + + +def matrix_to_quaternion(matrix: torch.Tensor) -> torch.Tensor: + """ + Convert rotations given as rotation matrices to quaternions. + + Args: + matrix: Rotation matrices as tensor of shape (..., 3, 3). + + Returns: + quaternions with real part first, as tensor of shape (..., 4). + """ + if matrix.size(-1) != 3 or matrix.size(-2) != 3: + raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.") + + batch_dim = matrix.shape[:-2] + m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind( + matrix.reshape(batch_dim + (9,)), dim=-1 + ) + + q_abs = _sqrt_positive_part( + torch.stack( + [ + 1.0 + m00 + m11 + m22, + 1.0 + m00 - m11 - m22, + 1.0 - m00 + m11 - m22, + 1.0 - m00 - m11 + m22, + ], + dim=-1, + ) + ) + + quat_by_rijk = torch.stack( + [ + torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1), + torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1), + torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1), + torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1), + ], + dim=-2, + ) + + flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device) + quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr)) + + out = quat_candidates[ + F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, : + ].reshape(batch_dim + (4,)) + return standardize_quaternion(out) + + +def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor: + """ + Convert a unit quaternion to a standard form: one in which the real + part is non negative. + + Args: + quaternions: Quaternions with real part first, + as tensor of shape (..., 4). + + Returns: + Standardized quaternions as tensor of shape (..., 4). + """ + quaternions = F.normalize(quaternions, p=2, dim=-1) + return torch.where(quaternions[..., 0:1] < 0, -quaternions, quaternions) + + +def camera_to_pose_encoding( + camera, + pose_encoding_type="absT_quaR", +): + """ + Inverse to pose_encoding_to_camera + camera: opencv, cam2world + """ + if pose_encoding_type == "absT_quaR": + + quaternion_R = matrix_to_quaternion(camera[:, :3, :3]) + + pose_encoding = torch.cat([camera[:, :3, 3], quaternion_R], dim=-1) + else: + raise ValueError(f"Unknown pose encoding {pose_encoding_type}") + + return pose_encoding + + +def quaternion_to_matrix(quaternions: torch.Tensor) -> torch.Tensor: + """ + Convert rotations given as quaternions to rotation matrices. + + Args: + quaternions: quaternions with real part first, + as tensor of shape (..., 4). + + Returns: + Rotation matrices as tensor of shape (..., 3, 3). + """ + r, i, j, k = torch.unbind(quaternions, -1) + + two_s = 2.0 / (quaternions * quaternions).sum(-1) + + o = torch.stack( + ( + 1 - two_s * (j * j + k * k), + two_s * (i * j - k * r), + two_s * (i * k + j * r), + two_s * (i * j + k * r), + 1 - two_s * (i * i + k * k), + two_s * (j * k - i * r), + two_s * (i * k - j * r), + two_s * (j * k + i * r), + 1 - two_s * (i * i + j * j), + ), + -1, + ) + return o.reshape(quaternions.shape[:-1] + (3, 3)) + + +def pose_encoding_to_camera( + pose_encoding, + pose_encoding_type="absT_quaR", +): + """ + Args: + pose_encoding: A tensor of shape `BxC`, containing a batch of + `B` `C`-dimensional pose encodings. + pose_encoding_type: The type of pose encoding, + """ + + if pose_encoding_type == "absT_quaR": + + abs_T = pose_encoding[:, :3] + quaternion_R = pose_encoding[:, 3:7] + R = quaternion_to_matrix(quaternion_R) + else: + raise ValueError(f"Unknown pose encoding {pose_encoding_type}") + + c2w_mats = torch.eye(4, 4).to(R.dtype).to(R.device) + c2w_mats = c2w_mats[None].repeat(len(R), 1, 1) + c2w_mats[:, :3, :3] = R + c2w_mats[:, :3, 3] = abs_T + + return c2w_mats + + +def quaternion_conjugate(q): + """Compute the conjugate of quaternion q (w, x, y, z).""" + + q_conj = torch.cat([q[..., :1], -q[..., 1:]], dim=-1) + return q_conj + + +def quaternion_multiply(q1, q2): + """Multiply two quaternions q1 and q2.""" + w1, x1, y1, z1 = q1.unbind(dim=-1) + w2, x2, y2, z2 = q2.unbind(dim=-1) + + w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 + x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 + y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2 + z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2 + + return torch.stack((w, x, y, z), dim=-1) + + +def rotate_vector(q, v): + """Rotate vector v by quaternion q.""" + q_vec = q[..., 1:] + q_w = q[..., :1] + + t = 2.0 * torch.cross(q_vec, v, dim=-1) + v_rot = v + q_w * t + torch.cross(q_vec, t, dim=-1) + return v_rot + + +def relative_pose_absT_quatR(t1, q1, t2, q2): + """Compute the relative translation and quaternion between two poses.""" + + q1_inv = quaternion_conjugate(q1) + + q_rel = quaternion_multiply(q1_inv, q2) + + delta_t = t2 - t1 + t_rel = rotate_vector(q1_inv, delta_t) + return t_rel, q_rel diff --git a/instantsplat/initializer/ttt3r/device.py b/instantsplat/initializer/ttt3r/device.py new file mode 100755 index 0000000..eee47e0 --- /dev/null +++ b/instantsplat/initializer/ttt3r/device.py @@ -0,0 +1,104 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import numpy as np +import torch + + +def todevice(batch, device, callback=None, non_blocking=False): + """Transfer some variables to another device (i.e. GPU, CPU:torch, CPU:numpy). + + batch: list, tuple, dict of tensors or other things + device: pytorch device or 'numpy' + callback: function that would be called on every sub-elements. + """ + if callback: + batch = callback(batch) + + if isinstance(batch, dict): + return {k: todevice(v, device) for k, v in batch.items()} + + if isinstance(batch, (tuple, list)): + return type(batch)(todevice(x, device) for x in batch) + + x = batch + if device == "numpy": + if isinstance(x, torch.Tensor): + x = x.detach().cpu().numpy() + elif x is not None: + if isinstance(x, np.ndarray): + x = torch.from_numpy(x) + if torch.is_tensor(x): + x = x.to(device, non_blocking=non_blocking) + return x + + +to_device = todevice # alias + + +def to_numpy(x): + return todevice(x, "numpy") + + +def to_cpu(x): + return todevice(x, "cpu") + + +def to_cuda(x): + return todevice(x, "cuda") + + +def collate_with_cat(whatever, lists=False): + if isinstance(whatever, dict): + return {k: collate_with_cat(vals, lists=lists) for k, vals in whatever.items()} + + elif isinstance(whatever, (tuple, list)): + if len(whatever) == 0: + return whatever + elem = whatever[0] + T = type(whatever) + + if elem is None: + return None + if isinstance(elem, (bool, float, int, str)): + return whatever + if isinstance(elem, tuple): + return T(collate_with_cat(x, lists=lists) for x in zip(*whatever)) + if isinstance(elem, dict): + return { + k: collate_with_cat([e[k] for e in whatever], lists=lists) for k in elem + } + + if isinstance(elem, torch.Tensor): + return listify(whatever) if lists else torch.cat(whatever) + if isinstance(elem, np.ndarray): + return ( + listify(whatever) + if lists + else torch.cat([torch.from_numpy(x) for x in whatever]) + ) + + return sum(whatever, T()) + + +def listify(elems): + return [x for e in elems for x in e] + + +def to_gpu(_view, device): + ignore_keys = set( + ["depthmap", "dataset", "label", "instance", "idx", "true_shape", "rng"] + ) + view = {} + for name in _view.keys(): + if name in ignore_keys: + continue + if isinstance(_view[name], tuple) or isinstance(_view[name], list): + view[name] = [x.clone().to(device, non_blocking=True) for x in _view[name]] + else: + view[name] = _view[name].clone().to(device, non_blocking=True) + + return view \ No newline at end of file diff --git a/instantsplat/initializer/ttt3r/export.py b/instantsplat/initializer/ttt3r/export.py new file mode 100644 index 0000000..e204b62 --- /dev/null +++ b/instantsplat/initializer/ttt3r/export.py @@ -0,0 +1,186 @@ +from copy import deepcopy + +import cv2 +import numpy as np +import PIL.Image +import torch +from PIL import Image +from PIL.ImageOps import exif_transpose + +from .bootstrap import ensure_runtime_paths + +ensure_runtime_paths() + +import torchvision.transforms as tvf +from dust3r.utils.image import _resize_pil_image + + +def load_images(image_paths, size: int | None = None): + imgs = [] + for path in image_paths: + image = exif_transpose(PIL.Image.open(path)).convert("RGB") + width_before, height_before = image.size + image = _resize_pil_image(image, size) + width_after, height_after = image.size + width_after = width_after // 16 * 16 + height_after = height_after // 16 * 16 + image = np.array(image) + image = cv2.resize( + image, + (width_after, height_after), + interpolation=cv2.INTER_LINEAR, + ) + image = PIL.Image.fromarray(image) + + print( + f" - adding {path} with resolution {width_before}x{height_before} --> " + f"{width_after}x{height_after}" + ) + image_norm = tvf.Compose( + [ + tvf.ToTensor(), + tvf.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ] + ) + imgs.append( + dict( + img=image_norm(image)[None], + true_shape=np.int32([image.size[::-1]]), + idx=len(imgs), + instance=str(len(imgs)), + ) + ) + return imgs, [None] * len(imgs) + + +def prepare_input(image_paths, size, reset_interval): + images, _ = load_images(image_paths, size=size) + + views = [] + for index, image in enumerate(images): + view = { + "img": image["img"], + "ray_map": torch.full( + ( + image["img"].shape[0], + 6, + image["img"].shape[-2], + image["img"].shape[-1], + ), + torch.nan, + ), + "true_shape": torch.from_numpy(image["true_shape"]), + "idx": index, + "instance": str(index), + "camera_pose": torch.from_numpy(np.eye(4, dtype=np.float32)).unsqueeze(0), + "img_mask": torch.tensor(True).unsqueeze(0), + "ray_mask": torch.tensor(False).unsqueeze(0), + "update": torch.tensor(True).unsqueeze(0), + "reset": torch.tensor((index + 1) % reset_interval == 0).unsqueeze(0), + } + views.append(view) + if (index + 1) % reset_interval == 0: + overlap_view = deepcopy(view) + overlap_view["reset"] = torch.tensor(False).unsqueeze(0) + views.append(overlap_view) + return views + + +def accumulate_reset_poses(pr_poses, reset_mask, matrix_cumprod): + if not reset_mask.any(): + return pr_poses + concatenated = torch.cat(pr_poses, 0) + identity = torch.eye(4, device=concatenated.device) + reset_poses = torch.where( + reset_mask.unsqueeze(-1).unsqueeze(-1), concatenated, identity + ) + cumulative_bases = matrix_cumprod(reset_poses) + shifted_bases = torch.cat([identity.unsqueeze(0), cumulative_bases[:-1]], dim=0) + composed = torch.einsum("bij,bjk->bik", shifted_bases, concatenated) + return list(composed.unsqueeze(1).unbind(0)) + + +def export_outputs(outputs, image_paths, min_conf_thr): + from dust3r.post_process import estimate_focal_knowing_depth + + from .camera import pose_encoding_to_camera + from .geometry import geotrf, matrix_cumprod + + outputs["pred"] = list(outputs["pred"]) + outputs["views"] = list(outputs["views"]) + + reset_mask = torch.cat([view["reset"] for view in outputs["views"]], 0) + shifted_reset_mask = torch.cat( + [torch.tensor(False).unsqueeze(0), reset_mask[:-1]], dim=0 + ) + + outputs["pred"] = [ + pred for pred, masked in zip(outputs["pred"], shifted_reset_mask) if not masked + ] + outputs["views"] = [ + view for view, masked in zip(outputs["views"], shifted_reset_mask) if not masked + ] + reset_mask = reset_mask[~shifted_reset_mask] + + pts3ds_self = torch.cat( + [output["pts3d_in_self_view"].cpu() for output in outputs["pred"]], 0 + ) + conf_self = torch.cat([output["conf_self"].cpu() for output in outputs["pred"]], 0) + colors = torch.cat( + [ + 0.5 * (view["img"].permute(0, 2, 3, 1).cpu() + 1.0) + for view in outputs["views"] + ], + 0, + ) + + pr_poses = [ + pose_encoding_to_camera(pred["camera_pose"].clone()).cpu() + for pred in outputs["pred"] + ] + pr_poses = accumulate_reset_poses(pr_poses, reset_mask, matrix_cumprod) + + transformed_points = [] + filtered_colors = [] + for pose, self_points, self_conf, color in zip(pr_poses, pts3ds_self, conf_self, colors): + mask = self_conf > min_conf_thr + world_points = geotrf(pose, self_points.unsqueeze(0)).squeeze(0) + transformed_points.append(world_points[mask]) + filtered_colors.append(color[mask]) + + if not transformed_points or sum(points.shape[0] for points in transformed_points) == 0: + raise RuntimeError( + f"TTT3R produced no points above confidence threshold {min_conf_thr}." + ) + + principal_points = torch.stack( + [ + torch.tensor( + [int(view["true_shape"][0][1]) // 2, int(view["true_shape"][0][0]) // 2], + device=pts3ds_self.device, + ).float() + for view in outputs["views"] + ], + 0, + ) + focals = estimate_focal_knowing_depth( + pts3ds_self, principal_points, focal_mode="weiszfeld" + ).cpu() + cam2worlds = torch.cat(pr_poses, 0) + world2cams = torch.linalg.inv(cam2worlds) + + original_sizes = [] + for image_path in image_paths: + with Image.open(image_path) as image: + original_sizes.append(image.size) + + return { + "points": torch.cat(transformed_points, 0).numpy().astype(np.float32), + "colors": torch.cat(filtered_colors, 0).numpy().astype(np.float32), + "world2cams": world2cams.numpy().astype(np.float32), + "focals": focals.numpy().astype(np.float32), + "principal_points": principal_points.cpu().numpy().astype(np.float32), + "image_paths": np.asarray(image_paths, dtype=object), + "image_widths": np.asarray([size[0] for size in original_sizes], dtype=np.int32), + "image_heights": np.asarray([size[1] for size in original_sizes], dtype=np.int32), + } diff --git a/instantsplat/initializer/ttt3r/geometry.py b/instantsplat/initializer/ttt3r/geometry.py new file mode 100755 index 0000000..9ba5ac9 --- /dev/null +++ b/instantsplat/initializer/ttt3r/geometry.py @@ -0,0 +1,567 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch +import numpy as np +from scipy.spatial import cKDTree as KDTree + +from .device import to_numpy +from .misc import invalid_to_zeros, invalid_to_nans + + +def xy_grid( + W, + H, + device=None, + origin=(0, 0), + unsqueeze=None, + cat_dim=-1, + homogeneous=False, + **arange_kw, +): + """Output a (H,W,2) array of int32 + with output[j,i,0] = i + origin[0] + output[j,i,1] = j + origin[1] + """ + if device is None: + + arange, meshgrid, stack, ones = np.arange, np.meshgrid, np.stack, np.ones + else: + + arange = lambda *a, **kw: torch.arange(*a, device=device, **kw) + meshgrid, stack = torch.meshgrid, torch.stack + ones = lambda *a: torch.ones(*a, device=device) + + tw, th = [arange(o, o + s, **arange_kw) for s, o in zip((W, H), origin)] + grid = meshgrid(tw, th, indexing="xy") + if homogeneous: + grid = grid + (ones((H, W)),) + if unsqueeze is not None: + grid = (grid[0].unsqueeze(unsqueeze), grid[1].unsqueeze(unsqueeze)) + if cat_dim is not None: + grid = stack(grid, cat_dim) + return grid + + +def geotrf(Trf, pts, ncol=None, norm=False): + """Apply a geometric transformation to a list of 3-D points. + + H: 3x3 or 4x4 projection matrix (typically a Homography) + p: numpy/torch/tuple of coordinates. Shape must be (...,2) or (...,3) + + ncol: int. number of columns of the result (2 or 3) + norm: float. if != 0, the resut is projected on the z=norm plane. + + Returns an array of projected 2d points. + """ + assert Trf.ndim >= 2 + if isinstance(Trf, np.ndarray): + pts = np.asarray(pts) + elif isinstance(Trf, torch.Tensor): + pts = torch.as_tensor(pts, dtype=Trf.dtype) + + output_reshape = pts.shape[:-1] + ncol = ncol or pts.shape[-1] + + if ( + isinstance(Trf, torch.Tensor) + and isinstance(pts, torch.Tensor) + and Trf.ndim == 3 + and pts.ndim == 4 + ): + d = pts.shape[3] + if Trf.shape[-1] == d: + pts = torch.einsum("bij, bhwj -> bhwi", Trf, pts) + elif Trf.shape[-1] == d + 1: + pts = ( + torch.einsum("bij, bhwj -> bhwi", Trf[:, :d, :d], pts) + + Trf[:, None, None, :d, d] + ) + else: + raise ValueError(f"bad shape, not ending with 3 or 4, for {pts.shape=}") + else: + if Trf.ndim >= 3: + n = Trf.ndim - 2 + assert Trf.shape[:n] == pts.shape[:n], "batch size does not match" + Trf = Trf.reshape(-1, Trf.shape[-2], Trf.shape[-1]) + + if pts.ndim > Trf.ndim: + + pts = pts.reshape(Trf.shape[0], -1, pts.shape[-1]) + elif pts.ndim == 2: + + pts = pts[:, None, :] + + if pts.shape[-1] + 1 == Trf.shape[-1]: + Trf = Trf.swapaxes(-1, -2) # transpose Trf + pts = pts @ Trf[..., :-1, :] + Trf[..., -1:, :] + elif pts.shape[-1] == Trf.shape[-1]: + Trf = Trf.swapaxes(-1, -2) # transpose Trf + pts = pts @ Trf + else: + pts = Trf @ pts.T + if pts.ndim >= 2: + pts = pts.swapaxes(-1, -2) + + if norm: + pts = pts / pts[..., -1:] # DONT DO /= BECAUSE OF WEIRD PYTORCH BUG + if norm != 1: + pts *= norm + + res = pts[..., :ncol].reshape(*output_reshape, ncol) + return res + + +def inv(mat): + """Invert a torch or numpy matrix""" + if isinstance(mat, torch.Tensor): + return torch.linalg.inv(mat) + if isinstance(mat, np.ndarray): + return np.linalg.inv(mat) + raise ValueError(f"bad matrix type = {type(mat)}") + + +def depthmap_to_pts3d(depth, pseudo_focal, pp=None, **_): + """ + Args: + - depthmap (BxHxW array): + - pseudo_focal: [B,H,W] ; [B,2,H,W] or [B,1,H,W] + Returns: + pointmap of absolute coordinates (BxHxWx3 array) + """ + + if len(depth.shape) == 4: + B, H, W, n = depth.shape + else: + B, H, W = depth.shape + n = None + + if len(pseudo_focal.shape) == 3: # [B,H,W] + pseudo_focalx = pseudo_focaly = pseudo_focal + elif len(pseudo_focal.shape) == 4: # [B,2,H,W] or [B,1,H,W] + pseudo_focalx = pseudo_focal[:, 0] + if pseudo_focal.shape[1] == 2: + pseudo_focaly = pseudo_focal[:, 1] + else: + pseudo_focaly = pseudo_focalx + else: + raise NotImplementedError("Error, unknown input focal shape format.") + + assert pseudo_focalx.shape == depth.shape[:3] + assert pseudo_focaly.shape == depth.shape[:3] + grid_x, grid_y = xy_grid(W, H, cat_dim=0, device=depth.device)[:, None] + + if pp is None: + grid_x = grid_x - (W - 1) / 2 + grid_y = grid_y - (H - 1) / 2 + else: + grid_x = grid_x.expand(B, -1, -1) - pp[:, 0, None, None] + grid_y = grid_y.expand(B, -1, -1) - pp[:, 1, None, None] + + if n is None: + pts3d = torch.empty((B, H, W, 3), device=depth.device) + pts3d[..., 0] = depth * grid_x / pseudo_focalx + pts3d[..., 1] = depth * grid_y / pseudo_focaly + pts3d[..., 2] = depth + else: + pts3d = torch.empty((B, H, W, 3, n), device=depth.device) + pts3d[..., 0, :] = depth * (grid_x / pseudo_focalx)[..., None] + pts3d[..., 1, :] = depth * (grid_y / pseudo_focaly)[..., None] + pts3d[..., 2, :] = depth + return pts3d + + +def depthmap_to_camera_coordinates(depthmap, camera_intrinsics, pseudo_focal=None): + """ + Args: + - depthmap (HxW array): + - camera_intrinsics: a 3x3 matrix + Returns: + pointmap of absolute coordinates (HxWx3 array), and a mask specifying valid pixels. + """ + camera_intrinsics = np.float32(camera_intrinsics) + H, W = depthmap.shape + + assert camera_intrinsics[0, 1] == 0.0 + assert camera_intrinsics[1, 0] == 0.0 + if pseudo_focal is None: + fu = camera_intrinsics[0, 0] + fv = camera_intrinsics[1, 1] + else: + assert pseudo_focal.shape == (H, W) + fu = fv = pseudo_focal + cu = camera_intrinsics[0, 2] + cv = camera_intrinsics[1, 2] + + u, v = np.meshgrid(np.arange(W), np.arange(H)) + z_cam = depthmap + x_cam = (u - cu) * z_cam / fu + y_cam = (v - cv) * z_cam / fv + X_cam = np.stack((x_cam, y_cam, z_cam), axis=-1).astype(np.float32) + + valid_mask = depthmap > 0.0 + return X_cam, valid_mask + + +def depthmap_to_absolute_camera_coordinates( + depthmap, camera_intrinsics, camera_pose, **kw +): + """ + Args: + - depthmap (HxW array): + - camera_intrinsics: a 3x3 matrix + - camera_pose: a 4x3 or 4x4 cam2world matrix + Returns: + pointmap of absolute coordinates (HxWx3 array), and a mask specifying valid pixels. + """ + X_cam, valid_mask = depthmap_to_camera_coordinates(depthmap, camera_intrinsics) + + X_world = X_cam # default + if camera_pose is not None: + + R_cam2world = camera_pose[:3, :3] + t_cam2world = camera_pose[:3, 3] + + X_world = ( + np.einsum("ik, vuk -> vui", R_cam2world, X_cam) + t_cam2world[None, None, :] + ) + + return X_world, valid_mask + + +def colmap_to_opencv_intrinsics(K): + """ + Modify camera intrinsics to follow a different convention. + Coordinates of the center of the top-left pixels are by default: + - (0.5, 0.5) in Colmap + - (0,0) in OpenCV + """ + K = K.copy() + K[0, 2] -= 0.5 + K[1, 2] -= 0.5 + return K + + +def opencv_to_colmap_intrinsics(K): + """ + Modify camera intrinsics to follow a different convention. + Coordinates of the center of the top-left pixels are by default: + - (0.5, 0.5) in Colmap + - (0,0) in OpenCV + """ + K = K.copy() + K[0, 2] += 0.5 + K[1, 2] += 0.5 + return K + + +def normalize_pointcloud( + pts1, pts2, norm_mode="avg_dis", valid1=None, valid2=None, ret_factor=False +): + """renorm pointmaps pts1, pts2 with norm_mode""" + assert pts1.ndim >= 3 and pts1.shape[-1] == 3 + assert pts2 is None or (pts2.ndim >= 3 and pts2.shape[-1] == 3) + norm_mode, dis_mode = norm_mode.split("_") + + if norm_mode == "avg": + + nan_pts1, nnz1 = invalid_to_zeros(pts1, valid1, ndim=3) + nan_pts2, nnz2 = ( + invalid_to_zeros(pts2, valid2, ndim=3) if pts2 is not None else (None, 0) + ) + all_pts = ( + torch.cat((nan_pts1, nan_pts2), dim=1) if pts2 is not None else nan_pts1 + ) + + all_dis = all_pts.norm(dim=-1) + if dis_mode == "dis": + pass # do nothing + elif dis_mode == "log1p": + all_dis = torch.log1p(all_dis) + elif dis_mode == "warp-log1p": + + log_dis = torch.log1p(all_dis) + warp_factor = log_dis / all_dis.clip(min=1e-8) + H1, W1 = pts1.shape[1:-1] + pts1 = pts1 * warp_factor[:, : W1 * H1].view(-1, H1, W1, 1) + if pts2 is not None: + H2, W2 = pts2.shape[1:-1] + pts2 = pts2 * warp_factor[:, W1 * H1 :].view(-1, H2, W2, 1) + all_dis = log_dis # this is their true distance afterwards + else: + raise ValueError(f"bad {dis_mode=}") + + norm_factor = all_dis.sum(dim=1) / (nnz1 + nnz2 + 1e-8) + else: + + nan_pts1 = invalid_to_nans(pts1, valid1, ndim=3) + nan_pts2 = invalid_to_nans(pts2, valid2, ndim=3) if pts2 is not None else None + all_pts = ( + torch.cat((nan_pts1, nan_pts2), dim=1) if pts2 is not None else nan_pts1 + ) + + all_dis = all_pts.norm(dim=-1) + + if norm_mode == "avg": + norm_factor = all_dis.nanmean(dim=1) + elif norm_mode == "median": + norm_factor = all_dis.nanmedian(dim=1).values.detach() + elif norm_mode == "sqrt": + norm_factor = all_dis.sqrt().nanmean(dim=1) ** 2 + else: + raise ValueError(f"bad {norm_mode=}") + + norm_factor = norm_factor.clip(min=1e-8) + while norm_factor.ndim < pts1.ndim: + norm_factor.unsqueeze_(-1) + + res = pts1 / norm_factor + if pts2 is not None: + res = (res, pts2 / norm_factor) + if ret_factor: + res = res + (norm_factor,) + return res + + +def normalize_pointcloud_group( + pts_list, + norm_mode="avg_dis", + valid_list=None, + conf_list=None, + ret_factor=False, + ret_factor_only=False, +): + """renorm pointmaps pts1, pts2 with norm_mode""" + for pts in pts_list: + assert pts.ndim >= 3 and pts.shape[-1] == 3 + + norm_mode, dis_mode = norm_mode.split("_") + + if norm_mode == "avg": + + nan_pts_list, nnz_list = zip( + *[ + invalid_to_zeros(pts1, valid1, ndim=3) + for pts1, valid1 in zip(pts_list, valid_list) + ] + ) + all_pts = torch.cat(nan_pts_list, dim=1) + if conf_list is not None: + nan_conf_list = [ + invalid_to_zeros(conf1[..., None], valid1, ndim=3)[0] + for conf1, valid1 in zip(conf_list, valid_list) + ] + all_conf = torch.cat(nan_conf_list, dim=1)[..., 0] + else: + all_conf = torch.ones_like(all_pts[..., 0]) + + all_dis = all_pts.norm(dim=-1) + if dis_mode == "dis": + pass # do nothing + elif dis_mode == "log1p": + all_dis = torch.log1p(all_dis) + elif dis_mode == "warp-log1p": + + log_dis = torch.log1p(all_dis) + warp_factor = log_dis / all_dis.clip(min=1e-8) + H_W_list = [pts.shape[1:-1] for pts in pts_list] + pts_list = [ + pts + * warp_factor[:, sum(H_W_list[:i]) : sum(H_W_list[: i + 1])].view( + -1, H, W, 1 + ) + for i, (pts, (H, W)) in enumerate(zip(pts_list, H_W_list)) + ] + all_dis = log_dis # this is their true distance afterwards + else: + raise ValueError(f"bad {dis_mode=}") + + norm_factor = (all_conf * all_dis).sum(dim=1) / (all_conf.sum(dim=1) + 1e-8) + else: + + nan_pts_list = [ + invalid_to_nans(pts1, valid1, ndim=3) + for pts1, valid1 in zip(pts_list, valid_list) + ] + + all_pts = torch.cat(nan_pts_list, dim=1) + + all_dis = all_pts.norm(dim=-1) + + if norm_mode == "avg": + norm_factor = all_dis.nanmean(dim=1) + elif norm_mode == "median": + norm_factor = all_dis.nanmedian(dim=1).values.detach() + elif norm_mode == "sqrt": + norm_factor = all_dis.sqrt().nanmean(dim=1) ** 2 + else: + raise ValueError(f"bad {norm_mode=}") + + norm_factor = norm_factor.clip(min=1e-8) + while norm_factor.ndim < pts_list[0].ndim: + norm_factor.unsqueeze_(-1) + + if ret_factor_only: + + return norm_factor + + res = [pts / norm_factor for pts in pts_list] + if ret_factor: + return res, norm_factor + return res + + +@torch.no_grad() +def get_joint_pointcloud_depth(z1, z2, valid_mask1, valid_mask2=None, quantile=0.5): + + _z1 = invalid_to_nans(z1, valid_mask1).reshape(len(z1), -1) + _z2 = ( + invalid_to_nans(z2, valid_mask2).reshape(len(z2), -1) + if z2 is not None + else None + ) + _z = torch.cat((_z1, _z2), dim=-1) if z2 is not None else _z1 + + if quantile == 0.5: + shift_z = torch.nanmedian(_z, dim=-1).values + else: + shift_z = torch.nanquantile(_z, quantile, dim=-1) + return shift_z # (B,) + + +@torch.no_grad() +def get_group_pointcloud_depth(zs, valid_masks, quantile=0.5): + + _zs = [ + invalid_to_nans(z1, valid_mask1).reshape(len(z1), -1) + for z1, valid_mask1 in zip(zs, valid_masks) + ] + _z = torch.cat(_zs, dim=-1) + + if quantile == 0.5: + shift_z = torch.nanmedian(_z, dim=-1).values + else: + shift_z = torch.nanquantile(_z, quantile, dim=-1) + return shift_z # (B,) + + +@torch.no_grad() +def get_joint_pointcloud_center_scale( + pts1, pts2, valid_mask1=None, valid_mask2=None, z_only=False, center=True +): + + _pts1 = invalid_to_nans(pts1, valid_mask1).reshape(len(pts1), -1, 3) + _pts2 = ( + invalid_to_nans(pts2, valid_mask2).reshape(len(pts2), -1, 3) + if pts2 is not None + else None + ) + _pts = torch.cat((_pts1, _pts2), dim=1) if pts2 is not None else _pts1 + + _center = torch.nanmedian(_pts, dim=1, keepdim=True).values # (B,1,3) + if z_only: + _center[..., :2] = 0 # do not center X and Y + + _norm = ((_pts - _center) if center else _pts).norm(dim=-1) + scale = torch.nanmedian(_norm, dim=1).values + return _center[:, None, :, :], scale[:, None, None, None] + + +@torch.no_grad() +def get_group_pointcloud_center_scale(pts, valid_masks=None, z_only=False, center=True): + + _pts = [ + invalid_to_nans(pts1, valid_mask1).reshape(len(pts1), -1, 3) + for pts1, valid_mask1 in zip(pts, valid_masks) + ] + _pts = torch.cat(_pts, dim=1) + + _center = torch.nanmedian(_pts, dim=1, keepdim=True).values # (B,1,3) + if z_only: + _center[..., :2] = 0 # do not center X and Y + + _norm = ((_pts - _center) if center else _pts).norm(dim=-1) + scale = torch.nanmedian(_norm, dim=1).values + return _center[:, None, :, :], scale[:, None, None, None] + + +def find_reciprocal_matches(P1, P2): + """ + returns 3 values: + 1 - reciprocal_in_P2: a boolean array of size P2.shape[0], a "True" value indicates a match + 2 - nn2_in_P1: a int array of size P2.shape[0], it contains the indexes of the closest points in P1 + 3 - reciprocal_in_P2.sum(): the number of matches + """ + tree1 = KDTree(P1) + tree2 = KDTree(P2) + + _, nn1_in_P2 = tree2.query(P1, workers=8) + _, nn2_in_P1 = tree1.query(P2, workers=8) + + reciprocal_in_P1 = nn2_in_P1[nn1_in_P2] == np.arange(len(nn1_in_P2)) + reciprocal_in_P2 = nn1_in_P2[nn2_in_P1] == np.arange(len(nn2_in_P1)) + assert reciprocal_in_P1.sum() == reciprocal_in_P2.sum() + return reciprocal_in_P2, nn2_in_P1, reciprocal_in_P2.sum() + + +def get_med_dist_between_poses(poses): + from scipy.spatial.distance import pdist + + return np.median(pdist([to_numpy(p[:3, 3]) for p in poses])) + + +def weighted_procrustes(A, B, w, use_weights=True, eps=1e-16, return_T=False): + """ + X: torch tensor B x N x 3 + Y: torch tensor B x N x 3 + w: torch tensor B x N + """ + assert len(A) == len(B) + if use_weights: + W1 = torch.abs(w).sum(1, keepdim=True) + w_norm = (w / (W1 + eps)).unsqueeze(-1) + a_mean = (w_norm * A).sum(dim=1, keepdim=True) + b_mean = (w_norm * B).sum(dim=1, keepdim=True) + + A_c = A - a_mean + B_c = B - b_mean + + H = torch.einsum("bni,bnj->bij", A_c, w_norm * B_c) + + else: + a_mean = A.mean(axis=1, keepdim=True) + b_mean = B.mean(axis=1, keepdim=True) + + A_c = A - a_mean + B_c = B - b_mean + + H = torch.einsum("bij,bik->bjk", A_c, B_c) + + U, S, V = torch.svd(H) # U: B x 3 x 3, S: B x 3, V: B x 3 x 3 + Z = torch.eye(3).unsqueeze(0).repeat(A.shape[0], 1, 1).to(A.device) + Z[:, -1, -1] = torch.sign(torch.linalg.det(U @ V.transpose(1, 2))) # B x 3 x 3 + R = V @ Z @ U.transpose(1, 2) # B x 3 x 3 + t = b_mean - torch.einsum("bij,bjk->bik", R, a_mean.transpose(-2, -1)).transpose( + -2, -1 + ) + if return_T: + T = torch.eye(4).unsqueeze(0).repeat(A.shape[0], 1, 1).to(A.device) + T[:, :3, :3] = R + T[:, :3, 3] = t.squeeze() + return T + return R, t.squeeze() + + +def matrix_cumprod(matrices): + if len(matrices) == 0: + return matrices + + result = torch.empty_like(matrices) + result[0] = matrices[0] + + for i in range(1, len(matrices)): + torch.matmul(result[i-1], matrices[i], out=result[i]) + return result diff --git a/instantsplat/initializer/ttt3r/heads.py b/instantsplat/initializer/ttt3r/heads.py new file mode 100755 index 0000000..35f735b --- /dev/null +++ b/instantsplat/initializer/ttt3r/heads.py @@ -0,0 +1,35 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +from .heads_linear import LinearPts3d, LinearPts3d_Desc, LinearPts3dPose +from .heads_dpt import DPTPts3dPose, create_dpt_head + + +def head_factory( + head_type, + output_mode, + net, + has_conf=False, + has_depth=False, + has_rgb=False, + has_pose_conf=False, + has_pose=False, +): + """ " build a prediction head for the decoder""" + if head_type == "linear" and output_mode == "pts3d": + return LinearPts3d(net, has_conf, has_depth, has_rgb, has_pose_conf) + elif head_type == "linear" and output_mode == "pts3d+pose": + return LinearPts3dPose(net, has_conf, has_rgb, has_pose) + elif head_type == "linear" and output_mode.startswith("pts3d+desc"): + local_feat_dim = int(output_mode[10:]) + return LinearPts3d_Desc(net, has_conf, has_depth, local_feat_dim) + elif head_type == "dpt" and output_mode == "pts3d": + raise NotImplementedError(f"unexpected {head_type=} and {output_mode=}") + return create_dpt_head(net, has_conf=has_conf) + elif head_type == "dpt" and output_mode == "pts3d+pose": + return DPTPts3dPose(net, has_conf, has_rgb, has_pose) + else: + raise NotImplementedError(f"unexpected {head_type=} and {output_mode=}") diff --git a/instantsplat/initializer/ttt3r/heads_dpt.py b/instantsplat/initializer/ttt3r/heads_dpt.py new file mode 100755 index 0000000..c046b32 --- /dev/null +++ b/instantsplat/initializer/ttt3r/heads_dpt.py @@ -0,0 +1,263 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +from einops import rearrange +from typing import List +import torch +import torch.nn as nn +from .bootstrap import ensure_runtime_paths +from .camera import PoseDecoder, pose_encoding_to_camera +from .heads_postprocess import ( + postprocess, + postprocess_desc, + postprocess_rgb, + postprocess_pose_conf, + postprocess_pose, + reg_dense_conf, +) + +ensure_runtime_paths() + +from models.dpt_block import DPTOutputAdapter # noqa +from .blocks import ConditionModulationBlock +from torch.utils.checkpoint import checkpoint + + +class DPTOutputAdapter_fix(DPTOutputAdapter): + """ + Adapt croco's DPTOutputAdapter implementation for dust3r: + remove duplicated weigths, and fix forward for dust3r + """ + + def init(self, dim_tokens_enc=768): + super().init(dim_tokens_enc) + + del self.act_1_postprocess + del self.act_2_postprocess + del self.act_3_postprocess + del self.act_4_postprocess + + def forward(self, encoder_tokens: List[torch.Tensor], image_size=None): + assert ( + self.dim_tokens_enc is not None + ), "Need to call init(dim_tokens_enc) function first" + + image_size = self.image_size if image_size is None else image_size + H, W = image_size + + N_H = H // (self.stride_level * self.P_H) + N_W = W // (self.stride_level * self.P_W) + + layers = [encoder_tokens[hook] for hook in self.hooks] + + layers = [self.adapt_tokens(l) for l in layers] + + layers = [ + rearrange(l, "b (nh nw) c -> b c nh nw", nh=N_H, nw=N_W) for l in layers + ] + + layers = [self.act_postprocess[idx](l) for idx, l in enumerate(layers)] + + layers = [self.scratch.layer_rn[idx](l) for idx, l in enumerate(layers)] + + path_4 = self.scratch.refinenet4(layers[3])[ + :, :, : layers[2].shape[2], : layers[2].shape[3] + ] + path_3 = self.scratch.refinenet3(path_4, layers[2]) + path_2 = self.scratch.refinenet2(path_3, layers[1]) + path_1 = self.scratch.refinenet1(path_2, layers[0]) + + out = self.head(path_1) + + return out + + +class PixelwiseTaskWithDPT(nn.Module): + """DPT module for dust3r, can return 3D points + confidence for all pixels""" + + def __init__( + self, + *, + n_cls_token=0, + hooks_idx=None, + dim_tokens=None, + output_width_ratio=1, + num_channels=1, + postprocess=None, + depth_mode=None, + conf_mode=None, + **kwargs + ): + super(PixelwiseTaskWithDPT, self).__init__() + self.return_all_layers = True # backbone needs to return all layers + self.postprocess = postprocess + self.depth_mode = depth_mode + self.conf_mode = conf_mode + + assert n_cls_token == 0, "Not implemented" + dpt_args = dict( + output_width_ratio=output_width_ratio, num_channels=num_channels, **kwargs + ) + if hooks_idx is not None: + dpt_args.update(hooks=hooks_idx) + self.dpt = DPTOutputAdapter_fix(**dpt_args) + dpt_init_args = {} if dim_tokens is None else {"dim_tokens_enc": dim_tokens} + self.dpt.init(**dpt_init_args) + + def forward(self, x, img_info): + out = self.dpt(x, image_size=(img_info[0], img_info[1])) + if self.postprocess: + out = self.postprocess(out, self.depth_mode, self.conf_mode) + return out + + +def create_dpt_head(net, has_conf=False): + """ + return PixelwiseTaskWithDPT for given net params + """ + assert net.dec_depth > 9 + l2 = net.dec_depth + feature_dim = 256 + last_dim = feature_dim // 2 + out_nchan = 3 + ed = net.enc_embed_dim + dd = net.dec_embed_dim + return PixelwiseTaskWithDPT( + num_channels=out_nchan + has_conf, + feature_dim=feature_dim, + last_dim=last_dim, + hooks_idx=[0, l2 * 2 // 4, l2 * 3 // 4, l2], + dim_tokens=[ed, dd, dd, dd], + postprocess=postprocess, + depth_mode=net.depth_mode, + conf_mode=net.conf_mode, + head_type="regression", + ) + + +class DPTPts3dPose(nn.Module): + def __init__(self, net, has_conf=False, has_rgb=False, has_pose=False): + super(DPTPts3dPose, self).__init__() + self.return_all_layers = True # backbone needs to return all layers + self.depth_mode = net.depth_mode + self.conf_mode = net.conf_mode + self.pose_mode = net.pose_mode + + self.has_conf = has_conf + self.has_rgb = has_rgb + self.has_pose = has_pose + + pts_channels = 3 + has_conf + rgb_channels = has_rgb * 3 + feature_dim = 256 + last_dim = feature_dim // 2 + ed = net.enc_embed_dim + dd = net.dec_embed_dim + hooks_idx = [0, 1, 2, 3] + dim_tokens = [ed, dd, dd, dd] + head_type = "regression" + output_width_ratio = 1 + + pts_dpt_args = dict( + output_width_ratio=output_width_ratio, + num_channels=pts_channels, + feature_dim=feature_dim, + last_dim=last_dim, + dim_tokens=dim_tokens, + hooks_idx=hooks_idx, + head_type=head_type, + ) + rgb_dpt_args = dict( + output_width_ratio=output_width_ratio, + num_channels=rgb_channels, + feature_dim=feature_dim, + last_dim=last_dim, + dim_tokens=dim_tokens, + hooks_idx=hooks_idx, + head_type=head_type, + ) + if hooks_idx is not None: + pts_dpt_args.update(hooks=hooks_idx) + rgb_dpt_args.update(hooks=hooks_idx) + + self.dpt_self = DPTOutputAdapter_fix(**pts_dpt_args) + dpt_init_args = {} if dim_tokens is None else {"dim_tokens_enc": dim_tokens} + self.dpt_self.init(**dpt_init_args) + + self.final_transform = nn.ModuleList( + [ + ConditionModulationBlock( + net.dec_embed_dim, + net.dec_num_heads, + mlp_ratio=4.0, + qkv_bias=True, + rope=net.rope, + ) + for _ in range(2) + ] + ) + + self.dpt_cross = DPTOutputAdapter_fix(**pts_dpt_args) + dpt_init_args = {} if dim_tokens is None else {"dim_tokens_enc": dim_tokens} + self.dpt_cross.init(**dpt_init_args) + + if has_rgb: + self.dpt_rgb = DPTOutputAdapter_fix(**rgb_dpt_args) + dpt_init_args = {} if dim_tokens is None else {"dim_tokens_enc": dim_tokens} + self.dpt_rgb.init(**dpt_init_args) + + if has_pose: + in_dim = net.dec_embed_dim + self.pose_head = PoseDecoder(hidden_size=in_dim) + + def forward(self, x, img_info, **kwargs): + if self.has_pose: + pose_token = x[-1][:, 0].clone() # [1, 768] + token = x[-1][:, 1:] # [1, 576, 768] + with torch.cuda.amp.autocast(enabled=False): + pose = self.pose_head(pose_token) # [1, 7] + + token_cross = token.clone() + for blk in self.final_transform: + token_cross = blk(token_cross, pose_token, kwargs.get("pos")) # [1, 576, 768] + x = x[:-1] + [token] + x_cross = x[:-1] + [token_cross] + + with torch.cuda.amp.autocast(enabled=False): + self_out = checkpoint( + self.dpt_self, + x, + image_size=(img_info[0], img_info[1]), + use_reentrant=False, + ) # [1, 4, 288, 512] + + final_output = postprocess(self_out, self.depth_mode, self.conf_mode) + final_output["pts3d_in_self_view"] = final_output.pop("pts3d") + final_output["conf_self"] = final_output.pop("conf") + + if self.has_rgb: + rgb_out = checkpoint( + self.dpt_rgb, + x, + image_size=(img_info[0], img_info[1]), + use_reentrant=False, + ) # [1, 3, 288, 512] + rgb_output = postprocess_rgb(rgb_out) + final_output.update(rgb_output) + + if self.has_pose: + pose = postprocess_pose(pose, self.pose_mode) + final_output["camera_pose"] = pose # B,7 + cross_out = checkpoint( + self.dpt_cross, + x_cross, + image_size=(img_info[0], img_info[1]), + use_reentrant=False, + ) # [1, 4, 288, 512] + tmp = postprocess(cross_out, self.depth_mode, self.conf_mode) + final_output["pts3d_in_other_view"] = tmp.pop("pts3d") + final_output["conf"] = tmp.pop("conf") + return final_output diff --git a/instantsplat/initializer/ttt3r/heads_linear.py b/instantsplat/initializer/ttt3r/heads_linear.py new file mode 100755 index 0000000..5449b39 --- /dev/null +++ b/instantsplat/initializer/ttt3r/heads_linear.py @@ -0,0 +1,349 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch +import torch.nn as nn +import torch.nn.functional as F +from .bootstrap import ensure_runtime_paths +from .camera import PoseDecoder, pose_encoding_to_camera +from .geometry import geotrf +from .heads_postprocess import ( + postprocess, + postprocess_desc, + postprocess_rgb, + postprocess_pose_conf, + postprocess_pose, + reg_dense_conf, +) + +ensure_runtime_paths() + +from models.blocks import Mlp # noqa +from .blocks import ConditionModulationBlock + + +class LinearPts3d(nn.Module): + """ + Linear head for dust3r + Each token outputs: - 16x16 3D points (+ confidence) + """ + + def __init__( + self, net, has_conf=False, has_depth=False, has_rgb=False, has_pose_conf=False + ): + super().__init__() + self.patch_size = net.patch_embed.patch_size[0] + self.depth_mode = net.depth_mode + self.conf_mode = net.conf_mode + self.has_conf = has_conf + self.has_rgb = has_rgb + self.has_pose_conf = has_pose_conf + self.has_depth = has_depth + self.proj = Mlp( + net.dec_embed_dim, out_features=(3 + has_conf) * self.patch_size**2 + ) + if has_depth: + self.self_proj = Mlp( + net.dec_embed_dim, out_features=(3 + has_conf) * self.patch_size**2 + ) + if has_rgb: + self.rgb_proj = Mlp(net.dec_embed_dim, out_features=3 * self.patch_size**2) + + def setup(self, croconet): + pass + + def forward(self, decout, img_shape): + H, W = img_shape + tokens = decout[-1] + B, S, D = tokens.shape + + feat = self.proj(tokens) # B,S,D + feat = feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + feat = F.pixel_shuffle(feat, self.patch_size) # B,3,H,W + + final_output = postprocess(feat, self.depth_mode, self.conf_mode) + final_output["pts3d_in_other_view"] = final_output.pop("pts3d") + + if self.has_depth: + self_feat = self.self_proj(tokens) # B,S,D + self_feat = self_feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + self_feat = F.pixel_shuffle(self_feat, self.patch_size) # B,3,H,W + self_3d_output = postprocess(self_feat, self.depth_mode, self.conf_mode) + self_3d_output["pts3d_in_self_view"] = self_3d_output.pop("pts3d") + self_3d_output["conf_self"] = self_3d_output.pop("conf") + final_output.update(self_3d_output) + + if self.has_rgb: + rgb_feat = self.rgb_proj(tokens) + rgb_feat = rgb_feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + rgb_feat = F.pixel_shuffle(rgb_feat, self.patch_size) # B,3,H,W + rgb_output = postprocess_rgb(rgb_feat) + final_output.update(rgb_output) + + if self.has_pose_conf: + pose_conf = self.pose_conf_proj(tokens) + pose_conf = pose_conf.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + pose_conf = F.pixel_shuffle(pose_conf, self.patch_size) + pose_conf_output = postprocess_pose_conf(pose_conf) + final_output.update(pose_conf_output) + + return final_output + + +class LinearPts3d_Desc(nn.Module): + """ + Linear head for dust3r + Each token outputs: - 16x16 3D points (+ confidence) + """ + + def __init__( + self, + net, + has_conf=False, + has_depth=False, + local_feat_dim=24, + hidden_dim_factor=4.0, + ): + super().__init__() + self.patch_size = net.patch_embed.patch_size[0] + self.depth_mode = net.depth_mode + self.conf_mode = net.conf_mode + self.has_conf = has_conf + self.double_channel = has_depth + self.local_feat_dim = local_feat_dim + + if not has_depth: + self.proj = nn.Linear( + net.dec_embed_dim, (3 + has_conf) * self.patch_size**2 + ) + else: + self.proj = nn.Linear( + net.dec_embed_dim, (3 + has_conf) * 2 * self.patch_size**2 + ) + idim = net.enc_embed_dim + net.dec_embed_dim + self.head_local_features = Mlp( + in_features=idim, + hidden_features=int(hidden_dim_factor * idim), + out_features=(self.local_feat_dim + 1) * self.patch_size**2, + ) + + def setup(self, croconet): + pass + + def forward(self, decout, img_shape): + H, W = img_shape + tokens = decout[-1] + B, S, D = tokens.shape + + feat = self.proj(tokens) # B,S,D + feat = feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + feat = F.pixel_shuffle(feat, self.patch_size) # B,3,H,W + + enc_output, dec_output = decout[0], decout[-1] + cat_output = torch.cat([enc_output, dec_output], dim=-1) + local_features = self.head_local_features(cat_output) # B,S,D + local_features = local_features.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + local_features = F.pixel_shuffle(local_features, self.patch_size) # B,d,H,W + feat = torch.cat([feat, local_features], dim=1) + + return postprocess_desc( + feat, + self.depth_mode, + self.conf_mode, + self.local_feat_dim, + self.double_channel, + ) + + +class LinearPts3dPoseDirect(nn.Module): + """ + Linear head for dust3r + Each token outputs: - 16x16 3D points (+ confidence) + """ + + def __init__(self, net, has_conf=False, has_rgb=False, has_pose=False): + super().__init__() + self.patch_size = net.patch_embed.patch_size[0] + self.depth_mode = net.depth_mode + self.conf_mode = net.conf_mode + self.pose_mode = net.pose_mode + self.has_conf = has_conf + self.has_rgb = has_rgb + self.has_pose = has_pose + + self.proj = Mlp( + net.dec_embed_dim, out_features=(3 + has_conf) * self.patch_size**2 + ) + if has_rgb: + self.rgb_proj = Mlp(net.dec_embed_dim, out_features=3 * self.patch_size**2) + if has_pose: + self.pose_head = PoseDecoder(hidden_size=net.dec_embed_dim) + if has_conf: + self.cross_conf_proj = Mlp( + net.dec_embed_dim, out_features=self.patch_size**2 + ) + + def setup(self, croconet): + pass + + def forward(self, decout, img_shape): + H, W = img_shape + tokens = decout[-1] + if self.has_pose: + pose_token = tokens[:, 0] + tokens = tokens[:, 1:] + B, S, D = tokens.shape + + feat = self.proj(tokens) # B,S,D + feat = feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + feat = F.pixel_shuffle(feat, self.patch_size) # B,3,H,W + final_output = postprocess(feat, self.depth_mode, self.conf_mode) + final_output["pts3d_in_self_view"] = final_output.pop("pts3d") + final_output["conf_self"] = final_output.pop("conf") + + if self.has_rgb: + rgb_feat = self.rgb_proj(tokens) + rgb_feat = rgb_feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + rgb_feat = F.pixel_shuffle(rgb_feat, self.patch_size) # B,3,H,W + rgb_output = postprocess_rgb(rgb_feat) + final_output.update(rgb_output) + + if self.has_pose: + pose = self.pose_head(pose_token) + pose = postprocess_pose(pose, self.pose_mode) + final_output["camera_pose"] = pose # B,7 + final_output["pts3d_in_other_view"] = geotrf( + pose_encoding_to_camera(final_output["camera_pose"]), + final_output["pts3d_in_self_view"], + ) + + if self.has_conf: + cross_conf = self.cross_conf_proj(tokens) + cross_conf = cross_conf.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + cross_conf = F.pixel_shuffle(cross_conf, self.patch_size)[:, 0] + final_output["conf"] = reg_dense_conf(cross_conf, mode=self.conf_mode) + return final_output + + +class LinearPts3dPose(nn.Module): + """ + Linear head for dust3r + Each token outputs: - 16x16 3D points (+ confidence) + """ + + def __init__( + self, net, has_conf=False, has_rgb=False, has_pose=False, mlp_ratio=4.0 + ): + super().__init__() + self.patch_size = net.patch_embed.patch_size[0] + self.depth_mode = net.depth_mode + self.conf_mode = net.conf_mode + self.pose_mode = net.pose_mode + self.has_conf = has_conf + self.has_rgb = has_rgb + self.has_pose = has_pose + + self.proj = Mlp( + net.dec_embed_dim, + hidden_features=int(mlp_ratio * net.dec_embed_dim), + out_features=(3 + has_conf) * self.patch_size**2, + ) + if has_rgb: + self.rgb_proj = Mlp( + net.dec_embed_dim, + hidden_features=int(mlp_ratio * net.dec_embed_dim), + out_features=3 * self.patch_size**2, + ) + if has_pose: + self.pose_head = PoseDecoder(hidden_size=net.dec_embed_dim) + self.final_transform = nn.ModuleList( + [ + ConditionModulationBlock( + net.dec_embed_dim, + net.dec_num_heads, + mlp_ratio=4.0, + qkv_bias=True, + rope=net.rope, + ) + for _ in range(2) + ] + ) + self.cross_proj = Mlp( + net.dec_embed_dim, + hidden_features=int(mlp_ratio * net.dec_embed_dim), + out_features=(3 + has_conf) * self.patch_size**2, + ) + + def setup(self, croconet): + pass + + def forward(self, decout, img_shape, **kwargs): + H, W = img_shape + tokens = decout[-1] + if self.has_pose: + pose_token = tokens[:, 0] + tokens = tokens[:, 1:] + with torch.cuda.amp.autocast(enabled=False): + pose = self.pose_head(pose_token) + cross_tokens = tokens + for blk in self.final_transform: + cross_tokens = blk(cross_tokens, pose_token, kwargs.get("pos")) + + with torch.cuda.amp.autocast(enabled=False): + B, S, D = tokens.shape + + feat = self.proj(tokens) # B,S,D + feat = feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + feat = F.pixel_shuffle(feat, self.patch_size) # B,3,H,W + final_output = postprocess( + feat, self.depth_mode, self.conf_mode, pos_z=True + ) + final_output["pts3d_in_self_view"] = final_output.pop("pts3d") + final_output["conf_self"] = final_output.pop("conf") + + if self.has_rgb: + rgb_feat = self.rgb_proj(tokens) + rgb_feat = rgb_feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + rgb_feat = F.pixel_shuffle(rgb_feat, self.patch_size) # B,3,H,W + rgb_output = postprocess_rgb(rgb_feat) + final_output.update(rgb_output) + + if self.has_pose: + pose = postprocess_pose(pose, self.pose_mode) + final_output["camera_pose"] = pose # B,7 + + cross_feat = self.cross_proj(cross_tokens) # B,S,D + cross_feat = cross_feat.transpose(-1, -2).view( + B, -1, H // self.patch_size, W // self.patch_size + ) + cross_feat = F.pixel_shuffle(cross_feat, self.patch_size) # B,3,H,W + tmp = postprocess(cross_feat, self.depth_mode, self.conf_mode) + final_output["pts3d_in_other_view"] = tmp.pop("pts3d") + final_output["conf"] = tmp.pop("conf") + + return final_output diff --git a/instantsplat/initializer/ttt3r/heads_postprocess.py b/instantsplat/initializer/ttt3r/heads_postprocess.py new file mode 100755 index 0000000..e760a09 --- /dev/null +++ b/instantsplat/initializer/ttt3r/heads_postprocess.py @@ -0,0 +1,167 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch +import torch.nn.functional as F + + +def postprocess(out, depth_mode, conf_mode, pos_z=False): + """ + extract 3D points/confidence from prediction head output + """ + fmap = out.permute(0, 2, 3, 1) # B,H,W,3 + res = dict(pts3d=reg_dense_depth(fmap[:, :, :, 0:3], mode=depth_mode, pos_z=pos_z)) + + if conf_mode is not None: + res["conf"] = reg_dense_conf(fmap[:, :, :, 3], mode=conf_mode) + return res + + +def postprocess_rgb(out, eps=1e-6): + fmap = out.permute(0, 2, 3, 1) # B,H,W,3 + res = torch.sigmoid(fmap) * (1 - 2 * eps) + eps + res = (res - 0.5) * 2 + return dict(rgb=res) + + +def postprocess_pose(out, mode, inverse=False): + """ + extract pose from prediction head output + """ + mode, vmin, vmax = mode + + no_bounds = (vmin == -float("inf")) and (vmax == float("inf")) + assert no_bounds + trans = out[..., 0:3] + quats = out[..., 3:7] + + if mode == "linear": + if no_bounds: + return trans # [-inf, +inf] + return trans.clip(min=vmin, max=vmax) + + d = trans.norm(dim=-1, keepdim=True) + + if mode == "square": + if inverse: + scale = d / d.square().clip(min=1e-8) + else: + scale = d.square() / d.clip(min=1e-8) + + if mode == "exp": + if inverse: + scale = d / torch.expm1(d).clip(min=1e-8) + else: + scale = torch.expm1(d) / d.clip(min=1e-8) + + trans = trans * scale + quats = standardize_quaternion(quats) + + return torch.cat([trans, quats], dim=-1) + + +def postprocess_pose_conf(out): + fmap = out.permute(0, 2, 3, 1) # B,H,W,1 + return dict(pose_conf=torch.sigmoid(fmap)) + + +def postprocess_desc(out, depth_mode, conf_mode, desc_dim, double_channel=False): + """ + extract 3D points/confidence from prediction head output + """ + fmap = out.permute(0, 2, 3, 1) # B,H,W,3 + res = dict(pts3d=reg_dense_depth(fmap[:, :, :, 0:3], mode=depth_mode)) + + if conf_mode is not None: + res["conf"] = reg_dense_conf(fmap[:, :, :, 3], mode=conf_mode) + + if double_channel: + res["pts3d_self"] = reg_dense_depth( + fmap[ + :, :, :, 3 + int(conf_mode is not None) : 6 + int(conf_mode is not None) + ], + mode=depth_mode, + ) + if conf_mode is not None: + res["conf_self"] = reg_dense_conf( + fmap[:, :, :, 6 + int(conf_mode is not None)], mode=conf_mode + ) + + start = ( + 3 + + int(conf_mode is not None) + + int(double_channel) * (3 + int(conf_mode is not None)) + ) + res["desc"] = reg_desc(fmap[:, :, :, start : start + desc_dim], mode="norm") + res["desc_conf"] = reg_dense_conf(fmap[:, :, :, start + desc_dim], mode=conf_mode) + assert start + desc_dim + 1 == fmap.shape[-1] + + return res + + +def reg_desc(desc, mode="norm"): + if "norm" in mode: + desc = desc / desc.norm(dim=-1, keepdim=True) + else: + raise ValueError(f"Unknown desc mode {mode}") + return desc + + +def reg_dense_depth(xyz, mode, pos_z=False): + """ + extract 3D points from prediction head output + """ + mode, vmin, vmax = mode + + no_bounds = (vmin == -float("inf")) and (vmax == float("inf")) + assert no_bounds + + if mode == "linear": + if no_bounds: + return xyz # [-inf, +inf] + return xyz.clip(min=vmin, max=vmax) + + if pos_z: + sign = torch.sign(xyz[..., -1:]) + xyz *= sign + d = xyz.norm(dim=-1, keepdim=True) # [1, H, W, 1] + xyz = xyz / d.clip(min=1e-8) # [1, H, W, 3] + + if mode == "square": + return xyz * d.square() + + if mode == "exp": + return xyz * torch.expm1(d) + + raise ValueError(f"bad {mode=}") + + +def reg_dense_conf(x, mode): + """ + extract confidence from prediction head output + """ + mode, vmin, vmax = mode + if mode == "exp": + return vmin + x.exp().clip(max=vmax - vmin) + if mode == "sigmoid": + return (vmax - vmin) * torch.sigmoid(x) + vmin + raise ValueError(f"bad {mode=}") + + +def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor: + """ + Convert a unit quaternion to a standard form: one in which the real + part is non negative. + + Args: + quaternions: Quaternions with real part first, + as tensor of shape (..., 4). + + Returns: + Standardized quaternions as tensor of shape (..., 4). + """ + quaternions = F.normalize(quaternions, p=2, dim=-1) + return torch.where(quaternions[..., 0:1] < 0, -quaternions, quaternions) diff --git a/instantsplat/initializer/ttt3r/inference.py b/instantsplat/initializer/ttt3r/inference.py new file mode 100755 index 0000000..67988e0 --- /dev/null +++ b/instantsplat/initializer/ttt3r/inference.py @@ -0,0 +1,398 @@ +import tqdm +import torch +from typing import Any + +from .device import to_cpu, collate_with_cat +from .geometry import depthmap_to_pts3d, geotrf +from .misc import invalid_to_nans +import re + + +def custom_sort_key(key): + text = key.split("/") + if len(text) > 1: + text, num = text[0], text[-1] + return (text, int(num)) + else: + return (key, -1) + + +def merge_chunk_dict(old_dict, curr_dict, add_number): + new_dict = {} + for key, value in curr_dict.items(): + + match = re.search(r"(\d+)$", key) + if match: + + num_part = int(match.group()) + add_number + + new_key = re.sub(r"(\d+)$", str(num_part), key, 1) + new_dict[new_key] = value + else: + new_dict[key] = value + new_dict = old_dict | new_dict + return {k: new_dict[k] for k in sorted(new_dict.keys(), key=custom_sort_key)} + + +def _interleave_imgs(img1, img2): + res = {} + for key, value1 in img1.items(): + value2 = img2[key] + if isinstance(value1, torch.Tensor): + value = torch.stack((value1, value2), dim=1).flatten(0, 1) + else: + value = [x for pair in zip(value1, value2) for x in pair] + res[key] = value + return res + + +def make_batch_symmetric(batch): + view1, view2 = batch + view1, view2 = (_interleave_imgs(view1, view2), _interleave_imgs(view2, view1)) + return view1, view2 + + +def loss_of_one_batch( + batch, + model, + criterion, + accelerator: Any, + symmetrize_batch=False, + use_amp=False, + ret=None, + img_mask=None, + inference=False, +): + if len(batch) > 2: + assert ( + symmetrize_batch is False + ), "cannot symmetrize batch with more than 2 views" + if symmetrize_batch: + batch = make_batch_symmetric(batch) + + with torch.cuda.amp.autocast(enabled=not inference): + if inference: + output, state_args = model(batch, ret_state=True) + preds, batch = output.ress, output.views + result = dict(views=batch, pred=preds) + return result[ret] if ret else result, state_args + else: + output = model(batch) + preds, batch = output.ress, output.views + + with torch.cuda.amp.autocast(enabled=False): + loss = criterion(batch, preds) if criterion is not None else None + + result = dict(views=batch, pred=preds, loss=loss) + return result[ret] if ret else result + + +def loss_of_one_batch_tbptt( + batch, + model, + criterion, + chunk_size, + loss_scaler, + optimizer, + accelerator: Any, + log_writer=None, + symmetrize_batch=False, + use_amp=False, + ret=None, + img_mask=None, + inference=False, +): + if len(batch) > 2: + assert ( + symmetrize_batch is False + ), "cannot symmetrize batch with more than 2 views" + if symmetrize_batch: + batch = make_batch_symmetric(batch) + all_preds = [] + all_loss = 0.0 + all_loss_details = {} + with torch.cuda.amp.autocast(enabled=not inference): + with torch.no_grad(): + (feat, pos, shape), ( + init_state_feat, + init_mem, + state_feat, + state_pos, + mem, + ) = accelerator.unwrap_model(model)._forward_encoder(batch) + feat = [f.detach() for f in feat] + pos = [p.detach() for p in pos] + shape = [s.detach() for s in shape] + init_state_feat = init_state_feat.detach() + init_mem = init_mem.detach() + + for chunk_id in range((len(batch) - 1) // chunk_size + 1): + preds = [] + chunk = [] + state_feat = state_feat.detach() + state_pos = state_pos.detach() + mem = mem.detach() + if chunk_id < ((len(batch) - 1) // chunk_size + 1) - 4: + with torch.no_grad(): + for in_chunk_idx in range(chunk_size): + i = chunk_id * chunk_size + in_chunk_idx + if i >= len(batch): + break + res, (state_feat, mem) = accelerator.unwrap_model( + model + )._forward_decoder_step( + batch, + i, + feat_i=feat[i], + pos_i=pos[i], + shape_i=shape[i], + init_state_feat=init_state_feat, + init_mem=init_mem, + state_feat=state_feat, + state_pos=state_pos, + mem=mem, + ) + preds.append(res) + all_preds.append({k: v.detach() for k, v in res.items()}) + chunk.append(batch[i]) + with torch.cuda.amp.autocast(enabled=False): + loss, loss_details = ( + criterion(chunk, preds, camera1=batch[0]["camera_pose"]) + if criterion is not None + else None + ) + all_loss += float(loss) + all_loss_details = merge_chunk_dict( + all_loss_details, loss_details, chunk_id * chunk_size + ) + del loss + else: # last 3 chunks with chunk_size=4: 3*4=12 images with gradient + for in_chunk_idx in range(chunk_size): + i = chunk_id * chunk_size + in_chunk_idx + if i >= len(batch): + break + res, (state_feat, mem) = accelerator.unwrap_model( + model + )._forward_decoder_step( + batch, + i, + feat_i=feat[i], + pos_i=pos[i], + shape_i=shape[i], + init_state_feat=init_state_feat, + init_mem=init_mem, + state_feat=state_feat, + state_pos=state_pos, + mem=mem, + ) + preds.append(res) + all_preds.append({k: v.detach() for k, v in res.items()}) + chunk.append(batch[i]) + with torch.cuda.amp.autocast(enabled=False): + loss, loss_details = ( + criterion(chunk, preds, camera1=batch[0]["camera_pose"]) + if criterion is not None + else None + ) + all_loss += float(loss) + all_loss_details = merge_chunk_dict( + all_loss_details, loss_details, chunk_id * chunk_size + ) + loss_scaler( + loss, + optimizer, + parameters=model.parameters(), + update_grad=True, + clip_grad=1.0, + ) + optimizer.zero_grad() + del loss + result = dict( + views=batch, + pred=all_preds, + loss=(all_loss / ((len(batch) - 1) // chunk_size + 1), all_loss_details), + already_backprop=True, + ) + return result[ret] if ret else result + + +@torch.no_grad() +def inference(groups, model, device, verbose=True): + ignore_keys = set( + ["depthmap", "dataset", "label", "instance", "idx", "true_shape", "rng"] + ) + for view in groups: + for name in view.keys(): # pseudo_focal + if name in ignore_keys: + continue + if isinstance(view[name], tuple) or isinstance(view[name], list): + view[name] = [x.to(device, non_blocking=True) for x in view[name]] + else: + view[name] = view[name].to(device, non_blocking=True) + + if verbose: + print(f">> Inference with model on {len(groups)} image/raymaps") + + res, state_args = loss_of_one_batch(groups, model, None, None, inference=True) + result = to_cpu(res) + return result, state_args + + +@torch.no_grad() +def inference_step(view, state_args, model, device, verbose=True): + ignore_keys = set( + ["depthmap", "dataset", "label", "instance", "idx", "true_shape", "rng"] + ) + for name in view.keys(): # pseudo_focal + if name in ignore_keys: + continue + if isinstance(view[name], tuple) or isinstance(view[name], list): + view[name] = [x.to(device, non_blocking=True) for x in view[name]] + else: + view[name] = view[name].to(device, non_blocking=True) + + with torch.cuda.amp.autocast(enabled=False): + state_feat, state_pos, init_state_feat, mem, init_mem = state_args + pred, _ = model.inference_step( + view, state_feat, state_pos, init_state_feat, mem, init_mem + ) + + res = dict(pred=pred) + result = to_cpu(res) + return result + + +@torch.no_grad() +def inference_recurrent(groups, model, device, verbose=True): + ignore_keys = set( + ["depthmap", "dataset", "label", "instance", "idx", "true_shape", "rng"] + ) + for view in groups: + for name in view.keys(): # pseudo_focal + if name in ignore_keys: + continue + if isinstance(view[name], tuple) or isinstance(view[name], list): + view[name] = [x.to(device, non_blocking=True) for x in view[name]] + else: + view[name] = view[name].to(device, non_blocking=True) + + if verbose: + print(f">> Inference with model on {len(groups)} image/raymaps") + + with torch.cuda.amp.autocast(enabled=False): + preds, batch, state_args = model.forward_recurrent( + groups, device, ret_state=True + ) + res = dict(views=batch, pred=preds) + result = to_cpu(res) + return result, state_args + +@torch.no_grad() +def inference_recurrent_lighter(groups, model, device, verbose=True): + if verbose: + print(f">> Inference with model on {len(groups)} image/raymaps") + + with torch.cuda.amp.autocast(enabled=False): + preds, batch, state_args = model.forward_recurrent_lighter( + groups, device, ret_state=True + ) + res = dict(views=batch, pred=preds) + return res, state_args + +def check_if_same_size(pairs): + shapes1 = [img1["img"].shape[-2:] for img1, img2 in pairs] + shapes2 = [img2["img"].shape[-2:] for img1, img2 in pairs] + return all(shapes1[0] == s for s in shapes1) and all( + shapes2[0] == s for s in shapes2 + ) + + +def get_pred_pts3d(gt, pred, use_pose=False, inplace=False): + if "depth" in pred and "pseudo_focal" in pred: + try: + pp = gt["camera_intrinsics"][..., :2, 2] + except KeyError: + pp = None + pts3d = depthmap_to_pts3d(**pred, pp=pp) + + elif "pts3d" in pred: + + pts3d = pred["pts3d"] + + elif "pts3d_in_other_view" in pred: + + assert use_pose is True + return ( + pred["pts3d_in_other_view"] + if inplace + else pred["pts3d_in_other_view"].clone() + ) + + if use_pose: + camera_pose = pred.get("camera_pose") + assert camera_pose is not None + pts3d = geotrf(camera_pose, pts3d) + + return pts3d + + +def find_opt_scaling( + gt_pts1, + gt_pts2, + pr_pts1, + pr_pts2=None, + fit_mode="weiszfeld_stop_grad", + valid1=None, + valid2=None, +): + assert gt_pts1.ndim == pr_pts1.ndim == 4 + assert gt_pts1.shape == pr_pts1.shape + if gt_pts2 is not None: + assert gt_pts2.ndim == pr_pts2.ndim == 4 + assert gt_pts2.shape == pr_pts2.shape + + nan_gt_pts1 = invalid_to_nans(gt_pts1, valid1).flatten(1, 2) + nan_gt_pts2 = ( + invalid_to_nans(gt_pts2, valid2).flatten(1, 2) if gt_pts2 is not None else None + ) + + pr_pts1 = invalid_to_nans(pr_pts1, valid1).flatten(1, 2) + pr_pts2 = ( + invalid_to_nans(pr_pts2, valid2).flatten(1, 2) if pr_pts2 is not None else None + ) + + all_gt = ( + torch.cat((nan_gt_pts1, nan_gt_pts2), dim=1) + if gt_pts2 is not None + else nan_gt_pts1 + ) + all_pr = torch.cat((pr_pts1, pr_pts2), dim=1) if pr_pts2 is not None else pr_pts1 + + dot_gt_pr = (all_pr * all_gt).sum(dim=-1) + dot_gt_gt = all_gt.square().sum(dim=-1) + + if fit_mode.startswith("avg"): + + scaling = dot_gt_pr.nanmean(dim=1) / dot_gt_gt.nanmean(dim=1) + elif fit_mode.startswith("median"): + scaling = (dot_gt_pr / dot_gt_gt).nanmedian(dim=1).values + elif fit_mode.startswith("weiszfeld"): + + scaling = dot_gt_pr.nanmean(dim=1) / dot_gt_gt.nanmean(dim=1) + + for iter in range(10): + + dis = (all_pr - scaling.view(-1, 1, 1) * all_gt).norm(dim=-1) + + w = dis.clip_(min=1e-8).reciprocal() + + scaling = (w * dot_gt_pr).nanmean(dim=1) / (w * dot_gt_gt).nanmean(dim=1) + else: + raise ValueError(f"bad {fit_mode=}") + + if fit_mode.endswith("stop_grad"): + scaling = scaling.detach() + + scaling = scaling.clip(min=1e-3) + + return scaling diff --git a/instantsplat/initializer/ttt3r/misc.py b/instantsplat/initializer/ttt3r/misc.py new file mode 100755 index 0000000..fbb3f22 --- /dev/null +++ b/instantsplat/initializer/ttt3r/misc.py @@ -0,0 +1,127 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch + + +def fill_default_args(kwargs, func): + import inspect # a bit hacky but it works reliably + + signature = inspect.signature(func) + + for k, v in signature.parameters.items(): + if v.default is inspect.Parameter.empty: + continue + kwargs.setdefault(k, v.default) + + return kwargs + + +def freeze_all_params(modules): + for module in modules: + try: + for n, param in module.named_parameters(): + param.requires_grad = False + except AttributeError: + + module.requires_grad = False + + +def is_symmetrized(gt1, gt2): + x = gt1["instance"] + y = gt2["instance"] + if len(x) == len(y) and len(x) == 1: + return False # special case of batchsize 1 + ok = True + for i in range(0, len(x), 2): + ok = ok and (x[i] == y[i + 1]) and (x[i + 1] == y[i]) + return ok + + +def flip(tensor): + """flip so that tensor[0::2] <=> tensor[1::2]""" + return torch.stack((tensor[1::2], tensor[0::2]), dim=1).flatten(0, 1) + + +def interleave(tensor1, tensor2): + res1 = torch.stack((tensor1, tensor2), dim=1).flatten(0, 1) + res2 = torch.stack((tensor2, tensor1), dim=1).flatten(0, 1) + return res1, res2 + + +def transpose_to_landscape(head, activate=True): + """Predict in the correct aspect-ratio, + then transpose the result in landscape + and stack everything back together. + """ + + def wrapper_no(decout, true_shape, **kwargs): + B = len(true_shape) + assert true_shape[0:1].allclose(true_shape), "true_shape must be all identical" + H, W = true_shape[0].cpu().tolist() + res = head(decout, (H, W), **kwargs) + return res + + def wrapper_yes(decout, true_shape, **kwargs): + B = len(true_shape) + + H, W = int(true_shape.min()), int(true_shape.max()) + + height, width = true_shape.T + is_landscape = width >= height + is_portrait = ~is_landscape + + if is_landscape.all(): + return head(decout, (H, W), **kwargs) + if is_portrait.all(): + return transposed(head(decout, (W, H), **kwargs)) + + def selout(ar): + return [d[ar] for d in decout] + + if "pos" in kwargs: + kwargs_landscape = kwargs.copy() + kwargs_landscape["pos"] = kwargs["pos"][is_landscape] + kwargs_portrait = kwargs.copy() + kwargs_portrait["pos"] = kwargs["pos"][is_portrait] + l_result = head(selout(is_landscape), (H, W), **kwargs_landscape) + p_result = transposed(head(selout(is_portrait), (W, H), **kwargs_portrait)) + + result = {} + for k in l_result | p_result: + x = l_result[k].new(B, *l_result[k].shape[1:]) + x[is_landscape] = l_result[k] + x[is_portrait] = p_result[k] + result[k] = x + + return result + + return wrapper_yes if activate else wrapper_no + + +def transposed(dic): + return {k: v.swapaxes(1, 2) if v.ndim > 2 else v for k, v in dic.items()} + + +def invalid_to_nans(arr, valid_mask, ndim=999): + if valid_mask is not None: + arr = arr.clone() + arr[~valid_mask] = float("nan") + if arr.ndim > ndim: + arr = arr.flatten(-2 - (arr.ndim - ndim), -2) + return arr + + +def invalid_to_zeros(arr, valid_mask, ndim=999): + if valid_mask is not None: + arr = arr.clone() + arr[~valid_mask] = 0 + nnz = valid_mask.view(len(valid_mask), -1).sum(1) + else: + nnz = arr.numel() // len(arr) if len(arr) else 0 # number of point per image + if arr.ndim > ndim: + arr = arr.flatten(-2 - (arr.ndim - ndim), -2) + return arr, nnz diff --git a/instantsplat/initializer/ttt3r/model.py b/instantsplat/initializer/ttt3r/model.py new file mode 100644 index 0000000..6c90a95 --- /dev/null +++ b/instantsplat/initializer/ttt3r/model.py @@ -0,0 +1,1298 @@ +import sys +import os + +from collections import OrderedDict +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.checkpoint import checkpoint +from copy import deepcopy +from functools import partial +from typing import Optional, Tuple, List, Any +from dataclasses import dataclass +from .bootstrap import ensure_runtime_paths + +ensure_runtime_paths() + +from .blocks import ( + Attention, + Block, + CrossAttention, + DecoderBlock, + DropPath, + Mlp, +) +from .device import to_cpu, to_gpu +from .heads import head_factory +from .misc import ( + fill_default_args, + freeze_all_params, + is_symmetrized, + interleave, + transpose_to_landscape, +) +from .patch_embed import get_patch_embed +from models.croco import CroCoNet # noqa + +inf = float("inf") +from einops import rearrange + + +@dataclass +class ARCroco3DStereoOutput: + ress: Optional[List[Any]] = None + views: Optional[List[Any]] = None + + +def strip_module(state_dict): + """ + Removes the 'module.' prefix from the keys of a state_dict. + Args: + state_dict (dict): The original state_dict with possible 'module.' prefixes. + Returns: + OrderedDict: A new state_dict with 'module.' prefixes removed. + """ + new_state_dict = OrderedDict() + for k, v in state_dict.items(): + name = k[7:] if k.startswith("module.") else k + new_state_dict[name] = v + return new_state_dict + + +def load_model(model_path, device, verbose=True): + if verbose: + print("... loading model from", model_path) + # TTT3R/CUT3R checkpoints bundle OmegaConf/argparse objects in addition to + # tensor weights, so PyTorch 2.6's default weights_only=True cannot load + # them. These checkpoints are a trusted local dependency for this initializer. + ckpt = torch.load(model_path, map_location="cpu", weights_only=False) + args = ckpt["args"].model.replace( + "ManyAR_PatchEmbed", "PatchEmbedDust3R" + ) # ManyAR only for aspect ratio not consistent + if "landscape_only" not in args: + args = args[:-2] + ", landscape_only=False))" + else: + args = args.replace(" ", "").replace( + "landscape_only=True", "landscape_only=False" + ) + assert "landscape_only=False" in args + if verbose: + print(f"instantiating : {args}") + net = eval(args) + s = net.load_state_dict(ckpt["model"], strict=False) + if verbose: + print(s) + return net.to(device) + + +class ARCroco3DStereoConfig: + model_type = "arcroco_3d_stereo" + + def __init__( + self, + output_mode="pts3d", + head_type="linear", # or dpt + depth_mode=("exp", -float("inf"), float("inf")), + conf_mode=("exp", 1, float("inf")), + pose_mode=("exp", -float("inf"), float("inf")), + freeze="none", + landscape_only=True, + patch_embed_cls="PatchEmbedDust3R", + ray_enc_depth=2, + state_size=324, + local_mem_size=256, + state_pe="2d", + state_dec_num_heads=16, + depth_head=False, + rgb_head=False, + pose_conf_head=False, + pose_head=False, + model_update_type="cut3r", + **croco_kwargs, + ): + self.output_mode = output_mode + self.head_type = head_type + self.depth_mode = depth_mode + self.conf_mode = conf_mode + self.pose_mode = pose_mode + self.freeze = freeze + self.landscape_only = landscape_only + self.patch_embed_cls = patch_embed_cls + self.ray_enc_depth = ray_enc_depth + self.state_size = state_size + self.state_pe = state_pe + self.state_dec_num_heads = state_dec_num_heads + self.local_mem_size = local_mem_size + self.depth_head = depth_head + self.rgb_head = rgb_head + self.pose_conf_head = pose_conf_head + self.pose_head = pose_head + self.model_update_type = model_update_type + self.croco_kwargs = croco_kwargs + + +class LocalMemory(nn.Module): + def __init__( + self, + size, + k_dim, + v_dim, + num_heads, + depth=2, + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + norm_mem=True, + rope=None, + ) -> None: + super().__init__() + self.v_dim = v_dim + self.proj_q = nn.Linear(k_dim, v_dim) + self.masked_token = nn.Parameter( + torch.randn(1, 1, v_dim) * 0.2, requires_grad=True + ) # [1, 1, 768] pose mask token + self.mem = nn.Parameter( + torch.randn(1, size, 2 * v_dim) * 0.2, requires_grad=True + ) # [1, 256, 1536] pose mem + self.write_blocks = nn.ModuleList( + [ + DecoderBlock( + 2 * v_dim, + num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + norm_layer=norm_layer, + attn_drop=attn_drop, + drop=drop, + drop_path=drop_path, + act_layer=act_layer, + norm_mem=norm_mem, + rope=rope, + ) + for _ in range(depth) + ] + ) + self.read_blocks = nn.ModuleList( + [ + DecoderBlock( + 2 * v_dim, + num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + norm_layer=norm_layer, + attn_drop=attn_drop, + drop=drop, + drop_path=drop_path, + act_layer=act_layer, + norm_mem=norm_mem, + rope=rope, + ) + for _ in range(depth) + ] + ) + + def update_mem(self, mem, feat_k, feat_v, return_attn=False): + """ + mem_k: [B, size, C] + mem_v: [B, size, C] + feat_k: [B, 1, C] global_img_feat + feat_v: [B, 1, C] out_pose_feat + """ + feat_k = self.proj_q(feat_k) # [B, 1, C] + feat = torch.cat([feat_k, feat_v], dim=-1) + + attention_maps = [] + for blk in self.write_blocks: + mem, _, self_attn, cross_attn = blk(mem, feat, None, None, return_attn=return_attn) + attention_maps.append((self_attn, cross_attn)) + return mem + + def inquire(self, query, mem, return_attn=False): + x = self.proj_q(query) # [B, 1, C] + x = torch.cat([x, self.masked_token.expand(x.shape[0], -1, -1)], dim=-1) # [1, 1, 768 global_img_feat_i + 768 masked_token(pose)] + attention_maps = [] + for blk in self.read_blocks: + x, _, self_attn, cross_attn = blk(x, mem, None, None, return_attn=return_attn) + attention_maps.append((self_attn, cross_attn)) + return x[..., -self.v_dim :] + + +class ARCroco3DStereo(CroCoNet): + supports_gradient_checkpointing = True + + def __init__(self, config: ARCroco3DStereoConfig): + self.gradient_checkpointing = False + self.fixed_input_length = True + config.croco_kwargs = fill_default_args(config.croco_kwargs, CroCoNet.__init__) + self.config = config + self.patch_embed_cls = config.patch_embed_cls + self.croco_args = config.croco_kwargs + super().__init__(**self.croco_args) + self.enc_blocks_ray_map = nn.ModuleList( + [ + Block( + self.enc_embed_dim, + 16, + 4, + qkv_bias=True, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + rope=self.rope, + ) + for _ in range(config.ray_enc_depth) + ] + ) + self.enc_norm_ray_map = nn.LayerNorm(self.enc_embed_dim, eps=1e-6) + self.dec_num_heads = self.croco_args["dec_num_heads"] + self.pose_head_flag = config.pose_head + if self.pose_head_flag: + self.pose_token = nn.Parameter( + torch.randn(1, 1, self.dec_embed_dim) * 0.02, requires_grad=True + ) # [1, 1, 768] + self.pose_retriever = LocalMemory( + size=config.local_mem_size, + k_dim=self.enc_embed_dim, + v_dim=self.dec_embed_dim, + num_heads=self.dec_num_heads, + mlp_ratio=4, + qkv_bias=True, + attn_drop=0.0, + norm_layer=partial(nn.LayerNorm, eps=1e-6), + rope=None, + ) + self.register_tokens = nn.Embedding(config.state_size, self.enc_embed_dim) # init state tokens [768, 1024] + self.state_size = config.state_size + self.state_pe = config.state_pe + self.masked_img_token = nn.Parameter( + torch.randn(1, self.enc_embed_dim) * 0.02, requires_grad=True + ) + self.masked_ray_map_token = nn.Parameter( + torch.randn(1, self.enc_embed_dim) * 0.02, requires_grad=True + ) + self._set_state_decoder( + self.enc_embed_dim, + self.dec_embed_dim, + config.state_dec_num_heads, + self.dec_depth, + self.croco_args.get("mlp_ratio", None), + self.croco_args.get("norm_layer", None), + self.croco_args.get("norm_im2_in_dec", None), + ) + self.set_downstream_head( + config.output_mode, + config.head_type, + config.landscape_only, + config.depth_mode, + config.conf_mode, + config.pose_mode, + config.depth_head, + config.rgb_head, + config.pose_conf_head, + config.pose_head, + **self.croco_args, + ) + self.set_freeze(config.freeze) + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path, **kw): + if os.path.isfile(pretrained_model_name_or_path): + return load_model(pretrained_model_name_or_path, device="cpu") + raise FileNotFoundError( + f"Local checkpoint not found: {pretrained_model_name_or_path}" + ) + + def _set_patch_embed(self, img_size=224, patch_size=16, enc_embed_dim=768): + self.patch_embed = get_patch_embed( + self.patch_embed_cls, img_size, patch_size, enc_embed_dim, in_chans=3 + ) + self.patch_embed_ray_map = get_patch_embed( + self.patch_embed_cls, img_size, patch_size, enc_embed_dim, in_chans=6 + ) + + def _set_decoder( + self, + enc_embed_dim, + dec_embed_dim, + dec_num_heads, + dec_depth, + mlp_ratio, + norm_layer, + norm_im2_in_dec, + ): + self.dec_depth = dec_depth + self.dec_embed_dim = dec_embed_dim + self.decoder_embed = nn.Linear(enc_embed_dim, dec_embed_dim, bias=True) + self.dec_blocks = nn.ModuleList( + [ + DecoderBlock( + dec_embed_dim, + dec_num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=True, + norm_layer=norm_layer, + norm_mem=norm_im2_in_dec, + rope=self.rope, + ) + for i in range(dec_depth) + ] + ) + self.dec_norm = norm_layer(dec_embed_dim) + + def _set_state_decoder( + self, + enc_embed_dim, + dec_embed_dim, + dec_num_heads, + dec_depth, + mlp_ratio, + norm_layer, + norm_im2_in_dec, + ): + self.dec_depth_state = dec_depth + self.dec_embed_dim_state = dec_embed_dim + self.decoder_embed_state = nn.Linear(enc_embed_dim, dec_embed_dim, bias=True) + self.dec_blocks_state = nn.ModuleList( + [ + DecoderBlock( + dec_embed_dim, + dec_num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=True, + norm_layer=norm_layer, + norm_mem=norm_im2_in_dec, + rope=self.rope, + ) + for i in range(dec_depth) + ] + ) + self.dec_norm_state = norm_layer(dec_embed_dim) + + def load_state_dict(self, ckpt, **kw): + if all(k.startswith("module") for k in ckpt): + ckpt = strip_module(ckpt) + new_ckpt = dict(ckpt) + if not any(k.startswith("dec_blocks_state") for k in ckpt): + for key, value in ckpt.items(): + if key.startswith("dec_blocks"): + new_ckpt[key.replace("dec_blocks", "dec_blocks_state")] = value + try: + return super().load_state_dict(new_ckpt, **kw) + except: + try: + new_new_ckpt = { + k: v + for k, v in new_ckpt.items() + if not k.startswith("dec_blocks") + and not k.startswith("dec_norm") + and not k.startswith("decoder_embed") + } + return super().load_state_dict(new_new_ckpt, **kw) + except: + new_new_ckpt = {} + for key in new_ckpt: + if key in self.state_dict(): + if new_ckpt[key].size() == self.state_dict()[key].size(): + new_new_ckpt[key] = new_ckpt[key] + else: + printer.info( + f"Skipping '{key}': size mismatch (ckpt: {new_ckpt[key].size()}, model: {self.state_dict()[key].size()})" + ) + else: + printer.info(f"Skipping '{key}': not found in model") + return super().load_state_dict(new_new_ckpt, **kw) + + def set_freeze(self, freeze): # this is for use by downstream models + self.freeze = freeze + to_be_frozen = { + "none": [], + "mask": [self.mask_token] if hasattr(self, "mask_token") else [], + "encoder": [ + self.patch_embed, + self.patch_embed_ray_map, + self.masked_img_token, + self.masked_ray_map_token, + self.enc_blocks, + self.enc_blocks_ray_map, + self.enc_norm, + self.enc_norm_ray_map, + ], + "encoder_and_head": [ + self.patch_embed, + self.patch_embed_ray_map, + self.masked_img_token, + self.masked_ray_map_token, + self.enc_blocks, + self.enc_blocks_ray_map, + self.enc_norm, + self.enc_norm_ray_map, + self.downstream_head, + ], + "encoder_and_decoder": [ + self.patch_embed, + self.patch_embed_ray_map, + self.masked_img_token, + self.masked_ray_map_token, + self.enc_blocks, + self.enc_blocks_ray_map, + self.enc_norm, + self.enc_norm_ray_map, + self.dec_blocks, + self.dec_blocks_state, + self.pose_retriever, + self.pose_token, + self.register_tokens, + self.decoder_embed_state, + self.decoder_embed, + self.dec_norm, + self.dec_norm_state, + ], + "decoder": [ + self.dec_blocks, + self.dec_blocks_state, + self.pose_retriever, + self.pose_token, + ], + } + freeze_all_params(to_be_frozen[freeze]) + + def _set_prediction_head(self, *args, **kwargs): + """No prediction head""" + return + + def set_downstream_head( + self, + output_mode, + head_type, + landscape_only, + depth_mode, + conf_mode, + pose_mode, + depth_head, + rgb_head, + pose_conf_head, + pose_head, + patch_size, + img_size, + **kw, + ): + assert ( + img_size[0] % patch_size == 0 and img_size[1] % patch_size == 0 + ), f"{img_size=} must be multiple of {patch_size=}" + self.output_mode = output_mode + self.head_type = head_type + self.depth_mode = depth_mode + self.conf_mode = conf_mode + self.pose_mode = pose_mode + self.downstream_head = head_factory( + head_type, + output_mode, + self, + has_conf=bool(conf_mode), + has_depth=bool(depth_head), + has_rgb=bool(rgb_head), + has_pose_conf=bool(pose_conf_head), + has_pose=bool(pose_head), + ) + self.head = transpose_to_landscape( + self.downstream_head, activate=landscape_only + ) + + def _encode_image(self, image, true_shape): + x, pos = self.patch_embed(image, true_shape=true_shape) + assert self.enc_pos_embed is None + for blk in self.enc_blocks: + if self.gradient_checkpointing and self.training: + x = checkpoint(blk, x, pos, use_reentrant=False) + else: + x = blk(x, pos) + x = self.enc_norm(x) + return [x], pos, None + + def _encode_ray_map(self, ray_map, true_shape): + x, pos = self.patch_embed_ray_map(ray_map, true_shape=true_shape) + assert self.enc_pos_embed is None + for blk in self.enc_blocks_ray_map: + if self.gradient_checkpointing and self.training: + x = checkpoint(blk, x, pos, use_reentrant=False) + else: + x = blk(x, pos) + x = self.enc_norm_ray_map(x) + return [x], pos, None + + def _encode_state(self, image_tokens, image_pos): + batch_size = image_tokens.shape[0] + state_feat = self.register_tokens( + torch.arange(self.state_size, device=image_pos.device) + ) # [768, 1024] + if self.state_pe == "1d": + state_pos = ( + torch.tensor( + [[i, i] for i in range(self.state_size)], + dtype=image_pos.dtype, + device=image_pos.device, + )[None] + .expand(batch_size, -1, -1) + .contiguous() + ) # .long() + elif self.state_pe == "2d": + width = int(self.state_size**0.5) + width = width + 1 if width % 2 == 1 else width + state_pos = ( + torch.tensor( + [[i // width, i % width] for i in range(self.state_size)], + dtype=image_pos.dtype, + device=image_pos.device, + )[None] + .expand(batch_size, -1, -1) + .contiguous() + ) + elif self.state_pe == "none": + state_pos = None + state_feat = state_feat[None].expand(batch_size, -1, -1) + return state_feat, state_pos, None + + def _encode_views(self, views, img_mask=None, ray_mask=None): + device = views[0]["img"].device + batch_size = views[0]["img"].shape[0] + given = True + if img_mask is None and ray_mask is None: + given = False + if not given: + img_mask = torch.stack( + [view["img_mask"] for view in views], dim=0 + ) # Shape: (num_views, batch_size) + ray_mask = torch.stack( + [view["ray_mask"] for view in views], dim=0 + ) # Shape: (num_views, batch_size) + imgs = torch.stack( + [view["img"] for view in views], dim=0 + ) # Shape: (num_views, batch_size, C, H, W) + ray_maps = torch.stack( + [view["ray_map"] for view in views], dim=0 + ) # Shape: (num_views, batch_size, H, W, C) + shapes = [] + for view in views: + if "true_shape" in view: + shapes.append(view["true_shape"]) + else: + shape = torch.tensor(view["img"].shape[-2:], device=device) + shapes.append(shape.unsqueeze(0).repeat(batch_size, 1)) + shapes = torch.stack(shapes, dim=0).to( + imgs.device + ) # Shape: (num_views, batch_size, 2) + imgs = imgs.view( + -1, *imgs.shape[2:] + ) # Shape: (num_views * batch_size, C, H, W) + ray_maps = ray_maps.view( + -1, *ray_maps.shape[2:] + ) # Shape: (num_views * batch_size, H, W, C) + shapes = shapes.view(-1, 2) # Shape: (num_views * batch_size, 2) + img_masks_flat = img_mask.view(-1) # Shape: (num_views * batch_size) + ray_masks_flat = ray_mask.view(-1) + selected_imgs = imgs[img_masks_flat] + selected_shapes = shapes[img_masks_flat] + if selected_imgs.size(0) > 0: + img_out, img_pos, _ = self._encode_image(selected_imgs, selected_shapes) + else: + raise NotImplementedError + full_out = [ + torch.zeros( + len(views) * batch_size, *img_out[0].shape[1:], device=img_out[0].device + ) + for _ in range(len(img_out)) + ] + full_pos = torch.zeros( + len(views) * batch_size, + *img_pos.shape[1:], + device=img_pos.device, + dtype=img_pos.dtype, + ) + for i in range(len(img_out)): + full_out[i][img_masks_flat] += img_out[i] + full_out[i][~img_masks_flat] += self.masked_img_token + full_pos[img_masks_flat] += img_pos + ray_maps = ray_maps.permute(0, 3, 1, 2) # Change shape to (N, C, H, W) + selected_ray_maps = ray_maps[ray_masks_flat] + selected_shapes_ray = shapes[ray_masks_flat] + if selected_ray_maps.size(0) > 0: + ray_out, ray_pos, _ = self._encode_ray_map( + selected_ray_maps, selected_shapes_ray + ) + assert len(ray_out) == len(full_out), f"{len(ray_out)}, {len(full_out)}" + for i in range(len(ray_out)): + full_out[i][ray_masks_flat] += ray_out[i] + full_out[i][~ray_masks_flat] += self.masked_ray_map_token + full_pos[ray_masks_flat] += ( + ray_pos * (~img_masks_flat[ray_masks_flat][:, None, None]).long() + ) + else: + raymaps = torch.zeros( + 1, 6, imgs[0].shape[-2], imgs[0].shape[-1], device=img_out[0].device + ) + ray_mask_flat = torch.zeros_like(img_masks_flat) + ray_mask_flat[:1] = True + ray_out, ray_pos, _ = self._encode_ray_map(raymaps, shapes[ray_mask_flat]) + for i in range(len(ray_out)): + full_out[i][ray_mask_flat] += ray_out[i] * 0.0 + full_out[i][~ray_mask_flat] += self.masked_ray_map_token * 0.0 + return ( + shapes.chunk(len(views), dim=0), + [out.chunk(len(views), dim=0) for out in full_out], + full_pos.chunk(len(views), dim=0), + ) + + def _decoder(self, f_state, pos_state, f_img, pos_img, f_pose, pos_pose, return_attn): + final_output = [(f_state, f_img)] # before projection + assert f_state.shape[-1] == self.dec_embed_dim + f_img = self.decoder_embed(f_img) # Linear: [1, 576, 1024] -> [1, 576, 768] + if self.pose_head_flag: + assert f_pose is not None and pos_pose is not None + f_img = torch.cat([f_pose, f_img], dim=1) # [1, 1 + 576, 768] + pos_img = torch.cat([pos_pose, pos_img], dim=1) # [1, 1 + 576, 2] + final_output.append((f_state, f_img)) + attention_maps = [] + for blk_state, blk_img in zip(self.dec_blocks_state, self.dec_blocks): + if ( + self.gradient_checkpointing + and self.training + and torch.is_grad_enabled() + ): + f_state, _, self_attn_state, cross_attn_state = checkpoint( + blk_state, + *final_output[-1][::+1], + pos_state, + pos_img, + return_attn, + use_reentrant=not self.fixed_input_length, + ) + f_img, _, self_attn_img, cross_attn_img = checkpoint( + blk_img, + *final_output[-1][::-1], + pos_img, + pos_state, + return_attn, + use_reentrant=not self.fixed_input_length, + ) + else: + f_state, _, self_attn_state, cross_attn_state = blk_state(*final_output[-1][::+1], pos_state, pos_img, return_attn=return_attn) + f_img, _, self_attn_img, cross_attn_img = blk_img(*final_output[-1][::-1], pos_img, pos_state, return_attn=return_attn) + final_output.append((f_state, f_img)) + attention_maps.append((self_attn_state, cross_attn_state, self_attn_img, cross_attn_img)) + del final_output[1] # duplicate with final_output[0] + final_output[-1] = ( + self.dec_norm_state(final_output[-1][0]), + self.dec_norm(final_output[-1][1]), + ) + return zip(*final_output), zip(*attention_maps) + + def _downstream_head(self, decout, img_shape, **kwargs): + B, S, D = decout[-1].shape + head = getattr(self, f"head") + return head(decout, img_shape, **kwargs) + + def _init_state(self, image_tokens, image_pos): + """ + Current Version: input the first frame img feature and pose to initialize the state feature and pose + # [1, 768, 768] [1, 768, 2] + """ + state_feat, state_pos, _ = self._encode_state(image_tokens, image_pos) + state_feat = self.decoder_embed_state(state_feat) # Linear: [1, 768, 1024] -> [1, 768, 768] + return state_feat, state_pos + + def _recurrent_rollout( + self, + state_feat, + state_pos, + current_feat, + current_pos, + pose_feat, + pose_pos, + init_state_feat, + img_mask=None, + reset_mask=None, + update=None, + return_attn=False, + ): + (new_state_feat, dec), (self_attn_state, cross_attn_state, self_attn_img, cross_attn_img) = self._decoder( + state_feat, state_pos, current_feat, current_pos, pose_feat, pose_pos, return_attn + ) + new_state_feat = new_state_feat[-1] + return new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img + + def _get_img_level_feat(self, feat): + return torch.mean(feat, dim=1, keepdim=True) + + # tbptt training encoder: Truncated Backpropagation Through Time + def _forward_encoder(self, views): + shape, feat_ls, pos = self._encode_views(views) + feat = feat_ls[-1] + state_feat, state_pos = self._init_state(feat[0], pos[0]) + mem = self.pose_retriever.mem.expand(feat[0].shape[0], -1, -1) + init_state_feat = state_feat.clone() + init_mem = mem.clone() + return (feat, pos, shape), ( + init_state_feat, + init_mem, + state_feat, + state_pos, + mem, + ) + + # tbptt training decoder step: Truncated Backpropagation Through Time + def _forward_decoder_step( + self, + views, + i, + feat_i, + pos_i, + shape_i, + init_state_feat, + init_mem, + state_feat, + state_pos, + mem, + ): + if self.pose_head_flag: + global_img_feat_i = self._get_img_level_feat(feat_i) + if i == 0: + pose_feat_i = self.pose_token.expand(feat_i.shape[0], -1, -1) + else: + pose_feat_i = self.pose_retriever.inquire(global_img_feat_i, mem) + pose_pos_i = -torch.ones( + feat_i.shape[0], 1, 2, device=feat_i.device, dtype=pos_i.dtype + ) + else: + pose_feat_i = None + pose_pos_i = None + new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img = self._recurrent_rollout( + state_feat, + state_pos, + feat_i, + pos_i, + pose_feat_i, + pose_pos_i, + init_state_feat, + img_mask=views[i]["img_mask"], + reset_mask=views[i]["reset"], + update=views[i].get("update", None), + return_attn=False, + ) + out_pose_feat_i = dec[-1][:, 0:1] + new_mem = self.pose_retriever.update_mem( + mem, global_img_feat_i, out_pose_feat_i + ) + head_input = [ + dec[0].float(), + dec[self.dec_depth * 2 // 4][:, 1:].float(), + dec[self.dec_depth * 3 // 4][:, 1:].float(), + dec[self.dec_depth].float(), + ] + res = self._downstream_head(head_input, shape_i, pos=pos_i) + img_mask = views[i]["img_mask"] + update = views[i].get("update", None) + if update is not None: + update_mask = img_mask & update # if don't update, then whatever img_mask + else: + update_mask = img_mask + update_mask = update_mask[:, None, None].float() + state_feat = new_state_feat * update_mask + state_feat * ( + 1 - update_mask + ) # update global state + mem = new_mem * update_mask + mem * (1 - update_mask) # then update local state + reset_mask = views[i]["reset"] + if reset_mask is not None: + reset_mask = reset_mask[:, None, None].float() + state_feat = init_state_feat * reset_mask + state_feat * (1 - reset_mask) + mem = init_mem * reset_mask + mem * (1 - reset_mask) + return res, (state_feat, mem) + + # training and testing + def _forward_impl(self, views, ret_state=False): + # [B, C, H, W] -> [B, H/16*W/16, 1024] + shape, feat_ls, pos = self._encode_views(views) # [15, 3, 288, 512] -> feat [15, 576, 1024], pos [15, 576, 2] + feat = feat_ls[-1] + state_feat, state_pos = self._init_state(feat[0], pos[0]) # init state feat [1, 768, 768], state_pos [1, 768, 2] + mem = self.pose_retriever.mem.expand(feat[0].shape[0], -1, -1) # [1, 256, 1536] init pose mem + init_state_feat = state_feat.clone() + init_mem = mem.clone() + all_state_args = [(state_feat, state_pos, init_state_feat, mem, init_mem)] + ress = [] + for i in range(len(views)): + feat_i = feat[i] + pos_i = pos[i] + if self.pose_head_flag: + global_img_feat_i = self._get_img_level_feat(feat_i) # avg pool: [1, 576, 1024] -> [1, 1, 1024] + if i == 0: + pose_feat_i = self.pose_token.expand(feat_i.shape[0], -1, -1) # [1, 1, 768] init pose token + else: + pose_feat_i = self.pose_retriever.inquire(global_img_feat_i, mem) + # [1, 1, 768] use [global_img_feat_i, masked_token(pose)] as query, cross-attend mem, get pose_feat_i + pose_pos_i = -torch.ones( + feat_i.shape[0], 1, 2, device=feat_i.device, dtype=pos_i.dtype + ) # [1, 1, 2] + else: + pose_feat_i = None + pose_pos_i = None + new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img = self._recurrent_rollout( + state_feat, # [1, 768, 768] + state_pos, # [1, 768, 2] + feat_i, # [1, 576, 1024] + pos_i, # [1, 576, 2] + pose_feat_i, # [1, 1, 768] coarse pose token from pose_retriever + pose_pos_i, # [1, 1, 2] + init_state_feat, + img_mask=views[i]["img_mask"], + reset_mask=views[i]["reset"], + update=views[i].get("update", None), + return_attn=True, + ) # [1, 768, 768] + out_pose_feat_i = dec[-1][:, 0:1] # [1, 1, 768] refined pose token from dust3r + new_mem = self.pose_retriever.update_mem( + mem, global_img_feat_i, out_pose_feat_i + ) # [1, 256, 1536] use mem as query, cross-attend [global_img_feat_i, out_pose_feat_i], get new_mem + assert len(dec) == self.dec_depth + 1 + head_input = [ + dec[0].float(), # [1, 576, 1024] + dec[self.dec_depth * 2 // 4][:, 1:].float(), # [1, 576, 768] + dec[self.dec_depth * 3 // 4][:, 1:].float(), # [1, 576, 768] + dec[self.dec_depth].float(), # [1, 1 + 576, 768] + ] + res = self._downstream_head(head_input, shape[i], pos=pos_i) + ress.append(res) + img_mask = views[i]["img_mask"] + update = views[i].get("update", None) + if update is not None: + update_mask = ( + img_mask & update + ) # if don't update, then whatever img_mask + else: + update_mask = img_mask + update_mask = update_mask[:, None, None].float() + + # update with learning rate + if i == 0: + update_mask1 = update_mask + else: + if self.config.model_update_type == "cut3r": + update_mask1 = update_mask + elif self.config.model_update_type == "ttt3r": + cross_attn_state = rearrange(torch.cat(cross_attn_state, dim=0), 'l h nstate nimg -> 1 nstate nimg (l h)') # [12, 16, 768, 1 + 576] -> [1, 768, 1 + 576, 12*16] + state_query_img_key = cross_attn_state.mean(dim=(-1, -2)) + update_mask1 = update_mask * torch.sigmoid(state_query_img_key)[..., None] * 1.0 + else: + raise ValueError(f"Invalid model type: {self.config.model_update_type}") + + update_mask2 = update_mask + state_feat = new_state_feat * update_mask1 + state_feat * ( + 1 - update_mask1 + ) # update global state + mem = new_mem * update_mask2 + mem * ( + 1 - update_mask2 + ) # then update local state + reset_mask = views[i]["reset"] + if reset_mask is not None: + reset_mask = reset_mask[:, None, None].float() + state_feat = init_state_feat * reset_mask + state_feat * ( + 1 - reset_mask + ) + mem = init_mem * reset_mask + mem * (1 - reset_mask) + all_state_args.append( + (state_feat, state_pos, init_state_feat, mem, init_mem) + ) + if ret_state: + return ress, views, all_state_args + return ress, views + + def forward(self, views, ret_state=False): + if ret_state: + ress, views, state_args = self._forward_impl(views, ret_state=ret_state) + return ARCroco3DStereoOutput(ress=ress, views=views), state_args + else: + ress, views = self._forward_impl(views, ret_state=ret_state) + return ARCroco3DStereoOutput(ress=ress, views=views) + + # testing: generate rgb xyz condition on raymap + def inference_step( + self, view, state_feat, state_pos, init_state_feat, mem, init_mem + ): + batch_size = view["img"].shape[0] + raymaps = [] + shapes = [] + for j in range(batch_size): + assert view["ray_mask"][j] + raymap = view["ray_map"][[j]].permute(0, 3, 1, 2) + raymaps.append(raymap) + shapes.append( + view.get( + "true_shape", + torch.tensor(view["ray_map"].shape[-2:])[None].repeat( + view["ray_map"].shape[0], 1 + ), + )[[j]] + ) + + raymaps = torch.cat(raymaps, dim=0) + shape = torch.cat(shapes, dim=0).to(raymaps.device) + feat_ls, pos, _ = self._encode_ray_map(raymaps, shapes) # [1, 6, 384, 512] -> feat [1, 768, 1024], pos [1, 768, 2] + + feat_i = feat_ls[-1] + pos_i = pos + if self.pose_head_flag: + global_img_feat_i = self._get_img_level_feat(feat_i) + pose_feat_i = self.pose_retriever.inquire(global_img_feat_i, mem) + pose_pos_i = -torch.ones( + feat_i.shape[0], 1, 2, device=feat_i.device, dtype=pos_i.dtype + ) + else: + pose_feat_i = None + pose_pos_i = None + new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img = self._recurrent_rollout( + state_feat, + state_pos, + feat_i, + pos_i, + pose_feat_i, + pose_pos_i, + init_state_feat, + img_mask=view["img_mask"], + reset_mask=view["reset"], + update=view.get("update", None), + return_attn=False, + ) + + out_pose_feat_i = dec[-1][:, 0:1] + new_mem = self.pose_retriever.update_mem( + mem, global_img_feat_i, out_pose_feat_i + ) + assert len(dec) == self.dec_depth + 1 + head_input = [ + dec[0].float(), + dec[self.dec_depth * 2 // 4][:, 1:].float(), + dec[self.dec_depth * 3 // 4][:, 1:].float(), + dec[self.dec_depth].float(), + ] + res = self._downstream_head(head_input, shape, pos=pos_i) + return res, view + + # recurrent testing + def forward_recurrent(self, views, device, ret_state=False): + ress = [] + all_state_args = [] + for i, view in enumerate(views): + device = view["img"].device + batch_size = view["img"].shape[0] + img_mask = view["img_mask"].reshape( + -1, batch_size + ) # Shape: (1, batch_size) + ray_mask = view["ray_mask"].reshape( + -1, batch_size + ) # Shape: (1, batch_size) + imgs = view["img"].unsqueeze(0) # Shape: (1, batch_size, C, H, W) + ray_maps = view["ray_map"].unsqueeze( + 0 + ) # Shape: (num_views, batch_size, H, W, C) + shapes = ( + view["true_shape"].unsqueeze(0) + if "true_shape" in view + else torch.tensor(view["img"].shape[-2:], device=device) + .unsqueeze(0) + .repeat(batch_size, 1) + .unsqueeze(0) + ) # Shape: (num_views, batch_size, 2) + imgs = imgs.view( + -1, *imgs.shape[2:] + ) # Shape: (num_views * batch_size, C, H, W) + ray_maps = ray_maps.view( + -1, *ray_maps.shape[2:] + ) # Shape: (num_views * batch_size, H, W, C) + shapes = shapes.view(-1, 2).to( + imgs.device + ) # Shape: (num_views * batch_size, 2) + img_masks_flat = img_mask.view(-1) # Shape: (num_views * batch_size) + ray_masks_flat = ray_mask.view(-1) + selected_imgs = imgs[img_masks_flat] + selected_shapes = shapes[img_masks_flat] + if selected_imgs.size(0) > 0: + img_out, img_pos, _ = self._encode_image(selected_imgs, selected_shapes) + else: + img_out, img_pos = None, None + ray_maps = ray_maps.permute(0, 3, 1, 2) # Change shape to (N, C, H, W) + selected_ray_maps = ray_maps[ray_masks_flat] + selected_shapes_ray = shapes[ray_masks_flat] + if selected_ray_maps.size(0) > 0: + ray_out, ray_pos, _ = self._encode_ray_map( + selected_ray_maps, selected_shapes_ray + ) + else: + ray_out, ray_pos = None, None + + shape = shapes + if img_out is not None and ray_out is None: + feat_i = img_out[-1] + pos_i = img_pos + elif img_out is None and ray_out is not None: + feat_i = ray_out[-1] + pos_i = ray_pos + elif img_out is not None and ray_out is not None: + feat_i = img_out[-1] + ray_out[-1] + pos_i = img_pos + else: + raise NotImplementedError + + if i == 0: + state_feat, state_pos = self._init_state(feat_i, pos_i) + mem = self.pose_retriever.mem.expand(feat_i.shape[0], -1, -1) + init_state_feat = state_feat.clone() + init_mem = mem.clone() + all_state_args.append( + (state_feat, state_pos, init_state_feat, mem, init_mem) + ) + + if self.pose_head_flag: + global_img_feat_i = self._get_img_level_feat(feat_i) + if i == 0: + pose_feat_i = self.pose_token.expand(feat_i.shape[0], -1, -1) + else: + pose_feat_i = self.pose_retriever.inquire(global_img_feat_i, mem) + pose_pos_i = -torch.ones( + feat_i.shape[0], 1, 2, device=feat_i.device, dtype=pos_i.dtype + ) + else: + pose_feat_i = None + pose_pos_i = None + new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img = self._recurrent_rollout( + state_feat, + state_pos, + feat_i, + pos_i, + pose_feat_i, + pose_pos_i, + init_state_feat, + img_mask=view["img_mask"], + reset_mask=view["reset"], + update=view.get("update", None), + return_attn=False, + ) + out_pose_feat_i = dec[-1][:, 0:1] + new_mem = self.pose_retriever.update_mem( + mem, global_img_feat_i, out_pose_feat_i + ) + assert len(dec) == self.dec_depth + 1 + head_input = [ + dec[0].float(), + dec[self.dec_depth * 2 // 4][:, 1:].float(), + dec[self.dec_depth * 3 // 4][:, 1:].float(), + dec[self.dec_depth].float(), + ] + res = self._downstream_head(head_input, shape, pos=pos_i) + ress.append(res) + img_mask = view["img_mask"] + update = view.get("update", None) + if update is not None: + update_mask = ( + img_mask & update + ) # if don't update, then whatever img_mask + else: + update_mask = img_mask + update_mask = update_mask[:, None, None].float() + state_feat = new_state_feat * update_mask + state_feat * ( + 1 - update_mask + ) # update global state + mem = new_mem * update_mask + mem * ( + 1 - update_mask + ) # then update local state + reset_mask = view["reset"] + if reset_mask is not None: + reset_mask = reset_mask[:, None, None].float() + state_feat = init_state_feat * reset_mask + state_feat * ( + 1 - reset_mask + ) + mem = init_mem * reset_mask + mem * (1 - reset_mask) + all_state_args.append( + (state_feat, state_pos, init_state_feat, mem, init_mem) + ) + if ret_state: + return ress, views, all_state_args + return ress, views + + def forward_recurrent_lighter(self, views, device='cuda', ret_state=False): + ress = [] + all_state_args = [] + reset_mask = False + for i, _view in enumerate(views): + view = to_gpu(_view, device) + device = view["img"].device + batch_size = view["img"].shape[0] + img_mask = view["img_mask"].reshape( + -1, batch_size + ) # Shape: (1, batch_size) + ray_mask = view["ray_mask"].reshape( + -1, batch_size + ) # Shape: (1, batch_size) + imgs = view["img"].unsqueeze(0) # Shape: (1, batch_size, C, H, W) + ray_maps = view["ray_map"].unsqueeze( + 0 + ) # Shape: (num_views, batch_size, H, W, C) + shapes = ( + view["true_shape"].unsqueeze(0) + if "true_shape" in view + else torch.tensor(view["img"].shape[-2:], device=device) + .unsqueeze(0) + .repeat(batch_size, 1) + .unsqueeze(0) + ) # Shape: (num_views, batch_size, 2) + imgs = imgs.view( + -1, *imgs.shape[2:] + ) # Shape: (num_views * batch_size, C, H, W) + ray_maps = ray_maps.view( + -1, *ray_maps.shape[2:] + ) # Shape: (num_views * batch_size, H, W, C) + shapes = shapes.view(-1, 2).to( + imgs.device + ) # Shape: (num_views * batch_size, 2) + img_masks_flat = img_mask.view(-1) # Shape: (num_views * batch_size) + ray_masks_flat = ray_mask.view(-1) + selected_imgs = imgs[img_masks_flat] + selected_shapes = shapes[img_masks_flat] + if selected_imgs.size(0) > 0: + img_out, img_pos, _ = self._encode_image(selected_imgs, selected_shapes) + else: + img_out, img_pos = None, None + ray_maps = ray_maps.permute(0, 3, 1, 2) # Change shape to (N, C, H, W) + selected_ray_maps = ray_maps[ray_masks_flat] + selected_shapes_ray = shapes[ray_masks_flat] + if selected_ray_maps.size(0) > 0: + ray_out, ray_pos, _ = self._encode_ray_map( + selected_ray_maps, selected_shapes_ray + ) + else: + ray_out, ray_pos = None, None + + shape = shapes + if img_out is not None and ray_out is None: + feat_i = img_out[-1] + pos_i = img_pos + elif img_out is None and ray_out is not None: + feat_i = ray_out[-1] + pos_i = ray_pos + elif img_out is not None and ray_out is not None: + feat_i = img_out[-1] + ray_out[-1] + pos_i = img_pos + else: + raise NotImplementedError + + if i == 0: + state_feat, state_pos = self._init_state(feat_i, pos_i) + mem = self.pose_retriever.mem.expand(feat_i.shape[0], -1, -1) + init_state_feat = state_feat.clone() + init_mem = mem.clone() + + if self.pose_head_flag: + global_img_feat_i = self._get_img_level_feat(feat_i) + if i == 0 or reset_mask: + pose_feat_i = self.pose_token.expand(feat_i.shape[0], -1, -1) + else: + pose_feat_i = self.pose_retriever.inquire(global_img_feat_i, mem) + pose_pos_i = -torch.ones( + feat_i.shape[0], 1, 2, device=feat_i.device, dtype=pos_i.dtype + ) + else: + pose_feat_i = None + pose_pos_i = None + new_state_feat, dec, self_attn_state, cross_attn_state, self_attn_img, cross_attn_img = self._recurrent_rollout( + state_feat, + state_pos, + feat_i, + pos_i, + pose_feat_i, + pose_pos_i, + init_state_feat, + img_mask=view["img_mask"], + reset_mask=view["reset"], + update=view.get("update", None), + return_attn=True, + ) + out_pose_feat_i = dec[-1][:, 0:1] + + # update mem + new_mem = self.pose_retriever.update_mem( + mem, global_img_feat_i, out_pose_feat_i + ) + + assert len(dec) == self.dec_depth + 1 + head_input = [ + dec[0].float(), + dec[self.dec_depth * 2 // 4][:, 1:].float(), + dec[self.dec_depth * 3 // 4][:, 1:].float(), + dec[self.dec_depth].float(), + ] + res = self._downstream_head(head_input, shape, pos=pos_i) + res_cpu = to_cpu(res) + ress.append(res_cpu) + img_mask = view["img_mask"] + update = view.get("update", None) + if update is not None: + update_mask = ( + img_mask & update + ) # if don't update, then whatever img_mask + else: + update_mask = img_mask + update_mask = update_mask[:, None, None].float() + + # update with learning rate + if i == 0 or reset_mask: + update_mask1 = update_mask + else: + if self.config.model_update_type == "cut3r": + update_mask1 = update_mask + elif self.config.model_update_type == "ttt3r": + cross_attn_state = rearrange(torch.cat(cross_attn_state, dim=0), 'l h nstate nimg -> 1 nstate nimg (l h)') # [12, 16, 768, 1 + 576] -> [1, 768, 1 + 576, 12*16] + state_query_img_key = cross_attn_state.mean(dim=(-1, -2)) + update_mask1 = update_mask * torch.sigmoid(state_query_img_key)[..., None] * 1.0 + else: + raise ValueError(f"Invalid model type: {self.config.model_update_type}") + + update_mask2 = update_mask + state_feat = new_state_feat * update_mask1 + state_feat * ( + 1 - update_mask1 + ) # update global state + mem = new_mem * update_mask2 + mem * ( + 1 - update_mask2 + ) # then update local state + + reset_mask = view["reset"] + if reset_mask is not None: + reset_mask = reset_mask[:, None, None].float() + state_feat = init_state_feat * reset_mask + state_feat * ( + 1 - reset_mask + ) + mem = init_mem * reset_mask + mem * (1 - reset_mask) + + if ret_state: + return ress, views, all_state_args + return ress, views + +if __name__ == "__main__": + print(ARCroco3DStereo.mro()) + cfg = ARCroco3DStereoConfig( + state_size=256, + pos_embed="RoPE100", + rgb_head=True, + pose_head=True, + img_size=(224, 224), + head_type="linear", + output_mode="pts3d+pose", + depth_mode=("exp", -inf, inf), + conf_mode=("exp", 1, inf), + pose_mode=("exp", -inf, inf), + enc_embed_dim=1024, + enc_depth=24, + enc_num_heads=16, + dec_embed_dim=768, + dec_depth=12, + dec_num_heads=12, + ) + ARCroco3DStereo(cfg) diff --git a/instantsplat/initializer/ttt3r/patch_embed.py b/instantsplat/initializer/ttt3r/patch_embed.py new file mode 100755 index 0000000..577045d --- /dev/null +++ b/instantsplat/initializer/ttt3r/patch_embed.py @@ -0,0 +1,96 @@ +# Copyright (C) 2024-present Naver Corporation. All rights reserved. +# Licensed under CC BY-NC-SA 4.0 (non-commercial use only). +# +# -------------------------------------------------------- +# modified from DUSt3R + +import torch +from .bootstrap import ensure_runtime_paths + +ensure_runtime_paths() + +from models.blocks import PatchEmbed # noqa + + +def get_patch_embed(patch_embed_cls, img_size, patch_size, enc_embed_dim, in_chans=3): + assert patch_embed_cls in ["PatchEmbedDust3R", "ManyAR_PatchEmbed"] + patch_embed = eval(patch_embed_cls)(img_size, patch_size, in_chans, enc_embed_dim) + return patch_embed + + +class PatchEmbedDust3R(PatchEmbed): + def forward(self, x, **kw): + B, C, H, W = x.shape + assert ( + H % self.patch_size[0] == 0 + ), f"Input image height ({H}) is not a multiple of patch size ({self.patch_size[0]})." + assert ( + W % self.patch_size[1] == 0 + ), f"Input image width ({W}) is not a multiple of patch size ({self.patch_size[1]})." + x = self.proj(x) + pos = self.position_getter(B, x.size(2), x.size(3), x.device) + if self.flatten: + x = x.flatten(2).transpose(1, 2) # BCHW -> BNC + x = self.norm(x) + return x, pos + + +class ManyAR_PatchEmbed(PatchEmbed): + """Handle images with non-square aspect ratio. + All images in the same batch have the same aspect ratio. + true_shape = [(height, width) ...] indicates the actual shape of each image. + """ + + def __init__( + self, + img_size=224, + patch_size=16, + in_chans=3, + embed_dim=768, + norm_layer=None, + flatten=True, + ): + self.embed_dim = embed_dim + super().__init__(img_size, patch_size, in_chans, embed_dim, norm_layer, flatten) + + def forward(self, img, true_shape): + B, C, H, W = img.shape + + assert ( + H % self.patch_size[0] == 0 + ), f"Input image height ({H}) is not a multiple of patch size ({self.patch_size[0]})." + assert ( + W % self.patch_size[1] == 0 + ), f"Input image width ({W}) is not a multiple of patch size ({self.patch_size[1]})." + assert true_shape.shape == ( + B, + 2, + ), f"true_shape has the wrong shape={true_shape.shape}" + + W //= self.patch_size[0] + H //= self.patch_size[1] + n_tokens = H * W + + height, width = true_shape.T + + is_landscape = torch.ones_like(width, dtype=torch.bool) + is_portrait = ~is_landscape + + x = img.new_zeros((B, n_tokens, self.embed_dim)) + pos = img.new_zeros((B, n_tokens, 2), dtype=torch.int64) + + x[is_landscape] = ( + self.proj(img[is_landscape]).permute(0, 2, 3, 1).flatten(1, 2).float() + ) + x[is_portrait] = ( + self.proj(img[is_portrait].swapaxes(-1, -2)) + .permute(0, 2, 3, 1) + .flatten(1, 2) + .float() + ) + + pos[is_landscape] = self.position_getter(1, H, W, pos.device) + pos[is_portrait] = self.position_getter(1, W, H, pos.device) + + x = self.norm(x) + return x, pos diff --git a/instantsplat/initializer/ttt3r/ttt3r.py b/instantsplat/initializer/ttt3r/ttt3r.py new file mode 100644 index 0000000..3bb6d29 --- /dev/null +++ b/instantsplat/initializer/ttt3r/ttt3r.py @@ -0,0 +1,108 @@ +from math import atan +from pathlib import Path +from typing import List + +import torch + +from .bootstrap import ensure_runtime_paths +from .export import export_outputs, prepare_input + +from instantsplat.initializer.abc import ( + AbstractInitializer, + InitializedPointCloud, + InitializingCamera, +) + + +def focal2fov(focal: float, pixels: float) -> float: + return 2 * atan(pixels / (2 * focal)) + + +def _project_root() -> Path: + return Path(__file__).resolve().parents[3] + + +class Ttt3rInitializer(AbstractInitializer): + def __init__( + self, + model_path: str = "checkpoints/cut3r_512_dpt_4_64.pth", + model_update_type: str = "ttt3r", + min_conf_thr: float = 1.5, + scene_scale: float = 1.0, + resize: int = 512, + reset_interval: int = 1000000, + ): + self.model_path = model_path + self.model_update_type = model_update_type + self.min_conf_thr = min_conf_thr + self.scene_scale = scene_scale + self.resize = resize + self.reset_interval = reset_interval + self.device = "cuda" if torch.cuda.is_available() else "cpu" + + def _resolve_path(self, path: str, root: Path | None = None) -> Path: + candidate = Path(path) + if candidate.is_absolute(): + return candidate + base = root if root is not None else _project_root() + return (base / candidate).resolve() + + def to(self, device): + self.device = str(device) + return self + + def __call__(self, image_path_list: List[str]): + ensure_runtime_paths() + + model_path = self._resolve_path(self.model_path) + if not model_path.exists(): + raise FileNotFoundError( + f"TTT3R checkpoint not found at '{model_path}'." + ) + + from .inference import inference_recurrent_lighter + from .model import ARCroco3DStereo + + device = self.device + if device.startswith("cuda") and not torch.cuda.is_available(): + device = "cpu" + + model = ARCroco3DStereo.from_pretrained(str(model_path)).to(device) + model.config.model_update_type = self.model_update_type + model.eval() + + views = prepare_input(image_path_list, self.resize, self.reset_interval) + outputs, _ = inference_recurrent_lighter(views, model, device, verbose=False) + exported = export_outputs(outputs, image_path_list, self.min_conf_thr) + + points = torch.from_numpy(exported["points"]).float() * self.scene_scale + colors = torch.from_numpy(exported["colors"]).float() + world2cams = torch.from_numpy(exported["world2cams"]).float() + focals = exported["focals"] + principal_points = exported["principal_points"] + image_widths = exported["image_widths"] + image_heights = exported["image_heights"] + exported_paths = exported["image_paths"].tolist() + + cameras = [] + for world2cam, focal, principal_point, image_path, image_width, image_height in zip( + world2cams, + focals, + principal_points, + exported_paths, + image_widths, + image_heights, + ): + cameras.append( + InitializingCamera( + image_width=int(image_width), + image_height=int(image_height), + FoVx=focal2fov(float(focal), float(principal_point[0]) * 2.0), + FoVy=focal2fov(float(focal), float(principal_point[1]) * 2.0), + R=world2cam[:3, :3], + T=world2cam[:3, 3] * self.scene_scale, + image_path=str(image_path), + ) + ) + + return InitializedPointCloud(points=points, colors=colors), cameras