Spatial Partitioning and Search Operations with Octrees#
An octree is a tree data structure in which each internal node has exactly eight children. Octrees are most often used to partition a three-dimensional space by recursively subdividing it into eight octants. Octrees are the three-dimensional analog of quadtrees. The word is derived from oct (Greek root meaning “eight”) + tree. Octrees are often used in 3D graphic and 3D game engines.
In this tutorial we will learn how to use the octree for spatial partitioning and neighbor search within the point cloud data. This tutorial covers the process of performing “Neighbors within Radius Search”, “Approximate Nearest Neighbor (ANN) Search” and “K-Nearest Neighbors (KNN) Search”.
Note
This tutorial can be run both inside and outside a Docker image. We assume that the pcl-oneapi-tutorials Deb package has been installed, and the user has copied the tutorial directory from /opt/intel/pcl/oneapi/tutorials/ to a user-writable directory.
Prepare the environment:
cd <path-to-oneapi-tutorials>/octree
oneapi_octree_search.cpp
should be in the directory with following content:1// SPDX-License-Identifier: Apache-2.0 2// Copyright (C) 2025 Intel Corporation 3#include <iostream> 4#include <fstream> 5#include <numeric> 6#include <pcl/oneapi/octree/octree.hpp> 7#include <pcl/oneapi/containers/device_array.h> 8#include <pcl/point_cloud.h> 9 10using namespace pcl::oneapi; 11 12float dist(Octree::PointType p, Octree::PointType q) { 13 return std::sqrt((p.x-q.x)*(p.x-q.x) + (p.y-q.y)*(p.y-q.y) + (p.z-q.z)*(p.z-q.z)); 14} 15 16int main (int argc, char** argv) 17{ 18 std::cout << "Running on device: " << dpct::get_default_queue().get_device().get_info<sycl::info::device::name>() << "\n"; 19 20 std::size_t data_size = 871000; 21 std::size_t query_size = 10000; 22 float cube_size = 1024.f; 23 float max_radius = cube_size / 30.f; 24 float shared_radius = cube_size / 30.f; 25 const int max_answers = 5; 26 const int k = 5; 27 std::size_t i; 28 std::vector<Octree::PointType> points; 29 std::vector<Octree::PointType> queries; 30 std::vector<float> radiuses; 31 std::vector<int> indices; 32 33 //Generate point cloud data, queries, radiuses, indices 34 srand (0); 35 points.resize(data_size); 36 for(i = 0; i < data_size; ++i) 37 { 38 points[i].x = ((float)rand())/(float)RAND_MAX * cube_size; 39 points[i].y = ((float)rand())/(float)RAND_MAX * cube_size; 40 points[i].z = ((float)rand())/(float)RAND_MAX * cube_size; 41 } 42 43 queries.resize(query_size); 44 radiuses.resize(query_size); 45 for (i = 0; i < query_size; ++i) 46 { 47 queries[i].x = ((float)rand())/(float)RAND_MAX * cube_size; 48 queries[i].y = ((float)rand())/(float)RAND_MAX * cube_size; 49 queries[i].z = ((float)rand())/(float)RAND_MAX * cube_size; 50 radiuses[i] = ((float)rand())/(float)RAND_MAX * max_radius; 51 }; 52 53 indices.resize(query_size / 2); 54 for(i = 0; i < query_size / 2; ++i) 55 { 56 indices[i] = i * 2; 57 } 58 59 //Prepare oneAPI cloud 60 pcl::oneapi::Octree::PointCloud cloud_device; 61 cloud_device.upload(points); 62 63 //oneAPI build 64 pcl::oneapi::Octree octree_device; 65 octree_device.setCloud(cloud_device); 66 octree_device.build(); 67 68 //Upload queries and radiuses 69 pcl::oneapi::Octree::Queries queries_device; 70 pcl::oneapi::Octree::Radiuses radiuses_device; 71 queries_device.upload(queries); 72 radiuses_device.upload(radiuses); 73 74 //Prepare output buffers on device 75 pcl::oneapi::NeighborIndices result_device1(queries_device.size(), max_answers); 76 pcl::oneapi::NeighborIndices result_device2(queries_device.size(), max_answers); 77 pcl::oneapi::NeighborIndices result_device3(indices.size(), max_answers); 78 pcl::oneapi::NeighborIndices result_device_ann(queries_device.size(), 1); 79 pcl::oneapi::Octree::ResultSqrDists dists_device_ann; 80 pcl::oneapi::NeighborIndices result_device_knn(queries_device.size(), k); 81 pcl::oneapi::Octree::ResultSqrDists dists_device_knn; 82 83 //oneAPI octree radius search with shared radius 84 octree_device.radiusSearch(queries_device, shared_radius, max_answers, result_device1); 85 86 //oneAPI octree radius search with individual radius 87 octree_device.radiusSearch(queries_device, radiuses_device, max_answers, result_device2); 88 89 //oneAPI octree radius search with shared radius using indices to specify 90 //the queries. 91 pcl::oneapi::Octree::Indices cloud_indices; 92 cloud_indices.upload(indices); 93 octree_device.radiusSearch(queries_device, cloud_indices, shared_radius, max_answers, result_device3); 94 95 //oneAPI octree ANN search 96 //if neighbor points distances results are not required, can just call 97 //octree_device.approxNearestSearch(queries_device, result_device_ann) 98 octree_device.approxNearestSearch(queries_device, result_device_ann, dists_device_ann); 99 100 //oneAPI octree KNN search 101 //if neighbor points distances results are not required, can just call 102 //octree_device.nearestKSearchBatch(queries_device, k, result_device_knn) 103 octree_device.nearestKSearchBatch(queries_device, k, result_device_knn, dists_device_knn); 104 105 //Download results 106 std::vector<int> sizes1; 107 std::vector<int> sizes2; 108 std::vector<int> sizes3; 109 result_device1.sizes.download(sizes1); 110 result_device2.sizes.download(sizes2); 111 result_device3.sizes.download(sizes3); 112 113 std::vector<int> downloaded_buffer1, downloaded_buffer2, downloaded_buffer3, results_batch; 114 result_device1.data.download(downloaded_buffer1); 115 result_device2.data.download(downloaded_buffer2); 116 result_device3.data.download(downloaded_buffer3); 117 118 int query_idx = 2; 119 std::cout << "Neighbors within shared radius search at (" 120 << queries[query_idx].x << " " 121 << queries[query_idx].y << " " 122 << queries[query_idx].z << ") with radius=" << shared_radius << std::endl; 123 for (i = 0; i < sizes1[query_idx]; ++i) 124 { 125 std::cout << " " << points[downloaded_buffer1[max_answers * query_idx + i]].x 126 << " " << points[downloaded_buffer1[max_answers * query_idx + i]].y 127 << " " << points[downloaded_buffer1[max_answers * query_idx + i]].z 128 << " (distance: " << dist(points[downloaded_buffer1[max_answers * query_idx + i]], queries[query_idx]) << ")" << std::endl; 129 } 130 131 std::cout << "Neighbors within individual radius search at (" 132 << queries[query_idx].x << " " 133 << queries[query_idx].y << " " 134 << queries[query_idx].z << ") with radius=" << radiuses[query_idx] << std::endl; 135 for (i = 0; i < sizes2[query_idx]; ++i) 136 { 137 std::cout << " " << points[downloaded_buffer2[max_answers * query_idx + i]].x 138 << " " << points[downloaded_buffer2[max_answers * query_idx + i]].y 139 << " " << points[downloaded_buffer2[max_answers * query_idx + i]].z 140 << " (distance: " << dist(points[downloaded_buffer2[max_answers * query_idx + i]], queries[query_idx]) << ")" << std::endl; 141 } 142 143 std::cout << "Neighbors within indices radius search at (" 144 << queries[query_idx].x << " " 145 << queries[query_idx].y << " " 146 << queries[query_idx].z << ") with radius=" << shared_radius << std::endl; 147 for (i = 0; i < sizes3[query_idx/2]; ++i) 148 { 149 std::cout << " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].x 150 << " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].y 151 << " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].z 152 << " (distance: " << dist(points[downloaded_buffer3[max_answers * query_idx / 2 + i]], queries[2]) << ")" << std::endl; 153 } 154 155 std::cout << "Approximate nearest neighbor at (" 156 << queries[query_idx].x << " " 157 << queries[query_idx].y << " " 158 << queries[query_idx].z << ")" << std::endl; 159 std::cout << " " << points[result_device_ann.data[query_idx]].x 160 << " " << points[result_device_ann.data[query_idx]].y 161 << " " << points[result_device_ann.data[query_idx]].z 162 << " (distance: " << std::sqrt(dists_device_ann[query_idx]) << ")" << std::endl; 163 164 std::cout << "K-nearest neighbors (k = " << k << ") at (" 165 << queries[query_idx].x << " " 166 << queries[query_idx].y << " " 167 << queries[query_idx].z << ")" << std::endl; 168 for (i = query_idx * k; i < (query_idx + 1) * k; ++i) 169 { 170 std::cout << " " << points[result_device_knn.data[i]].x 171 << " " << points[result_device_knn.data[i]].y 172 << " " << points[result_device_knn.data[i]].z 173 << " (distance: " << std::sqrt(dists_device_knn[i]) << ")" << std::endl; 174 } 175}
Source the Intel® oneAPI Base Toolkit environment:
source /opt/intel/oneapi/setvars.sh
(Optional) Set up proxy setting to download test data:
export http_proxy="http://<http_proxy>:port" export https_proxy="http://<https_proxy>:port"
Build the code:
mkdir build && cd build cmake ../ make -j
Run the binary:
./oneapi_octree_search
Expected results example:
The search only finds the first five neighbors (as specified by
max_answers
), so a different radius finds different points.Neighbors within shared radius search at (671.675 733.78 466.178) with radius=34.1333 660.296 725.957 439.677 (distance: 29.8829) 665.768 721.884 442.919 (distance: 26.7846) 683.988 714.608 445.164 (distance: 30.9962) 677.927 725.08 446.531 (distance: 22.3788) 695.066 723.509 445.762 (distance: 32.7028) Neighbors within individual radius search at (671.675 733.78 466.178) with radius=19.3623 672.71 736.679 447.835 (distance: 18.6) 664.46 731.504 452.074 (distance: 16.0048) 671.238 725.881 461.408 (distance: 9.23819) 667.707 718.527 466.622 (distance: 15.7669) 654.552 733.636 467.795 (distance: 17.1993) Neighbors within indices radius search at (671.675 733.78 466.178) with radius=34.1333 660.296 725.957 439.677 (distance: 29.8829) 665.768 721.884 442.919 (distance: 26.7846) 683.988 714.608 445.164 (distance: 30.9962) 677.927 725.08 446.531 (distance: 22.3788) 695.066 723.509 445.762 (distance: 32.7028)
Code Explanation#
Generate point cloud data, queries, radiuses, indices with a random number.
//Generate point cloud data, queries, radiuses, indices
srand (0);
points.resize(data_size);
for(i = 0; i < data_size; ++i)
{
points[i].x = ((float)rand())/(float)RAND_MAX * cube_size;
points[i].y = ((float)rand())/(float)RAND_MAX * cube_size;
points[i].z = ((float)rand())/(float)RAND_MAX * cube_size;
}
queries.resize(query_size);
radiuses.resize(query_size);
for (i = 0; i < query_size; ++i)
{
queries[i].x = ((float)rand())/(float)RAND_MAX * cube_size;
queries[i].y = ((float)rand())/(float)RAND_MAX * cube_size;
queries[i].z = ((float)rand())/(float)RAND_MAX * cube_size;
radiuses[i] = ((float)rand())/(float)RAND_MAX * max_radius;
};
indices.resize(query_size / 2);
for(i = 0; i < query_size / 2; ++i)
{
indices[i] = i * 2;
}
- Create and build the oneAPI point cloud; then upload the queries and
radiuses to a oneAPI device.
//Prepare oneAPI cloud
pcl::oneapi::Octree::PointCloud cloud_device;
cloud_device.upload(points);
//oneAPI build
pcl::oneapi::Octree octree_device;
octree_device.setCloud(cloud_device);
octree_device.build();
//Upload queries and radiuses
pcl::oneapi::Octree::Queries queries_device;
pcl::oneapi::Octree::Radiuses radiuses_device;
queries_device.upload(queries);
radiuses_device.upload(radiuses);
Create output buffers where we can download output from the oneAPI device.
//Prepare output buffers on device
pcl::oneapi::NeighborIndices result_device1(queries_device.size(), max_answers);
pcl::oneapi::NeighborIndices result_device2(queries_device.size(), max_answers);
pcl::oneapi::NeighborIndices result_device3(indices.size(), max_answers);
pcl::oneapi::NeighborIndices result_device_ann(queries_device.size(), 1);
pcl::oneapi::Octree::ResultSqrDists dists_device_ann;
pcl::oneapi::NeighborIndices result_device_knn(queries_device.size(), k);
pcl::oneapi::Octree::ResultSqrDists dists_device_knn;
The fist radius search method is “search with shared radius”. In this search method, all queries use the same radius to find the neighbors.
//oneAPI octree radius search with shared radius
octree_device.radiusSearch(queries_device, shared_radius, max_answers, result_device1);
//oneAPI octree radius search with individual radius
octree_device.radiusSearch(queries_device, radiuses_device, max_answers, result_device2);
The second radius search method is “search with individual radius”. In this search method, each query uses its own specific radius to find the neighbors.
//oneAPI octree radius search with individual radius
octree_device.radiusSearch(queries_device, radiuses_device, max_answers, result_device2);
The third radius search method is “search with shared radius using indices”. In this search method, all queries use the same radius, and indices specify the queries.
//oneAPI octree radius search with shared radius using indices to specify
//the queries.
pcl::oneapi::Octree::Indices cloud_indices;
cloud_indices.upload(indices);
octree_device.radiusSearch(queries_device, cloud_indices, shared_radius, max_answers, result_device3);
Perform ANN search.
//oneAPI octree ANN search
//if neighbor points distances results are not required, can just call
//octree_device.approxNearestSearch(queries_device, result_device_ann)
octree_device.approxNearestSearch(queries_device, result_device_ann, dists_device_ann);
Perform KNN search.
//oneAPI octree KNN search
//if neighbor points distances results are not required, can just call
//octree_device.nearestKSearchBatch(queries_device, k, result_device_knn)
octree_device.nearestKSearchBatch(queries_device, k, result_device_knn, dists_device_knn);
Download the search results from the oneAPI device. The size vector contains the size of found neighbors for each query. The downloaded_buffer vector contains the index of all found neighbors for each query.
//Download results
std::vector<int> sizes1;
std::vector<int> sizes2;
std::vector<int> sizes3;
result_device1.sizes.download(sizes1);
result_device2.sizes.download(sizes2);
result_device3.sizes.download(sizes3);
std::vector<int> downloaded_buffer1, downloaded_buffer2, downloaded_buffer3, results_batch;
result_device1.data.download(downloaded_buffer1);
result_device2.data.download(downloaded_buffer2);
result_device3.data.download(downloaded_buffer3);
Print the query, radius, and found neighbors to verify that the result is correct.
int query_idx = 2;
std::cout << "Neighbors within shared radius search at ("
<< queries[query_idx].x << " "
<< queries[query_idx].y << " "
<< queries[query_idx].z << ") with radius=" << shared_radius << std::endl;
for (i = 0; i < sizes1[query_idx]; ++i)
{
std::cout << " " << points[downloaded_buffer1[max_answers * query_idx + i]].x
<< " " << points[downloaded_buffer1[max_answers * query_idx + i]].y
<< " " << points[downloaded_buffer1[max_answers * query_idx + i]].z
<< " (distance: " << dist(points[downloaded_buffer1[max_answers * query_idx + i]], queries[query_idx]) << ")" << std::endl;
}
std::cout << "Neighbors within individual radius search at ("
<< queries[query_idx].x << " "
<< queries[query_idx].y << " "
<< queries[query_idx].z << ") with radius=" << radiuses[query_idx] << std::endl;
for (i = 0; i < sizes2[query_idx]; ++i)
{
std::cout << " " << points[downloaded_buffer2[max_answers * query_idx + i]].x
<< " " << points[downloaded_buffer2[max_answers * query_idx + i]].y
<< " " << points[downloaded_buffer2[max_answers * query_idx + i]].z
<< " (distance: " << dist(points[downloaded_buffer2[max_answers * query_idx + i]], queries[query_idx]) << ")" << std::endl;
}
std::cout << "Neighbors within indices radius search at ("
<< queries[query_idx].x << " "
<< queries[query_idx].y << " "
<< queries[query_idx].z << ") with radius=" << shared_radius << std::endl;
for (i = 0; i < sizes3[query_idx/2]; ++i)
{
std::cout << " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].x
<< " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].y
<< " " << points[downloaded_buffer3[max_answers * query_idx / 2 + i]].z
<< " (distance: " << dist(points[downloaded_buffer3[max_answers * query_idx / 2 + i]], queries[2]) << ")" << std::endl;
}
std::cout << "Approximate nearest neighbor at ("
<< queries[query_idx].x << " "
<< queries[query_idx].y << " "
<< queries[query_idx].z << ")" << std::endl;
std::cout << " " << points[result_device_ann.data[query_idx]].x
<< " " << points[result_device_ann.data[query_idx]].y
<< " " << points[result_device_ann.data[query_idx]].z
<< " (distance: " << std::sqrt(dists_device_ann[query_idx]) << ")" << std::endl;
std::cout << "K-nearest neighbors (k = " << k << ") at ("
<< queries[query_idx].x << " "
<< queries[query_idx].y << " "
<< queries[query_idx].z << ")" << std::endl;
for (i = query_idx * k; i < (query_idx + 1) * k; ++i)
{
std::cout << " " << points[result_device_knn.data[i]].x
<< " " << points[result_device_knn.data[i]].y
<< " " << points[result_device_knn.data[i]].z
<< " (distance: " << std::sqrt(dists_device_knn[i]) << ")" << std::endl;
}