Learn to create Python processors for use in Inviwo
Edit me

Python Processors

You can define Inviwo processors using Python instead of C++. Python often offers simpler means to implement certain functionality and brings a large palette of libraries that can easy processor development. However note, that Python in general runs a lot slower than C++ and should thus be avoided for performance critical processors. The following explains how Python processors can be created, how an Inviwo processor is properly defined in Python and how you can easily access data using NumPy. The code from the following sections builds up to a simple processor that reads serialized Numpy arrays (.npy) from disk and serves them as Volume to the network.

Building InviwoPy

To use Inviwopy, you have to build the appropriate .dll/.so yourself, since it is currently not available through Pypi. To do so, enable the IVW_MODULE_PYTHON3 and IVW_MODULE_PYTHON3QT CMake flags. Next you need to specify the Python executable to which the produced library shall be compatible in the PYTHON_EXECUTABLE flag.

Using Inviwopy with Anaconda environments
  1. Set the PYTHON_EXECUTABLE flag to your environment’s executable (e.g. ~/.conda/envs/inviwo/bin/python3)

  2. If you Python libraries are not found correctly, also adapt the PYTHON_LIBRARY flag to <conda env>/lib/libpython3.6m.so (according to your Python version).

Note: If CMake warns you that <conda env>/lib overrides libraries from your system path, this may lead to linking errors. They can usually be resolved by unsetting PYTHON_LIBRARY (it is not necessary most of the time).

Processor Creation

In order to create a Python Processor, Inviwo must be built with the Python3 module enabled in CMake. To create a new Python processor, open Inviwo and select Python > New Python Processor from the menu. After specifying a processor name, the Python script containing the new processor is created in $HOME/.inviwo/python_processors/<processor name>.py. The script is already filled with a processor template containing the required methods etc. The newly created processor is also automatically added to your processor list for immediate use. Inviwo will attempt to reload Python scripts whenever they change on disk (no need to re-run or re-build inviwo in such cases) and any changes made to the source code will immediately reflect in the processor. Note that for this, the Python processor is re-instantiated when reloading successfully. If reloading fails due to Python errors, the old version stays in place and the Python errors are printed in the output console (View -> Output Console). If you have a faulty Python script upon starting Inviwo, then the processor cannot be loaded and the output console will print the related error.

Processor Definition

First of all inviwopy needs to be imported (called ivw in the following) to get access to ivw.Processor, ivw.properties.*, ivw.ports.* and ivw.glm.* wrappers. In our NumpySource example the imports are:

import inviwopy as ivw
from inviwopy.properties import FileProperty, InvalidationLevel
from inviwopy.data       import Volume, VolumeOutport

import numpy as np
from pathlib import Path

Using those wrappers you can define Inviwo processors very similar to how it is done in C++. The actual processor definition happens inside your own processors class, inheriting from ivw.Processor and defining all the following methods:

class NumpySource(ivw.Processor):
    def __init__(self, id, name):    # Default processor signature
        super().__init__(id, name)   # Call super class (Processor) with id, name

        [ Here comes normal constructor contents like adding ports, properties ]

    @staticmethod
    def processorInfo():                 # General information on this processor
        return ivw.ProcessorInfo(
            classIdentifier = "org.inviwo.numpysource",
    		displayName = "NumpySource",
    		category = "Python",
    		codeState = ivw.CodeState.Stable,
    		tags = ivw.Tags.PY
        )

    def getProcessorInfo(self): return NumpySource.processorInfo()

    def initializeResources(self):
        pass

    def process(self):
        pass

As you can see, just as in C++, a processor needs to define a constructor to define all ports, properties etc., some information about the processor itself, the initializeResources() and the process() method. If you don’t need initializeResources(), just define it with the pass no-op as function body. Also do not forget the call to ivw.Processor.__init__ in your processors __init__.

In our NumpySource example we can use the following __init__:

class NumpySource(ivw.Processor):
    def __init__(self, id, name):
        ivw.Processor.__init__(self, id, name)
        self.outport = VolumeOutport("outport")  # Define Outport
        self.addOutport(self.outport)            # Add port to processor

        self.file = FileProperty("file", "Numpy Volume (.npy)",
            invalidationLevel=InvalidationLevel(2)) # invalidate resources on change
        self.addProperty(self.file)
        self.array = None    # Init to None

This __init__ defines a volume outport to pass the loaded array to the network and a file property to locate the serialized Numpy array. Note that this FileProperty has its invalidationLevel=InvalidationLevel(2), which lets the FileProperty invalidate the processors resources upon changing the property. This will automatically call the initializeResources() method which will take care of actually loading the Numpy file (see below).

NumPy Compatibility

In order to transfer data between Python and C++, the Inviwo data structures Volume (example below), Layer (for Image, example slide 34-35) and Buffer (for Mesh, example) can take Numpy arrays (numpy.ndarray) for initialization.

Loading a Numpy array from disk, wrapping it in a Volume and outputting it to the network can be realized as follows:

def initializeResources(self):
    if Path(self.file.value).exists() and self.file.value.endswith('.npy'):
        self.array = np.load(self.file.value)
    else: print('Invalid file path.')

def process(self):
    if self.array is not None:
        vol = Volume(self.array)
        self.outport.setData(vol)

Note that Path is a class from Python’s pathlib that handles file paths and in this case is just used to check whether the given file path exists.

That is all the code necesssary to making Inviwo able to read Numpy-serialized arrays from disk and supplying them as Volume to the network. From here on you can do arbitrarily complex stuff using Python with your favorite Python libraries or you can wrap your existing Python-based algorithms in Inviwo processors to use them in your visualization.