Source code for src.utils.visualization.helpers

from __future__ import annotations

from typing import Any

import numpy as np
import pyvista as pv


[docs] def add_world_floor_and_object( plotter: pv.Plotter, object_type: str = "cube", center: tuple[float, float, float] = (0.0, 0.0, 0.035), color: str = "tomato", cube_size: tuple[float, float, float] = (0.05, 0.04, 0.06), sphere_radius: float = 0.02, floor_size: tuple[float, float] = (0.35, 0.35), floor_color: str = "lightgray", floor_opacity: float = 0.9, ) -> None: """Add a floor plane and one target object to a PyVista plotter. The floor lies in the XY plane at z = 0. The target object is either a cube or a sphere centred at *center*. World-frame XYZ axes are also drawn. Args: plotter: PyVista plotter to add meshes to. object_type: ``"cube"`` or ``"sphere"``. center: World-frame centre of the target object ``(x, y, z)`` in metres. color: Colour of the target object. cube_size: ``(x_length, y_length, z_length)`` for cube objects. sphere_radius: Radius in metres for sphere objects. floor_size: ``(i_size, j_size)`` of the XY floor plane in metres. floor_color: Colour of the floor plane. floor_opacity: Opacity ``[0, 1]`` of the floor plane. """ floor = pv.Plane( center=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), i_size=float(floor_size[0]), j_size=float(floor_size[1]), ) obj_type = str(object_type).strip().lower() if obj_type == "sphere": obj = pv.Sphere(radius=float(sphere_radius), center=tuple(center)) else: obj = pv.Cube( center=tuple(center), x_length=float(cube_size[0]), y_length=float(cube_size[1]), z_length=float(cube_size[2]), ) plotter.add_mesh(floor, color=floor_color, opacity=float(floor_opacity)) plotter.add_mesh(obj, color=color, smooth_shading=True) plotter.add_axes(line_width=2) # type: ignore[misc]
[docs] def hfov_to_vfov_deg(hfov_deg: float, width_px: int, height_px: int) -> float: """Convert a horizontal field-of-view angle to a vertical field-of-view angle. Uses the pinhole camera relationship: .. math:: \\text{VFOV} = 2 \\arctan\\!\\left(\\frac{\\tan(\\text{HFOV}/2)}{w/h}\\right) Args: hfov_deg: Horizontal field of view in degrees. width_px: Image width in pixels. height_px: Image height in pixels. Returns: Vertical field of view in degrees. """ aspect = float(width_px) / float(height_px) hfov_rad = np.deg2rad(float(hfov_deg)) vfov_rad = 2.0 * np.arctan(np.tan(hfov_rad / 2.0) / max(aspect, 1e-12)) return float(np.rad2deg(vfov_rad))
[docs] def apply_camera_pose( plotter: pv.Plotter, pose: Any, vfov_deg: float, near: float, far: float, ) -> None: """Apply a camera pose to a PyVista plotter. Sets position, focal point, up vector, view angle, and clipping range from the fields of *pose*, which must expose: ``position``, ``focal_point``, and ``viewup`` attributes. Args: plotter: PyVista plotter whose camera will be configured. pose: Camera pose object with ``position``, ``focal_point``, and ``viewup`` array-like attributes. vfov_deg: Vertical field of view in degrees (camera view angle). near: Near clipping plane distance in metres. far: Far clipping plane distance in metres. """ cam = plotter.camera cam.position = tuple(np.asarray(pose.position, dtype=float).tolist()) cam.focal_point = tuple(np.asarray(pose.focal_point, dtype=float).tolist()) cam.up = tuple(np.asarray(pose.viewup, dtype=float).tolist()) cam.view_angle = float(vfov_deg) cam.clipping_range = (float(near), float(far))