Source code for wiser.bandmath.types

import abc
import enum

from typing import Any, Dict, List, Optional, Tuple, Union

import numpy as np

import copy

from wiser.raster.dataset import (
    RasterDataSet,
    RasterDataBand,
    RasterDataDynamicBand,
    RasterDataBatchBand,
    RasterBand,
    SpectralMetadata,
    SpatialMetadata,
)
from wiser.raster.spectrum import Spectrum


FolderPathType = str
BANDMATH_VALUE_TYPE = Union[
    RasterDataSet,
    RasterDataBand,
    RasterDataBatchBand,
    Spectrum,
    FolderPathType,
    bool,
    np.float32,
]


[docs] class VariableType(enum.IntEnum): """ Types of variables that are supported by the band-math functionality. """ IMAGE_CUBE = 1 IMAGE_BAND = 2 SPECTRUM = 3 REGION_OF_INTEREST = 4 NUMBER = 5 BOOLEAN = 6 STRING = 7 IMAGE_CUBE_BATCH = 8 IMAGE_BAND_BATCH = 9
class BandMathExprInfo: """ This class holds information produced by the band-math expression analyzer. This is a value that is used to keep track of the meta data information for BandMathValues. For example, the resulting type of a*b when a is an image cube and b is a spectrum would be an image cube. The variables for this class and their purposes are defined below: # The result-type of the band-math expression. self.result_type: Optional[VariableType] = result_type # If the result is an array, this is the element type. self.elem_type: Optional[np.dtype] = None # If the result is an array, this is the shape of the array. self.shape: Tuple = None # If the result should have spatial metadata (e.g. geographic projection # info or spatial reference system) associated with it, this is the # source of that metadata. self.spatial_metadata_source: SpatialMetadata = None # If the result should have spectral metadata (e.g. band wavelengths) # associated with it, this is the source of that metadata. self.spectral_metadata_source: SpectralMetadata = None """ def __init__(self, result_type=None): # The result-type of the band-math expression. self.result_type: Optional[VariableType] = result_type # If the result is an array, this is the element type. self.elem_type: Optional[np.dtype] = None # If the result is an array, this is the shape of the array. self.shape: Tuple = None # If the result should have spatial metadata (e.g. geographic projection # info or spatial reference system) associated with it, this is that # metadata self.spatial_metadata_source: SpatialMetadata = None # If the result should have spectral metadata (e.g. band wavelengths) # associated with it, this is that metadata self.spectral_metadata_source: SpectralMetadata = None def result_size(self): """Returns an estimate of this result's size in bytes.""" shape_size = np.prod(self.shape) if self.shape is not None else 1 return np.dtype(self.elem_type).itemsize * shape_size def __deepcopy__(self, memo): cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): if isinstance(v, BandMathValue): setattr(result, k, v) else: setattr(result, k, copy.deepcopy(v, memo)) return result def __repr__(self) -> str: if self.result_type in [ VariableType.IMAGE_CUBE, VariableType.IMAGE_BAND, VariableType.SPECTRUM, ]: return f"[type={self.result_type}, elem_type={self.elem_type}, " + f"shape={self.shape}]" else: return f"[type={self.result_type}]"
[docs] class BandMathValue: """ This is a value created or consumed by a band-math expression during evaluation. The high-level type of the variable is stored, along with the actual value. The value may be one of several possible types, since most band-math operations work directly on NumPy arrays rather than other WISER types. Whether the band-math value is a computed result or not is also recorded in this type, so that math operations can reuse an argument's memory where that would be more efficient. :ivar type: The type of the band-math value. :ivar value: The value itself. :ivar computed: If True, the value was computed from an expression. """ def __init__( self, type: VariableType, value: Any, computed: bool = True, is_intermediate=False, ): if type not in VariableType: raise ValueError(f"Unrecognized variable-type {type}") self.name: Optional[str] = None self.type: VariableType = type self.value: BANDMATH_VALUE_TYPE = value self.computed: bool = computed self.is_intermediate = is_intermediate def set_name(self, name: Optional[str]) -> None: self.name = name def get_shape(self) -> Tuple: if isinstance(self.value, np.ndarray): return self.value.shape if self.type == VariableType.IMAGE_CUBE: if isinstance(self.value, RasterDataSet): return self.value.get_shape() elif self.type == VariableType.IMAGE_BAND: if isinstance(self.value, RasterBand): return self.value.get_shape() elif self.type == VariableType.SPECTRUM: if isinstance(self.value, Spectrum): return self.value.get_shape() # If we got here, we don't know how to convert the value into a NumPy # array. raise TypeError(f"Don't know how to get shape of {self.type} value") def get_elem_type(self) -> np.dtype: if isinstance(self.value, np.ndarray): return self.value.dtype if self.type == VariableType.IMAGE_CUBE: if isinstance(self.value, RasterDataSet): return self.value.get_elem_type() elif self.type == VariableType.IMAGE_BAND: if isinstance(self.value, RasterBand): return self.value.get_elem_type() elif self.type == VariableType.SPECTRUM: if isinstance(self.value, Spectrum): return self.value.get_elem_type() # If we got here, we don't know how to convert the value into a NumPy # array. raise TypeError(f"Don't know how to get element-type of {self.type} value")
[docs] def as_numpy_array(self) -> np.ndarray: """ If a band-math value is an image cube, image band, or spectrum, this function returns the value as a NumPy ``ndarray``. If a band-math value is some other type, the function raises a ``TypeError``. """ # If the value is already a NumPy array, we are done! if isinstance(self.value, np.ndarray): return self.value if self.type == VariableType.IMAGE_CUBE: if isinstance(self.value, RasterDataSet): return self.value.get_image_data() elif self.type == VariableType.IMAGE_BAND: if isinstance(self.value, RasterBand): return self.value.get_data() elif self.type == VariableType.SPECTRUM: if isinstance(self.value, Spectrum): return self.value.get_spectrum() # If we got here, we don't know how to convert the value into a NumPy # array. raise TypeError(f"Don't know how to convert {self.type} " + f"value {self.value} into a NumPy array")
[docs] def as_numpy_array_by_bands(self, band_list: List[int]) -> np.ndarray: """ If a band-math value is an image cube, image band, or spectrum, this function returns the value as a NumPy ``ndarray``. If a band-math value is some other type, the function raises a ``TypeError``. This function should really only be called on image_cubes unless its called through make_image_cube_compatible_by_bands """ # If the value is already a NumPy array, we are done! if isinstance(self.value, np.ndarray): # Assumes all numpy arrays have band as the first dimension min_band = min(band_list) band_list_base = [band - min_band for band in band_list] if self.type == VariableType.IMAGE_CUBE: if len(band_list_base) == 1: return self.value return self.value[band_list_base, :, :] elif self.type == VariableType.IMAGE_BAND: return self.value elif self.type == VariableType.SPECTRUM: band_start = band_list[0] band_end = band_list[-1] arr = self.value[band_start : band_end + 1] return arr raise TypeError( "Type value is incorrect, should be" + "IMAGE_CUBE, IMAGE_BAND, OR SPECTRUM" + f"but got {type(self.value)}" ) if self.type == VariableType.IMAGE_CUBE: if isinstance(self.value, RasterDataSet): return self.value.get_multiple_band_data(band_list) elif self.type == VariableType.IMAGE_BAND: if isinstance(self.value, RasterBand): return self.value.get_data() elif self.type == VariableType.SPECTRUM: if isinstance(self.value, Spectrum): arr = self.value.get_spectrum() band_start = band_list[0] band_end = band_list[-1] arr = arr[band_start : band_end + 1] return arr # We only want this function to work for numpy arrays and RasterDataSets # because these can be very big 3D objects raise TypeError( "This function should only be called on numpy" + f"arrays and image cubes, not {self.type}" )
[docs] class BandMathFunction(abc.ABC): """ The abstract base-class for all band-math functions and built-in operators. Functions must be able to report useful documentation, as well as the type of the result based on their input types, so that the user interface can provide useful feedback to users. This class should be serializable. """
[docs] def get_description(self): """ Return a helpful description of the band-math function. """ return self.__doc__
[docs] def analyze(self, args: List[BandMathExprInfo]) -> BandMathExprInfo: """ Given the indicated argument types, this function reports the result-type of the function. """ raise NotImplementedError()
[docs] def apply(self, args: List[BandMathValue]) -> BandMathValue: """ Apply the function to the specified arguments to produce a value. If the function gets the wrong number or types of arguments, it should raise a suitably-typed Exception. """ raise NotImplementedError()
class BandMathEvalError(RuntimeError): """ This subtype of the RuntimeError exception is raised when band-math evaluation fails. """ pass