Defining a Python plugin for an existing algorithm on existing form data structures

Create a Python module for the plugin

Create a Python class inheriting the algorithm abstraction

Image filter plugin

In the following example, we will create a plugin class that performs a Gaussian smoothing of a multichannel image. It is related to the gnomonAbstractImageFilter workspace that encompasses all algorithms that input an image and output another image, transformed by a filter of some kind.

If you want to choose another workspace/abstraction see TODO

First, let’s create the module in the algorithm section of the package (see making a package for more information on how to make a plugin package)

linearFilterTimagetk.py
from gnomon.core import gnomonAbstractImageFilter

class linearFilterTimagetk(gnomonAbstractImageFilter):

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

In order for the plugin to be registered to the plugin factory some specific decorators must be added to the class.

gnomonPlugin decorator family

Gnomon defines a family Python class decorator that implements all the necessary methods to register the plugin to the platform, with just one line of code. The available decorators are:

  • formDataPlugin

  • algorithmPlugin

  • modelPlugin

  • visualizationPlugin

In this example we will use algorithmPlugin because we want to implement a filter which Gnomon considers an algorithm.

algorithmPlugin(version, coreversion, name='', base_class=None)
Parameters
  • version (str) – version of the plugin

  • coreversion (str) – version of gnomoncore

import gnomon.core
from gnomon.core import gnomonAbstractImageFilter

from gnomon.utils import algorithmPlugin

@algorithmPlugin(version="0.1.0", coreversion="1.0.1")
class linearFilterTimagetk(gnomonAbstractImageFilter):

Warning

Since the algorithm is imported from the gnomoncore module, the plugin should be registered to the gnomoncore namespace.

Provide the specifications of the plugin

Define the inputs and outputs of the plugin

Note

Python decorators allow to map the attributes of the class that represent the temporal series of forms (in the example case, images) to the setter and getter methods of the abstraction.

Inputs

The gnomonAbstractImageFilter abstraction requires an image as input. Therefore we use the imageInput decorator to make the link between the images attribute of the class (that will contain the input images) and the accessor methods from the signature of the abstraction : setInput and input.

Outputs

The gnomonAbstractImageFilter abstraction returns an image as output. Therefore we use the imageOutput decorator to make the link between the filtered_images attribute of the class (that will contain the output images) and the accessor method from the signature of the abstraction : output.

Warning

The class attributes are Python dictionaries for which the keys are temporal indices and values are data structures representing the forms. In the example case, they are assumed to be the default data structures representing intensity images : a dictionnary of instances SpatialImage class from the timagetk Python library for which keys are channel names.

from gnomon.utils.decorators import imageInput, imageOutput

@algorithmPlugin(version="0.1.0", coreversion="1.0.1")
@imageInput(attr='images')
@imageOutput(attr='filtered_images')
class linearFilterTimagetk(gnomonAbstractImageFilter):

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

        self.images = {}
        self.filtered_images = {}

Define the parameters of the algorithm

To provide parameters for the algorithm which will be accessible from the interface, gnomon uses the parameter classes provided by the dtkcore module.

dtkcore provides for a variety of classes for different types notably for integers, real numbers, character strings, boolean and so on:

simple

  • dtk::d_string

Numeric

  • dtk::d_uchar

  • dtk::d_char

  • dtk::d_uint

  • dtk::d_int

  • dtk::d_real

  • dtk::d_bool

List parameters

  • dtk::d_inliststring

  • dtk::d_inlistreal

  • dtk::d_inlistint

List of List

  • dtk::d_inliststringlist

Path

  • dtk::d_path

Range

  • dtk::d_range_uchar

  • dtk::d_range_char

  • dtk::d_range_uint

  • dtk::d_range_int

  • dtk::d_range_real

In the example case, we define a single parameter that corresponds to the sigma of the Gaussian kernel used for the filtering of the image. As it is a real-valued parameter, we use the d_real class from the dtkcore module to define the parameter with the following prototype:

class d_real(label: str, default_value: float, min: float, max: float, decimals: int, doc: str)

Parameters are defined as a dictionary attribute _parameters of the plugin class for which keys are the names of the parameter. This dictionary must be filled at init.

self._parameters["sigma"] = dtkcore.d_real("sigma", 1., 0, 10., 2, "Standard deviation of the Gaussian kernel")

gnomonParametric decorator

The gnomonParametric Python class decorator allows to map the parameters to a graphical rendering in the Gnomon interface by defining all the necessary functions. It also provides shortened access to the parameter values as self['parameter_name']. It is included in the algorithmPlugin decorator.

import gnomon.core
from gnomon.core import gnomonAbstractImageFilter

from gnomon.utils import algorithmPlugin
from gnomon.utils.decorators import imageInput, imageOutput

import dtkcore

@algorithmPlugin(version="0.1.0", coreversion="1.0.1")
@imageInput("images")
@imageOutput("filtered_images")
class linearFilterTimagetk(gnomonAbstractImageFilter):

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

        self.images = {}
        self.filtered_images = {}

        self._parameters = {}
        self._parameters["sigma"] = dtkcore.d_real("sigma", 1., 0, 10., 2,
            "Standard deviation of the Gaussian kernel")

