|
"""Miscellaneous functions and classes for the geocalib_inference package.""" |
|
|
|
import functools |
|
import inspect |
|
import logging |
|
from typing import Callable, List |
|
|
|
import numpy as np |
|
import torch |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
def autocast(func: Callable) -> Callable: |
|
"""Cast the inputs of a TensorWrapper method to PyTorch tensors if they are numpy arrays. |
|
|
|
Use the device and dtype of the wrapper. |
|
|
|
Args: |
|
func (Callable): Method of a TensorWrapper class. |
|
|
|
Returns: |
|
Callable: Wrapped method. |
|
""" |
|
|
|
@functools.wraps(func) |
|
def wrap(self, *args): |
|
device = torch.device("cpu") |
|
dtype = None |
|
if isinstance(self, TensorWrapper): |
|
if self._data is not None: |
|
device = self.device |
|
dtype = self.dtype |
|
elif not inspect.isclass(self) or not issubclass(self, TensorWrapper): |
|
raise ValueError(self) |
|
|
|
cast_args = [] |
|
for arg in args: |
|
if isinstance(arg, np.ndarray): |
|
arg = torch.from_numpy(arg) |
|
arg = arg.to(device=device, dtype=dtype) |
|
cast_args.append(arg) |
|
return func(self, *cast_args) |
|
|
|
return wrap |
|
|
|
|
|
class TensorWrapper: |
|
"""Wrapper for PyTorch tensors.""" |
|
|
|
_data = None |
|
|
|
@autocast |
|
def __init__(self, data: torch.Tensor): |
|
"""Wrapper for PyTorch tensors.""" |
|
self._data = data |
|
|
|
@property |
|
def shape(self) -> torch.Size: |
|
"""Shape of the underlying tensor.""" |
|
return self._data.shape[:-1] |
|
|
|
@property |
|
def device(self) -> torch.device: |
|
"""Get the device of the underlying tensor.""" |
|
return self._data.device |
|
|
|
@property |
|
def dtype(self) -> torch.dtype: |
|
"""Get the dtype of the underlying tensor.""" |
|
return self._data.dtype |
|
|
|
def __getitem__(self, index) -> torch.Tensor: |
|
"""Get the underlying tensor.""" |
|
return self.__class__(self._data[index]) |
|
|
|
def __setitem__(self, index, item): |
|
"""Set the underlying tensor.""" |
|
self._data[index] = item.data |
|
|
|
def to(self, *args, **kwargs): |
|
"""Move the underlying tensor to a new device.""" |
|
return self.__class__(self._data.to(*args, **kwargs)) |
|
|
|
def cpu(self): |
|
"""Move the underlying tensor to the CPU.""" |
|
return self.__class__(self._data.cpu()) |
|
|
|
def cuda(self): |
|
"""Move the underlying tensor to the GPU.""" |
|
return self.__class__(self._data.cuda()) |
|
|
|
def pin_memory(self): |
|
"""Pin the underlying tensor to memory.""" |
|
return self.__class__(self._data.pin_memory()) |
|
|
|
def float(self): |
|
"""Cast the underlying tensor to float.""" |
|
return self.__class__(self._data.float()) |
|
|
|
def double(self): |
|
"""Cast the underlying tensor to double.""" |
|
return self.__class__(self._data.double()) |
|
|
|
def detach(self): |
|
"""Detach the underlying tensor.""" |
|
return self.__class__(self._data.detach()) |
|
|
|
def numpy(self): |
|
"""Convert the underlying tensor to a numpy array.""" |
|
return self._data.detach().cpu().numpy() |
|
|
|
def new_tensor(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self._data.new_tensor(*args, **kwargs) |
|
|
|
def new_zeros(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self._data.new_zeros(*args, **kwargs) |
|
|
|
def new_ones(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self._data.new_ones(*args, **kwargs) |
|
|
|
def new_full(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self._data.new_full(*args, **kwargs) |
|
|
|
def new_empty(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self._data.new_empty(*args, **kwargs) |
|
|
|
def unsqueeze(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self.__class__(self._data.unsqueeze(*args, **kwargs)) |
|
|
|
def squeeze(self, *args, **kwargs): |
|
"""Create a new tensor of the same type and device.""" |
|
return self.__class__(self._data.squeeze(*args, **kwargs)) |
|
|
|
@classmethod |
|
def stack(cls, objects: List, dim=0, *, out=None): |
|
"""Stack a list of objects with the same type and shape.""" |
|
data = torch.stack([obj._data for obj in objects], dim=dim, out=out) |
|
return cls(data) |
|
|
|
@classmethod |
|
def __torch_function__(cls, func, types, args=(), kwargs=None): |
|
"""Support torch functions.""" |
|
if kwargs is None: |
|
kwargs = {} |
|
return cls.stack(*args, **kwargs) if func is torch.stack else NotImplemented |
|
|
|
|
|
class EuclideanManifold: |
|
"""Simple euclidean manifold.""" |
|
|
|
@staticmethod |
|
def J_plus(x: torch.Tensor) -> torch.Tensor: |
|
"""Plus operator Jacobian.""" |
|
return torch.eye(x.shape[-1]).to(x) |
|
|
|
@staticmethod |
|
def plus(x: torch.Tensor, delta: torch.Tensor) -> torch.Tensor: |
|
"""Plus operator.""" |
|
return x + delta |
|
|
|
|
|
class SphericalManifold: |
|
"""Implementation of the spherical manifold. |
|
|
|
Following the derivation from 'Integrating Generic Sensor Fusion Algorithms with Sound State |
|
Representations through Encapsulation of Manifolds' by Hertzberg et al. (B.2, p. 25). |
|
|
|
Householder transformation following Algorithm 5.1.1 (p. 210) from 'Matrix Computations' by |
|
Golub et al. |
|
""" |
|
|
|
@staticmethod |
|
def householder_vector(x: torch.Tensor) -> torch.Tensor: |
|
"""Return the Householder vector and beta. |
|
|
|
Algorithm 5.1.1 (p. 210) from 'Matrix Computations' by Golub et al. (Johns Hopkins Studies |
|
in Mathematical Sciences) but using the nth element of the input vector as pivot instead of |
|
first. |
|
|
|
This computes the vector v with v(n) = 1 and beta such that H = I - beta * v * v^T is |
|
orthogonal and H * x = ||x||_2 * e_n. |
|
|
|
Args: |
|
x (torch.Tensor): [..., n] tensor. |
|
|
|
Returns: |
|
torch.Tensor: v of shape [..., n] |
|
torch.Tensor: beta of shape [...] |
|
""" |
|
sigma = torch.sum(x[..., :-1] ** 2, -1) |
|
xpiv = x[..., -1] |
|
norm = torch.norm(x, dim=-1) |
|
if torch.any(sigma < 1e-7): |
|
sigma = torch.where(sigma < 1e-7, sigma + 1e-7, sigma) |
|
logger.warning("sigma < 1e-7") |
|
|
|
vpiv = torch.where(xpiv < 0, xpiv - norm, -sigma / (xpiv + norm)) |
|
beta = 2 * vpiv**2 / (sigma + vpiv**2) |
|
v = torch.cat([x[..., :-1] / vpiv[..., None], torch.ones_like(vpiv)[..., None]], -1) |
|
return v, beta |
|
|
|
@staticmethod |
|
def apply_householder(y: torch.Tensor, v: torch.Tensor, beta: torch.Tensor) -> torch.Tensor: |
|
"""Apply Householder transformation. |
|
|
|
Args: |
|
y (torch.Tensor): Vector to transform of shape [..., n]. |
|
v (torch.Tensor): Householder vector of shape [..., n]. |
|
beta (torch.Tensor): Householder beta of shape [...]. |
|
|
|
Returns: |
|
torch.Tensor: Transformed vector of shape [..., n]. |
|
""" |
|
return y - v * (beta * torch.einsum("...i,...i->...", v, y))[..., None] |
|
|
|
@classmethod |
|
def J_plus(cls, x: torch.Tensor) -> torch.Tensor: |
|
"""Plus operator Jacobian.""" |
|
v, beta = cls.householder_vector(x) |
|
H = -torch.einsum("..., ...k, ...l->...kl", beta, v, v) |
|
H = H + torch.eye(H.shape[-1]).to(H) |
|
return H[..., :-1] |
|
|
|
@classmethod |
|
def plus(cls, x: torch.Tensor, delta: torch.Tensor) -> torch.Tensor: |
|
"""Plus operator. |
|
|
|
Equation 109 (p. 25) from 'Integrating Generic Sensor Fusion Algorithms with Sound State |
|
Representations through Encapsulation of Manifolds' by Hertzberg et al. but using the nth |
|
element of the input vector as pivot instead of first. |
|
|
|
Args: |
|
x: point on the manifold |
|
delta: tangent vector |
|
""" |
|
eps = 1e-7 |
|
|
|
nx = torch.norm(x, dim=-1, keepdim=True) |
|
nd = torch.norm(delta, dim=-1, keepdim=True) |
|
|
|
|
|
|
|
nd_ = torch.where(nd < eps, nd + eps, nd) |
|
sinc = torch.where(nd < eps, nd.new_ones(nd.shape), torch.sin(nd_) / nd_) |
|
|
|
|
|
exp_delta = torch.cat([sinc * delta, torch.cos(nd)], -1) |
|
|
|
v, beta = cls.householder_vector(x) |
|
return nx * cls.apply_householder(exp_delta, v, beta) |
|
|
|
|
|
@torch.jit.script |
|
def J_vecnorm(vec: torch.Tensor) -> torch.Tensor: |
|
"""Compute the jacobian of vec / norm2(vec). |
|
|
|
Args: |
|
vec (torch.Tensor): [..., D] tensor. |
|
|
|
Returns: |
|
torch.Tensor: [..., D, D] Jacobian. |
|
""" |
|
D = vec.shape[-1] |
|
norm_x = torch.norm(vec, dim=-1, keepdim=True).unsqueeze(-1) |
|
|
|
if (norm_x == 0).any(): |
|
norm_x = norm_x + 1e-6 |
|
|
|
xxT = torch.einsum("...i,...j->...ij", vec, vec) |
|
identity = torch.eye(D, device=vec.device, dtype=vec.dtype) |
|
|
|
return identity / norm_x - (xxT / norm_x**3) |
|
|
|
|
|
@torch.jit.script |
|
def J_focal2fov(focal: torch.Tensor, h: torch.Tensor) -> torch.Tensor: |
|
"""Compute the jacobian of the focal2fov function.""" |
|
return -4 * h / (4 * focal**2 + h**2) |
|
|
|
|
|
@torch.jit.script |
|
def J_up_projection(uv: torch.Tensor, abc: torch.Tensor, wrt: str = "uv") -> torch.Tensor: |
|
"""Compute the jacobian of the up-vector projection. |
|
|
|
Args: |
|
uv (torch.Tensor): Normalized image coordinates of shape (..., 2). |
|
abc (torch.Tensor): Gravity vector of shape (..., 3). |
|
wrt (str, optional): Parameter to differentiate with respect to. Defaults to "uv". |
|
|
|
Raises: |
|
ValueError: If the wrt parameter is unknown. |
|
|
|
Returns: |
|
torch.Tensor: Jacobian with respect to the parameter. |
|
""" |
|
if wrt == "uv": |
|
c = abc[..., 2][..., None, None, None] |
|
return -c * torch.eye(2, device=uv.device, dtype=uv.dtype).expand(uv.shape[:-1] + (2, 2)) |
|
|
|
elif wrt == "abc": |
|
J = uv.new_zeros(uv.shape[:-1] + (2, 3)) |
|
J[..., 0, 0] = 1 |
|
J[..., 1, 1] = 1 |
|
J[..., 0, 2] = -uv[..., 0] |
|
J[..., 1, 2] = -uv[..., 1] |
|
return J |
|
|
|
else: |
|
raise ValueError(f"Unknown wrt: {wrt}") |
|
|