Scientific visualisations using Python and the VTK library.

Author: | Published: | Modified: 23 April 2019 | Permalink



OR



This chapter provides a high level overview of the VTK library. You will learn the basic ideas necessary to use VTK, including: an understanding of the VTK pipeline; the important objects used to read and filter data; then, how to arrange and render models. Later chapters will expand more on visualising scalar and tensor fields, manipulating meshes and 3D geometry, and adding interactivity to your visualisation.

Contents

High-level overview

VTK is implemented in C++, but has been designed to allow bindings to a several other languages. This means that there are several languages from which you can use VTK. These include Java, Tcl, and of course Python. You get the performance of C++, since it is still C++ driving the visualisation, but the ease of using you preferred language—​in this case Python. VTK provides a set of tools to read almost any kind of scientific data, in any sort of format/structure, filter that data, then render it in a 3D graphical environment.

Installation

VTK can be installed by a single pip command:

pip install vtk

and only requires a single import statement to use the library from within Python:

import vtk

The VTK pipeline

The key to using VTK is to construct a pipeline. Most applications will go through similar stages when creating their pipeline. In approximately 4 general steps these are:

  • Reading data: A visualisation needs data to visualise!

  • Filtering and processing data: Once data is read into the pipeline it often needs further processing and manipulation before it can be packaged into an actor.

  • Creating actors from that data and assembling them in a scene: Data and geometry are packaged into an actor (don’t worry, more on this will follow below).

  • Assembling pieces of geometry into a final render: The actors described above are positioned and an interactive 3D environment is rendered.

You can probably tell by now that VTK uses the metaphor of a theatre/film set when describing the rendering environment. The goal of the early stages of a VTK pipeline is to read data—​including information about the geometry of a model—​and package it into an actor. An actor is an instance of an object that includes all of the information necessary for rendering (how to do this will be explained below). These actors will be arranged in a scene by you and rendered. The scene created by VTK is is interactive. The user will be able to move around and rotate the objects. It is possible, if programmed into the visualisation, to cut, move, and otherwise manipulate these actors to get at precisely the part of the geometry that is of most interest to the user.

The VTK visualisation and rendering pipelines.
Figure 1. The VTK visualisation and rendering pipelines.

A basic outline of this VTK pipeline is shown above. Each step in the flowchart will correspond to at least one and possibly several objects instantiated from the VTK library. In that way Figure 1 maps more nicely to the actual pipeline that you need to construct and to the objects you need to create than the list just above in this section.

The VTK documentation distinguishes between what it calls the visualisation pipeline and the rendering pipeline. This distinction made in VTK’s documentation is somewhat of a misnomer as rendering is an important step in creating a visualisation. Once an actor is created the focus is on rendering. When the terms visualisation pipeline or rendering pipeline are used they refer to those sections shown in Figure 1. The term VTK pipeline will refer to the entire pipeline. Otherwise, visualisation will refer in a general sense to every part of the process of visualising our data.

Chaining together stages in the visualisation pipeline

To further explain Figure 1 we begin with data sources. There will be more on data in the next section, but for now we note that every visualisation must start with some source of data that is read and processed by the visualisation. There are many formats that are already implemented in VTK. Once data has been read it is then connected to the next stage of the pipeline. It is this process of connecting filters together into a pipeline that is central to understanding VTK and how it works.

A filter is an algorithm that transforms data in some way. Their use is optional, but you will likely use several of them when constructing your visualisation pipeline. Once you have your data you connect it to the next step in the pipeline using the following idiom of VTK: filter2.SetInputConnection(filter1.GetOutputPort()). Here the output of filter1 is connected to the input of filter2. These filters are chained together like this to create the pipeline.

Lazy updating

When filters are chained together as shown just above they do not actually process any data at that step. Instead, what is happening is only that these filters are only being connected. Their computations on data only take place when a request is made from further down the pipeline, usually at the moment the visualisation is rendered. Evaluation is lazy, so it only takes place when needed. When an update is called, the update moves back up the pipeline, then processed data moves back down towards the rendering steps of the pipeline.

This way of chaining filters together with lazy evaluation is the recommended way to create a pipeline. Some objects have a method .SetInputData(data) which reads a source of input data once and does not automatically update if changes are made to the data source. This should not be used if that data is ever expected to be changed.

Example: Rendering a simple glyph

cone
Figure 2. Render of a single cone.

Let’s get started with a simple example. The following is the VTK version of, "hello world." We will render a single cone in 3D. Although simple, it will step through creating a rendering pipeline. Once understood it becomes very straightforward to then create much more complex scenes.

Scientific visualisation usually takes data collected from the real world. In this case we will use one of the built-in geometries available in VTK. These are simple geometric shapes: arrows, cylinders, cubes, etc. which can be useful when representing data. Their names follow a convention: vtkXXXXSource where XXXX is the name of the shape, i.e., Cone, Arrow, Cylinder, etc. In this case our data source a cone.

cone = vtk.vtkConeSource()

The cone is described implicitly. We can set some parameters to the cone by calling its associated methods. This can include .SetRadius(radius) or .SetResolution(facets) which set the radius of the cone and the number of facets that are used to represent the cone.

To use geometry data like a polygon mesh we need to use this data to create an instance of a vtkActor. To get to there requires at least one intermediate step. We create a mapper from our geometry, then connect that mapper to the actor.

Here the output of a cone is connected to a mapper to continue the pipeline.

mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cone.GetOutputPort())

The idiom above was discussed in the previous section on the VTK pipeline. The output of cone is connected to the input of mapper. An instance of the vtkPolyDataMapper class requires a connection from an object that outputs unstructured polygon data. Such an object could be an instance of vtkPolyData which stores each point and cell or in this case vtkConeSource where the geometry is generated procedurally from a small number of parameters. Alternatively, the output of cone could be connected to a filter, with the output of the filter connected to the mapper. No filters are used in this example.

