Writing Algorithms ================== In :ref:`running-algorithms-label`, 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 :mod:`imfusion.algorithm` interface. To register an algorithm, decorate a Python class with :func:`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__``. .. figure:: images/algorithm_minimal_example.png :width: 100% :align: left .. 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-algorithm-inputs-label: Declaring Inputs ---------------- Input compatibility is declared using class attributes of type :class:`imfusion.algorithm.Input`. This allows you to specify exactly which data types your algorithm accepts and in what order, such as :class:`imfusion.SharedImageSet` for image input or :class:`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 :class:`imfusion.Data`, or a ``list``/``tuple`` containing objects derived from :class:`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 .. figure:: images/algorithm_configurable_threshold.png :width: 100% :align: left .. raw:: html
There are many parameter types available in :mod:`imfusion.algorithm`: Each parameter type provides additional optional features through constructor arguments. To explore them, follow the links below: - :class:`imfusion.algorithm.ParamBool` - :class:`imfusion.algorithm.ParamInt` - :class:`imfusion.algorithm.ParamDouble` - :class:`imfusion.algorithm.ParamString` - :class:`imfusion.algorithm.ParamPath` - :class:`imfusion.algorithm.ParamColor` - :class:`imfusion.algorithm.ParamChoice` 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 .. figure:: images/algorithm_parameters.png :width: 100% :align: left .. raw:: html
Adding Actions -------------- Actions are methods decorated with :func:`~imfusion.algorithm.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 .. figure:: images/algorithm_action.png :width: 100% :align: left .. 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. .. _execute-algorithm-in-python-label: 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 :ref:`declaring-algorithm-inputs-label`). .. 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 :func:`~imfusion.algorithm.create` or :func:`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 :class:`~imfusion.algorithm.Algorithm` interface when calling :func:`~imfusion.algorithm.create`, not the concrete Python class instance.