Writing Algorithms

In Running Algorithms, you already saw how to execute algorithms on images. This chapter explains how to implement your own algorithm in Python and make it available in the ImFusion Suite GUI.

Minimal Example

Algorithms are implemented using the imfusion.algorithm interface. To register an algorithm, decorate a Python class with imfusion.algorithm.register().

>>> import imfusion as imf
>>> @imf.algorithm.register(display_name="My Algorithm")
... class MyAlgorithm:
...    def __call__(self):
...        # Main computation happens here.
...        return None
...
>>> "PYTHON.MyAlgorithm" in imf.algorithm.list_available()
True

After registration, the algorithm is available under the ID PYTHON.MyAlgorithm. The name shown in the ImFusion Suite controller is defined by the display_name argument.

Once you’ve followed the instructions for importing Python scripts (see Running Python Scripts), the algorithm will appear in the ImFusion Suite with a Compute button that invokes __call__.

_images/algorithm_minimal_example.png

Warning

Defining __init__ in a registered Python algorithm is not allowed. Registration raises an exception if __init__ is present.

Warning

id and display_name are reserved attribute names used internally by the framework. To avoid shadowing user-defined attributes, registration raises an exception if either of these names is used.

Note

Methods such as __call__ may raise exceptions. Exceptions are handled internally by the framework and will be reflected as algorithm failures.

Declaring Inputs

Input compatibility is declared using class attributes of type imfusion.algorithm.Input. This allows you to specify exactly which data types your algorithm accepts and in what order, such as imfusion.SharedImageSet for image input or imfusion.Mesh for mesh input.

>>> import imfusion as imf
>>> from imfusion.algorithm import Input
>>> @imf.algorithm.register(display_name="Volume and Mesh Input Example")
... class VolumeLabelAlgorithm:
...    volume = Input(
...        imf.SharedImageSet,
...        validator=lambda sis: sis.descriptor().dimension == 3,
...    )
...    mesh = Input(imf.Mesh)
...
...    def __call__(self):
...        my_volume = self.volume
...        my_mesh = self.mesh
...        return None
...
>>> "PYTHON.VolumeLabelAlgorithm" in imf.algorithm.list_available()
True

The following rules apply for declared inputs:

  • Input order matters: Input descriptors define the expected input sequence. In the ImFusion Suite, clicked/selected data objects are interpreted in that same order: first click corresponds to the first input, second click to the second input, and so on (for more details see Running Python Scripts).

  • validator=... allows custom validation logic. The validator should return True for valid data and False for invalid data. Raising an exception is also treated as invalid.

  • is_optional=True marks trailing optional inputs. All required inputs must be declared before optional ones.

Warning

If a required input follows an optional input, the registration raises an exception.

Generating Output

Let’s implement a slightly more useful algorithm that takes one input image and produces a thresholded version as output.

>>> import numpy as np
>>> import imfusion as imf
>>> from imfusion.algorithm import Input
>>> @imf.algorithm.register(display_name="Threshold Algorithm")
... class ThresholdAlgorithm:
...    image = Input(imf.SharedImageSet)
...
...    def __call__(self) -> imf.SharedImageSet:
...        output_image = imf.SharedImageSet()
...        threshold = 2500
...        for img in self.image:
...            arr = np.array(img)
...            arr[arr < threshold] = 0
...            arr[arr >= threshold] = 1
...            output_image.add(imf.SharedImage(arr))
...        return output_image
...
>>> "PYTHON.ThresholdAlgorithm" in imf.algorithm.list_available()
True

After executing this algorithm in the ImFusion Suite, output_image will appear in the Data widget.

Note

The return value of __call__ may be a single object derived from imfusion.Data, or a list/tuple containing objects derived from imfusion.Data.

Adding Parameters

The previous threshold example is a good starting point, but not very practical. Instead of hardcoding the threshold, you can expose it as a parameter and easily adjust it in the ImFusion Suite controller.

>>> import numpy as np
>>> import imfusion as imf
>>> from imfusion.algorithm import Input, ParamInt
>>> @imf.algorithm.register(display_name="Configurable Threshold Algorithm")
... class ConfigurableThresholdAlgorithm:
...     image = Input(imf.SharedImageSet)
...     threshold = ParamInt("Threshold", default=2500, with_slider=True, min=0, max=4096, step=1)
...
...     def __call__(self) -> imf.SharedImageSet:
...         output_image = imf.SharedImageSet()
...         for img in self.image:
...             arr = np.array(img)
...             arr[arr < self.threshold] = 0
...             arr[arr >= self.threshold] = 1
...             output_image.add(imf.SharedImage(arr))
...         return output_image
...
>>> "PYTHON.ConfigurableThresholdAlgorithm" in imf.algorithm.list_available()
True
_images/algorithm_configurable_threshold.png