actor = vtk.vtkActor()
actor.SetMapper(mapper)

The above is a little different to the previous code listing. A vtkActor has a mapper set using a different syntax. In VTK the actor is considered the end of the visualisation pipeline (see above regarding the VTK pipeline). From here we are building the rendering pipeline.

Mappers and actors

Creating a mapper is an intermediate step in the visualisation pipeline. It takes data and eeds into an actor. Its role is to map data into graphics primitives. For polygon meshes this is more straightforward than other types of data. It also includes making settings about how data is converted to colours (see lookup tables below), or whether to use cell data or node data to colour a model. The actor is concerned with how the model fits into the scene. As a result parameters that can be set from within the actor often concern texturing, lighting, positioning etc. of the object within the scene.

The final steps to complete the visualisation are the following.

window = vtk.vtkRenderWindow()
# Sets the pixel width, length of the window.
window.SetSize(500, 500)

interactor = vtk.vtkRenderWindowInteractor()
interactor.SetRenderWindow(window)

renderer = vtk.vtkRenderer()
window.AddRenderer(renderer)

renderer.AddActor(actor)
# Setting the background to blue.
renderer.SetBackground(0.1, 0.1, 0.4)

window.Render()
interactor.Start()

The default for a VTK render is that some degree of interactivity is enabled by default. This we get for free whereas further interactivity needs to be programmed into the visualisation. When the user interacts with the visualisation doing things like clicking/moving the mouse, pressing keys, etc. these events are handled by the interactor. Different interactor settings can be used to change how the visualisation responds to particular user events. Here we used the common vtkRenderWindowInteractor. Other interactor instances have different behaviours and these can always be modified. An interactor needs to be connected to a window using the .SetRenderWindow() method.

Each VTK visualisation requires a window and each window requires at least one renderer (although multiple renderers per window are possible). The window is represented by the vtkRenderWindow instance. A renderer can be considered as a separate scene. Each can have different actors, camera positions, lighting etc. A renderer is connected to a window by the .AddRenderer(renderer) method. By default the entire window area will feature the rendering from this renderer. It is however possible to set multiple renderers to draw in different regions of the window. If multiple renderers are used, a rectangle must be specified which describes the portion of the window on which the renderer will draw. This is controlled using the .SetViewport( (x0, y0, x1, y1) ) (see more).

The background colour for the renderer is set using the .SetBackground() method. The final steps are to initialise the visualisation.

Data in VTK

The VTK pipeline begins with one or many data sources. Most often, this data is the result of experiments, calculations, or other measurements from the real world. There are many different data structures that VTK offers which you can use to store your data. They have advantages with how much—​or little—​information needs to be provided to completely describe your data.

image grid 2
Figure 3. Illustration of data structured in 4 different ways. Beginning from the top-left, moving clock-wise: a structured image format, rectilinear grid, structured grid, and completely unstructured polygonal data.

VTK can read and display numerical data with almost any kind of structure. Figure 3 shows data with 4 different structures. In order of most to least structure, these are image, rectilinear grid, structured grid, and polygonal data. The more structured that your data is the less information that needs to be supplied to VTK to completely represent that data. For example, to fully describe image data you only need to provide:

  • The origin of the grid. This is a position vector that points to the corner of the grid with the lowest x, y, z coordinate values. These are 3 scalar values.

  • The pitch between cells: This is a single scalar value and it describes the size (width) of all of the cells.

  • The dimensions of the image data. These are 3 scalar values which describe how many image cells are in each direction. For l × m × n image data these three values would need to be specified. For 1D and 2D image data a value of 1 is signifies that the corresponding dimension is not used. For example, an l × m sized image would use n = 1.

The rectilinear grid requires more information to fully define it. The same information as an image is required, along with the pitch between each row of nodes. If you look at the rectilinear grid from Figure 3 you notice that cell edges are aligned, but that the width of any cell is different for each row. This distance must be specified for each row. This is an additional scalar value for each row in each dimension. The amount of information then increases further for the other data structures.

The type of data structure to use depends on the data that it will represent. Ideally it is the most structured type of data which fully represents the real life data/measurements. Image data from a camera could be represented by a VTK image object, whereas a polygon mesh imported from a CAD programme would need to be represented by unstructured polygon data. The VTK image object is too rigid to model a polygon mesh.

Using and creating polydata

In the previous example a polygon mesh was created from the cone source object. This was put through the pipeline and rendered without any further modification. In the following example a simple polygon mesh is created by manually specifying the location of each point then the connectivity between points to create a cell. Each mesh contains just one triangular cell. Data values are assigned to the points in one mesh. Values are assigned to the cell in the other mesh.

tri diagram
Figure 4. Diagram of the single triangle cell created in this example.

Scientific visualisation will usually involve some kind of 3D geometry. In mechanical engineering this might be the geometry of a metal structure that is the subject of finite element analysis or the geometry of bones or organs visualised using MRI. Together with the geometry is data that is layered on that geometry. A heatmap could show the temperature around a component, a stress field on a structure could show the distribution of mechanical stress on that same component. VTK can visualise scalar or higher order tensor data. VTK lets you create an array of values that map to either the points or cells in a mesh.

points = vtk.vtkPoints()
cell = vtk.vtkCellArray()
mesh_1 = vtk.vtkPolyData()
mesh_2 = vtk.vtkPolyData()
tri = vtk.vtkTriangle()

Above we see the objects that are used to generate the polygon mesh geometry. These are composed together to create the final mesh. The result will be two vtkPolyData meshes of a single triangle, where the geometry of that triangle is referenced by both meshes.

