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__.
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 returnTruefor valid data andFalsefor invalid data. Raising an exception is also treated as invalid.is_optional=Truemarks 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
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
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
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)