There are many parameter types available in imfusion.algorithm: Each parameter type provides additional optional features through constructor arguments. To explore them, follow the links below:

With just a few lines of parameter declarations, you can build feature-rich controllers in the ImFusion Suite. Here is a demo example:

>>> from enum import Enum
>>> from pathlib import Path
>>> import imfusion as imf
>>> from imfusion.algorithm import ParamBool, ParamInt, ParamDouble, ParamString, ParamPath, ParamColor, ParamChoice
>>> class Quality(Enum):
...   LOW = "LOW"
...   MEDIUM = "MEDIUM"
...   HIGH = "HIGH"
>>> @imf.algorithm.register(display_name="Algorithm Parameters Example")
... class ParametersExample:
...    enabled = ParamBool("Enabled", default=True)
...    iterations = ParamInt("Iterations", default=5)
...    distance = ParamDouble("Distance", default=0.5, unit="mm")
...    note = ParamString("Note", default="Demo")
...    input_path = ParamPath("Input Path", default=Path("./input.png"))
...    overlay_color = ParamColor("Overlay Color", default=ParamColor.Color(0, 0, 255))
...    quality = ParamChoice("Quality", default=Quality.MEDIUM)
...
...    def __call__(self):
...        pass
...
>>> "PYTHON.ParametersExample" in imf.algorithm.list_available()
True
_images/algorithm_parameters.png

Adding Actions

Actions are methods decorated with action(). Each action registers a clickable button in the ImFusion Suite controller (alongside the main Compute button), allowing the method to be executed directly from the interface.

>>> import imfusion as imf
>>> from imfusion.algorithm import ParamInt
>>> @imf.algorithm.register(display_name="Counting Algorithm")
... class CountingAlgorithm:
...     counter = ParamInt("Counter", default=0)
...
...     def __call__(self):
...         pass
...
...     @imf.algorithm.action(display_name="Increment")
...     def increment(self):
...         self.counter += 1
...
>>> "PYTHON.CountingAlgorithm" in imf.algorithm.list_available()
True
_images/algorithm_action.png

Note

Action methods are allowed to throw exceptions, which will be handled by the framework.

Warning

Action methods must take only self as a single argument and return None. If this requirement is violated, an exception is raised.

Executing Algorithms in Python

The primary use case for registered algorithms is to run them from the ImFusion Suite. However, if you want to execute your customized algorithm from Python, you can instantiate the algorithm with the required inputs and then invoke __call__. Inputs are positional-only and are mapped according to the declared input order (see Declaring Inputs).

Note

The respective __init__ method is injected during registration.

>>> import numpy as np
>>> import imfusion as imf
>>> from imfusion.algorithm import Input, ParamInt
>>> @imf.algorithm.register(display_name="Python Execution Example")
... class PythonExecutionExample:
...     input_a = Input(imf.SharedImageSet)
...     iterations = ParamInt("Iterations", default=3, min=1, max=100)
...
...     def __call__(self):
...         my_input: imf.SharedImageSet = self.input_a
...         # Do work here
...         pass
...
>>> sample_input = imf.SharedImageSet(np.ones((1, 32, 32, 1)))
>>> algo = PythonExecutionExample(sample_input)
>>> algo.iterations = 5  # Optionally, override the default value of `iterations`.
>>> output = algo()

Alternatively, as registered Python algorithms are fully integrated into the generic algorithm framework, they can also be invoked via create() or imfusion.algorithm.execute() using their algorithm ID:

>>> import numpy as np
>>> import imfusion as imf
>>> sample_input = imf.SharedImageSet(np.ones((1, 32, 32, 1)))  # type: ignore[arg-type]
>>> data = [sample_input]
>>> algorithm = imf.algorithm.create("PYTHON.PythonExecutionExample", data)
>>> output = imf.algorithm.execute("PYTHON.PythonExecutionExample", data)

Note

This approach returns the generic Algorithm interface when calling create(), not the concrete Python class instance.