# First point with default ID of 0.
points.InsertNextPoint( (0.0, 0.0, 0.0) )
points.InsertNextPoint( (0.0, 1.0, 0.0) )
points.InsertNextPoint( (1.0, 0.0, 0.0) )
Memory management in VTK

It was noted earlier that VTK is implemented in C++ but we are using from Python through bindings. Language bindings allow access to code written in one language from another. In Python memory is managed automatically and so may have never needed to worry about it in previous Python code. Despite being written in C++ VTK memory management is usually hidden from you when you use the library from Python.

Above we see an instance of vtkPoints with data appended via the .InsertNextPoint() method. All points are also allocated an ID number incrementally from 0 in the order in which they were inserted. An alternative is to use the method .SetNumberOfPoints(numPoints) where numPoints is the number of points that will be stored. Individual points are then set using .SetPoint(id, (x, y, z) ). Importantly id cannot exceed 1 less than the value used as numPoints in the previous command; in other words, the largest allowable value for id is`numPoints` - 1. Exceeding the maximum allowable ID number or setting a point .SetPoint() before memory is first allocated by first calling .SetNumberOfPoints() will cause errors.

When .InsertNextPoint() is called, new memory in RAM is allocated for the new point. Similarly, the space necessary to store numPoints is allocated when .SetNumberOfPoints() is called. Other classes are similar in that the dimensions of a structure must first be specified. Then individual items can be added to that structure, up to the number that was specified. If values are appended their ID is automatically set.

A triangle has 3 points. It is necessary to match global point IDs to the three points of the triangle. Points are matched using .GetPointIds().SetId(i, i) where the first instance of i is either 0, 1, or 2 and specify the triangle’s local point ID. The second i can be any global point ID. These correspond to the IDs that were automatically allocated to nodes using .InsertNextPoint() or explicitly when calling .SetPoint() on an instance of vtkPoints.

for i in range(3):
    tri.GetPointIds().SetId(i, i)

cell.InsertNextCell(tri)

In this case there is only 1 cell and 3 points in the entire mesh so this is simple. Objects in VTK are often composed of many smaller objects. Parameters/properties are often set by getting the relevant object in which the parameter is set, then setting that parameter. Code like the above is common when using VTK. Here .GetPointIds() returns a vtkIdList object which then calls its .SetId() method. It is noted that polygons are oriented. Orientation is determined by the right-hand rule and the match between the 3 local point IDs of the element and the global point IDs.

Points and cells are then set to a mesh.

mesh_1.SetPoints(points)
mesh_1.SetPolys(cell)

As stated, data for a polygon mesh can be set at the mesh’s points or cells. Below we set a single scalar value to the cell data for mesh_1. These scalar values are stored as double precision floating point values, which are suitable for most cases.

cell_data = vtk.vtkDoubleArray()
cell_data.SetNumberOfComponents(1)
cell_data.InsertNextTuple([0.5])
mesh_1.GetCellData().SetScalars(cell_data)

In the above the array is first created, then the number of components are set. Regular scalar data will have 1 component, vectors will use 3 components and higher order tensors will use more. Data, even for scalars, is entered using a list. To enter vector quantities with three components the number of components needs to be set to 3 with .SetNumberOfComponents(1) and the one component list is replaced with with a list of three quantities.

The last line assigns the data to the mesh itself. The order in which data is inserted into the vtkDoubleArray corresponds to how that data will line up with to each cell in a mesh. Here the first (and only) tuple value added into the array corresponds with the first (and only) cell added to mesh_1. The process shown below is similar, but is applied to the three points in the triangle and sets point data.

point_data.InsertNextTuple([0.0])
point_data.InsertNextTuple([1.0])
point_data.InsertNextTuple([0.5])
mesh_2.GetPointData().SetScalars(point_data)

The rest of this code example is boilerplate that remains essentially unchanged from the previous example with the exception of the construction of the lookuptable. Lookuptables are discussed later in this chapter. A lookuptable is used to decide which colour to use to represent a given numerical value.

2 tri
Figure 5. Two triangle polygons with cell data (left) and point data (right) connected to the mesh.

From Figure 5 we see the single coloured cell on the left. Here the cell’s scalar value maps via the lookuptable to a single colour. On the right the cell is filled with linear gradients of colours. Colours are selected using the scalar values at the nodes, then gradients between these fill the contents of the cell.

Lookup tables

In the last section a lookup table was used to colour polygons based on cell and node data. The precise workings lookup tables are described in more detail here. This section assumes a solid understanding of the formats for specifying colours. You may want to read the section at the end of this chapter with more information on this.

Colour can be very useful in visualisation. It can communicate information across a large area in a way that is very intuitive. To set a single colour for an actor, you can use the actor.GetProperty().SetColor(r, g, b) command. This will set the entire model to a single colour defined by the (r, g, b) values provided. In visualisations however you will want to draw objects in varying colours which serve to communicate information about the data associated with the model. In VTK the lookup table is the object that maps scalar values to colours. The analyst modifies the lookup table to adjust the colour scheme or otherwise control how scalar values are mapped to colours to represent data.

A lookup table for polygon data is created using lut = vtk.vtkPolyDataMapper(). The lookup table is associated with a model through the mapper object. The method .SetLookupTable(lut) connects the lookup table lut to the mapper. Further, the method .SetUseLookupTableScalarRange(True) needs to be called on the mapper. This causes the mapper to use the same range as that used by the lookup table. Not using this method will cause the mapper to override the range set in the lookup table which is probably not what you want.

No matter the parameters used in a lookup table it is necessary to call the .Build() method. This updates the lookup table to reflect the updated settings.

