vtkPythonAlgorithm is great

September 10, 2014

Here is the blog I meant to write last time. In this blog, I will actually talk about vtkPythonAlgorithm. I will also cover some VTK pipeline and algorithm basics so those that want to start developing C++ algorithms will also benefit from reading it.

As I covered previously, vtkProgrammableFilter is a great tool and useful for many purposes. However, it has the following drawbacks:

  • Its output type is always the same as its input type. So one cannot implement filters such as a contour filter (which accepts a vtkDataSet and outputs a vtkPolyData) using it.
  • It is not possible to properly manipulate pipeline execution by setting keys when using a vtkProgrammableFilter.
  • The source counterpart of vtkProgrammableFilter, vtkProgrammableSource, is very difficult to use and is very limited.

We developed vtkPythonAlgorithm to remedy these issues. At its heart, vtkPythonAlgorithm is very simple (although more complicated than vtkProgrammableFilter).

  • It is an algorithm,
  • You assign to it a Python object using SetPythonObject(),
  • It calls Initialize(self, vtkself) on this Python object, passing a reference of itself,
  • It delegates the following methods to the Python object:
    • ProcessRequest(self, vtkself, request, inInfo, outInfo)
    • FillInputPortInformation(self, vtkself, port, info)
    • FillOutputPortInformation(self, vtkself, port, info)

By implementing some of these 4 methods in your Python class, you have access to the entire pipeline execution capability of VTK, including managing parallel execution, streaming, passing arbitrary keys up and down the pipeline etc.

For those that are not familiar with how VTK algorithms work, it is worthwhile to explain what these methods do.

Initialize: The main purpose of this method is to initialize the number of input and output ports an algorithm has. This is normally done in a C++ algorithm’s constructor. But since the Python object’s constructor is called before it is passed to vtkPythonAlgorithm, we can’t do it there. Hence Initialize(). This method takes 1 argument : the vtkPythonAlgorithm calling it. Here is a simple example:

import vtk

class MyAlgorithm(object):
    def Initialize(self, vtkself):
        vtkself.SetNumberOfInputPorts(1)
        vtkself.SetNumberOfOutputPorts(1)
  • 1 input port + 1 output port == your common VTK filter.
  • 0 input port + 1 output port == your common VTK source.
  • 1 input port + 0 output port == your common VTK sink (writer, mapper etc.).

FillInputPortInformation and FillOutputPortInformation: These methods are overwritten to tell the VTK pipeline what data type an algorithm expects as input and what data type it will produce. This information is used to do run-time sanity checking. It can also be used to ask the pipeline to automatically create the output of the algorithm, if an concrete data type is known when the algorithm is initialized.

Here is a simple example:

import vtk

