More Example Plugins¶
This section provides additional example plugins to demonstrate various capabilities of the WISER plugin system. These examples build upon the basic concepts introduced in each plugin section.
Normalized Difference Index Example Plugin¶
This plugin uses both a Tools Menu plugin and a Context Menu plugin to create the desired functionality. You use the Tool Menu plugin to create a new normalized difference index and the context menu plugin to use it on a displayed image quickly.
This plugin also teaches you how to get data from WISER and add data back into WISER.
import logging
from wiser.plugins import ToolsMenuPlugin
from PySide2.QtWidgets import QMenu, QMessageBox
from wiser.raster.dataset import RasterDataSet
from wiser.gui.app_state import ApplicationState
import numpy as np
from astropy import units as u
from typing import Any, Dict, List, Optional, Union
from wiser.plugins import ContextMenuPlugin, ContextMenuType
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())