Below I will discuss different ways of setting up lookup tables to achieve different results. At the end of this section there is an example on a simple mesh with 4 triangle elements. Each of the techniques are applied to visualise this same mesh in 4 different ways.

Default lookup tables

By default a lookup table will use the rainbow range of colours that often appear on scientific visualisations. These colour are approximately red, yellow, green, aqua, blue, with red mapped to the lower end of the table’s range and blue at the upper end.

The range for a lookup table is set with the method .SetTableRange(low, high). For values above or below this range the default setting is that they will be shown with the same colour as the nearest value within the range, e.g., a value of 400.0 for a table with range [500, 600] would have the same colour as a value of 500.0. It is possible to set custom colours for values outside the range using the methods .SetAboveRangeColor(colour) and .SetBelowRangeColor(colour). Here colour is a list of 4 values, the usual 3 RGB values plus an alpha channel. No matter whether one or both ofthese two methods have been called, it is possible to control whether this feature—​using a custom colour for values outside the range—​is activated with these 4 methods: .UseBelowRangeColorOn(), .UseBelowRangeColorOff(), .UseAboveRangeColorOn(), .UseAboveRangeColorOff(). The feature is off by default and so must be activated.

Specifying custom colour ranges when creating gradients

Although the default rainbow colour scheme is useful in many applications it is possible to control how colours are mapped. The range for hue, saturation, or value can be constrained to control how data is mapped to a colour. The methods for this are .SetHueRange(low, high), .SetSaturationRange(low, high), and .SetValueRange(low, high). If low and high are the same value the corresponding parameter is fixed and does not vary with the input data.

Different effects can be created by fixing some of the 3 parameters of the HSV model while allowing others to vary with input data. The default is that the saturation and value are fixed at the maximum of 1.0 while the hue varies with input data. The result is the rainbow colour described above. This can be changed. The example below creates a monochrome colour map. It fixes the hue at an arbitrary value, fixes the saturation at 0.0, while allowing the value to vary. This creates a map that varies from brighter to darker with input data.

lut.SetHueRange(0.5, 0.5)
lut.SetSaturationRange(0.0, 0.0)
lut.SetValueRange(0.25, 1.0)

Creating arbitrary colour mappings

The previous examples have mostly relied on the default lookup table provided by VTK, while tweaking some of its settings. The first was the default rainbow lookup table. The second modified the ranges of the 3 HSV parameters to provide different effects. VTK allows complete control over the lookup table to allow any particular mapping of values to colours that you wish to specify. This means individually specifying each mapping from value → colour.

Each lookup table has a fixed number of colours. This is set by the method .SetNumberOfColors(number) for an integer value number. The default is 256. This maps 256 colours to the range of values specified by .SetTableRange(low, high). Values are equally distant and chosen to provide the default rainbow visualisation. Colours do not necessarily need to be set in such a way that the smooth gradient effect is created.

Once the number of colours in a lookup table has been set with .SetNumberOfColors(number) each individual mapping can be set with the method .SetTableValue(id, R, G, B, A). The parameters R, G, and B specify the colour to map to. The alpha channel is specified by A. The parameter A is optional and will default to 1.0 if not specified, i.e., .SetTableValue() is called with just 4 parameters. The id is an integer that specifies where within the range this colour sits. If \$N\$ colours are used in a table then id varies from 0 to \$(N-1)\$. A value at the bottom of the range maps to the colour with id 0; a value in the centre of the range will map to the colour with id of approximately \$\frac{N}{2}\$; and, a value at the top end of the range will map to the colour with id \$N-1\$.

Displaying categorical data

The ability to set arbitrary mappings between values and colours can be used to represent categorical information. An example could be to visualise a heat simulation of a model. Elements with temperatures above or below a melting point could be visualised with blue/red to show where melting could occur. The following method can be used to categorise cells and visualise each cell’s membership in their allocated category.

For \$N\$ categories of data we set .SetNumberOfColors(N). Each category is then assigned a floating point value beginning from 0.0 and increasing each by \$frac{1}{N-1}\$, e.g., for 4 categories these would be values of 0.0, 0.3333, 0.6666, and 1.0. If we allocate these categories an integer category number, cat_no, then the first category has cat_no = 0 and all cells of this category have a scalar cell value of 0.0, the second category has cat_no = 1 and cell value of 0.3333, and so on. Each category is then set its own colour using .SetTableValue(cat_no, R, G, B).

Creating custom colour gradients

The previous examples have shown how colour gradients could be controlled by manipulating the ranges used across the HSV colour model when mapping data. The colours and their order is fixed. These are approximately red, yellow, green, aqua, blue. We can cut off this range of colours at either end by specifying a range for the hue, as we did before, but we couldn’t select an arbitrary order in which these colours vary with input values. VTK does not provide a convenient facility to simply specify several colours and to then have VTK create smooth gradients between those colours. For example, we can’t specify, green, then yellow, then aqua, as the colours across the range of the lookup table and have VTK automatically create smooth colour gradients between these colours. To achieve such an effect we need to specify each individual colour in the lookup table.

VTK provides an object, a vtkColorTransferFunction, to achieve what we need. We use the method .AddRGBPoint(value, R, G, B) which takes 4 parameters. These are the value and a colour, specified using RGB values. Once these value-colour pairs have been set the method .GetColor(value) can be queried. For an arbitrary value it returns a colour which has been interpolated from the colours with the nearest value. The example below is a vtkColorTransferFunction with yellow (RGB: 1.0, 1.0, 0.0) set to the value 0.0 and red (RGB: 1.0, 0.0, 0.0) set to 1.0. The colour orange (RGB: 1.0, 0.5, 0.0) is the colour half way between these two. Its RGB values are correctly returned when .GetColor(value) is called with a value of 0.5.

