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.
import time, vtk from vtk.numpy_interface import dataset_adapter as dsa from vtk.numpy_interface import algorithms as alg def create_scene(flt): m = vtk.vtkPolyDataMapper() m.SetInputConnection(flt.GetOutputPort()) a = vtk.vtkActor() a.SetMapper(m) ren = vtk.vtkRenderer() ren.AddActor(a) renWin = vtk.vtkRenderWindow() renWin.AddRenderer(ren) renWin.SetSize(600, 600) return renWin s = vtk.vtkSphereSource() 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') pf = vtk.vtkProgrammableFilter() pf.SetInputConnection(s.GetOutputPort()) pf.SetExecuteMethod(execute) renWin = create_scene(pf) renWin.Render() 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:
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.
class MyAlgorithm(object): def __init__(self, algorithm): import weakref self.__Algorithm = weakref.ref(algorithm) self.__Factor = 1 def __call__(self): inp = dsa.WrapDataObject(self.__Algorithm().GetInput()) opt = dsa.WrapDataObject(self.__Algorithm().GetOutput()) opt.ShallowCopy(inp.VTKObject) print self.Factor opt.PointData.append(inp.PointData['Normals'][:, 0] * self.Factor, 'Normals-x') opt.PointData.SetActiveScalars('Normals-x') def SetFactor(self, factor): self.__Factor = factor self.__Algorithm().Modified() def GetFactor(self): return self.__Factor Factor = property(GetFactor, SetFactor) pf = vtk.vtkProgrammableFilter() pf.SetInputConnection(s.GetOutputPort()) alg = MyAlgorithm(pf) pf.SetExecuteMethod(alg) pf.Update() alg.Factor = 2 renWin = create_scene(pf) 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 avtkPolyData
) using it. * It is not possible to properly manipulate pipeline execution by setting keys. We’ll discuss how this can be done usingvtkPythonAlgorithm
in a future blog. * The source counterpart ofvtkProgrammableFilter
,vtkProgrammableSource
, is very difficult to use and is very limited. So if you want to create sources (readers for example), I strongly recommend usingvtkPythonAlgorithm
.
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