Source code for ms_camera_model.filter_sensor

'''
Multispectral Camera Model - Filters and Sensors
================================================

* **Description:** Dataclasses and their methods for filters and sensors
* **Author:** Tomas Vacek
'''

from __future__ import annotations

import logging
from dataclasses import dataclass

import numpy as np
import pandas as pd

from ms_camera_model.errors import NoProvidedFilepaths, WavelengthMismatch

logger = logging.getLogger(__name__)


[docs] @dataclass class FilterSpecs: """ Filter specification """ filter_transmittance: np.ndarray name: str = "Generic" supplier: str = "Generic" band_center: float = 0 band_width: float = 0 def __post_init__(self) -> None: """ Post init checking to avoid empty filters """ if not np.size(self.filter_transmittance): raise ValueError("Filter transmittance is an empty array") if self.filter_transmittance.ndim != 2 or self.filter_transmittance.shape[1] != 2: raise ValueError( f"Expected 2 columns (wavelength, transmittance), got {np.shape(self.filter_transmittance)[1]}")
[docs] @dataclass class SensorSpecs: """ Sensor specification """ sensor_qe_curve: np.ndarray name: str = "Generic" supplier: str = "Generic" sensor_type: str = "CMOS" def __post_init__(self) -> None: """ Post init checking to avoid empty qe curve """ if not np.size(self.sensor_qe_curve): raise ValueError("Sensor QE curve is an empty array") if self.sensor_qe_curve.ndim != 2 or self.sensor_qe_curve.shape[1] != 2: raise ValueError( f"Expected 2 columns (wavelength, quantum_efficiency), got {np.shape(self.sensor_qe_curve)[1]}")
[docs] @dataclass class FilterSensorUnit: """ Combination of filter and sensor """ filter_spec: FilterSpecs sensor_spec: SensorSpecs
[docs] @classmethod def from_excel(cls, filename_filter: str, filename_sensor: str) -> FilterSensorUnit: """ Load filter transmission and sensor spectral sensitivity from Excel file :param filename_filter: Filename or path to the filter xlsx file :param filename_sensor: Filename or path to the sensor xlsx file """ if not filename_filter or not filename_sensor: raise NoProvidedFilepaths logger.info( f"[FilterSensorUnit] Loading specifications\nFilter data: {filename_filter}\nSensor data: {filename_sensor}" ) filter_data = pd.read_excel(filename_filter) sensor_data = pd.read_excel(filename_sensor) try: filter_values = filter_data.values.astype(np.float32) sensor_values = sensor_data.values.astype(np.float32) except ValueError as e: logger.error(f"[FilterSensorUnit] Filter or sensor Excel files contain non-numeric characters: {e}") raise ValueError from e filter_spec = FilterSpecs(np.array(filter_values)) sensor_spec = SensorSpecs(np.array(sensor_values)) return FilterSensorUnit(filter_spec, sensor_spec)
[docs] @dataclass class InterpolatedFilterSensorUnit(FilterSensorUnit): """ FilterSensorUnit interpolated to hyperspectral data """ combined_response: np.ndarray | None = None
[docs] @classmethod def interpolate_to_hs_data(cls, fs_unit: FilterSensorUnit, hs_band_centers: list[float]) -> InterpolatedFilterSensorUnit: """ Interpolate the provided filter and sensor data to hyperspectral data """ logger.info("[FilterSensorUnit] Beginning FilterSensorUnit interpolation...") if not hs_band_centers: raise ValueError("Missing band center data for interpolation") min_hs_centers = min(hs_band_centers) max_hs_centers = max(hs_band_centers) active_response_mask = fs_unit.filter_spec.filter_transmittance[:, 1] > 0.01 if not np.any(active_response_mask): raise ValueError(f"Filter {fs_unit.filter_spec.name} has no passband") active_response = fs_unit.filter_spec.filter_transmittance[active_response_mask] min_active_w = np.min(active_response[:, 0]) max_active_w = np.max(active_response[:, 0]) if min_active_w < min_hs_centers or max_active_w > max_hs_centers: raise WavelengthMismatch( "The defined filter has active response outside of available hyperspectral data wavelengths") filter_interp = np.interp(hs_band_centers, fs_unit.filter_spec.filter_transmittance[:, 0], fs_unit.filter_spec.filter_transmittance[:, 1], left=0.0, right=0.0) sensor_interp = np.interp(hs_band_centers, fs_unit.sensor_spec.sensor_qe_curve[:, 0], fs_unit.sensor_spec.sensor_qe_curve[:, 1], left=0.0, right=0.0) logger.info(f"[FilterSensorUnit] Filter interp {filter_interp.shape}") logger.info(f"[FilterSensorUnit] Sensor interp {sensor_interp.shape}") interpolated_filter = FilterSpecs(np.column_stack([hs_band_centers, filter_interp]), fs_unit.filter_spec.name, fs_unit.filter_spec.supplier, fs_unit.filter_spec.band_center, fs_unit.filter_spec.band_width) interpolated_sensor = SensorSpecs(np.column_stack([hs_band_centers, sensor_interp]), fs_unit.sensor_spec.name, fs_unit.sensor_spec.supplier, fs_unit.sensor_spec.sensor_type) combined_response = filter_interp * sensor_interp return InterpolatedFilterSensorUnit(interpolated_filter, interpolated_sensor, combined_response)