ctransfer = vtk.vtkColorTransferFunction()
ctransfer.AddRGBPoint(0.0, 1.0, 1.0, 0.0) # Yellow
ctransfer.AddRGBPoint(1.0, 1.0, 0.0, 0.0) # Red
# Correctly outputs the colour orange.
ctransfer.GetColor(0.5) # (1.0, 0.5, 0.0)

Once we create the vtkColorTransferFunction we need to set each individual colour in the lookup table. To create smooth gradients we need enough colours in the lookup table. The default number of colours is 256 and is usually sufficient but you can use a larger number if necessary. Below, the number of colours is set to N and the lookup table has a range of [a, b]. The equation \$i \times \frac{b-a}{N}\$ gives even stepped values from a to b over N.

lut = vtk.vtkLookupTable()
lut.SetTableRange(a, b)
for i in range(N):
    new_colour = ctransfer.GetColor( (i * ((b-a)/N) ) )
    lut.SetTableValue(i, *new_colour)
lut.Build()

Example: Creating custom lookup tables

An example mesh of 4 elements and 5 nodes is used to display different visualisations of the same data with different lookup tables. The mesh has both scalar cell and node data. Its values are shown in Figure 6. If both types of data are present in the model the default in VTK is to visualise the node data.

Example mesh.
Figure 6. A mesh with 4 elements and 5 nodes. Scalar node data shown in square brackets [ ] along with the node number outside of the brackets. Scalar cell data is shown in curly brackets { }. Node labels start in the bottom-left and increase, moving counter-clockwise.

Four different lookup tables were created to visualise the data shown in Figure 6. The final results are shown in Figure 7. They are discussed below in the same order in which they are described in the caption to Figure 7. Lookup tables in the code listings are named lut_n where n is the number of the lookup table. Similarly the corresponding mappers and other objects are numbered correspondingly. Only one mesh, an instance of vtkPolyData is created. The difference in visualisations occurs because each mapper is linked to their corresponding lookup table. The ranges were set using the following:

lut_1.SetTableRange(5.0, 7.5)
lut_2.SetTableRange(5.0, 7.5)
lut_3.SetTableRange(5.0, 7.5)
lut_4.SetTableRange(0.0, 1.0)

The first three lookup tables visualise point data using smooth gradients whereas the fourth is used to visualise category data stored for the cells. For that reason it has a different range. The first lookup table was kept with the default settings. Only the .Build() method was called before being assigned to the corresponding mapper.

lut_1.Build()
mesh_mapper_1.SetLookupTable(lut_1)

The second lookup table modifies the HSV colour values to create a single coloured gradient in which the magnitude of V in the HSV colour description varies, while the hue and saturation are fixed. The result is that darker and lighter regions are created which corresponded with lower and higher scalar data values. This method should be used with caution with 3D models. In 3D shading is also used when rendering a model in a way that is unrelated to mesh data so this particular colouring technique should be used with caution.

lut_2.SetHueRange(0.5, 0.5)
lut_2.SetSaturationRange(1.0, 1.0)
lut_2.SetValueRange(0.25, 1.0)
lut_2.Build()
mesh_mapper_2.SetLookupTable(lut_2)

The third lookup table creates a custom combination of colours from which gradients are created when building the lookup table. This is created using the method described above. Three colours are selected and they are set to reflect values at the low, middle, and high end of the value range. A vtkColorTransferFunction is used to interpolate the colour values to create a gradient between these three points.

no_of_colours = 256
lut_3.SetNumberOfColors(no_of_colours)

ctransfer = vtk.vtkColorTransferFunction()
ctransfer.AddRGBPoint(0.0, 1.0, 0.25, 0.0) # Orange
ctransfer.AddRGBPoint(0.5, 1.0, 0.00, 0.0) # Red
ctransfer.AddRGBPoint(1.0, 1.0, 0.95, 0.95) # White

for i in range(no_of_colours):
    new_colour = ctransfer.GetColor(i / float(no_of_colours))
    lut_3.SetTableValue(i, *new_colour)

lut_3.Build()
mesh_mapper_3.SetLookupTable(lut_3)

The final lookup table uses 3 custom colours to display membership of each cell in one of 4 categories. The number of colours is set to 3 with lut_4.SetNumberOfColors(3) and each of the 3 colours to use are set individually. The table range [0.0, 1.0] was set earlier. This means that to set a cell’s colour to red, amber, or green its value needs to be set to 0.0, 0.5, or 1.0. Note from Figure 6 that each cell has one of these values as its cell data. After the lookup table is set to the corresponding mapper the method .SetScalarModeToUseCellData() is used. As the mesh includes both cell and point data VTK will default to using point data for visualisation. This method needs to be called. The corresponding method for point data is .SetScalarModeToUsePointData() however as point data is the default the method was never for the previous lookup tables.

lut_4.SetNumberOfColors(3)
lut_4.SetTableValue(0, 1.0, 0.0, 0.0) # Red
lut_4.SetTableValue(1, 1.0, 0.75, 0.0) # Amber
lut_4.SetTableValue(2, 0.0, 1.0, 0.0) # Green

lut_4.Build()
mesh_mapper_4.SetLookupTable(lut_4)
mesh_mapper_4.SetScalarModeToUseCellData()

An instance of vtkScalarBarActor was added to each renderer. They help to annotate a visualisation by showing colours and the values to which they correspond. For categorical information the cell value is used by our code to assign membership to a category, but the actual value itself is not useful to the user looking at the visualisation. The default labels were removed from the scalar bar by setting their number to 0 using the method .SetNumberOfLabels(0) on the vtkScalarBarActor bar_4. Annotations for each category were set in the lookup table using the lookup table method .SetAnnotation(value, annotation).

