Fast Triangulation of Unordered Point Clouds (OpenMP Version)#

This tutorial explains how to run a greedy surface triangulation algorithm (OpenMP version) on a PointCloud with normals, to obtain a triangle mesh based on projections of the local neighborhoods. The OpenMP version divides the point cloud into multiple segments and invokes the original version of the greedy surface triangulation based on the segment count. By running the segments independently and concurrently, there will be a disjoint result between each segment. To overcome the disjoint issue, additional overlap segments are created to go through greedy surface triangulation and combine with other processed segments.

Note

The output of OpenMP version may differ from serial version of greedy projection triangulation.

Background: Algorithm and Parameters#

Refer to original greedy_triangulation for more detail original greedy triangulation parameters. All greedy triangulation parameters are supported except getPointStates(), getPartIDs(), getSFN() and getFFN().

Additional parameters are defined to provide control segments and overlap.

  • setNumberofThreads(unsigned) controls how many segments are created from the input point cloud. Each segment size is determined using the formula (maximum x - minimum x) / number of threads, and all segment sizes are equal. Each segment will be assigned to a CPU thread/core. It is recommended to obtain the number of threads/cores using omp_get_max_threads().

  • setBlockOverlapPercentage(double) controls how wide to create the overlap region among two segments. The overlap region is this parameter percentage multiply the size of segment. If there is still gap between segments, can overcome by increase the overlap percentage.

  • setRemoveDuplicateMesh(bool) controls whether to clean up the duplicate point cloud and polygons after combining all the segments output from GP3.

Note

This tutorial is applicable for execution for both within inside and outside a Docker image. It assumes that the pcl-oneapi-tutorials Deb package is installed, and the user has copied the tutorial directory from /opt/intel/pcl/oneapi/tutorials/ to a user-writable directory.

  1. Prepare the environment:

    cd <path-to-oneapi-tutorials>/greedy_projection
    
  2. greedy_projectoin.cpp should be in the directory with following content:

     1// SPDX-License-Identifier: Apache-2.0
     2// Copyright (C) 2025 Intel Corporation
     3#include <pcl/point_types.h>
     4#include <pcl/io/pcd_io.h>
     5#include <pcl/io/vtk_io.h>
     6#include <pcl/search/kdtree.h> // for KdTree
     7#include <pcl/features/normal_3d.h>
     8#include <pcl/oneapi/surface_omp/gp3.h>
     9
    10int
    11main ()
    12{
    13  // Load input file into a PointCloud<T> with an appropriate type
    14  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    15  pcl::PCLPointCloud2 cloud_blob;
    16  pcl::io::loadPCDFile ("bun0.pcd", cloud_blob);
    17  pcl::fromPCLPointCloud2 (cloud_blob, *cloud);
    18  //* the data should be available in cloud
    19
    20  // Normal estimation*
    21  pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> n;
    22  pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal>);
    23  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
    24  tree->setInputCloud (cloud);
    25  n.setInputCloud (cloud);
    26  n.setSearchMethod (tree);
    27  n.setKSearch (20);
    28  n.compute (*normals);
    29  //* normals should not contain the point normals + surface curvatures
    30
    31  // Concatenate the XYZ and normal fields*
    32  pcl::PointCloud<pcl::PointNormal>::Ptr cloud_with_normals (new pcl::PointCloud<pcl::PointNormal>);
    33  pcl::concatenateFields (*cloud, *normals, *cloud_with_normals);
    34  //* cloud_with_normals = cloud + normals
    35
    36  // Create search tree*
    37  pcl::search::KdTree<pcl::PointNormal>::Ptr tree2 (new pcl::search::KdTree<pcl::PointNormal>);
    38  tree2->setInputCloud (cloud_with_normals);
    39
    40  // Initialize objects
    41  pcl::GreedyProjectionTriangulationOMP<pcl::PointNormal> gp3;
    42  pcl::PolygonMesh triangles;
    43
    44  // Set the maximum distance between connected points (maximum edge length)
    45  gp3.setSearchRadius (0.025);
    46
    47  // Set typical values for the parameters
    48  gp3.setMu (2.5);
    49  gp3.setMaximumNearestNeighbors (100);
    50  gp3.setMaximumSurfaceAngle(M_PI/4); // 45 degrees
    51  gp3.setMinimumAngle(M_PI/18); // 10 degrees
    52  gp3.setMaximumAngle(2*M_PI/3); // 120 degrees
    53  gp3.setNormalConsistency(false);
    54  gp3.setNumberOfThreads(3);
    55  gp3.setBlockOverlapPercentage(0.04);
    56  gp3.setRemoveDuplicateMesh(true);
    57
    58  // Get result
    59  gp3.setInputCloud (cloud_with_normals);
    60  gp3.setSearchMethod (tree2);
    61  gp3.reconstruct (triangles);
    62
    63  pcl::io::saveVTKFile("mesh.vtk", triangles);
    64
    65  // Finish
    66  return (0);
    67}
    
  3. Source the Intel® oneAPI Base Toolkit environment:

    source /opt/intel/oneapi/setvars.sh
    
  4. (Optional) Set up proxy setting to download test data:

    export http_proxy="http://<http_proxy>:port"
    export https_proxy="http://<https_proxy>:port"
    
  5. Build the code:

    mkdir build && cd build
    cmake ../
    make -j
    
  6. Run the binary:

    ./greedy_projection
    
  7. Output:

    The program will save the output as “mesh.vtk”. View the VTK file by:

    pcl_viewer mesh.vtk
    

