Source code for ct.camera

"""
Functions for creating camera frustums and related visualizations.
"""

import open3d as o3d
import numpy as np
from typing import List, Optional, Dict, Tuple
from jaxtyping import Float
from . import convert
from . import sanity
from . import solver


[docs] def create_camera_frustums( Ks: Optional[List[Float[np.ndarray, "3 3"]]], Ts: List[Float[np.ndarray, "4 4"]], image_whs: Optional[List[List[int]]] = None, size: float = 0.1, color: Tuple[float, float, float] = (0, 0, 1), highlight_color_map: Optional[Dict[int, Tuple[float, float, float]]] = None, center_line: bool = True, center_line_color: Tuple[float, float, float] = (1, 0, 0), up_triangle: bool = True, center_ray: bool = False, ) -> o3d.geometry.LineSet: """ Create Open3D line sets representing camera frustums. Args: Ks: Optional list of 3x3 intrinsic matrices. If None, uses default intrinsics. Ts: List of 4x4 extrinsic matrices (world-to-camera). image_whs: Optional list of [width, height] pairs for each image. If None, dimensions are inferred from intrinsics. size: Distance from camera center to image plane in world coordinates. color: RGB color tuple for frustum lines. highlight_color_map: Optional dict mapping camera indices to highlight colors. Supports negative indexing (e.g., {0: [0,1,0], -1: [1,0,0]}). center_line: If True, draws line connecting camera centers. center_line_color: RGB color tuple for center line. up_triangle: If True, draws triangle indicating camera up direction. center_ray: If True, draws ray from camera center to image center. Returns: o3d.geometry.LineSet: Open3D line set containing all frustum geometry. """ if Ks is None: cx = 320 cy = 240 fx = 320 fy = 320 K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) Ks = [K for _ in range(len(Ts))] if len(Ts) != len(Ks): raise ValueError(f"len(Ts) != len(Ks): {len(Ts)} != {len(Ks)}") for K in Ks: sanity.assert_K(K) for T in Ts: sanity.assert_T(T) if image_whs is None: image_whs = [] for K in Ks: w = int((K[0, 2] + 0.5) * 2) h = int((K[1, 2] + 0.5) * 2) image_whs.append([w, h]) else: if len(image_whs) != len(Ts): raise ValueError( f"len(image_whs) != len(Ts): {len(image_whs)} != {len(Ts)}" ) for image_wh in image_whs: # Must be 2D. sanity.assert_shape_ndim(image_wh, nd=2) # Must be integer. w, h = image_wh if not isinstance(w, (int, np.integer)) or not isinstance( h, (int, np.integer) ): raise ValueError(f"image_wh must be integer, but got {image_wh}.") # Wrap the highlight_color_map dimensions. if highlight_color_map is not None: max_dim = len(Ts) highlight_color_map = { _wrap_dim(dim, max_dim, inclusive=False): color for dim, color in highlight_color_map.items() } # Draw camera frustums. ls = o3d.geometry.LineSet() for index, (T, K, image_wh) in enumerate(zip(Ts, Ks, image_whs)): if highlight_color_map is not None and index in highlight_color_map: frustum_color = highlight_color_map[index] else: frustum_color = color frustum = _create_camera_frustum( K, T, image_wh=image_wh, size=size, color=frustum_color, up_triangle=up_triangle, center_ray=center_ray, ) ls += frustum # Draw camera center lines. if len(Ts) > 1 and center_line: # TODO: fix open3d linese += when one of the line set is empty, the # color will be missing, center_line = create_camera_center_line(Ts, color=center_line_color) ls += center_line return ls
[docs] def create_camera_frustum_with_Ts( Ts: List[Float[np.ndarray, "4 4"]], image_whs: Optional[List[List[int]]] = None, size: float = 0.1, color: Tuple[float, float, float] = (0, 0, 1), highlight_color_map: Optional[Dict[int, Tuple[float, float, float]]] = None, center_line: bool = True, center_line_color: Tuple[float, float, float] = (1, 0, 0), up_triangle: bool = True, center_ray: bool = False, ) -> o3d.geometry.LineSet: """ Create camera frustums using only extrinsic matrices. Args: Ts: List of 4x4 extrinsic matrices (world-to-camera). image_whs: Optional list of [width, height] pairs for each image. size: Distance from camera center to image plane in world coordinates. color: RGB color tuple for frustum lines. highlight_color_map: Optional dict mapping camera indices to highlight colors. center_line: If True, draws line connecting camera centers. center_line_color: RGB color tuple for center line. up_triangle: If True, draws triangle indicating camera up direction. center_ray: If True, draws ray from camera center to image center. Returns: o3d.geometry.LineSet: Open3D line set containing all frustum geometry. """ return create_camera_frustums( Ks=None, Ts=Ts, image_whs=image_whs, size=size, color=color, highlight_color_map=highlight_color_map, center_line=center_line, center_line_color=center_line_color, up_triangle=up_triangle, center_ray=center_ray, )
[docs] def create_camera_center_line( Ts: List[Float[np.ndarray, "4 4"]], color: Tuple[float, float, float] = (1, 0, 0), ) -> o3d.geometry.LineSet: """ Create a line connecting camera centers. Args: Ts: List of 4x4 extrinsic matrices (world-to-camera). color: RGB color tuple for the center line. Returns: o3d.geometry.LineSet: Open3D line set connecting camera centers. """ num_nodes = len(Ts) camera_centers = [convert.T_to_C(T) for T in Ts] ls = o3d.geometry.LineSet() lines = [[x, x + 1] for x in range(num_nodes - 1)] colors = np.tile(color, (len(lines), 1)) ls.points = o3d.utility.Vector3dVector(camera_centers) ls.lines = o3d.utility.Vector2iVector(lines) ls.colors = o3d.utility.Vector3dVector(colors) return ls
def _create_camera_frustum( K: Float[np.ndarray, "3 3"], T: Float[np.ndarray, "4 4"], image_wh: List[int], size: float, color: Tuple[float, float, float], up_triangle: bool, center_ray: bool, ) -> o3d.geometry.LineSet: """ Create a single camera frustum as an Open3D line set. Args: K: 3x3 intrinsic matrix. T: 4x4 extrinsic matrix (world-to-camera). image_wh: Image dimensions [width, height]. size: Distance from camera center to image plane in world coordinates. color: RGB color tuple for frustum lines. up_triangle: If True, draws triangle indicating camera up direction. center_ray: If True, draws ray from camera center to image center. Returns: o3d.geometry.LineSet: Open3D line set representing the camera frustum. """ T, K, color = np.asarray(T), np.asarray(K), np.asarray(color) sanity.assert_T(T) sanity.assert_K(K) sanity.assert_shape_3(color, "color") w, h = image_wh if not isinstance(w, (int, np.integer)) or not isinstance(h, (int, np.integer)): raise ValueError(f"image_wh must be integer, but got {image_wh}.") R, _ = convert.T_to_R_t(T) C = convert.T_to_C(T) # Compute distance of camera plane to origin. camera_plane_points_2d_homo = np.array( [ [0, 0, 1], [w - 1, 0, 1], [0, h - 1, 1], ] ) camera_plane_points_3d = (np.linalg.inv(K) @ camera_plane_points_2d_homo.T).T camera_plane_dist = solver.point_plane_distance_three_points( [0, 0, 0], camera_plane_points_3d ) def points_2d_to_3d_world(points_2d): """ Convert 2D points to 2D points in world coordinates. The points will be normalized by camera_plane_dist and scaled by size. """ # Convert to homo coords. points_2d_homo = np.hstack((points_2d, np.ones((len(points_2d), 1)))) # Camera space in world scale. points_3d_cam = (np.linalg.inv(K) @ points_2d_homo.T).T # Normalize to have distance 1 and scale by size. points_3d_cam = points_3d_cam / camera_plane_dist * size # Transform to world space. points_3d_world = (np.linalg.inv(R) @ points_3d_cam.T).T + C return points_3d_world # Camera frustum line set. points_2d = np.array( [ [0, 0], [w, 0], [w, h], [0, h], ] ) points_3d = points_2d_to_3d_world(points_2d) points = np.vstack((C, points_3d)) lines = np.array( [ [0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [2, 3], [3, 4], [4, 1], ] ) ls = o3d.geometry.LineSet() ls.points = o3d.utility.Vector3dVector(points) ls.lines = o3d.utility.Vector2iVector(lines) ls.paint_uniform_color(color) if up_triangle: up_gap = 0.1 * h up_height = 0.5 * h up_points_2d = np.array( [ [up_gap, -up_gap], [w - up_gap, -up_gap], [w / 2, -up_gap - up_height], ] ) up_points = points_2d_to_3d_world(up_points_2d) up_lines = np.array( [ [0, 1], [1, 2], [2, 0], ] ) up_ls = o3d.geometry.LineSet() up_ls.points = o3d.utility.Vector3dVector(up_points) up_ls.lines = o3d.utility.Vector2iVector(up_lines) up_ls.paint_uniform_color(color) ls += up_ls if center_ray: center_px_2d = np.array([[(w - 1) / 2, (h - 1) / 2]]) center_px_3d = points_2d_to_3d_world(center_px_2d) center_ray_points = np.vstack((C, center_px_3d)) center_ray_lines = np.array([[0, 1]]) center_ray_ls = o3d.geometry.LineSet() center_ray_ls.points = o3d.utility.Vector3dVector(center_ray_points) center_ray_ls.lines = o3d.utility.Vector2iVector(center_ray_lines) center_ray_ls.paint_uniform_color(color) ls += center_ray_ls return ls def _wrap_dim( dim: int, max_dim: int, inclusive: bool = False, ) -> int: """ Wrap a dimension index to be within valid bounds. Args: dim: The dimension index to wrap. max_dim: Maximum dimension size. inclusive: If True, max_dim is included in valid range. Returns: int: Wrapped dimension index within valid range. Raises: ValueError: If max_dim is <= 0 or dim is out of bounds. """ if max_dim <= 0: raise ValueError(f"max_dim {max_dim} must be > 0.") min = -max_dim max = max_dim if inclusive else max_dim - 1 if dim < min or dim > max: raise ValueError( f"Index out-of-range: dim == {dim}, " f"but it must satisfy {min} <= dim <= {max}." ) if dim < 0: dim += max_dim return dim