Berk Geveci     About     Archive     Feed

vtkProgrammableFilter ain't so bad

When I started preparing for this blog, my goal was to write about how awful vtkProgrammableFilter is to use in Python and how the new vtkPythonAlgorithm is superior. Funny enough, as I worked on a few examples, I discovered that vtkProgrammableFilter is not so bad if you know a few tricks. It does what it is supposed to fairly well. vtkPythonAlgorithm can do a lot more but more on that later.

In summary, vtkProgrammableFilter enables one to execute Python code within a pipeline. It is designed to perform relatively simple tasks without having to compile C++ classes. Actually, when combined with the numpy_interface module that I previously wrote about, it can do fairly complex and computationally intensive things too. Let's start with a very simple example.

1   import time, vtk
2   from vtk.numpy_interface import dataset_adapter as dsa
3   from vtk.numpy_interface import algorithms as alg
4   
5   def create_scene(flt):
6       m = vtk.vtkPolyDataMapper()
7       m.SetInputConnection(flt.GetOutputPort())
8   
9       a = vtk.vtkActor()
10      a.SetMapper(m)
11  
12      ren = vtk.vtkRenderer()
13      ren.AddActor(a)
14  
15      renWin = vtk.vtkRenderWindow()
16      renWin.AddRenderer(ren)
17      renWin.SetSize(600, 600)
18  
19      return renWin
20  
21  s = vtk.vtkSphereSource()
22  
23  def execute():
24      inp = dsa.WrapDataObject(pf.GetInput())
25      opt = dsa.WrapDataObject(pf.GetOutput())
26      opt.ShallowCopy(inp.VTKObject)
27      opt.PointData.append(inp.PointData['Normals'][:, 0], 'Normals-x')
28      opt.PointData.SetActiveScalars('Normals-x')
29  
30  pf = vtk.vtkProgrammableFilter()
31  pf.SetInputConnection(s.GetOutputPort())
32  pf.SetExecuteMethod(execute)
33  
34  renWin = create_scene(pf)
35  renWin.Render()
36  time.sleep(3)

Lines 5 - 19 should be pretty obvious to a VTK initiate. They simply create a rendering scene. Line 21 - 32 create a simple pipeline consisting of a vtkSphereSource and a vtkProgrammableFilter. The Programmable Filter creates a new scalar array called Normals-x by extracting the first component of the Normals vector generated by the Sphere Source (lines 27-28). This function is assigned to the Programmable Filter on line 32. Pretty standard stuff. Still quite powerful if you consider that you have almost the entire VTK API plus the numpy_interface module.

What I really dislike about this is that on lines 24 and 25, we have to refer to the pf object that is in the global namespace. So, if we add something like this:

39  pf = None

We will get the following error during execution:

Traceback (most recent call last):
  File "lame.py", line 34, in execute
    inp = dsa.WrapDataObject(pf.GetInput())
AttributeError: 'NoneType' object has no attribute 'GetInput'

Since the method passed to SetExecuteMethod cannot take any arguments, this seems like the only solution. It is kind of yucky. What is worse, if you want to re-use this method and tweak it by setting some parameters, you have to do it by adjusting global variables or write a new function for each new combination. Ugh.

We can get around the first problem if we can use a closure as follows.

def create_execute(filter):
    pf = filter
    def execute():
        inp = dsa.WrapDataObject(pf.GetInput())
        opt = dsa.WrapDataObject(pf.GetOutput())
        opt.ShallowCopy(inp.VTKObject)
        opt.PointData.append(inp.PointData['Normals'][:, 0], 'Normals-x')
        opt.PointData.SetActiveScalars('Normals-x')
    return execute

pf = vtk.vtkProgrammableFilter()
pf.SetInputConnection(s.GetOutputPort())
pf.SetExecuteMethod(create_execute(pf))

renWin = create_scene(pf)

pf = None

renWin.Render()

This works fine because create_execute() returns a function object which has its argument bound within the function object's scope (a closure). It still doesn't address the configurability issue. I didn't think that this problem could be solved. I was wrong. SetExecuteMethod accepts any callable object. So the following works very nicely.

s = vtk.vtkSphereSource()

class MyAlgorithm(object):
    def __init__(self, algorithm):
        import weakref
        self.__Algorithm = weakref.ref(algorithm)

    def __call__(self):
        inp = dsa.WrapDataObject(self.__Algorithm().GetInput())
        opt = dsa.WrapDataObject(self.__Algorithm().GetOutput())
        opt.ShallowCopy(inp.VTKObject)
        opt.PointData.append(inp.PointData['Normals'][:, 0], 'Normals-x')
        opt.PointData.SetActiveScalars('Normals-x')


pf = vtk.vtkProgrammableFilter()
pf.SetInputConnection(s.GetOutputPort())
pf.SetExecuteMethod(MyAlgorithm(pf))

An instance of a class that implements the __call__() method is callable. So we can call a MyAlgorithm as if it is a function:

o = MyAlgorithm(pf)
o()

Note that I used a weak reference to avoid a reference loop between the Programmable Filter and MyAlgorithm. We can now customize the programmable filter using the class interface.

Consider another example.

1   class MyAlgorithm(object):
2       def __init__(self, algorithm):
3           import weakref
4           self.__Algorithm = weakref.ref(algorithm)
5           self.__Factor = 1
6   
7       def __call__(self):
8           inp = dsa.WrapDataObject(self.__Algorithm().GetInput())
9           opt = dsa.WrapDataObject(self.__Algorithm().GetOutput())
10          opt.ShallowCopy(inp.VTKObject)
11          print self.Factor
12          opt.PointData.append(inp.PointData['Normals'][:, 0] * self.Factor, 'Normals-x')
13          opt.PointData.SetActiveScalars('Normals-x')
14  
15      def SetFactor(self, factor):
16          self.__Factor = factor
17          self.__Algorithm().Modified()
18  
19      def GetFactor(self):
20          return self.__Factor
21  
22      Factor = property(GetFactor, SetFactor)
23  
24  pf = vtk.vtkProgrammableFilter()
25  pf.SetInputConnection(s.GetOutputPort())
26  alg = MyAlgorithm(pf)
27  pf.SetExecuteMethod(alg)
28  pf.Update()
29  
30  alg.Factor = 2
31  
32  renWin = create_scene(pf)
33  renWin.Render()

Here I added a Factor property to allow the configuration of the filter. In this example, pf will execute twice: the first when Update() is called (with Factor == 1), second during render (with Factor == 2). Note that Modified() on line 17 is necessary for this to work.

vtkProgrammableFilter works great for many use cases but has some limitations:

  • 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. We'll discuss how this can be done using vtkPythonAlgorithm in a future blog.
  • The source counterpart of vtkProgrammableFilter, vtkProgrammableSource, is very difficult to use and is very limited. So if you want to create sources (readers for example), I strongly recommend using vtkPythonAlgorithm.

I'll follow up with one or more blogs on vtkPythonAlgorithm.

One final note. The callable objects also work nicely as observers. See below.

import vtk

class MyObserver(object):
    def __call__(self, obj, event):
        print obj.GetClassName(), event

s = vtk.vtkSphereSource()
s.AddObserver('ModifiedEvent', MyObserver())
s.Modified()

will print

vtkSphereSource ModifiedEvent

Note: This article was originally published on the Kitware blog. Please see the Kitware web site, the VTK web site and the ParaView web site for more information.