lut_4.SetAnnotation(0.0, "Red")
lut_4.SetAnnotation(0.5, "Amber")
lut_4.SetAnnotation(1.0, "Green")

Bars are assicated with a corresponding lookup table using bar.SetLookupTable(lut) and added to a renderer as an actor. Being a 2D actor they are added using the method .AddActor2D(actor). More discussion of the 2D canvas and 2D actors will feature in a later chapter.

All 4 results are shown in Figure 7.

lookup tables
Figure 7. Visualisations created from the mesh in Figure 6. Results from the top-left going clockwise are: the default lookup table; fixing the hue and saturation in the HSV model, with the value allowed to vary; a custom colour gradient created by interpolating colours between arbitrarily selected colours; and, cell data used to categorise elements into three categories.

Transformations

There are several ways in which VTK can transform models in a scene. A polygon mesh will have its own local origin for that mesh. There is also an origin for the entire scene being constructed by the analyst. When an actor is added to a scene using the .AddActor(actor) method of a vtkRenderer the mesh referenced by the actor is added to the scene. The mesh is placed such that the local origin of the mesh is coincident with the global origin of the assembled scene with local and global axes algined.

If you want to move an model you can modify the polygon data itself. If you do this you will be modifying the data used by every object that references that data. Previous examples have shown that it is possible to create one instance of your geometry, then have multiple mappers refer back to that same data. If the data is then modified, all references to that data will be modified. Instead, each actor allows you to define a transformation that describes how a model is moved around a scene. There are several ways to define these transformations. Below we discuss how this is done by calling methods on vtkActor. Two other methods are described in later chapters which involve defining a vtkTransform object which is referenced by that actor. The method described here is simpler and suitable for most cases.

An actor has several methods to define transformations. We are interested in the 3 operations to translate, scale, and, rotate the actor.

Moving an Actor

The .SetPosition(x, y, z) method of an actor will move the actor such that the local origin of the actor is moved to the position specified by x, y, and z. The current position of an actor is available via the .GetPosition() method. If you want to move an actor relative to its current postion, rather than to an absolute position you could use .GetPosition(), add to these returned x, y, z coordinates, then call .SetPosition(x, y, z) however VTK also offers the .AddPosition(x, y, z) method which performs all of this from a single method.

Scaling an Actor

The scale of an actor is set through the .SetScale() method. It can take either 1 or 3 parameters. A single scalar value will scale the actor equally in all directions, e.g., use .SetScale(3.0) to triple the size of the model equally in all directions. To scale the model in its width and depth but not height (remember VTK uses the Y axis as its vertical direction) you can use .SetScale(x, 1, x) for a scale factor x. Below the actor is first scaled by a factor of 5 in each direction, then scaled by factors of 4, 2, and 3, in the x, y, and z directions.

factor = 5
actor.SetScale(factor)
factor = [4.0, 2.0, 3.0]
actor.SetScale(factor)
Rotating an Actor

Actors can be rotated using the three methods: .RotateX(theta), .RotateY(theta), and .RotateZ(theta). Here theta is the angle in degrees by which the model is rotated. These angles are relative to the model’s local coordinate system, not the global coordinate system.

A note about ordering

It is important to consider the order in which rotations are applied. The same rotations performed in a different order will likely provide a different orientation. Translations occur in the global coordinate system, scale factors and rotations are relative to the local coordinate system of a model. You need to consider not only that rotations in general or non-commutative (that means that they may produce different results if performed in a different order) but the coordinate system in which rotations are defined changes with rotations. So, calling .RotateX(theta) actually specifies a rotation around a different axis depending on the rotations that have proceeded it.

Together with the previous point is that there are multiple ways to rotate an object to arrive at a particular orientation. Actors have a .GetOrientation() method which returns the current orientation of the model. These values are specified such that an actor would arrive at its current orientation if rotations are performed in the order of z, x, then y. Similarly, the method .AddOrientation(x, y, z) is equivalent to performing .RotateZ(z), .RotateX(x), and .RotateY(y) in that order.

Example: A 3D plotting script

Many of the smaller skills that were developed earlier in this chapter are brought together in this example of a 3D plotting application. The VTK library often provides several ways to achieve the same result. There are likely many ways to develop this example. The methods used here are chosen because they build on skills developed above. The end result is a small application that can plot the equation for an arbitrary surface in 3D. Whereas this displays a more abstract surface defined by an equation, rather than a real life object or geometry—​something for which VTK is very suitable—​it is an example of a useful and complete application created using only the skills developed above.

The first step is to provide the necessary input parameters for plotting a surface. Of course, the first of these parameters is the function itself which will be plotted. The function is func_to_calculate(x, y). It can be any valid function that returns the Z value when given X and Y coordinates.

x0 = -7 # X domain low end
y0 = -7 # Y domain low end
x_max = 7 # X domain high end
y_max = 7 # Y domain high end
X = 100 # Resolution in x direction. Integer
Y = 100 # Resolution in y direction. Integer
A diagram of the input parameters
Figure 8. A diagram of the input parameters and the dimensions of the output surface to which they correspond.

The next input parameters are listed above. They describe the domain of the function to plot and the numerical resolution with which that plot is made. These parameters are also shown in [mesh_grid_with_origin_figure]. The 4 variables which specify the domain are x0, x_max, y_0, and y_max. They describe the lower and upper end of the domain in the \$x\$ and \$y\$ directions.

In the code listing for this example there is no error checking, but a more polished example would check these parameters to ensure they were correct. This could include ensuring that x_max were larger than x0 among other checks. In fact, a more polished example would find a more elegant way to input these parameters than entering them at the top of the Python file! The parameters X and Y specify the number of cells to use in their respective directions. With these details we can calculate the \$x\$ and \$y\$ dimensions of each cell.

