Assignment 05
Isosurfaces
Preliminaries
Scalar data defined over a grid is an incredibly common data type, showing up in medical applications, computational science, and many other settings. The purpose of this assignment is to give you experience with several different ways to visualize scalar data. For this assignment you will create visualizations of several data sets, all of which are mappings of \(f: \mathbb{R}^{3}\rightarrow\mathbb{R}\). In particular, our visualizations will all use isosurfaces.
For this assignment, and the next two, we are going to be working with both openFrameworks and VTK. This presents certain challenges with compiling: VTK requires CMake to compile, but OF support of CMake is not yet standardized.
To enable CMake, we’re going to leverage ofnode/of, a github project to enable CMake support in OF. In my tests, it supports both OSX and Linux (in theory, Fedora, Ubuntu, and Arch), but support for Windows / Visual Studio does not yet exist. If you’d like to try to get this running on your own, please see my installation notes. If you’re a windows user, or you just prefer to use something prebuilt, I have provided a virtual machine for anyone to use on Piazza. One should be able to download and import this after installing VirtualBox.
As a final introductory note, you may find that using ParaView to explore the data that I provide below to be helpful. All of the datasets will load in ParaView, and the VM I provide also has ParaView installed.
Submission
For this assignment you will submit two directories that demonstrate the various parts of your assignment, named A05P01
and A05P02
. In each, I will expect the following structure, in particular with a CMakeLists.txt that conforms to the ofnode/of examples I provide below:
A05P0X/
src/*
bin/ <-- Do not include data/* in your git repo; the files are large
CMakeLists.txt
report.YYY
Note that ofxCMake, as we’ve been using in the past, will not be sufficient (it only appears to work correctly on OSX).
Data Loading
This time around, to read the data you will be need to write a class, ImageReader
in openFrameworks that relies on VTK functionality. ImageReader
will take on the same role in your App as TableReader
did in the previous assignment. Your code must support readomg files with *.vti
, the VTK ImageData format. Files with the extension .vti.
are an XML format, that can be parsed using the vtkXMLImageDataReader class using a few lines of C++ code.
Once read from file, I recommend converting the data internally to a data structure of your choice (e.g. a three-dimensional array of floats). This will potentially make further queries easier and decouple your project from the VTK pipeline. For this assignment, you are not required to use VTK for anything other than data reading. As you read through the design spec of the assignment, please take the time to consider what functionality your ImageReader
will need.
I have provide an example OF app (testVTK.zip) that demonstrates reading in a two-dimensional vti file and drawing it using ofImage. In ofApp::setup()
for this code, it shows an example of all VTK calls that you need and should be used to test your environment to make sure that you can compile code that jointly depends on openFrameworks and VTK. A CMakeLists.txt file is provided that works with ofnode/of. Testing to make sure this file compiles will help you ensure that your environment is configured correctly.
VTK ImageData is a fairly simple structure. While a number of extra features are supported, all VTK ImageData have data members for
- an origin (\(= (o_x, o_y, o_z)\)) which encodes the 3d position at the minimum of the bounding box of the image,
- a spacing (\(= (s_x, s_y, s_z)\)) which encodes the sampling rate in each dimension, and
- the image dimensions (\(= (d_x, d_y, d_z)\)) which encode how many samples in \(x\), \(y\), and \(z\). An image having dimensions \((d_x, d_y, d_z)\) will have \(d_x \times d_y \times d_z\) pixels/voxels.
These dimensions, plus the origin and spacing, implicitly define a regular grid whose vertices span from \((o_x, o_y, o_z)\) to \((o_x+s_x(d_x-1), o_y+s_y(d_y-1), o_z+s_z(d_z-1))\). On that grid, we store a value at each vertex, encoded by
- A flat array of scalar values, stored as a point data set (one value per vertex). This array is flattened so that \(z\) strides the slowest and \(x\) strides the fastest, just like an image raster order, i.e. \(f(o_x, o_y, o_z)\), \(f(o_x+s_x,o_y,o_z)\), \(f(o_x+2s_x,o_y,o_z)\), \(\ldots\)
I have included some same datasets in addition to the other .vti
files that you have access to in past examples. Please download them here: data05.zip. These files were converted from The Volume Library and from TU Wien’s Visualization Group Data Sets.
Part 1: 2D Slice Viewer
Your first task is to read in the 3D data set and provide an interface to visualize slices of the data. Reading should happen only once and in your setup()
function.
Your interface should support the user selecting any axis-aligned slice, which requires them to select either an \(x-\), \(y-\), or \(z-\)plane in the range of the dimensions of the data. At a bare minimum, your code must be able to produce slices that are direct subsets of the input data (e.g. for a \(z-\)slice, the values \(f(x,y,z)\) for all integer-valued \(\{(x,y,z) : x \in [0,d_x-1], y \in [0,d_y-1]\}\) tuples where \(z\) is a constant). Extra credit may be awarded for interfaces that allow for slices interpolated through the volume.
Upon selecting an axis, you should display the corresponding slice as a greyscale ofImage. Your App should leave sufficient room for the interface to select which slice, but otherwise should resize the image in the remaining space (ofImage::resize()
will be helpful for this).
Finally, your slice viewer should also support colorizing the image. To do so, you must encode at least one fixed color map. First, choose an appropriate color map from http://colorbrewer2.org/ or another source. Interpret this color map into a continuous space; do not use a binned color map. One way to do this is to use two or three colors that are starting, middle, and ending values for your color map. You will need to implement a function that will take a scalar value and then map it to a color along your new continuous colormap. Using this colormap, you will populate an ofImage.
In both the greyscale and colorized versions, your visualization must also display a legend that reports the data range.
In your report for this part, be sure to briefly describe your interface design concept for the slice viewer. How did you choose to let the user select slices and what contextual information did you provide for understanding the slice? What other features did you choose to implement? Also describe how you chose your color map? What makes it an appropriate color map for this data? You may want to experiment with several colormaps. If you do, make sure to describe these and/or include screenshots in your report. Specifically, include a description of slices and colormaps used for visualizing:
- fuel.vti
- engine.vti
Be sure to report any interesting discoveries in the part 1 report.
Part 2: 3D Contouring
In the next part of this assignment, you will implement the original form of Marching Cubes by Lorensen and Cline. Marching cubes is an algorithm to extract an approximation of an isosurface – the set of points \(f^{-1}(\rho)\) for a given isovalue \(\rho\).
Your code must load a scalar volume (using your ImageReader
), let the user specify an isovalue \(\rho\), extract the triangular mesh associated with \(f^{-1}(\rho)\), and finally display the triangle mesh using an ofMesh (or ofVboMesh) so that the user can rotate and investigate the surface.
As a point of comparison, I have provide an example, testMC.zip, that implements a basic isosurfacer using VTK’s vtkMarchingCubes filter. Your code must, at the very least, replicate this functionality without using vtkMarchingCubes. I have only provided this code to use so that you can test to make sure that your code produces a similar isosurface for a given isovalue. In addition, this example provides an illustration of how to populate an ofMesh data structure with points and triangles, as well as how to use the ofEasyCam for a simple mouse interface to rotate a three-dimensional object.
The simple algorithm for computing a Marching Cubes surface is based on a pass through the data, where one “marches” through all of the cubes defined by 8 adjacent voxels. For each cube, one classifies all corners as to whether they are above or below the isovalue \(\rho\). This produces an eight-bit signature that one can use to index into a lookup table. This lookup table maps the signature to a list of triangles for that cube.
The final step is then to identify the vertices for these triangles. We call any edge of the cube that has both a corner with a scalar value above \(\rho\) and a corner with a scalar value below \(\rho\) a sign change edge. Sign change edges must contain at least one point that has precisely the scalar value \(\rho\) by the intermediate value theorem. Finding this point involves linearly interpolating the scalar values on the end points of this edge.
After extracting the surface, consider implementing different visual encodings of the triangle mesh, for example using an optional wireframe drawing and color encoding. You may want to add normals and lighting. In your report for this part, be sure to briefly describe your interface design for your isosurfacer. What other visual elements did you choose to include to help the user understand context and interact with the surface? What other features would you include to make these viewer more effective? How does isocontouring compare to viewing slices?
Finally, use your isocontouring code to investigate these two datasets.
- hydrogen.vti
- tooth.vti
Try to find interesting values and patterns that you maybe could not see in the slice viewer map. You are welcome to explore data beyond the above. Include your findings in the part 2 report.
Grading
My expectation is that you will submit two new folders in your git repository. Each folder should contain everything necessary to compile and run your program as described above. Each folder should contain a report document in a format of your choosing.
Each part of the assignment is weighed as follows:
- 40% Completing Part 1
- 60% Completing Part 2
For each part, a correct implementation is worth half of the points. A satisfactory report is worth the other half.
This percentage will be scaled to the total value of this assignment for your final grade (7%). For each of the coding parts, I will specifically check that you’ve read in the data correctly and processed it as described above. I will also check for coding style, commenting, and organization where appropriate.
Extra credit will be awarded for implementing features that significantly go beyond the requirements.