Source code for opticks.imaging_model.imaging_chain

# opticks Models and analysis tools for optical system engineering
#
# Copyright (C) Egemen Imre
#
# Licensed under GNU GPL v3.0. See LICENSE.md for more info.
"""Top-level imaging pipeline composed of an Imager and an optional Processing."""

from collections import namedtuple

import numpy as np
from astropy.units import Quantity

from opticks import u
from opticks.imaging_model.imager import Imager
from opticks.imaging_model.processing import Processing


[docs] class ImagingChain: """Top-level imaging pipeline: an Imager and an optional Processing. Holds an Imager (the physical imaging hardware: optics, detector, read/write electronics) and an optional Processing component (resampling, sharpening, etc.) that operates on data after the imager produces it. Parameters ---------- imager : Imager Imaging hardware (optics + detector + optional read/write electronics). processing : Processing, optional Post-acquisition processing component. """ def __init__( self, imager: Imager, processing: Processing | None = None, ): self.imager = imager self.processing = processing
[docs] @u.quantity_input(distance="length") def projected_horiz_img_extent( self, distance: Quantity, band_id: str, ) -> Quantity: """ Computes the projected horizontal image extent at some distance. The image extent is computed on a "flat surface" at the `distance`. Parameters ---------- distance : Quantity distance to the target or plane band_id : str band ID (to select the correct band or channel) Returns ------- Quantity Projected horizontal image extent """ return 2 * distance * np.tan(self.imager.horizontal_fov(band_id) / 2.0)
[docs] @u.quantity_input(distance="length") def projected_vert_img_extent( self, distance: Quantity, band_id: str, ) -> Quantity: """ Computes the projected vertical image extent at some distance. The image extent is computed on a "flat surface" at the `distance`. Parameters ---------- distance : Quantity distance to the target or plane band_id : str band ID (to select the correct band or channel) Returns ------- Quantity Projected vertical image extent """ return 2 * distance * np.tan(self.imager.vertical_fov(band_id) / 2.0)
[docs] @u.quantity_input(distance="length") def spatial_sample_distance( self, distance: Quantity, band_id: str, with_binning=True, location="centre", ) -> tuple[Quantity, Quantity]: """ Computes the Spatial Sampling Distance (SSD) at some distance. The SSD is computed on a "flat surface" at the `distance`. The location can be `centre`, `centre left`, `centre right`, `centre top`, `centre bottom` or `corner`. - `centre` corresponds to the boresight LoS vector corresponding to the channel pixels. - `centre left`, `centre right` are equivalent and they correspond to the horizontal centre points or 3 o'clock and 9 o'clock positions of the channel pixels. - `centre top`, `centre bottom` are equivalent and they correspond to the vertical centre points or 12 o'clock and 6 o'clock positions of the channel pixels - `corner` corresponds to any corner of the channel pixels. As the ifov is constant, the horizontal and vertical SSD are not necessarily equal. This is particularly evident with large pixel sizes and large FoVs. The result is a `namedtuple` and the horizontal and vertical SSD values can be queried with `horiz` and `vert`, respectively. Parameters ---------- distance : Quantity distance between the imager and the target band_id : str band ID (to select the correct band or channel) with_binning : bool, optional Return the value with binning or not location : str, optional Location on the detector Returns ------- (Quantity, Quantity) Spatial Sampling Distance with or without binning (horizontal and vertical) """ # select the binned/unbinned ifov values ifov = self.imager.ifov(band_id, with_binning) d = distance # compute the SSD, depending on the location if location == "centre": ssd_h = 2 * d * np.tan(ifov / 2.0) ssd_v = ssd_h elif location == "centre left" or location == "centre right": s_h = self.projected_horiz_img_extent(distance, band_id) fov_h = self.imager.horizontal_fov(band_id) ssd_h = s_h / 2.0 - d * np.tan(fov_h / 2.0 - ifov) a_v = np.sqrt(d**2 + (s_h / 2) ** 2) ssd_v = 2 * a_v * np.tan(ifov / 2) elif location == "centre top" or location == "centre bottom": s_v = self.projected_vert_img_extent(distance, band_id) fov_v = self.imager.vertical_fov(band_id) ssd_v = s_v / 2.0 - d * np.tan(fov_v / 2.0 - ifov) a_h = np.sqrt(d**2 + (s_v / 2) ** 2) ssd_h = 2 * a_h * np.tan(ifov / 2) elif location == "corner": s_v = self.projected_vert_img_extent(distance, band_id) fov_v = self.imager.vertical_fov(band_id) s_h = self.projected_horiz_img_extent(distance, band_id) fov_h = self.imager.horizontal_fov(band_id) a_h = np.sqrt(d**2 + (s_v / 2) ** 2) gamma_h = np.arctan((s_h / 2) / a_h) ssd_h = s_h / 2.0 - a_h * np.tan(gamma_h - ifov) a_v = np.sqrt(d**2 + (s_h / 2) ** 2) gamma_v = np.arctan((s_v / 2) / a_v) ssd_v = s_v / 2.0 - a_v * np.tan(gamma_v - ifov) else: raise ValueError(f"Invalid location flag: {location}") SSD = namedtuple("SSD", "horiz, vert") ssd = SSD(ssd_h.decompose().to(u.m), ssd_v.decompose().to(u.m)) # type: ignore[union-attr] return ssd