deltax = (x_max - x0) / float(X)
deltay = (y_max - y0) / float(Y)

The use of float() is not strictly necessary, but is useful because Python version 2 throws away remainders if division is performed on int (integer) only values. This function forces floating point division and avoids this issue.

Just as for previous examples, we create instances of vtkPoints and vtkCellArray in which we store cells and points to define our surface.

points = vtk.vtkPoints()
cells = vtk.vtkCellArray()

We need to calculate all the points in the mesh. The surface is defined by quad cells with an even pitch in the x and y direction. For the x and y directions there is one more row of points than cells. As X and Y contain the number of cells across each dimension we loop over X+1 and Y+1 points in each dimension.

zvals = []
for j in range(Y+1):
    for i in range(X+1):
        x = x0 + deltax*i
        y = y0 + deltay*j

        z_val = func_to_calculate(x, y)
        zvals.append(z_val)
        coord = x, y, z_val
        points.InsertNextPoint(coord)

In the above the Z values are stored in a list zvals for use later in the code as well as in the coordinate point using the .InsertNextPoint(coord) method. It is also more convenient that these be stored in native Python data structures. Note that the .InsertNextPoint() and .append() methods are called in the same loop. The same global ID of the point, set implicitly when .InsertNextPoint() was called, can also be used to index zvals to get the same Z value. This is used later when creating the scalar point data that is added to the mesh.

for j in range(Y):
    for i in range(X):
        quad = vtk.vtkQuad()
        corner_id = get_id(i, j)

        quad.GetPointIds().SetId(0, corner_id)
        quad.GetPointIds().SetId(1, corner_id + 1)
        quad.GetPointIds().SetId(2, corner_id + (X+2))
        quad.GetPointIds().SetId(3, corner_id + (X+1))
        cells.InsertNextCell(quad)
arbitrary cell
Figure 9. The global point IDs of an arbitrary cell. The point on the lowest corner has an ID of N and there are X cells across the x-axis.

The next step is to create the vtkQuad elements in the polygon mesh. These elements have four local points with ID 0, 1, 2, and 3 which need to be associated with 4 global point IDs. Point IDs were assigned in the order in which a point was inserted using points.InsertNextPoint(coord). If a 2-dimensional loop as that shown in the code listing above is used, then the ID, \$N\$, of the node with the lowest \$x\$ and \$y\$ coordinate (see [arbitrary_cell]) is given by \$i + j(X+1)\$, where X is the input parameter provided. With \$N\$ the other 3 point IDs are straightforward to determine. With our points and cells created they are added to the mesh.

mesh = vtk.vtkPolyData()
mesh.SetPoints(points)
mesh.SetPolys(cells)

The point data is placed in a vtkDoubleArray (see below). It is then set as the point data in the mesh.

point_data = vtk.vtkDoubleArray()
point_data.SetNumberOfComponents(1)

for zval in zvals:
    point_data.InsertNextTuple([zval])

mesh.GetPointData().SetScalars(point_data)

There are two new features in this visualisation that have not yet been introduced in this chapter. Although new their use is straightforward if you have worked through the previous content in this chapter. When we render the surface it is surrounded by the outline of a box and by an axis. These two additional features can help visualise an object: the first by outlining a box around the model and the second by displaying coordinate axes to help determine dimensions. Both of these require that a vtkPolyDataNormals instance be derived from the polygon mesh. This information is easily generated using the following:

norms_generator = vtk.vtkPolyDataNormals()
norms_generator.SetInputData(mesh)

Using this data coordinate axes are then generated.

axes = vtk.vtkCubeAxesActor2D()
axes.SetInputConnection(norms_generator.GetOutputPort())
axes.SetCamera(renderer.GetActiveCamera())
axes.SetLabelFormat("%1.1g")

Similarly the outline filter is created with the following.

outline_filter = vtk.vtkOutlineFilter()
outline_filter.SetInputConnection(norms_generator.GetOutputPort())

The axes are an Actor2D and can be added to the renderer like any other actor. The outline filter can be treated just like a regular instance of vtkPolyData which means that it is connected to a vtkPolyDataMapper, a vtkActor, then added to the renderer with renderer.AddActor(outline_actor).

The final results are shown below.

Image of plot of function z = 0.2 * (x^2 + y^2).
Figure 10. Final result showing a plot of the function: \$f(x,y) = \sin \left[ 0.2 \times \left( x^2 + y^2 \right) \right] \$.
Image of plot of function z = x^2 - y^2.
Figure 11. Plot of function \$f(x,y) = x^2 - y^2\$.

A note about colour

The following information about colour is appended to this chapter for anybody unfamiliar with the RGB and HSV formats for specifying colours in VTK and most other software.

The use of colour is very important in scientific visualisations. In VTK data is often mapped to a colour palette which when presented allows the user to very quickly absorb a large amount of information about a set of data. Understanding how colours are defined in VTK is important, especially for defining lookup tables.

Any colour can be described by specifying 3 separate floating values. VTK uses the range [0.0, 1.0]. There are two models that VTK uses to describe colours. The first colour model is the red/green/blue (RGB) model[2]. Methods in VTK take or return 3 floating point numbers from the range [0.0, 1.0]. These describe the amount of red, green, and blue, that constitute that colour. An RGB value of (1.0, 0.0, 0.0) is bright red; an RGB value of (1.0, 1.0, 1.0) is white and (0.0, 0.0, 0.0) is black. Increasing or decreasing all three values together will lighten or darken the resulting colour.