class MyAlgorithm(object):

    def FillInputPortInformation(self, vtkself, port, info):
        info.Set(vtk.vtkAlgorithm.INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet")
        return 1

    def FillOutputPortInformation(self, vtkself, port, info):
        info.Set(vtk.vtkDataObject.DATA_TYPE_NAME(), "vtkPolyData")
        return 1

This is a classic VTK filter that accepts a vtkDataSet and produces vtkPolyData. Think a contour filter, a streamline filter, a slice filter etc. Note that this is impossible to achieve with vtkProgrammableFilter, which always produces the same type of output as input.

A few things to note here:

  • port is an integer referring to the index of the current input or output port. These methods are called once for each input and output port.
  • info is a key-value store object (vtkInformation) used to hold meta-data about the current input or output port. The keys are vtkObjects that are accessed through class methods. This allows VTK to avoid any key collisions while still supporting run-time addition of keys.
  • If the output DATA_TYPE_NAME() is a concrete class name, the pipeline (executive) will create the output data object for that port automatically before the filter executes. If it is the name of an abstract class, it is the developer’s responsibility to create the output data object later.

In addition to setting the input and output data types, these methods can be used to define additional properties. Here are a few:

  • vtkAlgorithm.INPUT_IS_REPEATABLE() : This means that a particular input port can accept multiple connections. Think vtkAppendFilter.
  • vtkAlgorithm.INPUT_IS_OPTIONAL() : Can be used to mark an input as optional. Which means that it is not an error if a connection is not made to that port.

ProcessRequest: This is the meat of the algorithm. This method is called for each pipeline pass VTK implements. It is used to ask the algorithm to create its output data object(s), to provide meta-data to downstream, to modify a request coming from downstream and to fill the output data object(s). Each of these pipeline passes are identified by the request type. Let’s look at an example:

import vtk

class MyAlgorithm(object):
    def ProcessRequest(self, vtkself, request, inInfo, outInfo):
        if request.Has(vtk.vtkDemandDrivenPipeline.REQUEST_DATA()):
            print 'I am supposed to execute'
        return 1

The arguments need explaining:

  • request: What the pipeline is asking of the algorithm. Common requests are
    • REQUEST_DATA_OBJECT() : create your output data object(s)
    • REQUEST_INFORMATION() : provide meta-data for downstream
    • REQUEST_UPDATE_EXTENT() : modify any data coming from downstream or create a data request (for sinks)
    • REQUEST_DATA() : do your thing. Take input data (if any), do something with it, produce output data (if any)
  • inInfo : a list of vectors of key-value store objects (vtkInformation). It is a list because each member corresponds to an input port. The list contains vectors because each input port can have multiple connections.
  • outInfo : a vector of key-value store objects. Each entry in the vector corresponds to an output port.

Let’s put it all together

import vtk

class MyAlgorithm(object):
    def Initialize(self, vtkself):
        vtkself.SetNumberOfInputPorts(1)
        vtkself.SetNumberOfOutputPorts(1)

    def FillInputPortInformation(self, vtkself, port, info):
        info.Set(vtk.vtkAlgorithm.INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet")
        return 1

    def FillOutputPortInformation(self, vtkself, port, info):
        info.Set(vtk.vtkDataObject.DATA_TYPE_NAME(), "vtkPolyData")
        return 1

    def ProcessRequest(self, vtkself, request, inInfo, outInfo):
        if request.Has(vtk.vtkDemandDrivenPipeline.REQUEST_DATA()):
            print 'I am supposed to execute'
        return 1

w = vtk.vtkRTAnalyticSource()

pa = vtk.vtkPythonAlgorithm()
pa.SetPythonObject(MyAlgorithm())
pa.SetInputConnection(w.GetOutputPort())

pa.Update()
print pa.GetOutputDataObject(0).GetClassName()
print pa.GetOutputDataObject(0).GetNumberOfCells()

This will print:

I am supposed to execute
vtkPolyData
0

Dealing With Data

Let’s change ProcessRequest() to accept input data and produce output data.

    def ProcessRequest(self, vtkself, request, inInfo, outInfo):
        if request.Has(vtk.vtkDemandDrivenPipeline.REQUEST_DATA()):
            inp = inInfo[0].GetInformationObject(0).Get(vtk.vtkDataObject.DATA_OBJECT())
            opt = outInfo.GetInformationObject(0).Get(vtk.vtkDataObject.DATA_OBJECT())

            cf = vtk.vtkContourFilter()
            cf.SetInputData(inp)
            cf.SetValue(0, 200)

            sf = vtk.vtkShrinkPolyData()
            sf.SetInputConnection(cf.GetOutputPort())
            sf.Update()

            opt.ShallowCopy(sf.GetOutput())
        return 1

On line 3, we get the data object from the information object (key-value store) associated with the first connection of the first port (inInfo[0] corresponds to all connections of the first port). This is our input. vtkDataObject.DATA_OBJECT() is the key used to refer to input and output data objects. On line 4, we get the data object associated with the first output port. This is our output. Lines 6-12 create a simple pipeline. On line 14, we copy the output from the shrink filter to the output of the vtkPythonAlgorithm.

When run, this filter will produce:

vtkPolyData
3124

Tip: Lines 3 and 4 can be simplified by using convenience functions as follows.

            inp = vtk.vtkDataSet.GetData(inInfo[0])
            opt = vtk.vtkPolyData.GetData(outInfo)

A Convenience Superclass

Note: For what is described here, you need VTK from a very recent master. As of writing of this blog, VTKPythonAlgorithmBase had been in VTK git master for only a few days.

Now that we covered the basics of vtkPythonAlgorithm, let me introduce a convenience class that makes algorithms development a bit more convenient. Let’s start with updating our previous example.

  import vtk
  from vtk.util.vtkAlgorithm import VTKPythonAlgorithmBase
  
  class ContourShrink(VTKPythonAlgorithmBase):
      def __init__(self):
          VTKPythonAlgorithmBase.__init__(self)
  
      def RequestData(self, request, inInfo, outInfo):
          inp = vtk.vtkDataSet.GetData(inInfo[0])
          opt = vtk.vtkPolyData.GetData(outInfo)
  
          cf = vtk.vtkContourFilter()
          cf.SetInputData(inp)
          cf.SetValue(0, 200)
  
          sf = vtk.vtkShrinkPolyData()
          sf.SetInputConnection(cf.GetOutputPort())
          sf.Update()
  
          opt.ShallowCopy(sf.GetOutput())
  
          return 1
  
  w = vtk.vtkRTAnalyticSource()
  
  pa = ContourShrink()
  pa.SetInputConnection(w.GetOutputPort())
  
  pa.Update()
  print pa.GetOutputDataObject(0).GetClassName()
  print pa.GetOutputDataObject(0).GetNumberOfCells()

Neat huh? I am not going to explain how this works under the covers because it is a bit convoluted. Suffice it to say that you can subclass VTKPythonAlgorithmBase to overwrite a number of methods and also treat it as a vtkPythonAlgorithm. In fact, it is a subclass of vtkPythonAlgorithm. The methods available to be overwritten are:

  • FillInputPortInformation(self, port, info) : Same as described above. Except self == vtkself.
  • FillOutputPortInformation(self, port, info) : Same as described above. Except self == vtkself.
  • RequestDataObject(self, request, inInfo, outInfo) : This is where you can create output data objects if the output DATA_TYPE_NAME() is not a concrete type.
  • RequestInformation(self, request, inInfo, outInfo) : Provide meta-data downstream. More on this on later blogs.
  • RequestUpdateExtent(self, request, inInfo, outInfo) : Modify requests coming from downstream. More on this on later blogs.
  • RequestData(self, request, inInfo, outInfo) : Produce data. As described before.

In addtion, you can use the constructor to manage the number of input and output ports the static input and output types (rather than overwriting FillInputPortInformation() and FillOutputPortInformation()). For example, we could have done this:

class ContourShrink(VTKPythonAlgorithmBase):
    def __init__(self):
        VTKPythonAlgorithmBase.__init__(self,
            nInputPorts=1, inputType='vtkDataSet',
            nOutputPorts=1, outputType='vtkPolyData')

We didn’t have to do this in our example because these happen to be the default values.

Important: You have to define an __init__() method that chains to VTKPythonAlgorithmBase for all of this to work. Don’t leave it out!

Adding Parameters

This blog has gotten long. Let’s look at one more capability to wrap up. Say you want to change the contour value or the shrink factor. You could of course edit the class every time but since that’s lame, you’d probably rather use methods. There are a few things to know to get this right. Here is an updated version:

  import vtk
  from vtk.util.vtkAlgorithm import VTKPythonAlgorithmBase
  
  class ContourShrink(VTKPythonAlgorithmBase):
      def __init__(self):
          VTKPythonAlgorithmBase.__init__(self,
              nInputPorts=1, inputType='vtkDataSet',
              nOutputPorts=1, outputType='vtkPolyData')
  
          self.__ShrinkFactor = 0.5
          self.__ContourValue = 200
  
      def SetShrinkFactor(self, factor):
          if factor != self.__ShrinkFactor:
              self.__ShrinkFactor = factor
              self.Modified()
  
      def GetShrinkFactor(self):
          return self.__ShrinkFactor
  
      def SetContourValue(self, value):
          if value != self.__ContourValue:
              self.__ContourValue = value
              self.Modified()
  
      def GetContourValue(self):
          return self.__ContourValue
  
      def RequestData(self, request, inInfo, outInfo):
          print 'Executing'
          inp = vtk.vtkDataSet.GetData(inInfo[0])
          opt = vtk.vtkPolyData.GetData(outInfo)
  
          cf = vtk.vtkContourFilter()
          cf.SetInputData(inp)
          cf.SetValue(0, self.__ContourValue)
  
          sf = vtk.vtkShrinkPolyData()
          sf.SetShrinkFactor(self.__ShrinkFactor)
          sf.SetInputConnection(cf.GetOutputPort())
          sf.Update()
  
          opt.ShallowCopy(sf.GetOutput())
  
          return 1
  
  w = vtk.vtkRTAnalyticSource()
  
  pa = ContourShrink()
  pa.SetInputConnection(w.GetOutputPort())
  
  pa.Update()
  print pa.GetOutputDataObject(0).GetClassName()
  print pa.GetOutputDataObject(0).GetNumberOfCells()
  
  pa.SetShrinkFactor(0.7)
  pa.SetContourValue(100)
  pa.Update()
  print pa.GetOutputDataObject(0).GetClassName()
  print pa.GetOutputDataObject(0).GetNumberOfCells()

This will print the following.

Executing
vtkPolyData
3124
Executing
vtkPolyData
2516

There are a few things to note here:

  • Instead of directly setting data members, we used setters and getters. The main reason behind this is what happens on lines 16 and 24. Unless you call Modified() as shown, the filter will not re-execute after you change a value.
  • We did not use Python properties for ShrinkFactor and ContourValue. As nice as this would have been, it is not currently possible. This is because VTKPythonAlgorithmBase derives from a wrapped VTK class, which does not support properties. Properties are only supported by class that descend from object.
  • It is good practice to do the values comparisons on lines 14 and 22 to avoid unnecessary re-execution of the filter.

Next

We have covered a lot of ground. In my next blog, I will put what we learned to use to develop a simple HDF5 reader. We will cover cool concepts such as providing meta-data in a reader and asking for a subset in a filter. Until then cheers.

Special thanks to Ben Boeckel who developed vtkPythonAlgorithm.

3 comments to vtkPythonAlgorithm is great

Leave a Reply to Will SchroederCancel reply