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.
To use Inviwopy, you have to build the appropriate
.so yourself, since it is currently not available through Pypi. To do so, enable the
IVW_MODULE_PYTHON3QT CMake flags. Next you need to specify the Python executable to which the produced library shall be compatible in the
Using Inviwopy with Anaconda environments
PYTHON_EXECUTABLEflag to your environment’s executable (e.g.
If you Python libraries are not found correctly, also adapt the
<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).
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.
First of all
inviwopy needs to be imported (called
ivw in the following) to get access to
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
NumpySource example we can use the following
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
__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).
In order to transfer data between Python and C++, the Inviwo data structures
Volume (example below),
Image, example slide 34-35) and
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)
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.