Colour swatches showing the effect to the resulting colour when changing HSV parameters.
Figure 12. Three rows of colour swatches showing the effect of the resulting colour when changing HSV parameters. In each row a single parameter is varied while the others remain at maximum values. Top row: the hue varies, showing variation through red, yellow, green, and blue. Second row: A purple colour has the saturation reduced, changing towards the colour grey. Third row: The same colour as the second row instead has value reduced, changing towards black.

The other model is the hue/saturation/value (HSV) model[3]. This also takes 3 parameters to describe a colour but these three values describe different pieces of information. This model may be more intuitive for some people. The first number, hue, is what a person would likely think of as the colour. Imagine moving across a rainbow, the hue would decide whether the colour is red, orange, yellow, etc. The saturation and value describe the intensity of this colour, but in two different ways. The saturation describes how much colour there is. A saturation of 0.0 would change all colours to appear in greyscale. The value describes how bright or dark the colour is. A value of 0.0 would always produce black, no matter the other values, with 1.0 the brightest.

In some places a 4th value is specified: the alpha value. This specifies how transparent a surface is. An alpha value of 0.0 is a surface that is entirely transparent, no matter the value of the other 3 colour values. With an alpha value of 1.0 the surface is entirely solid/opaque and appears as the colour specified by the other 3 values. It will not be at all transparent with an alpha of 1.0.

Important commands

Commands described in this chapter are summarised in the table below.

Command Description

Fundamental commands

vtk.vtkConeSource()

Create a cone polygon mesh.

mapper.SetInputConnection(cone.GetOutputPort())

Connect the output port of the cone to the input connection of mapper. This is a common idiom in VTK to connect outputs and inputs to build a pipeline.

actor.SetMapper(mapper)

Set mapper for the vtkActor named actor.

window.SetSize(x, y)

Set the window dimensions in pixels to x wide and y tall.

interactor.SetRenderWindow(window)

Set interactor for use with the render window.

window.AddRenderer(renderer)

Add the renderer to the window.

renderer.SetViewport( (x0, y0, x1, y1) )

Set the renderer to draw in the section of the window with bottom-left corner at (x0, y0) and top-right corner at (x1, y1). These values are ratios in the range [0.0, 1.0]. Default is (0.0, 0.0, 1.0, 1.0).

renderer.SetBackground(R, G, B)

Set the background colour to RGB colour define by (R, G, B).

Polygon mesh commands

points.InsertNextPoint( (x, y, z) )

Insert the next point at position (x, y, z) into the vtkPoints instance.

tri.GetPointIds().SetId(i, j)

For the vtkTriangle instance tri, set the local ID i for this triangle to the global ID j of the mesh in which tri is a cell. NOTE: this command is identical for other polygon cells, including vtkQuad.

data.SetNumberOfComponents(1)

For the vtkDoubleArray data set the number of components of each tuple to 1.

data.InsertNextTuple([val])

For the vtkDoubleArray data set the next tuple value to the float val.

mesh.GetCellData().SetScalars(data) mesh.GetPointData().SetScalars(data)

For the vtkPolyData instance mesh set the cell/point data to the vtkDoubleArray instance data.

Lookup Tables

actor.GetProperty().SetColor(r, g, b)

For actor set the colour of the entire model of that actor to a single colour, specificied by the RGB values (r, g, b).

vtk.vtkPolyDataMapper()

Create an instance of vtkPolyDataMapper, a lookup table that can be applied to a polygon mesh.

lut.SetTableRange(low, high)

Set the scale of the lookup table from low to high.

mapper.SetLookupTable(lut)

Connects the lookup table lut to the data mapped by mapper.

mapper.SetUseLookupTableScalarRange(True)

The data range in the lookup table set to mapper is used. In most cases this method should be called when modified lookup tables are used.

lut.Build()

After setting up a lookup table, lut, this method updates the lookup table to reflect the new parameters. Needs to be called once a lookup table has been setup.

lut.SetAboveRangeColor(colour) lut.SetBelowRangeColor(colour)

Set the colour to use for values that are outside of the range of the lookup table. Here colour can be 4 parameters or a list of length 4. These are the RGB values of the colour and the alpha channel as the 4th parameter. Must use in combination with .UseXXXXXRangeColorOn() to activate use of this colour.

lut.UseBelowRangeColorOn() lut.UseBelowRangeColorOff() lut.UseAboveRangeColorOn() lut.UseAboveRangeColorOff()

For lookup table lut controls whether to use a custom colour for values outside of the range. Set off by default.

lut.SetHueRange(low, high) lut.SetSaturationRange(low, high) lut.SetValueRange(low, high)

For the vtkLookupTable lut set the colour Hue, Saturation, or Value range from low to high. Set low = high to fix at a single value.

ctransfer.AddRGBPoint(val, R, G, B) ctransfer.AddRGBPoint(val, R, G, B, A)

For the vtkColorTransferFunction ctransfer set the colour R, G, B to the float val. An alpha value A can optionally be specified.

ctransfer.GetColor(val)

Get the colour as 3 RGB values that corresponds to float val for the vtkColorTransferFunction ctransfer.

lut.SetTableValue(val, R, G, B)

Manually set the colour (R, G, B) for the value val for the lookup table lut.

Transformations

actor.SetPosition(x, y, z)

Place vtkActor actor at coordinates (x, y, z).

actor.GetPosition()

Get the current (x, y, z) coordinates of actor.

actor.AddPosition(x, y, z)

Displace actor by (x, y, z) from its current position.

actor.SetScale(s) actor.SetScale(x, y, z)

Scale actor either by a factor of s in all directions or a factor of (x, y, z) in the corresponding directions of the local coordinate system of actor.

actor.RotateX(theta) actor.RotateY(theta) actor.RotateZ(theta)

Rotate actor around its local X, Y, or Z axis by theta degrees.


1. Visualization Toolkit (VTK)


OR