Band-Math Plugins

The WISER band-math functionality may be extended by implementing a wiser.plugins.BandMathPlugin instance that provides implementations of the wiser.bandmath.BandMathFunction type. The plugin itself is very straightforward, but the implementation of functions is somewhat involved, as WISER must manage various details in the band-math implementation.

The BandMathPlugin Class

Band-math plugins must derive from the BandMathPlugin class:

class wiser.plugins.BandMathPlugin[source]

This is the base type for plugins that provide custom band-math functions.

get_bandmath_functions() Dict[str, BandMathFunction][source]

This method returns a dictionary of all band-math functions provided by the plugin.

The keys are the function names, and must satisfy the parsing requirements of the band-math parser: names must start with an alphabetical character (a-z), and must include only alphanumeric characters and underscores (a-z, 0-9, _).

The values are instances of classes that extend the BandMathFunction type, to provide the various operations required by band-math functions.

Band-math expressions are case-insensitive. Therefore, all function names specified by a plugin are converted to lowercase when loaded into the band-math evaluator.

The plugin implementation is very straightforward; it must simply return a Python dict that associates string function-names and corresponding BandMathFunction instances.

The implementation of the band-math functions themselves is more involved; read on for details.

The BandMathFunction Class

Band-math functions must derive from the BandMathFunction class:

class wiser.bandmath.BandMathFunction[source]

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.

get_description()[source]

Return a helpful description of the band-math function.

analyze(args: List[BandMathExprInfo]) BandMathExprInfo[source]

Given the indicated argument types, this function reports the result-type of the function.

apply(args: List[BandMathValue]) BandMathValue[source]

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.

For maximum flexibility, band-math functions may accept any number and type of arguments. For example, a band-math function dotprod(a, b) may accept two spectra (returning a number), a spectrum and a data-set (for a pixel-wise dot-product of the spectrum with the data-set, returning a 1-band data-set), or a data-set and a spectrum (same). Therefore, functions are responsible for reporting errors if they are given the wrong number of arguments, or if the arguments are of the wrong types. Additional checks may also be necessary, if e.g. there is a mismatch in the number of bands between arguments, or a mismatch in the spatial dimensions of images or bands.

The BandMathValue Class

Band-math function arguments and return-values are wrapped in the BandMathValue class:

class wiser.bandmath.BandMathValue(type: VariableType, value: Any, computed: bool = True, is_intermediate=False)[source]

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.

Variables:
  • type – The type of the band-math value.

  • value – The value itself.

  • computed – If True, the value was computed from an expression.

as_numpy_array() ndarray[source]

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.

as_numpy_array_by_bands(band_list: List[int]) ndarray[source]

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

For arguments, non-scalar band-math values may be fetched from a BandMathValue object via the BandMathValue.as_numpy_array() method. Scalar values may of course be retrieved directly from the BandMathValue.value member.

For function return-values, results must also be wrapped in a BandMathValue object. The result may be a NumPy ndarray instance, or a specific kind of object (e.g. RasterDataSet, Spectrum, …), or a scalar. In all these cases, the type of the result must be reported with a value from the wiser.bandmath.VariableType enumeration:

enum wiser.bandmath.VariableType(value)[source]

Types of variables that are supported by the band-math functionality.

Member Type:

int

Valid values are as follows:

IMAGE_CUBE = <VariableType.IMAGE_CUBE: 1>
IMAGE_BAND = <VariableType.IMAGE_BAND: 2>
SPECTRUM = <VariableType.SPECTRUM: 3>
REGION_OF_INTEREST = <VariableType.REGION_OF_INTEREST: 4>
NUMBER = <VariableType.NUMBER: 5>
BOOLEAN = <VariableType.BOOLEAN: 6>
STRING = <VariableType.STRING: 7>
IMAGE_CUBE_BATCH = <VariableType.IMAGE_CUBE_BATCH: 8>
IMAGE_BAND_BATCH = <VariableType.IMAGE_BAND_BATCH: 9>

Reporting Other Function Details

BandMathFunction implementations should also implement the other specified operations, to fully integrate with the functionality exposed in WISER. For example, the wiser.bandmath.BandMathFunction.get_result_type() method reports the type of the result based on the number and types of the arguments, which is then reported to the user in the GUI.

Warning

