10. Custom Data Analysis: Writing Plugins¶
Spectronon has the capability to support user-defined data analysis algorithms, commonly called plugins. Writing plugins allows the user to extend Spectronon’s functionality by incorporating custom tools designed for their specific applications. Furthermore plugins can create interactive user interfaces and integrate directly into Spectronon’s menu system.
Warning
Plugins have direct access to much of Spectronon’s internal code. We make this functionality available in the hope that it will be a useful tool. Use of this tool is at the user’s own risk. Resonon cannot support every situation that may arise from the use of this information. The methods and objects documented here may change at any time. Only the methods and attributes documented here are intended to be used in custom plugin development – all other methods and properties of the workbench, plugin, datacube, and spec classes are for internal use only and should not be called or overridden.
10.1. Getting started¶
Spectronon plugins are written in the Python programming language, and a working knowledge of Python is a prerequisite for writing Spectronon plugins. If you’re new to programming or to Python, the Beginner’s Guide to Python provides a collection of useful resources for getting started. Additionally, familiarity with the computing tools package Numpy is important, as Resonon datacubes are based on Numpy arrays. Finally, the SciPy library provides an excellent collection of tools for scientific computing in Python using Numpy arrays. Many Spectronon features make use of SciPy libraries, and SciPy is available to the user for use in developing additional plugins.
To develop a Spectronon plugin, create a file with a “.py” extension (such as MyCustomPlugin.py), and place it in the folder:
%userprofile%\SpectrononAppData\user_plugins\
This folder is typically located at
C:\Users\<your user name>\SpectrononAppData\user_plugins\
.
In the file write a subclass of the plugin type you desire (as described below). You can use any text editor of your choice to create the plugin - a list of Python text editors can be found here. Upon loading, Spectronon will scan the user plugins folder for .py files and, if a valid plugin is found, add it to the Spectronon menu system.
10.2. Spectronon Plugin Overview¶
There are 4 types of plugins:
- CubePlugin
- RenderPlugin
- SelectPlugin
- FilterPlugin (built-in plugins only)
Plugins are creating by defining a subclass of one of the base plugin types, then placing the
source file that defines that plugin in the %userprofile%\SpectrononAppData/user_plugins
folder. The base plugin
type definitions must be imported from the spectronon.workbench.plugin
package and subclassed. Each plugin class has three methods that you can override to define your
custom behavior. For example:
from spectronon.workbench.plugin import CubePlugin
class MyPlugin(CubePlugin):
"""
The docstring text will appear on the user interface as help text when hovering
over the plugin's menu item.
"""
label = "My Plugin"
userLevel = 1
def setup(self):
"""
The setup method is where you will create the plugin's user interface.
You can also define any custom initialization behavior here. Do not override
CubePlugin.__init__().
"""
def update(self):
"""
The update method is called each time the user changes the value of a Spec.
You might need this for your plugin's book keeping.
"""
def action(self):
"""
The action method is called when the plugin is run, and should contain the
logic of your custom behavior.
"""
Always define a label attribute as above, which will become the text that is displayed in Spectronon menus. The docstring used for the plugin class will become the help text when the user hovers over your custom plugin’s menu item.
Always define userLevel = 1
at the top of the class definition.
Occasionally a user may use user level 2, which will render the plugin
invisible unless Spectronon is invoked in expert-user mode.
Warning
Failure to set the userLevel to 1 or 2 at the top of the plugin definition will prevent the plugin from attaching to Spectronon’s menu system.
Writing any plugin requires at minimum that you write an action method.
def action(self):
# your stuff here
The action method is called by Spectronon when the plugin is activated, and again each time the user interacts with any Specs defined by the plugin. This method returns the results of your plugin.
- Cube plugins must return a datacube from their action method.
- Render plugins return an image representation of a datacube as a numpy array.
- Select plugins do not return a value, but interact directly with the workbench.
- Filter plugins cannot be written by users.
If you want to get information from the user, you also define a ‘setup’ method and define some Specs (more on specs below) within it.
def setup(self):
# define some specs here
If your UI can be altered based on user entry into other UI elements (for example, the user selects how many inputs they want to provide), then you must write an ‘update’ method.
def update(self):
# add or remove specs or update paramaters of existing specs
None of these methods accepts arguments. Instead, there are attributes of the Plugin that you can use.
10.3. The attributes available to each plugin type¶
self.wb
(All Plugins)This is a handle on the spectronon workbench. The workbench gives you a lot of freedom, so use this reference wisely. Some common uses of self.wb are:
- popup dialog messages such as:
self.wb.postMessage("The combination of arguments is invalid for this cube")
- retrieve data from the workbench:
correctioncube = self.wb.getCube(self.correctCubeID.value)
- plot something to the plotter:
self.wb.plot(myarray)
- and there are specs (SpecCube, SpecSpectrum) that expect the workbench as a parameter to help them work their magic
For more information, see Using the Workbench
- popup dialog messages such as:
self.datacube
(CubePlugin, RenderPlugin, SelectPlugin)- This is the datacube on which this plugin will be applied. Cube plugins action methods are expected to return a new datacube based somehow on this one. Render plugins will use this datacube as a data source to build a viewable image.
For more information, see Using Datacubes
self.pointlist
(SelectPlugin)- For any selection this is a list of points that was inside the selected area in the form of a numpy array of shape (number of points, 2) where self.pointlist[i,:] = (sample, line) index of the ith selected point.
10.4. Specs¶
A Spec is a class that manages a piece of data and a user interface to that data. They underly much of
Spectronon’s codebase. A Spec maintains the connection between the user interface and the data in the back end.
When a plugin class has a spec member, Spectronon will generate an appropriate graphical widget to allow the user
to adjust that spec’s value in the plugin’s control panel. Specs can be imported from
resonon.utils.spec
. Usually you will access the spec by getting the
current value in an action method using the spec’s ‘value’ member.
value = mySpec.value
Here is the list of available Specs:
- General purpose input:
- SpecBool - allows the user to specify a true of false value
- SpecFloat - allows the user to specify a floating point value within a defined range
- SpecInt - allows the user to specify an integer value within a defined range
- SpecChoice - allows the user to choose an item from a defined list
- Selecting references to cubes and spectra on the workbench:
- SpecCube - allows the user to select one of the datacubes currently available on the workbench
- SpecSpectrum - allows the user to select one of the spectra currently available on the workbench
- Selecting portions of cubes or spectra:
- SpecWavelength - a spec that accepts a datacube as an argument and allows the user to select a wavelength from that cube
- SpecBandNumber - a spec that accepts a datacube as an argument and allows the user to select a single band from that cube.
All specs have certain members that can be set to impact the way the spec appears and behaves:
self.label
: The label that will appear on the graphical user interface next to the Spec’s widget.
self.units
: A string describing the Spec’s units that will appear on the GUI if not set to None.
self.help
: A string that will appear if the user hovers the mouse over the Spec.
10.4.1. SpecBool¶
class SpecBool(Spec):
"""
Creates button or a checkbox for entering a boolean.
"""
def __init__(self, label, defaultValue=True):
By default, a SpecBool is represented on screen by a checkbox. To change the interface type,
set the interfaceType
member of the spec. Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecBool
from resonon.constants import INTERFACES
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_bool = SpecBool(label='My Option', default=True)
self.my_bool.units = 'my units'
self.my_bool.help = 'my mouse hover help'
self.my_bool.interfaceType = INTERFACES.CHECKBOX
def action(self):
if self.my_bool.value:
pass
#action if true
else:
pass
#action if false
10.4.2. SpecFloat¶
class SpecFloat(SpecNumber):
"""Floa
Creates a slider or spinner for setting a floating point number
"""
def __init__(self, label, minval, maxval, stepsize=1, defaultValue=None):
The default interface type of a SpecFloat is a slider. A spin button can also be used. Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecFloat
from resonon.constants import INTERFACES
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_float = SpecFloat(label='My Float', minval=0, maxval=100,
stepsize=0.1, defaultValue=10.4)
self.my_float.units = 'my units'
self.my_float.help = 'my mouse hover help'
# INTERFACES.SPIN is also valid. This line is not needed if you
# just want default behaviour.
self.my_float.interfaceType = INTERFACES.SLIDER
def action(self):
my_product = self.my_float.value * 2.5
# more plugin logic here....
10.4.3. SpecInt¶
class SpecInt(SpecNumber):
"""
Creates a slider or spinner for setting an int
"""
def __init__(self, label, minval, maxval, stepsize=1, defaultValue=None):
Using a SpecInt is just like using a SpecFloat. See SpecFloat for an example.
10.4.4. SpecChoice¶
class SpecChoice(Spec):
"""
Creates a combo box depending allowing selection of a value from a list of choices
"""
def __init__(self, label, values=None, defaultValue=None):
A Spec choice allows the user to choose an option from a combo box. It expects a list of strings as
possible choices. It’s value
member will be one of the strings in the list. Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecChoice
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_options = SpecChoice(label='option',
values = ['choice one',
'choice two',
'choice three'],
defaultValue = 'choice one')
self.my_options.help = 'my mouse hover help'
def action(self):
if self.my_options.value == 'choice one':
pass # action for choice one
elif self.my_options.value == 'choice two':
pass # action for choice two
elif self.my_options.value == 'choice three':
pass # action for choice three
10.4.5. SpecCube¶
class SpecCube(Spec):
"""
Displays a ComboBox for selecting one of the loaded datacubes
"""
def __init__(self,
label,
datacube,
wb,
requireMatchedLineCount=False,
requireMatchedSampleCount=False,
requireMatchedBandCount=False,
requireBandCount=None,
defaultValue=None):
The SpecCube user interface is a combo box that will automatically be populated with the datacubes that are currently available on the Spectronon workbench (e.g. any cube that appears in the Spectronon resource tree). The parameters passed to SpecCube’s constructor allow you to provide criteria by which the list of applicable cubes will be filtered.
- if
requireMatchedLineCount=True
the list will only show cubes whose line count is equal to that of the cube passed asdatacube
.- if
requireMatchedSampleCount=True
the list will only show cubes whose sample count is equal to that of the cube passed asdatacube
.- if
requireMatchedBandCount=True
the list will only show cubes whose band count is equal to that of the cube passed asdatacube
.- if
requireBandCount=[an integer]
the list will only show cubes whose band count is equal the specified integer. This value overrides that provided inrequireMatchedBandCount
.
Most commonly, you will pass your plugin’s datacube member to SpecCube’s constructor as the datacube argument, but any datacube object is a valid choice. You should always pass the plugin’s self.wb (workbench) member to SpecCub as the wb argument.
The value of a SpecCube is an index to a cube within the workbench. To get the cube object itself, you must call
the wb.tree.getCube
method.
Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecCube
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_cube = SpecCube(label='Select a Datacube',
datacube=self.datacube,
wb=self.wb,
requireMatchedLineCount=False,
requireMatchedSampleCount=False,
# list only those cubes with the same number of
# bands as the cube we operate on
requireMatchedBandCount=True,
requireBandCount=None,
defaultValue=None)
self.my_cube.help = 'my mouse hover help'
def action(self):
primary_cube = self.datacube
secondary_cube = self.wb.tree.getCube(self.my_cube.value)
# operate on the datacubes...
10.4.6. SpecSpectrum¶
class SpecSpectrum(Spec):
"""
Displays a ComboBox for selecting one of the loaded spectra
"""
def __init__(self,
label,
datacube,
wb,
requireMatchedWavelengths=False,
requireMatchedBandCount=False,
defaultValue=None):
SpecSpectrum is similar to SpecCube, but allows for choice of a spectrum object from the workbench.
- if
requireMatchedWavelengths=True
, the list will only show spectra for which the wavelengths of the spectrum exactly match those of the cube passed asdatacube
.- if
requireMatchedBandCount=True
, the list will only show spectra for which the number of bands matches the number of bands of the cube passed asdatacube
.
Most commonly, you will pass your plugin’s datacube member to SpecCube’s constructor as the datacube argument, but any datacube object is a valid choice. You should always pass the plugin’s self.wb (workbench) member to SpecCub as the wb argument.
The value of a SpecSpectrum is an index to a spectrum object within the workbench. To get the spectrum object itself, you must call
the wb.tree.getSpectrum
method.
Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecSpectrum
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_spectrum = SpecSpectrum(label='Select a Spectrum',
datacube=self.datacube,
wb=self.wb,
requireMatchedWavelengths=True,
requireMatchedBandCount=False,
defaultValue=None)
self.my_spectrum.help = 'my mouse hover help'
def action(self):
spectrum = self.wb.tree.getSpectrum(self.my_spectrum.value)
# operate on the spectrum object...
10.4.7. SpecWavelength¶
class SpecWavelength(SpecFloat):
def __init__(self, label, datacube, defaultValue=None):
A SpecWavelength accepts a datacube as an argument and allows the user to select a wavelength that is present in that cube. It’s user interface is a slider with range equal to the wavelength range in the passed in datacube.
Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecWavelength
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_wavelength = SpecWavelength(label='Wavelength',
datacube=self.datacube,
defaultValue=None)
self.my_wavelength.help = 'my mouse hover help'
def action(self):
# a float
wavelength = self.my_wavelength.value
# a band at a user selected wavelength
band = self.datacube.getBandAtWavelength(self.my_wavelength.value)
10.4.8. SpecBandNumber¶
class SpecBandNumber(SpecInt):
def __init__(self, label, datacube, defaultValue=0):
A SpecBandNumber allows the user to select one of the bands present in a datacube by index. Example:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecBandNumber
class MyPlugin(CubePlugin):
"""
An example plugin
"""
label = "My Plugin"
userLevel = 1
def setup(self):
self.my_band_num = SpecBandNumber(label='Band',
datacube=self.datacube,
defaultValue=0)
self.my_band_num.help = 'my mouse hover help'
def action(self):
band = self.datacube.getBand(self.my_band_num.value)
10.5. Using the Workbench¶
Plugins automatically receive a reference to the Spectronon workbench,
self.wb
. The workbench gives you a lot of freedom (including the freedom to break things!),
so use this reference wisely. Most often, you will use the workbench to get references to other
datacubes or spectra on the resource tree, add additional items to the resource tree, or prompt the
user for a filename or other information. SourceIDs in the below methods refer to IDs returned from
SpecCubes or SpecSpectra.
Some important methods you can call are:
def getCube(self, sourceID=None):
"""
Returns the requested datacube. If sourceID is None,
returns the current cube.
"""
def getRendering(self, renderID=None):
"""
Returns the requested rendering. If sourceID is None,
returns the current rendering.
"""
def getSpectrum(self, sourceID=None):
"""
Returns the requested spectrum. If sourceID is None,
returns the current spectrum.
"""
def addCube(self, datacube, name=None, render=True):
"""
Adds a datacube to the workbench.
"""
def addSpectrum(self, spectrumObject, name=None):
"""
Adds a spectrum to the workbench.
"""
def postMessage(self, message, title=''):
"""
Post a message in a dialog box
"""
def postScrolledMessage(self, message, title=''):
"""
Post a message in a dialog box that can be scrolled and text
can be selected. Good for long messages
"""
def postQuestion(self, message, title="Proceed?"):
"""
post message. returns True if user selects 'OK' and False if 'Cancel'
params:
message: the message to show
title: the dialog box title
"""
def requestOpenFilename(self,
wildcard='',
message="Enter Filename To Open ",
multiple=False):
"""
Requests a filename(s) from the user. Returns
the filename as a string, or a list of strings
if multiple=True. Returns None if the user selects
'Cancel'.
"""
def requestDirectory(self,
message="Please give a directory path",
suggest=""):
"""
Requests a directory form the user. Returns
the selected directory path as a string. Returns
None if the user selects 'Cancel'.
"""
10.6. Using Datacubes¶
Plugins get a reference to their relevant datacube as self.datacube
.
It is possible to get references to other datacubes directly from the
workbench. Datacubes contain a data array in numpy format as well as a collection of metadata.
Some important methods you can call are:
def getSpectrumArray(self, sample, line):
"""
return a 1D array at location (sample, line)
params
sample: the sample number (int)
line: the line number (int)
returns
1D array of length 'bands'
"""
def getBandNumForWavelength(self, wavelength):
"""
return the band number nearest the given wavelength
params
wavelength: wavelength of light in nanometers(float)
returns
(2d array)
"""
def getBandCount(self):
"""return The number of bands in this cube
(aka. the number of different wavelengths)
"""
def getSampleCount(self):
"""
return the number of samples for the cube
(aka. the image height)
"""
def getLineCount(self):
"""
return the number of lines for the cube
(aka. the image width)
"""
def getBandAtWavelength(self, wavelength):
"""
return band at given wavelength
params
wavelength: the wavelength of the band you want (float)
returns
the band for the given wavelength frequency (2d array)
return the band with the closest wavelength to the given value
in the case of equidistance between two bands, the lower of the two
bands is returned
"""
def getBandWithName(self, name):
"""
return band with given name
params
name: the name of the band you want (float)
returns
the band with the given name (2d array)
you may pass the string form of a known wavelength
"""
names = self.getBandNames()
if name not in names:
raise KeyError("No Band with name %s" % name)
return self.getBand(names.index(name))
def getBand(self, bandnumber):
"""
Return band at given band number
params
bandnumber: band number between 0 and bands - 1 (integer)
returns
band: the image at the band number (2d array) always (lines, samples)
"""
def getFrame(self, line, asBIP=False):
"""
return frame at given line number
params
line: the line number (first = 0) of a frame in the cube (int)
returns:
frame: the spectrum of each pixel in a line of the image (2D array)
always (samples, bands)
"""
def appendFrame(self, frame):
"""
append frame to cube
params
frame: the content of a frame of the same (samples, bands)
dimensions as the cube (2d array)
appends a frame and increases number of lines in the cube by one
"""
def appendBandWithName(self, band, bandname=None):
"""
append band to cube
params
band: the content of a band of the same (lines, samples)
dimensions as the cube (2d array)
appends a band and increases number of bands in the cube by one
"""
def appendBandWithWavelength(self, band, wave=None):
"""
append band to cube
params
band: the content of a band of the same (lines, samples)
dimensions as the cube (2d array)
keywords
writeheader: default (True). if headerfile exists, save header
file to disk to reflect new larger dimensions of cube.
appends a band and increases number of bands in the cube by one
"""
def getSubCube(self, minsample=0, minline=0, maxsample= -1,
maxline= -1, mode="memory", interleave=None):
"""getSubCube
return a cube with the data in the given range, mode, and interleave
with no arguments, this will return a complete copy.
"""
def getFramelessCopy(self, makeTypeFloat=False, mode=None, asBIP=False):
"""
return an empty cube object with a header based on the header of this cube
optional makeTypeFloat will return
"""
def getBandlessCopy(self, makeTypeFloat=False, mode=None):
"""
return an empty cube object with a header based on the header of this cube
optional makeTypeFloat will return
"""
newcube = self._getEmptyCubeWithHeaderCopy(makeTypeFloat, mode)
newcube.setMetaValue("bands", 0)
return newcube
def setBand(self, bandnumber, band):
"""
writes given band at given band
params
bandnumber: the number of the band to overwrite
returns
None
"""
def setFrame(self, line, frame):
"""
write a given frame into location at given line
params
line: the line number to overwrite (int)
frame: the content of the frame (2d array)
returns
None
This is only for replacing frames that are already there.
to add Frames to a cube use appendFrame
"""
def getArray(self, asBIP=False):
"""
get the numpy data array for this datacube
params:
asBIP: force the data to be in BIP interleave (lines, samples, bands)
"""
Additionally, the function util.makeEmptyCube()
is convenient for constructing new datacubes. It’s usage is as follows:
from resonon.core.data import util
newcube = util.makeEmptyCube(mode="memory",
typechar='f',
rotationString=datacube.getRotationString())
10.7. Example Plugins¶
The following plugin examples are based on built-in plugins from Spectronon.
Warning
Each plugin in Spectronon needs to have a distinct class name. If you change the name of the following plugins, don’t just remove ‘Example’ from the class name or your plugin will conflict with a built-in plugin.
10.7.1. Cube Plugins¶
Cube plugins are the most common. They operate on a datacube and return a datacube. Cube plugins add themselves to the New Cube submenu and context menu.
A very simple example based on a Spectronon built-in plugin is shown below:
from spectronon.workbench.plugin import CubePlugin
from resonon.utils.spec import SpecWavelength
from resonon.core.data import util
class BandRatioExample(CubePlugin):
"""Create a new single band cube that results from divide two bands of an
existing cube""" # The plugin's help text
label = "Band Ratio Example" #name of plugin as it appears in Spectronon
# default rendering, others include SingleBand, ThreshToColor, TriBand, etc
defaultRenderer = "SingleBand"
# allows control for what which users the plugin is available.
# 1 is for normal users
userLevel = 1
def setup(self):
# GUI controls that allows the user to select wavelengths, as they
# exist in 'datacube'
self.numband = SpecWavelength(label="Numerator Wavelength",
datacube = self.datacube)
self.denomband = SpecWavelength(label="Denominator Wavelength",
datacube = self.datacube)
def action(self):
#make an empty cube for the results, preserving metadata from the old cube
newcube = util.makeEmptyCube(mode="memory",
typechar='f',
rotationString=self.datacube.getRotationString())
#get the data array from the datacube at the two specific wavelengths
topband = self.datacube.getBandAtWavelength(self.numband.value).astype('f')
bottomband =self.datacube.getBandAtWavelength(self.denomband.value).astype('f')
#perform the math on the numpy arrays
result = topband / bottomband
#put the results in the empty cube
newcube.appendBandWithName(result,bandname=self.label)
#return the new cube and it will be added to the Spectronon file tree
return newcube
10.7.2. Render Plugins (Image Plugins)¶
Render Plugins create a 2D representation of a datacube, known as an ‘Image’ in Spectronon. They operate on a cube and return a 2D image of 3 or fewer bands (‘RGB’, or ‘colors’). A simple example is shown below:
import numpy
from spectronon.workbench.plugin import RenderPlugin
class BandAverageExample(RenderPlugin):
"returns a greyscale image of the average across all bands"
label = "Band Average Example"
userLevel = 1
def action(self):
datacube = self.datacube
#create an numpy array of the same spatial dimensions of the datacube
out = numpy.zeros((datacube.getLineCount(), datacube.getSampleCount()),
dtype=numpy.float32)
for band in range(datacube.getBandCount()):
#for each band, add it to the accumulator
out += datacube.getBand(band)
#normalize the results by the number of bands
out = out/datacube.getBandCount()
return out
10.7.3. Filter Plugins¶
Filter Plugins operate on a render to enhance or otherwise filter its appearance. It is currently not possible for a user to write a Filter Plugin.
10.7.4. Select Plugins¶
Select Plugins operate on a Region of Interest created in Spectronon with the Lasso, Wand, or Marquee tools. An example is shown below that finds the spectral and spatial average value of the pixels inside of the ROI.
from spectronon.workbench.plugin import SelectPlugin
class AverageROI_Example(SelectPlugin):
'''
Calculate a single mean across all bands in a selected region
'''
label = "ROI Example"
userLevel = 1
def action(self):
# get the raw numpy data from the datacube
# a BIP is indexed as (lines, samples, bands)
cube_array = self.datacube.getArray(asBIP=True)
# Use a numpy slice to get the portion of the array that has been selected
# the pointlist is given in order(sample, line).
subarray = cube_array[self.pointlist[:, 1], self.pointlist[:, 0], :]
# calculate the average of the selected region
ave = subarray.mean()
# display the calculated result for the user
self.wb.postMessage('ROI Average : %s' % ave, title="Average")