2.5D Delaunay triangulations are also known as TIN (Triangulated Irregular Networks). They are like a classic 2D Delaunay triangulation, but with elevated points. This article describes the optional simplification of point clouds from LiDAR scans and surface metrology data using adaptive and grid-based clustering methods. Further it shows how to triangulate point clouds in a memory-efficient way and how to finally re-export the triangle mesh without memory-usage peaks.
For the sake of clarity, this example is divided into the Sub-Examples 0-4, each comprising only a few lines of code. You can find the source code as part of the Fade download in the file “examples_25D/a_terrain25d.cpp“.
Generating Points
The function getInputPoints()
of the example source code takes a filename and returns a vector of points.
- When the special filename “MOUNTAIN” is used a small demo point-cloud with 105×88=9240 points is returned.
- When an empty filename is provided, a random surface point cloud is created using
generateRandomSurfacePoints()
. If you are interested have a look at the module Test Data Generators that generates random geometry data for software tests. - Any other filename is assumed to name a “x y z”-ASCII file. You can substitute your own filename here. Have a look at
readXYZ()
in the module File I/O where various read/write functions for ASCII and binary files are documented.
void getInputPoints(const std::string& filename, std::vector<Point2>& vPointsOut) { if(filename=="MOUNTAIN") { getMountain(vPointsOut); } else if(!filename.empty()) { // Read terrain points from an ASCII file readXYZ(filename.c_str(),vPointsOut); } else { // No file given, create a random surface for the demo generateRandomSurfacePoints( 100, // numPointsX 100, // numPointsY 200, // numCenters (waviness) 0,0,0,100,100,50, // Bounds xmin,ymin,zmin,xmax,ymax,zmax vPointsOut, // Output vector 1 // Seed (0 is random, >0 is static) ); } std::cout<<"Original number of points: "<<vPointsOut.size()<<endl; }
2.5D Delaunay Triangulation
The code snippet below represents Sub-Example 0. It uses the input-function described above to fetch points. Then it creates a CloudPrepare
object, adds the points to it and finally inserts the CloudPrepare
object into the 2.5D Delaunay triangulation. The result is written as *.obj file (supported by practically any 3D viewer) and as *.list file for the Geomview viewer (Linux).

“In principle, you can also insert the vector with the points directly in
Fade_2D
. However, doing it via theCloudPrepare
object has a practical advantage: Since adding the points toCloudPrepare
and inserting theCloudPrepare
object into the Delaunay triangulation are two individual steps, you have the opportunity to delete the points from the data structures of your own software in an intermediate step i.e., before any additional memory for triangles is reserved by Fade. Otherwise, the points would be stored twice in memory for a short time. This avoids memory consumption peaks.”
// 0: Triangulate all points void terrain_full() { // * 1 * Get input points std::vector<Point2> vInputPoints; getInputPoints("MOUNTAIN",vInputPoints); // * 2 * Triangulate Fade_2D dt; // dt.insert(vInputPoints); // Still possible CloudPrepare cloudPrep; cloudPrep.add(vInputPoints); dt.insert(&cloudPrep,true); // More memory efficient // * 3 * Show write(0,"terrain_full",dt); }
Reducing a Point Cloud
Preventing memory consumption peaks is just one purpose of the CloudPrepare
class. It can also be used to reduce the size of point clouds before triangles are created and there are three methods for this:
- Sub-Example 1 demonstrates adaptive point cloud simplification by clustering points with similar height values. Geometrically, this method gives the best results because it removes those vertices that are least needed.
- Sub-Example 2 shows uniform simplification by a grid, where the user specifies the cell size. All points within a grid cell are summarized.
- Sub-Example 3 reduces the point cloud also with a grid, but determines the grid size itself to match the desired number of output points specified by the user.
// 1: Simplify: Adaptive, cluster points based on their height difference void terrain_adaptiveSimplify() { ... // * 3 * Reduce the cloud size double maxDiffZ(1.0); // z-tolerance per point group SumStrategy sms(SMS_AVERAGE); // or SMS_MEDIAN, SMS_MINIMUM, SMS_MAXIMUM ConvexHullStrategy chs(CHS_MAXHULL); // or CHS_NONE, CHS_MINHULL cloudPrep.adaptiveSimplify(maxDiffZ,sms,chs); ... }
// 2: Simplify: Use a grid to cluster points void terrain_gridSimplify() { ... // * 3 * Reduce the cloud size SumStrategy sms(SMS_AVERAGE); // or SMS_MEDIAN, SMS_MINIMUM, SMS_MAXIMUM ConvexHullStrategy chs(CHS_MAXHULL); // or CHS_NONE, CHS_MINHULL double gridLength(2); cloudPrep.uniformSimplifyGrid(gridLength,sms,chs); ... }
// 3: Simplify: Use approxNumPoints to cluster points (also grid-based) void terrain_numPointsSimplify() { ... // * 3 * Reduce the cloud size SumStrategy sms(SMS_AVERAGE); // or SMS_MEDIAN, SMS_MINIMUM, SMS_MAXIMUM ConvexHullStrategy chs(CHS_MAXHULL); // or CHS_NONE, CHS_MINHULL double approxNumPoints(vInputPoints.size()/4); cloudPrep.uniformSimplifyNum(approxNumPoints,sms,chs); ... }




Two parameters in the above code need an explanation:
- The
SumStrategy
determines how points in a group are summarized. Most of the timeSMS_AVERAGE
will give good results. If the data contains outliers,SMS_MEDIAN
is more stable. Simple ground filtering can be achieved withSMS_MINIMUM
, since selecting the lowest point in the cluster ignores those higher points corresponding to vegetation.SMS_MAXIMUM
is also available. - The
ConvexHullStrategy
determines whether and which points of the convex hull must be left unchanged. The valueCHS_NONE
ignores the convex hull and simply treats all points the same.CHS_MAXHULL
protects all points of the convex hull i.e., it keeps them constant,CHS_MINHULL
protects only those points of the convex hull whose absence would change the shape of the convex hull. That is, it does not protect points that lie in the middle of a convex-hull line segment.
Exporting the final Triangulation
You probably want to have your 2.5D Delaunay triangulation not only in the Fade library, but also bring it back into your own software. This should be done quickly, conveniently and without the triangle mesh residing in memory twice at any point. There is now a ready-made solution for triangulation-export in Fade and the earlier Example 8 describes it already for the 2D case. But for self-containedness we treat this export-solution also with the present 2.5D terrain example: A simple struct is used to store all triangulation data:
struct FadeExport { void print() const; bool writeObj(const char* filename) const; void extractTriangleNeighborships(std::vector<std::pair<int,int> >& vNeigs) const; void getCornerIndices(int triIdx,int& vtxIdx0,int& vtxIdx1,int& vtxIdx2) const; void getCoordinates(int vtxIdx,double& x,double& y) const; // DATA double* aCoords; ///< Cartesian coordinates (dim*numPoints) int* aCustomIndices; ///< Custom indices of the points (only when exported) int* aTriangles; ///< 3 counterclockwise oriented vertex-indices per triangle (3*numTriangles) };
// 4: Export data of the triangulation void exportData() { ... Fade_2D dt; dt.insert(&cloudPrep,true); // * 3 * Export to a struct FadeExport fadeExport; // Simple struct, see "FadeExport.h" bool bCustomIndices(true); // To retrieve custom indices also bool bClear(true); // Clear memory in dt to save memory dt.exportTriangulation(fadeExport,bCustomIndices,bClear); // All triangulation data is in 'fadeExport' now and dt has // been cleared during the export procedure to avoid memory // usage peaks: dt is empty now. // * 4 * Show that all data is in fadeExport fadeExport.print(); fadeExport.writeObj("exportTest25D.obj"); // NOTE: You have full access to the data in the FadeExport struct // and you have also access to the source code because it has been // implemented only in the header file "FadeExport.h". Adapt it // to your needs. Have also a look at the similar example in the // examples_2D folder. }
4 replies on “2.5D Terrain Triangulation (TIN)”
Hello,
I am a R&D Engineer at Penn State interested in extracting iso-contours from triangulated meshes.
We use CGAL in our research and would like to know more about terrain modeling and finding iso-contours from your experience.
Would you be able to discuss the process for terrain/bathymetric analysis or to help guide our approach?
We take in DEM data in an .xyz file, attain a 3D delaunay triangulation, “convert” the triangulation into a mesh, and use CGAL’s mesh-slicing functions. However this seems dreadfully slow. We are working on the scale of number of triangles (~500K) seen in your example, and would like to extract perhaps up to 100 contour levels at differing heights.
Would you be able to lend your experience/insight?
Thank you,
Robert
Hi,
I am not sure whether I have correctly understood your approach: Do you really put 3D points into a 3D Delaunay triangulation? The output is a convex tetrahedral complex and even if you use a fake point at infinity to extract an approximate surface representation, that will be very slow for two reasons: First, a tetrahedral mesh has more elements than a triangular mesh. Second, the math is more complex, especially due to the high number of almost degenerate tetrahedra that DEM data probably causes.
Fade2.5D should compute the ISO lines within a second. If your project is non-commercial I can give you a free research license, just send me an email with the details (research group, project leader, a short description of link to the project website)
Best regards
Bernhard
Hello,
I am a phd student of Wuhan University in point cloud processing.
Is there a function that can get all Vertices indice linked with a chosen point?
Hello,
you are right, such a function would probably be convenient. I can implement that for the next version. Meantime, let me provide this little piece of source:
[code]
void incidentIndices()
{
// * 1 * Create random points
vector<Point2> vInputPoints;
generateRandomPoints(25,0,10,vInputPoints,1);
// * 2 * Set indices
for(size_t i=0;i<vInputPoints.size();++i)
{
vInputPoints[i].setCustomIndex(i);
}
// * 3 * Insert the points, retrieve the vertex handles
Fade_2D dt;
vector<Point2*> vHandles;
dt.insert(vInputPoints,vHandles);
// * 4 * For a certain point handle, retrieve the incident triangles
Point2* pVtx(vHandles[0]);
vector<Triangle2*> vIncidentT;
dt.getIncidentTriangles(pVtx,vIncidentT);
// * 5 * Iterate over the triangles to fetch the indicent points
set<Point2*> vIncidentVtx;
for(vector<Triangle2*>::iterator it(vIncidentT.begin());it!=vIncidentT.end();++it)
{
Triangle2* pT(*it);
for(int i=0;i<3;++i)
{
Point2* pCorner(pT->getCorner(i));
if(pCorner!=pVtx) vIncidentVtx.insert(pCorner);
}
}
// * 6 * Print the indices
cout<<"Vertices incident to "<<pVtx->getCustomIndex()<<": ";
for(set<Point2*>::iterator it(vIncidentVtx.begin());it!=vIncidentVtx.end();++it)
{
Point2* pIncidentVtx(*it);
cout<<pIncidentVtx->getCustomIndex()<<" ";
}
// * 7 * Draw it
Visualizer2 v("triangles.ps");
dt.show(&v);
v.addObject(Label(*pVtx,std::to_string(pVtx->getCustomIndex()),true,25),Color(CRED));
for(set<Point2*>::iterator it(vIncidentVtx.begin());it!=vIncidentVtx.end();++it)
{
Point2* pIncidentVtx(*it);
v.addObject(Label(*pIncidentVtx,std::to_string(pIncidentVtx->getCustomIndex()),true,25),Color(CGREEN));
}
v.writeFile();
}
[/code]