Source code for ms_camera_model.data_comparison

'''
Multispectral Camera Model - Data Comparison
============================================

* **Description:** Classes and their methods used for comparing real MS data with modeled data
* **Author:** Tomas Vacek
'''
import logging

import numpy as np

from ms_camera_model.errors import (
    ImageDataIncompatible,
    InvalidProvidedArea,
    NoProvidedArea,
)
from ms_camera_model.image_data import AreaLocation, ImageData

logger = logging.getLogger(__name__)


[docs] class DataComparator: def __init__(self, ms_img_data: ImageData, modeled_img_data: ImageData) -> None: self.ms_img_data: ImageData = ms_img_data self.modeled_img_data: ImageData = modeled_img_data
[docs] def compare_band_ratios(self, real_ms_area_location: AreaLocation | list[AreaLocation], modeled_ms_area_location: AreaLocation | list[AreaLocation], set_areas_globally: bool = True) -> tuple[np.ndarray, np.ndarray]: """ Compare band ratios of real MS image data with modeled MS image data :param real_ms_area_location: AreaLocation or list[AreaLocation] objects describing the area that will be compared :param modeled_ms_square_mean: AreaLocation or list[AreaLocation] object describing the area that will be compared :param set_areas_globally: if True, provide only single AreaLocation, else list[AreaLocation] of length nbands :return: tuple(real_ms_ratios, modeled_ms_ratios) :raises ValueError: if sum of means of selected area is less than 1e-10 """ logging.info("[DataComparator] Preparing comparison...") if not real_ms_area_location or not modeled_ms_area_location: raise NoProvidedArea if self.ms_img_data.nbands != self.modeled_img_data.nbands: raise ImageDataIncompatible( f"Provided image data has incompatible number of bands ({self.ms_img_data.nbands} vs {self.modeled_img_data.nbands})" ) if set_areas_globally: if isinstance(real_ms_area_location, list) or isinstance(modeled_ms_area_location, list): raise InvalidProvidedArea( f"Expected single AreaLocation object for set_areas_globally = True, got {type(real_ms_area_location)} and {type(modeled_ms_area_location)}" ) real_ms_square_mean = ImageData.mean_spectrum_area(self.ms_img_data.img_data, real_ms_area_location.as_tuple()) modeled_ms_square_mean = ImageData.mean_spectrum_area(self.modeled_img_data.img_data, modeled_ms_area_location.as_tuple()) else: if not isinstance(real_ms_area_location, list) or not isinstance(modeled_ms_area_location, list): raise InvalidProvidedArea( f"Expected list of AreaLocation objects for set_areas_globally = False, got {type(real_ms_area_location)} and {type(modeled_ms_area_location)}" ) if len(real_ms_area_location) != self.ms_img_data.nbands: raise InvalidProvidedArea( f"Provided area locations ({len(real_ms_area_location)}) does not match the number of bands ({self.ms_img_data.nbands})" ) real_ms_square_mean = np.zeros(self.ms_img_data.nbands, dtype=np.float32) modeled_ms_square_mean = np.zeros(self.ms_img_data.nbands, dtype=np.float32) for band in range(self.ms_img_data.nbands): real_ms_square_mean[band] = ImageData.mean_spectrum_area(self.ms_img_data.img_data[:, :, band], real_ms_area_location[band].as_tuple())[0] modeled_ms_square_mean[band] = ImageData.mean_spectrum_area( self.modeled_img_data.img_data[:, :, band], modeled_ms_area_location[band].as_tuple())[0] sum_real_ms_mean = np.sum(real_ms_square_mean) sum_modeled_ms_mean = np.sum(modeled_ms_square_mean) if sum_real_ms_mean < 1e-10 or sum_modeled_ms_mean < 1e-10: raise ValueError("Denominator for next operation is 0 or close to 0") real_ms_ratios = real_ms_square_mean / sum_real_ms_mean modeled_ms_ratios = modeled_ms_square_mean / sum_modeled_ms_mean logging.info( f"[DataComparator] SQR_mean: {real_ms_square_mean}, SUM: {np.sum(real_ms_square_mean)}, ratios: {real_ms_ratios}" ) logging.info( f"[DataComparator] SQR_mean: {modeled_ms_square_mean}, SUM: {np.sum(modeled_ms_square_mean)}, ratios: {modeled_ms_ratios}" ) return real_ms_ratios, modeled_ms_ratios
[docs] def calculate_spectral_angle_mapper(self, real_ms_ratios: np.ndarray, modeled_ms_ratios: np.ndarray) -> float: """ Calculate the spectral angle mapper (shape similarity) between real and modeled data :param real_ms_ratios: real MS band ratios :param modeled_ms_ratios: modeled MS band ratios :return: angle in rad describing the similarity independent of brightness :raises ValueError: if the calculation would cause division by 0 """ numerator = np.sum(real_ms_ratios * modeled_ms_ratios) norm_real = np.linalg.norm(real_ms_ratios) norm_modeled = np.linalg.norm(modeled_ms_ratios) denominator = norm_real * norm_modeled if denominator < 1e-10: logger.error("[DataComparator] Calculation leads to zero division") raise ValueError("Invalid denominator value for SAM calculation") val = np.clip(numerator / denominator, -1.0, 1.0) angle = np.arccos(val) return angle