Spatial change detection on unorganized point cloud data

An octree is a tree-based data structure for organizing sparse 3-D data. In this tutorial we will learn how to use the octree implementation for detecting spatial changes between multiple unorganized point clouds which could vary in size, resolution, density and point ordering. By recursively comparing the tree structures of octrees, spatial changes represented by differences in voxel configuration can be identified. Additionally, we explain how to use the pcl octree “double buffering” technique allows us to efficiently process multiple point clouds over time.

The code:

First, create a file, let’s say, octree_change_detection.cpp and place the following inside it:

 1#include <pcl/point_cloud.h>
 2#include <pcl/octree/octree_pointcloud_changedetector.h>
 3
 4#include <iostream>
 5#include <vector>
 6#include <ctime>
 7
 8int
 9main ()
10{
11  srand ((unsigned int) time (NULL));
12
13  // Octree resolution - side length of octree voxels
14  float resolution = 32.0f;
15
16  // Instantiate octree-based point cloud change detection class
17  pcl::octree::OctreePointCloudChangeDetector<pcl::PointXYZ> octree (resolution);
18
19  pcl::PointCloud<pcl::PointXYZ>::Ptr cloudA (new pcl::PointCloud<pcl::PointXYZ> );
20
21  // Generate pointcloud data for cloudA
22  cloudA->width = 128;
23  cloudA->height = 1;
24  cloudA->points.resize (cloudA->width * cloudA->height);
25
26  for (std::size_t i = 0; i < cloudA->size (); ++i)
27  {
28    (*cloudA)[i].x = 64.0f * rand () / (RAND_MAX + 1.0f);
29    (*cloudA)[i].y = 64.0f * rand () / (RAND_MAX + 1.0f);
30    (*cloudA)[i].z = 64.0f * rand () / (RAND_MAX + 1.0f);
31  }
32
33  // Add points from cloudA to octree
34  octree.setInputCloud (cloudA);
35  octree.addPointsFromInputCloud ();
36
37  // Switch octree buffers: This resets octree but keeps previous tree structure in memory.
38  octree.switchBuffers ();
39
40  pcl::PointCloud<pcl::PointXYZ>::Ptr cloudB (new pcl::PointCloud<pcl::PointXYZ> );
41   
42  // Generate pointcloud data for cloudB 
43  cloudB->width = 128;
44  cloudB->height = 1;
45  cloudB->points.resize (cloudB->width * cloudB->height);
46
47  for (std::size_t i = 0; i < cloudB->size (); ++i)
48  {
49    (*cloudB)[i].x = 64.0f * rand () / (RAND_MAX + 1.0f);
50    (*cloudB)[i].y = 64.0f * rand () / (RAND_MAX + 1.0f);
51    (*cloudB)[i].z = 64.0f * rand () / (RAND_MAX + 1.0f);
52  }
53
54  // Add points from cloudB to octree
55  octree.setInputCloud (cloudB);
56  octree.addPointsFromInputCloud ();
57
58  std::vector<int> newPointIdxVector;
59
60  // Get vector of point indices from octree voxels which did not exist in previous buffer
61  octree.getPointIndicesFromNewVoxels (newPointIdxVector);
62
63  // Output points
64  std::cout << "Output from getPointIndicesFromNewVoxels:" << std::endl;
65  for (std::size_t i = 0; i < newPointIdxVector.size (); ++i){
66    std::cout << i << "# Index:" << newPointIdxVector[i]
67              << "  Point:" << (*cloudB)[newPointIdxVector[i]].x << " "
68              << (*cloudB)[newPointIdxVector[i]].y << " "
69              << (*cloudB)[newPointIdxVector[i]].z << std::endl;
70  }
71}

The explanation

Now, let’s discuss the code in detail.

We first instantiate the OctreePointCloudChangeDetector class and define its voxel resolution.

  srand ((unsigned int) time (NULL));

  // Octree resolution - side length of octree voxels
  float resolution = 32.0f;

  // Instantiate octree-based point cloud change detection class
  pcl::octree::OctreePointCloudChangeDetector<pcl::PointXYZ> octree (resolution);

Then we create a point cloud instance cloudA which is initialized with random point data. The generated point data is used to build an octree structure.

  pcl::PointCloud<pcl::PointXYZ>::Ptr cloudA (new pcl::PointCloud<pcl::PointXYZ> );

  // Generate pointcloud data for cloudA
  cloudA->width = 128;
  cloudA->height = 1;
  cloudA->points.resize (cloudA->width * cloudA->height);

  for (std::size_t i = 0; i < cloudA->size (); ++i)
  {
    (*cloudA)[i].x = 64.0f * rand () / (RAND_MAX + 1.0f);
    (*cloudA)[i].y = 64.0f * rand () / (RAND_MAX + 1.0f);
    (*cloudA)[i].z = 64.0f * rand () / (RAND_MAX + 1.0f);
  }

  // Add points from cloudA to octree
  octree.setInputCloud (cloudA);
  octree.addPointsFromInputCloud ();

