Examples & Community#

Normalized Difference Index Plugin#

This combined example uses both a Tools Menu plugin and a Context Menu plugin to implement a normalized difference index workflow. The Tools Menu plugin creates a new index definition; the Context Menu plugin applies it to a displayed image in one click.

The example also demonstrates how to read raster data from WISER’s application state and write a new derived dataset back into it.

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

import numpy as np
from PySide2.QtWidgets import QMenu, QMessageBox
from astropy import units as u

from wiser.gui.app_state import ApplicationState
from wiser.plugins import ContextMenuPlugin, ContextMenuType
from wiser.plugins import ToolsMenuPlugin
from wiser.raster.dataset import RasterDataSet

logger = logging.getLogger(__name__)

quick_indices = {
    "NDVI": [860 * u.nm, 660 * u.nm],
    "NDWI": [550 * u.nm, 860 * u.nm],
    "NDBI": [1600 * u.nm, 860 * u.nm],
}


class ApplyNDIndex(ContextMenuPlugin):
    """
    Use a user selected normalized difference index quickly on a dataset
    """

    def __init__(self):
        super().__init__()

    def add_context_menu_items(
        self,
        context_type: ContextMenuType,
        context_menu: QMenu,
        context: Dict[str, Any],
    ) -> None:
        """
        Use QMenu.addAction() to add individual actions, or QMenu.addMenu() to
        add sub-menus to the Tools menu.
        """
        if context_type == ContextMenuType.RASTER_VIEW:
            """
            Context-menu display in a raster-view, which probably is showing a
            dataset.  The current dataset is passed to the plugin.

            Example code to get all necessary pieces of data for this context_type:
            ```python
            # A RasterDataSet object
            dataset = context["dataset"]
            # A 3 or 1 tuple of integers
            display_bands = context["display_bands"]
            # Every context_type has the app_state in the "wiser" key
            app_state = context["wiser"]
            ```
            """
            act = context_menu.addAction("Use NDI")
            act.triggered.connect(lambda checked=False: self.select_and_use_ndi(context))

        else:
            raise ValueError(f"Unrecognized context_type value {context_type}")

    def select_and_use_ndi(self, context):
        app_state: ApplicationState = context["wiser"]
        form_inputs = [
            ("Select the NDI method to use", "ndi_method", 0, list(quick_indices.keys())),
        ]
        return_dict: Dict[str, Any] = app_state.create_form(
            form_inputs,
            title="Analysis Params",
            description="The equation for the NDI is (a-b)/(a+b)",
        )
        ndi_method: str = return_dict["ndi_method"]
        wvl_a, wvl_b = quick_indices[ndi_method]

        dataset: RasterDataSet = context["dataset"]
        wavelengths = dataset.get_spectral_metadata().get_wavelengths()
        if wavelengths is None:
            QMessageBox.warning(
                None,
                "Couldn't Apply NDI",
                "The selected dataset has no wavelengths, cannot apply NDI.",
            )
            return
        wvl_a_index_closest = find_closest_wavelength(
            wavelengths=wavelengths,
            input_wavelength=wvl_a,
            max_distance=5 * u.nm,
        )
        wvl_b_index_closest = find_closest_wavelength(
            wavelengths=wavelengths,
            input_wavelength=wvl_b,
            max_distance=5 * u.nm,
        )

        assert wvl_a_index_closest is not None, f"Couldn't find a matching wavelength for {wvl_a} within 5 nm"
        assert wvl_b_index_closest is not None, f"Couldn't find a matching wavelength for {wvl_b} within 5 nm"

        band_a = dataset.get_band_data(wvl_a_index_closest)
        band_b = dataset.get_band_data(wvl_b_index_closest)

        ndi = (band_a - band_b) / (band_a + band_b)

        # Get object that helps us load datasets from numpy arrays or file paths
        data_loader = app_state.get_loader()
        # We must add a dimension to the beginning of the array because WISER takes
        # 3D arrays in the form [band][y][x]
        ndi = ndi[np.newaxis, :, :]
        # Create the dataset object, you must set its name
        ndi_dataset = data_loader.dataset_from_numpy_array(ndi, app_state.get_cache())
        ndi_dataset.set_name(f"{ndi_method} for {dataset.get_name()}")
        # Now add it back to our app
        app_state.add_dataset(ndi_dataset)