Implement the plugin

Write the run method

run

This is where the actual code of the plugin is written, and it generally has always the same structure

  • set the outputs to empty dictionaries

  • iterate over the time points of the inputs

  • get the input data structures at the current time

  • compute the outputs from the inputs (actual algorithm)

  • fill the outputs data structures for the current time

Note

The run method does not return anything, it simply updates the values of the output data structures based on the content of the input data structures

Algorithm code

In the example case, the algorithm is implemented in a function of the timagetk Python package that we therefore need to import beforehand. The function is called to compute the Gaussian smoothing of each image channel at each time point, and fill the output dictionary filtered_images with the results.

from timagetk.plugins.linear_filtering import linear_filtering
from timagetk import MultiChannelimage
def run(self):
    self.filtered_images = {}

    for time in self.images.keys():
        self.filtered_images[time] = {}

        for channel in self.images[time].keys():
            img = self.images[time][channel]
            filtered_img = linear_filtering(img, method='gaussian_smoothing', sigma=self['gaussian_sigma'])
            self.filtered_images[time][channel] = filtered_img

        self.filtered_images[time] = MultiChannelImage(self.filtered_images[time])

Parameter values

Within the code of the run function, the parameters will be used to perform the computations and can be accessed easily thanks to the gnomonParametric decorator. Here for instance, the value of the gaussian_sigma parameter (that might have been set manually by the user using a graphical interface) is passed to the function as self['gaussian_sigma'].

Adding progress values

Now that we have a functioning plugin we might want to give some information on the progress of the computation back to gnomon. To achieve that, gnomon provides 3 pre-implemented methods to every plugin: set_max_progress(self, v: int) and increment_progress(self, increase: int = 1) to update the progress bar as well as set_progress_message(self, message: str) to provide a short message detailing what computation is happening.

First, we need to call self.increment_progress() and set_progress_message(message) every so often inside run(). Then, for this to work properly, we must set max progress to the number of expected calls to increment_progress.

For instance:

def run(self):
    self.set_max_progress(1*sum(len(img) for img in self.images.values))
    self.filtered_images = {}

    for time in self.images.keys():
        self.filtered_images[time] = {}

        for channel in self.images[time].keys():
            img = self.images[time][channel]
            self.set_progress_message(f"T {time} - channel {channel} : applying filter")
            filtered_img = linear_filtering(img, method='gaussian_smoothing', sigma=self['gaussian_sigma'])
            self.increment_progress()
            self.filtered_images[time][channel] = filtered_img

        self.filtered_images[time] = MultiChannelImage(self.filtered_images[time])

The maximum progress here is the sum of the number of channels of each image in the time series.

Note

The increment_progress serves another purpose additionaly. It can pause or stop the process if requested by gnomon.

Provide a documentation for the plugin

Fill in the class docstring

NumPy style docstring

You can provide a description text that will be included in the application and displayed when your plugin is selected by the user. It should follow the NumPy style for Python docstrings and contain at least

  • A short description on the first line (less than 80 characters)

  • A brief description paragraph detailing the algorithm and its parameters

class linearFilterTimagetk(gnomonAbstractImageFilter):
"""Compute the Gaussian smoothing of an image.

The algorithm performs a filtering of all the channels of a 3D image by an
isotropic Gaussian kernel of standard deviation equal to the value of the
gaussian_sigma parameter.
"""

Enable the dynamical discovery of the plugin

Entry points

To be dynamically discovered by the Gnomon platform, the plugin class should be referenced in the entry points of your Python interpreter. To do so, Gnomon offers a function that introspects a package looking for Gnomon plugins, and include them in a way that they will be found by the platform.

For more, see how they are declared here

Complete plugin module

import gnomon.core
from gnomon.core import gnomonAbstractImageFilter

from gnomon.utils import algorithmPlugin
from gnomon.utils.decorators import imageInput, imageOutput

import dtkcore

from timagetk.plugins.linear_filtering import linear_filtering
from timagetk import MultiChannelimage

@algorithmPlugin(version="0.1.0", coreversion="1.0.1")
@imageInput(attr="images")
@imageOutput(attr="filtered_images")
class linearFilterTimagetk(gnomonAbstractImageFilter):
    """Compute the Gaussian smoothing of an image.

    The algorithm performs a filtering of all the channels of a 3D image by an
    isotropic Gaussian kernel of standard deviation equal to the value of the
    gaussian_sigma parameter.
    """

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

        self.images = {}
        self.filtered_images = {}

        self._parameters = {}
        self._parameters["sigma"] = dtkcore.d_real("sigma", 1., 0, 10., 2,
            "Standard deviation of the Gaussian kernel")

    def run(self):
        self.set_max_progress(1*sum(len(img) for img in self.images.values))
        self.filtered_images = {}

        for time in self.images.keys():
            self.filtered_images[time] = {}

            for channel in self.images[time].keys():
                img = self.images[time][channel]
                filtered_img = linear_filtering(img, method='gaussian_smoothing', sigma=self['gaussian_sigma'])
                self.increment_progress()
                self.filtered_images[time][channel] = filtered_img

            self.filtered_images[time] = MultiChannelImage(self.filtered_images[time])