Point cloud cloudA is our reference point cloud and the octree structure describe its spatial distribution. The class OctreePointCloudChangeDetector inherits from class Octree2BufBase which enables to keep and manage two octrees in the memory at the same time. In addition, it implements a memory pool that reuses already allocated node objects and therefore reduces expensive memory allocation and deallocation operations when generating octrees of multiple point clouds. By calling “octree.switchBuffers()”, we reset the octree class while keeping the previous octree structure in memory.

  // Switch octree buffers: This resets octree but keeps previous tree structure in memory.
  octree.switchBuffers ();

Now we instantiate a second point cloud “cloudB” and fill it with random point data. This point cloud is used to build a new octree structure.

  pcl::PointCloud<pcl::PointXYZ>::Ptr cloudB (new pcl::PointCloud<pcl::PointXYZ> );
   
  // Generate pointcloud data for cloudB 
  cloudB->width = 128;
  cloudB->height = 1;
  cloudB->points.resize (cloudB->width * cloudB->height);

  for (std::size_t i = 0; i < cloudB->size (); ++i)
  {
    (*cloudB)[i].x = 64.0f * rand () / (RAND_MAX + 1.0f);
    (*cloudB)[i].y = 64.0f * rand () / (RAND_MAX + 1.0f);
    (*cloudB)[i].z = 64.0f * rand () / (RAND_MAX + 1.0f);
  }

  // Add points from cloudB to octree
  octree.setInputCloud (cloudB);
  octree.addPointsFromInputCloud ();

In order to retrieve points that are stored at voxels of the current octree structure (based on cloudB) which did not exist in the previous octree structure (based on cloudA), we can call the method “getPointIndicesFromNewVoxels” which return a vector of the result point indices.

  std::vector<int> newPointIdxVector;

  // Get vector of point indices from octree voxels which did not exist in previous buffer
  octree.getPointIndicesFromNewVoxels (newPointIdxVector);

Finally, we output the results to the std::cout stream.

  // Output points
  std::cout << "Output from getPointIndicesFromNewVoxels:" << std::endl;
  for (std::size_t i = 0; i < newPointIdxVector.size (); ++i){
    std::cout << i << "# Index:" << newPointIdxVector[i]
              << "  Point:" << (*cloudB)[newPointIdxVector[i]].x << " "
              << (*cloudB)[newPointIdxVector[i]].y << " "
              << (*cloudB)[newPointIdxVector[i]].z << std::endl;

Compiling and running the program

Add the following lines to your CMakeLists.txt file:

 1cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
 2
 3project(octree_change_detection)
 4
 5find_package(PCL 1.2 REQUIRED)
 6
 7include_directories(${PCL_INCLUDE_DIRS})
 8link_directories(${PCL_LIBRARY_DIRS})
 9add_definitions(${PCL_DEFINITIONS})
10
11add_executable (octree_change_detection octree_change_detection.cpp)
12target_link_libraries (octree_change_detection ${PCL_LIBRARIES})

After you have made the executable, you can run it. Simply do:

$ ./octree_change_detection

You will see something similar to:

Output from getPointIndicesFromNewVoxels:
0# Index:11  Point:5.56047 56.5082 10.2807
1# Index:34  Point:1.27106 63.8973 14.5316
2# Index:102  Point:6.42197 60.7727 14.7087
3# Index:105  Point:5.64673 57.736 25.7479
4# Index:66  Point:22.8585 56.4647 63.9779
5# Index:53  Point:52.0745 14.9643 63.5844

Another example application: OpenNI change viewer

The pcl visualization component contains an openNI change detector example. It displays grabbed point clouds from the OpenNI interface and displays detected spatial changes in red.

Simply execute:

$ cd visualization/tools
$ ./openni_change_viewer

And you should see something like this:

octreeChangeViewer

Conclusion

This octree-based change detection enables to analyse “unorganized” point clouds for spatial changes.

Additional Details

“Unorganized” point clouds are characterized by non-existing point references between points from different point clouds due to varying size, resolution, density and/or point ordering. In case of “organized” point clouds often based on a single 2D depth/disparity images with fixed width and height, a differential analysis of the corresponding 2D depth data might be faster.