class NormalizedDifferenceIndexMaker(ToolsMenuPlugin):
    """
    A plugin to make a new normalized difference index
    """

    def __init__(self):
        super().__init__()

    def add_tool_menu_items(self, tool_menu: QMenu, wiser) -> None:
        """
        Use QMenu.addAction() to add individual actions, or QMenu.addMenu() to
        add sub-menus to the Tools menu.
        """
        logger.info("NormalizedDifferenceIndexMaker is adding tool-menu items")
        act = tool_menu.addAction("Created new NDI")
        act.triggered.connect(self.create_nd_index)
        self._app_state = wiser

    def create_nd_index(self) -> None:
        # To create the ND index, we need a name and the two wavelengths to use
        # for the index, so lets make a form to get that

        form_inputs = [
            ("Enter Wavelength for A", "wvl_a", 2),
            ("Enter Wavelength for B", "wvl_b", 2),
            ("Enter Normalized Difference Index Name", "ndi_name", 5),
        ]
        return_dict: Dict[str, Any] = self._app_state.create_form(
            form_inputs,
            title="Analysis Params",
            description="The equation for the ND is (a-b)/(a+b)",
        )
        wvl_a: u.Quantity = return_dict["wvl_a"]
        wvl_b: u.Quantity = return_dict["wvl_b"]
        ndi_name: str = return_dict["ndi_name"]

        quick_indices[ndi_name] = [wvl_a, wvl_b]


def find_closest_wavelength(
    wavelengths: List[u.Quantity],
    input_wavelength: u.Quantity,
    max_distance: u.Quantity = None,
) -> Optional[int]:
    """
    Given a list of wavelengths and an input wavelength, this function returns
    the index of the wavelength closest to the input wavelength.  If no
    wavelength is within max_distance of the input then None is returned.
    """

    # Do the whole calculation in nm to keep things simple.
    if max_distance is None:
        max_distance = 20 * input_wavelength.unit.si
    input_value = convert_spectral(input_wavelength, u.nm).value
    max_dist_value = None
    if max_distance is not None:
        max_dist_value = convert_spectral(max_distance, u.nm).value

    values = [convert_spectral(v, u.nm).value for v in wavelengths]

    return find_closest_value(values, input_value, max_dist_value)


Number = Union[float, int]


def find_closest_value(
    values: List[Number], input_value: Number, max_distance: Optional[Number] = None
) -> Optional[int]:
    """
    Given a list of numbers (ints and/or floats) and an input number, this
    function returns the index of the number closest to the input number.
    If no number is within max_distance of the input then None is returned.
    """
    best_index = None
    best_distance = None

    for index, value in enumerate(values):
        distance = abs(value - input_value)

        if max_distance is not None and distance > max_distance:
            continue

        if best_index is None or distance < best_distance:
            best_index = index
            best_distance = distance

    return best_index


def convert_spectral(value: u.Quantity, to_unit: u.Unit) -> u.Quantity:
    """
    Convert a spectral value with units (e.g. a frequency or wavelength),
    to the specified units.
    """
    return value.to(to_unit, equivalencies=u.spectral())

Plugin Repository#

The WISER Plugin Repository hosts community-contributed plugins that extend WISER with additional remote sensing workflows and analysis tools.

For users — the repository README covers:

  • Browsing available plugins

  • Installing a plugin and its conda environment

  • Enabling a plugin in WISER via Settings → Plugins

For contributors — to submit a plugin, see the repository’s CONTRIBUTING.md and plugin specification.