GeoCalib / siclib /geometry /perspective_fields.py
veichta's picture
Upload folder using huggingface_hub
205a7af verified
raw
history blame
13.4 kB
"""Implementation of perspective fields.
Adapted from https://github.com/jinlinyi/PerspectiveFields/blob/main/perspective2d/utils/panocam.py
"""
from typing import Tuple
import torch
from torch.nn import functional as F
from siclib.geometry.base_camera import BaseCamera
from siclib.geometry.gravity import Gravity
from siclib.geometry.jacobians import J_up_projection, J_vecnorm
from siclib.geometry.manifolds import SphericalManifold
# flake8: noqa: E266
def get_horizon_line(camera: BaseCamera, gravity: Gravity, relative: bool = True) -> torch.Tensor:
"""Get the horizon line from the camera parameters.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
relative (bool, optional): Whether to normalize horizon line by img_h. Defaults to True.
Returns:
torch.Tensor: In image frame, fraction of image left/right border intersection with
respect to image height.
"""
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
# project horizon midpoint to image plane
horizon_midpoint = camera.new_tensor([0, 0, 1])
horizon_midpoint = camera.K @ gravity.R @ horizon_midpoint
midpoint = horizon_midpoint[:2] / horizon_midpoint[2]
# compute left and right offset to borders
left_offset = midpoint[0] * torch.tan(gravity.roll)
right_offset = (camera.size[0] - midpoint[0]) * torch.tan(gravity.roll)
left, right = midpoint[1] + left_offset, midpoint[1] - right_offset
horizon = camera.new_tensor([left, right])
return horizon / camera.size[1] if relative else horizon
def get_up_field(camera: BaseCamera, gravity: Gravity, normalize: bool = True) -> torch.Tensor:
"""Get the up vector field from the camera parameters.
Args:
camera (Camera): Camera parameters.
normalize (bool, optional): Whether to normalize the up vector. Defaults to True.
Returns:
torch.Tensor: up vector field as tensor of shape (..., h, w, 2).
"""
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
uv = camera.normalize(camera.pixel_coordinates())
# projected up is (a, b) - c * (u, v)
abc = gravity.vec3d
projected_up2d = abc[..., None, :2] - abc[..., 2, None, None] * uv # (..., N, 2)
if hasattr(camera, "dist"):
d_uv = camera.distort(uv, return_scale=True)[0] # (..., N, 1)
d_uv = torch.diag_embed(d_uv.expand(d_uv.shape[:-1] + (2,))) # (..., N, 2, 2)
offset = camera.up_projection_offset(uv) # (..., N, 2)
offset = torch.einsum("...i,...j->...ij", offset, uv) # (..., N, 2, 2)
# (..., N, 2)
projected_up2d = torch.einsum("...Nij,...Nj->...Ni", d_uv + offset, projected_up2d)
if normalize:
projected_up2d = F.normalize(projected_up2d, dim=-1) # (..., N, 2)
return projected_up2d.reshape(camera.shape[0], h, w, 2)
def J_up_field(
camera: BaseCamera, gravity: Gravity, spherical: bool = False, log_focal: bool = False
) -> torch.Tensor:
"""Get the jacobian of the up field.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
spherical (bool, optional): Whether to use spherical coordinates. Defaults to False.
log_focal (bool, optional): Whether to use log-focal length. Defaults to False.
Returns:
torch.Tensor: Jacobian of the up field as a tensor of shape (..., h, w, 2, 2, 3).
"""
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
# Forward
xy = camera.pixel_coordinates()
uv = camera.normalize(xy)
projected_up2d = gravity.vec3d[..., None, :2] - gravity.vec3d[..., 2, None, None] * uv
# Backward
J = []
# (..., N, 2, 2)
J_norm2proj = J_vecnorm(
get_up_field(camera, gravity, normalize=False).reshape(camera.shape[0], -1, 2)
)
# distortion values
if hasattr(camera, "dist"):
d_uv = camera.distort(uv, return_scale=True)[0] # (..., N, 1)
d_uv = torch.diag_embed(d_uv.expand(d_uv.shape[:-1] + (2,))) # (..., N, 2, 2)
offset = camera.up_projection_offset(uv) # (..., N, 2)
offset_uv = torch.einsum("...i,...j->...ij", offset, uv) # (..., N, 2, 2)
######################
## Gravity Jacobian ##
######################
J_proj2abc = J_up_projection(uv, gravity.vec3d, wrt="abc") # (..., N, 2, 3)
if hasattr(camera, "dist"):
# (..., N, 2, 3)
J_proj2abc = torch.einsum("...Nij,...Njk->...Nik", d_uv + offset_uv, J_proj2abc)
J_abc2delta = SphericalManifold.J_plus(gravity.vec3d) if spherical else gravity.J_rp()
J_proj2delta = torch.einsum("...Nij,...jk->...Nik", J_proj2abc, J_abc2delta)
J_up2delta = torch.einsum("...Nij,...Njk->...Nik", J_norm2proj, J_proj2delta)
J.append(J_up2delta)
######################
### Focal Jacobian ###
######################
J_proj2uv = J_up_projection(uv, gravity.vec3d, wrt="uv") # (..., N, 2, 2)
if hasattr(camera, "dist"):
J_proj2up = torch.einsum("...Nij,...Njk->...Nik", d_uv + offset_uv, J_proj2uv)
J_proj2duv = torch.einsum("...i,...j->...ji", offset, projected_up2d)
inner = (uv * projected_up2d).sum(-1)[..., None, None]
J_proj2offset1 = inner * camera.J_up_projection_offset(uv, wrt="uv")
J_proj2offset2 = torch.einsum("...i,...j->...ij", offset, projected_up2d) # (..., N, 2, 2)
J_proj2uv = (J_proj2duv + J_proj2offset1 + J_proj2offset2) + J_proj2up
J_uv2f = camera.J_normalize(xy) # (..., N, 2, 2)
if log_focal:
J_uv2f = J_uv2f * camera.f[..., None, None, :] # (..., N, 2, 2)
J_uv2f = J_uv2f.sum(-1) # (..., N, 2)
J_proj2f = torch.einsum("...ij,...j->...i", J_proj2uv, J_uv2f) # (..., N, 2)
J_up2f = torch.einsum("...Nij,...Nj->...Ni", J_norm2proj, J_proj2f)[..., None] # (..., N, 2, 1)
J.append(J_up2f)
######################
##### K1 Jacobian ####
######################
if hasattr(camera, "dist"):
J_duv = camera.J_distort(uv, wrt="scale2dist")
J_duv = torch.diag_embed(J_duv.expand(J_duv.shape[:-1] + (2,))) # (..., N, 2, 2)
J_offset = torch.einsum(
"...i,...j->...ij", camera.J_up_projection_offset(uv, wrt="dist"), uv
)
J_proj2k1 = torch.einsum("...Nij,...Nj->...Ni", J_duv + J_offset, projected_up2d)
J_k1 = torch.einsum("...Nij,...Nj->...Ni", J_norm2proj, J_proj2k1)[..., None]
J.append(J_k1)
n_params = sum(j.shape[-1] for j in J)
return torch.cat(J, axis=-1).reshape(camera.shape[0], h, w, 2, n_params)
def get_latitude_field(camera: BaseCamera, gravity: Gravity) -> torch.Tensor:
"""Get the latitudes of the camera pixels in radians.
Latitudes are defined as the angle between the ray and the up vector.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
Returns:
torch.Tensor: Latitudes in radians as a tensor of shape (..., h, w, 1).
"""
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
uv1, _ = camera.image2world(camera.pixel_coordinates())
rays = camera.pixel_bearing_many(uv1)
lat = torch.einsum("...Nj,...j->...N", rays, gravity.vec3d)
eps = 1e-6
lat_asin = torch.asin(lat.clamp(min=-1 + eps, max=1 - eps))
return lat_asin.reshape(camera.shape[0], h, w, 1)
def J_latitude_field(
camera: BaseCamera, gravity: Gravity, spherical: bool = False, log_focal: bool = False
) -> torch.Tensor:
"""Get the jacobian of the latitude field.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
spherical (bool, optional): Whether to use spherical coordinates. Defaults to False.
log_focal (bool, optional): Whether to use log-focal length. Defaults to False.
Returns:
torch.Tensor: Jacobian of the latitude field as a tensor of shape (..., h, w, 1, 3).
"""
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
# Forward
xy = camera.pixel_coordinates()
uv1, _ = camera.image2world(xy)
uv1_norm = camera.pixel_bearing_many(uv1) # (..., N, 3)
# Backward
J = []
J_norm2w_to_img = J_vecnorm(uv1)[..., :2] # (..., N, 2)
######################
## Gravity Jacobian ##
######################
J_delta = SphericalManifold.J_plus(gravity.vec3d) if spherical else gravity.J_rp()
J_delta = torch.einsum("...Ni,...ij->...Nj", uv1_norm, J_delta) # (..., N, 2)
J.append(J_delta)
######################
### Focal Jacobian ###
######################
J_w_to_img2f = camera.J_image2world(xy, "f") # (..., N, 2, 2)
if log_focal:
J_w_to_img2f = J_w_to_img2f * camera.f[..., None, None, :]
J_w_to_img2f = J_w_to_img2f.sum(-1) # (..., N, 2)
J_norm2f = torch.einsum("...Nij,...Nj->...Ni", J_norm2w_to_img, J_w_to_img2f) # (..., N, 3)
J_f = torch.einsum("...Ni,...i->...N", J_norm2f, gravity.vec3d).unsqueeze(-1) # (..., N, 1)
J.append(J_f)
######################
##### K1 Jacobian ####
######################
if hasattr(camera, "dist"):
J_w_to_img2k1 = camera.J_image2world(xy, "dist") # (..., N, 2)
# (..., N, 2)
J_norm2k1 = torch.einsum("...Nij,...Nj->...Ni", J_norm2w_to_img, J_w_to_img2k1)
# (..., N, 1)
J_k1 = torch.einsum("...Ni,...i->...N", J_norm2k1, gravity.vec3d).unsqueeze(-1)
J.append(J_k1)
n_params = sum(j.shape[-1] for j in J)
return torch.cat(J, axis=-1).reshape(camera.shape[0], h, w, 1, n_params)
def get_perspective_field(
camera: BaseCamera,
gravity: Gravity,
use_up: bool = True,
use_latitude: bool = True,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Get the perspective field from the camera parameters.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
use_up (bool, optional): Whether to include the up vector field. Defaults to True.
use_latitude (bool, optional): Whether to include the latitude field. Defaults to True.
Returns:
Tuple[torch.Tensor, torch.Tensor]: Up and latitude fields as tensors of shape
(..., 2, h, w) and (..., 1, h, w).
"""
assert use_up or use_latitude, "At least one of use_up or use_latitude must be True."
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
if use_up:
permute = (0, 3, 1, 2)
# (..., 2, h, w)
up = get_up_field(camera, gravity).permute(permute)
else:
shape = (camera.shape[0], 2, h, w)
up = camera.new_zeros(shape)
if use_latitude:
permute = (0, 3, 1, 2)
# (..., 1, h, w)
lat = get_latitude_field(camera, gravity).permute(permute)
else:
shape = (camera.shape[0], 1, h, w)
lat = camera.new_zeros(shape)
return up, lat
def J_perspective_field(
camera: BaseCamera,
gravity: Gravity,
use_up: bool = True,
use_latitude: bool = True,
spherical: bool = False,
log_focal: bool = False,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Get the jacobian of the perspective field.
Args:
camera (Camera): Camera parameters.
gravity (Gravity): Gravity vector.
use_up (bool, optional): Whether to include the up vector field. Defaults to True.
use_latitude (bool, optional): Whether to include the latitude field. Defaults to True.
spherical (bool, optional): Whether to use spherical coordinates. Defaults to False.
log_focal (bool, optional): Whether to use log-focal length. Defaults to False.
Returns:
Tuple[torch.Tensor, torch.Tensor]: Up and latitude jacobians as tensors of shape
(..., h, w, 2, 4) and (..., h, w, 1, 4).
"""
assert use_up or use_latitude, "At least one of use_up or use_latitude must be True."
camera = camera.unsqueeze(0) if len(camera.shape) == 0 else camera
gravity = gravity.unsqueeze(0) if len(gravity.shape) == 0 else gravity
w, h = camera.size[0].unbind(-1)
h, w = h.round().to(int), w.round().to(int)
if use_up:
J_up = J_up_field(camera, gravity, spherical, log_focal) # (..., h, w, 2, 4)
else:
shape = (camera.shape[0], h, w, 2, 4)
J_up = camera.new_zeros(shape)
if use_latitude:
J_lat = J_latitude_field(camera, gravity, spherical, log_focal) # (..., h, w, 1, 4)
else:
shape = (camera.shape[0], h, w, 1, 4)
J_lat = camera.new_zeros(shape)
return J_up, J_lat