Using The Tinfour Contouring API - gwlucastrig/Tinfour GitHub Wiki

Under Construction: Additional content coming soon

Introduction

The Tinfour contouring package provides the ability to create contour elements from Delaunay triangulations. It produces contours directly from the triangulated mesh structure. Intermediate forms such as grids (rasters) are not required. Because the Tinfour implementation operates directly over the source data, it avoids the overhead and possible loss of accuracy that result from algorithms that involve interpolating unstructured data into a grid before contouring.

The figure to the right shows that a contouring operation can produce both contour lines and polygons constructed from those lines. The Tinfour implementation refers to these polygons as "contour regions". Both contours and regions are produced by the constructor for the ContourBuilderForTin class.

 
The steps for creating a set of contour objects are as follows:

  1. Identify a set of sample points giving measurements of a surface taken at arbitrary positions. Such collections of sample points are often referred to as "unstructured data" or "scattered data sets".
  2. Create a Tinfour incremental TIN (Delaunay triangulation) instance. Populate the TIN using the sample points from step 1 as vertices.
  3. Use the TIN as an input into Tinfour's ContourBuilderForTin class (the "builder"). Specify values for the desired contours (e.g. the "z values" for the contours). All contour-producing operations are performed by the ContourBuilderForTin class' constructor.
  4. Extract the resulting contours and contour regions from the builder and use them for rendering or analysis operations.

The following code example shows how the steps described above could be embodied by a Java application. There are, of course, many ways of using the contour elements. For clarity, this example sticks to a simple approach.

// Obtain a list of vertices from an appropriate data source
// and incorporate them into a Triangulated Irregular Network (TIN).
List<Vertex> vertices = obtainVertices();  // an arbitrary source of data
IncrementalTin tin = new IncrementalTin(1.0f);
tin.add(vertices, null);

// Find the limits of the data and use it to pick contour values.
// There are many different ways to make this selection.
// This example assumes a fixed contour spacing of 10 units
// and uses a convenience method from the ContourUtilities class
// to pick evenly spaced intervals.
double zMin = getMinValue(vertices);
double zMax = getMaxValue(vertices);
double[] zContour
  = ContourUtilities.computeContourValues(zMin, zMax, 10.0);

// Construct an instance of ContourBuilderForTin.  The constructor
// will produce the contours and, optionally, the contour regions.
// The elements can be obtained using "get" methods as shown below.
boolean enableRegions = true;
ContourBuilderForTin builder
  = new ContourBuilderForTin(tin, null, zContour, enableRegions);
List<Contour> contours = builder.getContours();
List<ContourRegion> regions = builder.getRegions();

// The RenderingSurfaceAid is just a utility for preparing a
// buffered image and an affine transform that can map the Cartesian
// coordinates for the input Vertices to pixel coordinates.
// In this case, the width and height of the output image are arbitrary
int imageWidth = 800;
int imageHeight = 600;
int imagePadding = 5;
Rectangle2D r2d = tin.getBounds();  // extent of input vertices
double x0 = r2d.getMinX();
double y0 = r2d.getMinY();
double x1 = r2d.getMaxX();
double y1 = r2d.getMaxY();

// The RenderingSurfaceAid will compute scaling and coordinate mapping
// based on the parameters we gave it and the range of the input coordinates.
// Internally, it will create graphics resources for our use.
// Again, we note that the rendering surface aid is used here as a convenience,
// but application code is free to use whatever approach best meets its requirements.  
RenderingSurfaceAid rsAid =
     new RenderingSurfaceAid(
         imageWidth, imageHeight, imagePadding, x0, y0, x1, y1);
rsAid.fillBackground(Color.white);
BufferedImage bImage = rsAid.getBufferedImage();
Graphics2D g2d = rsAid.getGraphics2D();
AffineTransform af = rsAid.getCartesianToPixelTransform();

// Plot the contours.
// Both the Contour and ContourRegion classes have methods that
// map the Cartesian coordinate of the contours to pixel coordinates
// based on a specified AffineTransform. In this case, we use getPath2D
// which uses the contour to produce a Java Path2D object suitable
// for plotting.
g2d.setColor(Color.black);
for (Contour c : contours) {
  Path2D path = c.getPath2D(af); // transforms Cartesian contours to pixels.
  g2d.draw(path);
}

// For this example, we output the image to an example file.
File outputFile = new File("ExampleOutput.png");
ImageIO.write(bImage, "PNG", outputFile);

Assumptions about Input Data