Additional abstract methods will be added to the BandMathFunction type in the near future. For example, WISER needs the ability to estimate the memory requirements for evaluating a given band-math expression, as operations may be very resource-intensive. Thus, functions will need to expose this kind of information in the future.

The goal of the band-math implementation will be to not require these new operations, so that existing band-math plugins are rendered unusable when new implementation details are added. However, for full integration into WISER’s capabilities, it would be valuable to implement missing functionality as quickly as possible.

Implementation Suggestions

The present form of the band-math functionality requires the use of a number of wrapper objects for functions and values. Thus, plugin writers are encouraged to separate the operations themselves from the band-math integration code. This will ensure that useful operations you may create are still widely useful in your own code, and only the integration-points with the WISER band-math functionality will require the additional overhead of the above classes.

Additionally, it is suggested that computational operations should be implemented to operate on NumPy ndarray objects for maximum generality, although it may also be useful to implement certain operations against wiser.raster.RasterDataSet or wiser.raster.Spectrum objects, where metadata is available for use.

Example BandMath Plugin

Okay, lets get into an example plugin that lets you do a spectral angle calculation between a spectrum and a dataset.

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

import numpy as np

from wiser.plugins import BandMathPlugin

from wiser.bandmath import (
    BandMathValue,
    BandMathEvalError,
    VariableType,
    BandMathExprInfo,
)
from wiser.bandmath.functions import BandMathFunction
from wiser.bandmath.utils import reorder_args, check_image_cube_compatible


class SpectralAnglePlugin(BandMathPlugin):
    def __init__(self):
        super().__init__()

    def get_bandmath_functions(self) -> Dict[str, BandMathFunction]:
        return {"spectral_angle": SpectralAngle()}


class SpectralAngle(BandMathFunction):
    def _report_type_error(self, lhs_type, rhs_type):
        raise TypeError(f"Operands {lhs_type} and {rhs_type} not compatible for spectral angle operation.")

    def analyze(self, infos: List[BandMathExprInfo]) -> BandMathExprInfo:
        if len(infos) != 2:
            raise ValueError("spectral_angle function requires exactly two arguments.")

        lhs, rhs = infos[0], infos[1]
        lhs, rhs = reorder_args(lhs.result_type, rhs.result_type, lhs, rhs)

        if lhs.result_type == VariableType.IMAGE_CUBE and rhs.result_type == VariableType.SPECTRUM:
            check_image_cube_compatible(rhs, lhs.shape)
            info = BandMathExprInfo(VariableType.IMAGE_BAND)
            info.shape = (lhs.shape[1], lhs.shape[2])
            info.elem_type = lhs.elem_type
            info.spatial_metadata_source = lhs.spatial_metadata_source
            info.spectral_metadata_source = lhs.spectral_metadata_source
            return info
        else:
            self._report_type_error(lhs.result_type, rhs.result_type)

    def apply(self, args: List[BandMathValue]) -> BandMathValue:
        if len(args) != 2:
            raise BandMathEvalError("spectral_angle function requires exactly two arguments.")

        lhs, rhs = args[0], args[1]
        lhs, rhs = reorder_args(lhs.type, rhs.type, lhs, rhs)

        if lhs.type == VariableType.IMAGE_CUBE and rhs.type == VariableType.SPECTRUM:
            img_arr = lhs.as_numpy_array()
            spectrum_arr = rhs.as_numpy_array()
        elif lhs.type == VariableType.SPECTRUM and rhs.type == VariableType.IMAGE_CUBE:
            spectrum_arr = lhs.as_numpy_array()
            img_arr = rhs.as_numpy_array()
        else:
            raise BandMathEvalError(
                "spectral_angle function requires two arguments, an IMAGE_CUBE and a SPECTRUM."
            )

        # Compute the spectral angle
        spectrum_arr = np.nan_to_num(spectrum_arr, nan=0.0)
        img_arr = np.nan_to_num(img_arr, nan=0.0)
        spectrum_mag = np.linalg.norm(spectrum_arr)
        img_mags = np.linalg.norm(img_arr, axis=0)
        result_arr = np.moveaxis(img_arr, 0, -1)
        result_arr_no_nan = np.nan_to_num(result_arr, nan=0.0)
        result_arr = np.dot(result_arr_no_nan, spectrum_arr)
        result_arr = result_arr / (spectrum_mag * img_mags)
        result_arr = np.arccos(result_arr)

        return BandMathValue(VariableType.IMAGE_BAND, result_arr)