Source code for iactrace.viz.plotting

from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import PolyCollection

from ..camera.sensor_group import HexagonalSensorGroup, SquareSensorGroup
from ..core.transforms import euler_to_matrix


[docs] def show_image( image, sensor, ax=None, *, cmap="viridis", vmin=None, vmax=None, norm=None, edgecolor="none", linewidth=0.0, colorbar=False, cbar_label=None, ): """Render a camera image at actual focal-plane positions. Builds a single PolyCollection holding every pixel in the camera. Pixel polygons are projected onto the camera (x, y) plane after applying each tile's position and Euler rotation, so curvature and tile tilt are reflected faithfully. This draws the image -- pixel values on the focal plane. For the camera's physical 3D geometry (every pixel's concentrator and photosensor) see iactrace.viz.show_camera. Parameters ---------- image Pixel image array. Shape (n_sensors, height, width) for SquareSensorGroup, (n_sensors, n_pixels) for HexagonalSensorGroup. sensor The sensor group that produced image. ax Matplotlib axes. Created with a square aspect figure if None. cmap Colormap name or instance. vmin, vmax Explicit colour limits. Default: data min/max. norm Optional matplotlib.colors.Normalize. Overrides vmin/vmax when given. edgecolor Pixel-boundary colour. Default "none" (no outlines). linewidth Pixel-boundary line width. colorbar Attach a vertical colorbar to ax. cbar_label Colorbar label text. Returns ------- ax (the axes that received the collection). """ image = np.asarray(image) if isinstance(sensor, SquareSensorGroup): polys, values = _square_polygons(image, sensor) elif isinstance(sensor, HexagonalSensorGroup): polys, values = _hex_polygons(image, sensor) else: raise TypeError(f"show_image does not support sensor type {type(sensor).__name__}") if ax is None: _, ax = plt.subplots(figsize=(6, 6)) if norm is None: if vmin is None: vmin = float(np.nanmin(values)) if vmax is None: vmax = float(np.nanmax(values)) pc = PolyCollection( polys, array=values, cmap=cmap, norm=norm, edgecolors=edgecolor, linewidths=linewidth, ) if norm is None: pc.set_clim(vmin, vmax) ax.add_collection(pc) ax.set_aspect("equal") ax.autoscale_view() if colorbar: cb = ax.get_figure().colorbar(pc, ax=ax) if cbar_label is not None: cb.set_label(cbar_label) return ax
def _stack_rotations(rotations): """Per-sensor rotation matrices, shape (n_sensors, 3, 3). Uses the canonical iactrace.core.transforms.euler_to_matrix so the 2D camera view shares the exact Euler convention of the 3D geometry. """ return np.stack([np.asarray(euler_to_matrix(r)) for r in np.asarray(rotations)]) def _square_polygons(image, sensor): """Return (polys, values) for a SquareSensorGroup image.""" expected_shape = (sensor.n_sensors, sensor.height, sensor.width) if image.shape != expected_shape: raise ValueError(f"image shape {image.shape} does not match sensor shape {expected_shape}") h, w = sensor.height, sensor.width xs = sensor.x0 + np.arange(w + 1) * sensor.dx # (w+1,) ys = sensor.y0 + np.arange(h + 1) * sensor.dy # (h+1,) XX, YY = np.meshgrid(xs, ys, indexing="xy") # (h+1, w+1) # CCW pixel quads: BL, BR, TR, TL. corners_2d = np.stack( [ np.stack([XX[:-1, :-1], YY[:-1, :-1]], axis=-1), np.stack([XX[:-1, 1:], YY[:-1, 1:]], axis=-1), np.stack([XX[1:, 1:], YY[1:, 1:]], axis=-1), np.stack([XX[1:, :-1], YY[1:, :-1]], axis=-1), ], axis=2, ) # (h, w, 4, 2) z = np.zeros(corners_2d.shape[:-1] + (1,)) local = np.concatenate([corners_2d, z], axis=-1) # (h, w, 4, 3) Rs = _stack_rotations(sensor.rotations) # (n, 3, 3) pos = np.asarray(sensor.positions) # (n, 3) world = np.einsum("sij,hwcj->shwci", Rs, local) + pos[:, None, None, None, :] # (n, h, w, 4, 3) polys = world[..., :2].reshape(-1, 4, 2) values = np.asarray(image).reshape(-1) return polys, values def _hex_polygons(image, sensor): """Return (polys, values) for a HexagonalSensorGroup image.""" expected_shape = (sensor.n_sensors, sensor.n_pixels) if image.shape != expected_shape: raise ValueError(f"image shape {image.shape} does not match sensor shape {expected_shape}") centers = np.asarray(sensor.hex_centers) # (n_pix, 2) s = sensor.hex_size angles = np.deg2rad(np.arange(30.0, 360.0, 60.0)) + sensor.grid_rotation vertex_offsets = s * np.stack([np.cos(angles), np.sin(angles)], axis=-1) # (6, 2) pix_corners_2d = centers[:, None, :] + vertex_offsets[None, :, :] # (n_pix, 6, 2) z = np.zeros(pix_corners_2d.shape[:-1] + (1,)) local = np.concatenate([pix_corners_2d, z], axis=-1) # (n_pix, 6, 3) Rs = _stack_rotations(sensor.rotations) pos = np.asarray(sensor.positions) world = np.einsum("sij,pcj->spci", Rs, local) + pos[:, None, None, :] # (n, n_pix, 6, 3) polys = world[..., :2].reshape(-1, 6, 2) values = np.asarray(image).reshape(-1) return polys, values