The current implementation of the Tinfour contour builder is based on the assumption that the input vertices describe a real-valued surface that can be treated as continuous and finite across its entire domain. At this time, infinite and null (Java's Float.NaN) data values are not supported. Also, there is currently no support for discrete (non-continuous) data types. Implementation of these features will be considered for future implementation is there is sufficient interest from the user community.

Nested Regions and Enclosures

Contours that represent features such as mountains or lake bottoms often form sets of concentric polygons. An example of such "nested" polygons is shown in the image of Lake Victoria shown below. The image was produced from a collection of approximately 4 million depth soundings compiled from various sources by a team from Salisbury University (Hamilton, 2016).

The Tinfour contouring API treats nested polygons as contour regions. When rendering images that draw contour regions as color-filled polygons, applications need some means to ensure that the larger enclosing polygons do not "overpaint" the smaller interior polygons. If larger polygons were rendered on top of their nested interior polygons, the smaller polygons would be obscured. Tinfour implements two mechanisms to avoid overpainting. First, contour regions are sorted and drawn in order of largest area to smallest. So, lacking any other mechanisms for preventing overpainting, the smaller polygons would be drawn after the larger ones and would always remain visible. But the Tinfour API also includes logic for detecting nesting structures. Each contour region instance includes a list of any smaller contour regions that are contained within them. The ContourRegion class provides a method called getChildRegions() that returns a list of those ContourRegions immediately inside the region.

 
For rendering purposes, the ContourRegion's getPath2D() method automatically constructs Path2D objects that include "holes" where any enclosed regions would be positioned. Thus, Java "fill" operations would not overpaint the interior features. This feature is useful when filling areas with either a texture or with a semi-transparent color specification. The getPath2D() method uses Java's WIND_EVEN_ODD rule for performing rendering operations.

The nesting logic would also support the creation of Geographic Information System (GIS) products given in the standard Shapefile format. A Shapefile implementation is being considered for future work.

Styling and Color

The example images shown above use color to illustrate trends in the contoured surface. When the builder creates contours and contour regions, it assigns them integer index values based on its input array of contour values (z values). For the images above, these index values were tied to entries in a color palette. The result was that regions of deep water were assigned dark blue entries from the palette, regions with shallow water were assigned yellow colors, and those in between were assigned colors using a gradual scale.

The contour and region index values supplied by the contour builder allow application code to identify the associated values for the contouring objects and apply styling elements such as color, line weight, textures, etc.

To see how the contour builder assigns index values to regions and contours, consider the case where a surface is divided by a single contour with a z value of 10 given in some unit of measure (feet, meters, degrees K, Pascals, Watts, chemical concentrations, etc.). Tinfour orients contours so that the higher valued region is always given to the left of the contour and the lower valued region is always to its right. So for a contour with a z value of 10, contour index of zero, the region to its right (minimum values in the input set) would receive a region index of zero. The region to its left (maximum values in the input set) would receive a region index of 1. This scheme extends to cases where multiple input contour values are specified by the application (i.e. 10, 20, etc.).

Contours actually have two index values, a left index and a right index. In most cases, the right index corresponds to the index of the contour array that was specified by the application. But there is one important exception. Boundary contours, those that lie along the outside of a contour data set, have a right index of -1. Boundary contours are oriented so that the undefined area outside the data set lies to their right.

The figure below illustrates the results of a simple contour and region data set derived from a triangulation consisting of three vertices. Contour z values were specified using an array of length 2 giving the values 10 and 20.

At this time, regions with a region index of -1 are not constructed. In the future, if support for no-data and infinite value areas is implemented, such undefined areas will be assigned region indices of -1. Application developers should plan accordingly.

Filtering and Contour Smoothing

Real-world data is often noisey. When developing contours from noisey sources, we can sometimes improve the aesthetics of a presentation by applying a smoothing filter to the data. Conventional smoothing filters (also known as low-pass filters) require data to be organized in regular grid structures. But, in keeping with the goal of contouring directly from a Delaunay triangulation, Tinfour implements a smoothing filter that operates over an instance of its incremental TIN classes.

As shown in the code snippet below, the smoothing filter takes two arguments. The first argument, is just the input TIN object. The second is an integer value indicating the degree of smoothing. Once established, the filter is passed into the constructure for the contour builder.

SmoothingFilter filter = new SmoothingFilter(tin, 5);
ContourBuilderForTin builder = new ContourBuilderForTin(tin, filter, zContour, true);

The effects of different degrees of filtering are shown in the image below. The source data for this image is taken from a Lidar elevation survey of Bear Mountain in Salisbury, Connecticut (USA). The surface for the survey area is rough, rocky, and uneven. This characteristic results in the appearance of many small-scale artifacts in the source data. Reducing those artifacts results in simpler contours.

The Art of Hiding the Artifice

I invite you now to consider a bit of artistic sleight-of-hand. Taking a second look at the depictions of the water-depth contours for Lake Victoria and for the Lake of the Arbuckles, you will notice that the complexity of the shoreline features is much more pronounced that that of the depth contours. That difference is not just due to the application of the contour smoothing logic described above. The shorelines represent a completely different kind of data collection than the depth soundings that were used to produce the contours.

In fact, the shoreline features are not based on contouring at all. They are taken from hand-digitized polygons obtained from aerial photographs and terrestrial survey data. The application that produced the lake images used the shoreline polygons to draw color-filled areas showing the overall extent of the lakes. It then overpainted those polygons with the contour regions produced by the Tinfour API. The regions did not actually start at the shoreline (zero depth), but rather a few meters down. So, the images presented are not, strictly speaking, contour images. Rather, they are a kind of artistic hybrid of two different techniques.

What I hope this example illustrates is that using a custom Java application offers a developer a lot of flexibility in terms of the way information is provided to the user. The Tinfour contour elements are not the end product in themselves, but rather part of a larger presentation. If you have a need for contour-based imagery in your work, I hope you will find that the Tinfour API offers a suitable solution.

References

Hamilton, Stuart; Kayanda, Robert; Ongore, Collins; Krach, Noah (2016). Bathymetry Points, Lake Victoria, vector points, 1900 - 2017, V7, Web site accessed April 2021 from https://doi.org/10.7910/DVN/75JI3A Harvard Dataverse, V10

Additional citations to follow

⚠️ **GitHub.com Fallback** ⚠️