Code Explanation#

As the example PCD has only XYZ coordinates, load it into a PointCloud<PointXYZ>.

// Load input file into a PointCloud<T> with an appropriate type
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::PCLPointCloud2 cloud_blob;
pcl::io::loadPCDFile ("bun0.pcd", cloud_blob);
pcl::fromPCLPointCloud2 (cloud_blob, *cloud);

The method requires normals, so normals are estimated using the standard method from PCL.

// Normal estimation*
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> n;
pcl::PointCloud<pcl::Normal>::Ptr normals (new pcl::PointCloud<pcl::Normal>);
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ>);
tree->setInputCloud (cloud);
n.setInputCloud (cloud);
n.setSearchMethod (tree);
n.setKSearch (20);
n.compute (*normals);

Since coordinates and normals are required to be in the same PointCloud, a PointNormal type point cloud is created.

// Concatenate the XYZ and normal fields*
pcl::PointCloud<pcl::PointNormal>::Ptr cloud_with_normals (new pcl::PointCloud<pcl::PointNormal>);
pcl::concatenateFields (*cloud, *normals, *cloud_with_normals);

The lines below deals with the initialization of the required objects. In the OpenMP version, include <pcl/oneapi/surface_omp/gp3.h> and declare the class by appending OMP to the original class.”

// Create search tree*
pcl::search::KdTree<pcl::PointNormal>::Ptr tree2 (new pcl::search::KdTree<pcl::PointNormal>);
tree2->setInputCloud (cloud_with_normals);

// Initialize objects
pcl::GreedyProjectionTriangulationOMP<pcl::PointNormal> gp3;

The lines below set the parameters, as explained above, to configure the algorithm.

// Set the maximum distance between connected points (maximum edge length)
gp3.setSearchRadius (0.025);

// Set typical values for the parameters
gp3.setMu (2.5);
gp3.setMaximumNearestNeighbors (100);
gp3.setMaximumSurfaceAngle(M_PI/4); // 45 degrees
gp3.setMinimumAngle(M_PI/18); // 10 degrees
gp3.setMaximumAngle(2*M_PI/3); // 120 degrees
gp3.setNormalConsistency(false);
gp3.setNumberOfThreads(3);
gp3.setBlockOverlapPercentage(0.04);
gp3.setRemoveDuplicateMesh(true);

The lines below set the input objects and perform the actual triangulation.

gp3.setInputCloud (cloud_with_normals);
gp3.setSearchMethod (tree2);
gp3.reconstruct (triangles);