TinyRobotics
Loading...
Searching...
No Matches
PointCloud.h
1#pragma once
2#include <cmath>
3#include <cstdint>
4#include <limits>
5#include <unordered_map>
6#include <unordered_set>
7#include <vector>
8
9#include "TinyRobotics/coordinates/Coordinate.h"
10#include "TinyRobotics/utils/Common.h"
11#include "TinyRobotics/utils/LoggerClass.h"
12
13namespace tinyrobotics {
14/**
15 * @struct Point3D
16 * @brief Represents a 3D point with x, y, z coordinates (in meters).
17 *
18 * Used throughout the mapping and robotics modules for point cloud processing,
19 * spatial queries, and geometric computations. Each coordinate is stored as a T
20 * (meters).
21 */
22template <typename T = DistanceM>
23struct Point3D {
24 T x;
25 T y;
26 T z;
27};
28
29/**
30 * @class PointCloud
31 * @ingroup maps
32 * @brief Represents a 3D point cloud for robotics and mapping applications.
33 *
34 * The PointCloud class manages a collection of 3D points and provides:
35 * - Efficient addition of points (individually, from arrays, or from
36 * Coordinate).
37 * - Automatic bounding box tracking for fast spatial queries.
38 * - Voxel grid downsampling to reduce point density while preserving structure.
39 * - Fast occupancy queries via a voxel grid (for collision checking or
40 * mapping).
41 * - Optional "live" voxel grid mode for real-time occupancy updates as points
42 * are added.
43 *
44 * Usage scenarios include 3D mapping, obstacle detection, SLAM, and environment
45 * representation. The class is designed for embedded and robotics use,
46 * balancing efficiency and flexibility.
47 *
48 * Key features:
49 * - Add, clear, and access points.
50 * - Retrieve the bounding box (min/max x, y, z).
51 * - Downsample using a voxel grid (keep one point per voxel).
52 * - Build and query a voxel occupancy grid for fast lookups.
53 * - Optionally enable live voxel grid updates for real-time occupancy.
54 *
55 * Example:
56 * @code
57 * PointCloud cloud(0.1f);
58 * cloud.add(1.0, 2.0, 0.5);
59 * cloud.buildVoxelGrid(); // 10cm voxels
60 * bool occ = cloud.isOccupied(1.0, 2.0, 0.5);
61 * @endcode
62 */
63template <typename T = DistanceM>
64class PointCloud : public IMapNeighbors<T> {
65 /// Bounding box
66 struct Bounds {
67 Point3D<T> min;
68 Point3D<T> max;
69 };
70
71 struct Key {
72 int x, y, z;
73 bool operator==(const Key& other) const {
74 return x == other.x && y == other.y && z == other.z;
75 }
76 };
77
78 struct KeyHash {
79 size_t operator()(const Key& k) const {
80 return ((std::hash<int>()(k.x) ^ (std::hash<int>()(k.y) << 1)) >> 1) ^
81 (std::hash<int>()(k.z) << 1);
82 }
83 };
84
85 public:
87 using value_type = T;
88
89 PointCloud(T voxalSize, bool isliveVoxel = false) {
90 setVoxalSize(voxalSize);
91 resetBounds();
92 setLiveVoxelGrid(isliveVoxel);
93 }
94
95 /// Update the voxel grid as points are added (for real-time occupancy
96 /// updates).
97 void setLiveVoxelGrid(bool live) { liveVocelGrid_ = live; }
98 /// Set the size of each voxel in the grid (e.g., 0.1 m for 10cm voxels).
99 void setVoxalSize(T size) { voxelSize_ = size; }
100
101 /// Add a single point
102 void add(T x, T y, T z = 0.0f) {
103 Point3D<T> p{x, y, z};
104 if (!liveVocelGrid_) points_.push_back(p);
105 updateBounds(p);
106 if (liveVocelGrid_) addVoxel(x, y, z);
107 }
108
109 /// Add from array
110 void add(const Point3D<T>* pts, size_t count) {
111 for (size_t i = 0; i < count; ++i) add(pts[i].x, pts[i].y, pts[i].z);
112 }
113
114 /// Add from Coordinate
115 void add(const Coordinate<T>& coord) { add(coord.x, coord.y, coord.z); }
116
117 /// Provides access to the points in the point cloud.
118 const VectorT& points() const { return points_; }
119
120 /// Provides access to the points in the point cloud.
121 VectorT& points() { return points_; }
122
123 /// Provide the number of points in the point cloud.
124 size_t size() const { return points_.size(); }
125
126 /// Checks if the point cloud is empty (i.e., contains no points).
127 bool isEmpty() const { return points_.empty(); }
128
129 /// @brief Returns the bounding box of the point cloud, which is defined by
130 /// the minimum and maximum coordinates along each axis (x, y, z). The
131 /// bounding box is updated whenever new points are added to the cloud,
132 /// allowing for efficient retrieval of the spatial extent of the point cloud.
133 Bounds bounds() const { return bounds_; }
134
135 // /// Simple voxel downsampling: e.g. 0.5 m to reduce number of points while
136 // /// keeping overall structure.
137 // PointCloud voxelDownsample(float voxelSize) const {
138 // PointCloud out;
139 // if (voxelSize <= 0.0f) return out;
140
141 // std::unordered_map<Key, Point3D<T>, KeyHash> voxels;
142
143 // for (const auto& p : points_) {
144 // Key key{int(std::floor(p.x / voxelSize)),
145 // int(std::floor(p.y / voxelSize)),
146 // int(std::floor(p.z / voxelSize))};
147
148 // // Keep first point per voxel (simple version)
149 // if (voxels.find(key) == voxels.end()) voxels[key] = p;
150 // }
151
152 // for (const auto& kv : voxels)
153 // out.add(kv.second.x, kv.second.y, kv.second.z);
154
155 // return out;
156 // }
157
158 /// Builds a voxel grid for fast occupancy queries: A voxel is a grid cell
159 void buildVoxelGrid() {
160 voxelGrid_.clear();
161
162 for (const auto& p : points_) {
163 addVoxel(p.x, p.y, p.z);
164 }
165 TRLogger.debug("Voxel grid built with voxel size: %f, total voxels: %d",
166 voxelSize_, (int)voxelGrid_.size());
167 for (const auto& key : voxelGrid_) {
168 TRLogger.debug("Voxel: (%d, %d, %d)", key.x, key.y, key.z);
169 }
170 // Clear the points
171 points_.clear();
172 }
173
174 /**
175 * @brief Returns the free (unoccupied) neighboring coordinates of a given
176 * point in the voxel grid.
177 *
178 * If the voxel grid is built (voxelSize_ > 0), this method returns all
179 * adjacent unoccupied voxels (6-connectivity in 3D, or 4/8-connectivity in 2D
180 * if z=0). Only neighbors that are not present in the voxel grid are
181 * returned.
182 *
183 * @param from The coordinate to find neighbors for.
184 * @return std::vector<Coordinate<T>> List of free neighboring
185 * coordinates.
186 */
187 std::vector<Coordinate<T>> getNeighbors(Coordinate<T> from) const override {
188 std::vector<Coordinate<T>> neighbors;
189 if (voxelSize_ <= 0.0f) return neighbors;
190 TRLogger.debug("-------------------");
191 TRLogger.debug(
192 "Finding neighbors for: %s with bounding box min:(%f,%f,%f) / "
193 "max:(%f,%f,%f)",
194 from.toCString(), bounds_.min.x, bounds_.min.y, bounds_.min.z,
195 bounds_.max.x, bounds_.max.y, bounds_.max.z);
196
197 // Compute voxel index of 'from'
198 int x0 = int(std::floor(from.x / voxelSize_));
199 int y0 = int(std::floor(from.y / voxelSize_));
200 int z0 = int(std::floor(from.z / voxelSize_));
201
202 if (is_3d) {
203 // 6-connectivity in 3D (face neighbors)
204 const int dx[6] = {1, -1, 0, 0, 0, 0};
205 const int dy[6] = {0, 0, 1, -1, 0, 0};
206 const int dz[6] = {0, 0, 0, 0, 1, -1};
207 for (int i = 0; i < 6; ++i) {
208 int nx = x0 + dx[i];
209 int ny = y0 + dy[i];
210 int nz = z0 + dz[i];
211 // Convert voxel index back to coordinate (center of voxel)
212 T cx = (nx + 0.5f) * voxelSize_;
213 T cy = (ny + 0.5f) * voxelSize_;
214 T cz = (nz + 0.5f) * voxelSize_;
215 // Check bounding box
216 if (cx < bounds_.min.x || cx > bounds_.max.x || cy < bounds_.min.y ||
217 cy > bounds_.max.y || cz < bounds_.min.z || cz > bounds_.max.z) {
218 continue;
219 }
220 Key nkey{nx, ny, nz};
221 // Only add if NOT occupied (free neighbor)
222 if (voxelGrid_.find(nkey) == voxelGrid_.end()) {
223 neighbors.emplace_back(cx, cy, cz);
224 }
225 }
226 } else {
227 // 4-connectivity in 2D (z=0 plane)
228 const int dx[4] = {1, -1, 0, 0};
229 const int dy[4] = {0, 0, 1, -1};
230 for (int i = 0; i < 4; ++i) {
231 int nx = x0 + dx[i];
232 int ny = y0 + dy[i];
233 int nz = z0; // keep z the same
234 T cx = (nx + 0.5f) * voxelSize_;
235 T cy = (ny + 0.5f) * voxelSize_;
236 T cz = 0.0f; // Always use z=0 for 2D
237 // Check bounding box (z=0 plane)
238 if (cx < bounds_.min.x || cx > bounds_.max.x || cy < bounds_.min.y ||
239 cy > bounds_.max.y) {
240 continue;
241 }
242 Key nkey{nx, ny, nz};
243 if (voxelGrid_.find(nkey) == voxelGrid_.end()) {
244 neighbors.emplace_back(cx, cy, cz);
245 }
246 }
247 }
248 for (auto& n : neighbors) {
249 TRLogger.debug("Neighbor: %s", n.toCString());
250 }
251 TRLogger.debug("-------------------");
252
253 return neighbors;
254 }
255
256 /// Use 3D in findNeighbors (if false, only consider z=0 plane for 2D
257 /// occupancy)
258 void set3D(bool is3d) { is_3d = is3d; }
259
260 /// Manually set the bounding box (if known in advance, can save time instead
261 /// of
262 void setBounds(float minX, float minY, float minZ, float maxX, float maxY,
263 float maxZ) {
264 bounds_.min = {minX, minY, minZ};
265 bounds_.max = {maxX, maxY, maxZ};
266 }
267
268 /// Checks if a given point is occupied based on the voxel grid.
269 bool isOccupied(float x, float y, float z) const {
270 if (voxelSize_ <= 0.0f) return false;
271
272 Key key{int(std::floor(x / voxelSize_)), int(std::floor(y / voxelSize_)),
273 int(std::floor(z / voxelSize_))};
274
275 return voxelGrid_.find(key) != voxelGrid_.end();
276 }
277
278 /// Checks if a coordinate is valid (within bounds and not occupied)
279 bool isValid(const Coordinate<T>& coord) const override {
280 // Check bounding box
281 if (coord.x < bounds_.min.x || coord.x > bounds_.max.x ||
282 coord.y < bounds_.min.y || coord.y > bounds_.max.y ||
283 coord.z < bounds_.min.z || coord.z > bounds_.max.z) {
284 return false;
285 }
286 // If voxel grid is enabled, check occupancy
287 if (voxelSize_ > 0.0f && isOccupied(coord.x, coord.y, coord.z)) {
288 return false;
289 }
290 return true;
291 }
292
293 /// Provides sequential (iterator) access to all occupied voxels
294 auto beginVoxels() { return voxelGrid_.begin(); }
295 /// Provides sequential (iterator) access to all occupied voxels
296 auto endVoxels() { return voxelGrid_.end(); }
297 /// Provides sequential (iterator) access to all occupied voxels
298 auto beginVoxels() const { return voxelGrid_.begin(); }
299 /// Provides sequential (iterator) access to all occupied voxels
300 auto endVoxels() const { return voxelGrid_.end(); }
301
302 /// Write map to output
304 return serializer.write(*this, out);
305 }
306
307 /// Read map from input
309 return serializer.read(*this, in);
310 }
311
312 /// Clear all data
313 void clear() {
314 points_.clear();
315 voxelGrid_.clear();
316 resetBounds();
317 }
318
319 protected:
320 bool is_3d = false;
321 VectorT points_;
322 Bounds bounds_;
323 T voxelSize_ = 0.0f;
324 bool liveVocelGrid_ = false;
325 // voxel grid preferrably is psram
327 std::unordered_set<Key, KeyHash, std::equal_to<Key>, PSRAMKeyAllocator>
328 voxelGrid_;
329 // Serialization
330 PointCloudSerializer<PointCloud<T>> serializer;
331
332 void resetBounds() {
333 bounds_.min = {std::numeric_limits<float>::max(),
334 std::numeric_limits<float>::max(),
335 std::numeric_limits<float>::max()};
336
337 bounds_.max = {std::numeric_limits<float>::lowest(),
338 std::numeric_limits<float>::lowest(),
339 std::numeric_limits<float>::lowest()};
340 }
341
342 void updateBounds(const Point3D<T>& p) {
343 if (p.x < bounds_.min.x) bounds_.min.x = p.x;
344 if (p.y < bounds_.min.y) bounds_.min.y = p.y;
345 if (p.z < bounds_.min.z) bounds_.min.z = p.z;
346
347 if (p.x > bounds_.max.x) bounds_.max.x = p.x;
348 if (p.y > bounds_.max.y) bounds_.max.y = p.y;
349 if (p.z > bounds_.max.z) bounds_.max.z = p.z;
350 }
351
352 /// Adds a point to the voxel grid if voxelSize_ > 0
353 void addVoxel(float x, float y, float z) {
354 if (voxelSize_ > 0.0f) {
355 Key key{int(std::floor(x / voxelSize_)), int(std::floor(y / voxelSize_)),
356 int(std::floor(z / voxelSize_))};
357 voxelGrid_.insert(key);
358 } else {
359 TRLogger.warn("Voxel size must be > 0 to add voxels");
360 }
361 }
362
363
364 /// Convert voxel grid index (cx, cy) to world coordinate (center of voxel,
365 /// z=0)
366 Coordinate<T> toVoxel(int cx, int cy) const {
367 // This matches the logic in getNeighbors for 2D
368 T wx = (cx + 0.5f) * voxelSize_;
369 T wy = (cy + 0.5f) * voxelSize_;
370 T wz = 0.0f;
371 return Coordinate<T>(wx, wy, wz);
372 }
373
374
375};
376
377} // namespace tinyrobotics
A generic 3D coordinate class for robotics, navigation, and spatial calculations.
Definition: Coordinate.h:57
Represents a 3D point cloud for robotics and mapping applications.
Definition: PointCloud.h:64
size_t readFrom(Stream &in)
Read map from input.
Definition: PointCloud.h:308
auto endVoxels() const
Provides sequential (iterator) access to all occupied voxels.
Definition: PointCloud.h:300
size_t size() const
Provide the number of points in the point cloud.
Definition: PointCloud.h:124
std::vector< Coordinate< T > > getNeighbors(Coordinate< T > from) const override
Returns the free (unoccupied) neighboring coordinates of a given point in the voxel grid.
Definition: PointCloud.h:187
size_t writeTo(Print &out)
Write map to output.
Definition: PointCloud.h:303
bool isValid(const Coordinate< T > &coord) const override
Checks if a coordinate is valid (within bounds and not occupied)
Definition: PointCloud.h:279
void setVoxalSize(T size)
Set the size of each voxel in the grid (e.g., 0.1 m for 10cm voxels).
Definition: PointCloud.h:99
void addVoxel(float x, float y, float z)
Adds a point to the voxel grid if voxelSize_ > 0.
Definition: PointCloud.h:353
void setBounds(float minX, float minY, float minZ, float maxX, float maxY, float maxZ)
Definition: PointCloud.h:262
void add(T x, T y, T z=0.0f)
Add a single point.
Definition: PointCloud.h:102
auto beginVoxels() const
Provides sequential (iterator) access to all occupied voxels.
Definition: PointCloud.h:298
Coordinate< T > toVoxel(int cx, int cy) const
Definition: PointCloud.h:366
auto endVoxels()
Provides sequential (iterator) access to all occupied voxels.
Definition: PointCloud.h:296
bool isOccupied(float x, float y, float z) const
Checks if a given point is occupied based on the voxel grid.
Definition: PointCloud.h:269
Bounds bounds() const
Returns the bounding box of the point cloud, which is defined by the minimum and maximum coordinates ...
Definition: PointCloud.h:133
void add(const Coordinate< T > &coord)
Add from Coordinate.
Definition: PointCloud.h:115
void clear()
Clear all data.
Definition: PointCloud.h:313
void set3D(bool is3d)
Definition: PointCloud.h:258
bool isEmpty() const
Checks if the point cloud is empty (i.e., contains no points).
Definition: PointCloud.h:127
auto beginVoxels()
Provides sequential (iterator) access to all occupied voxels.
Definition: PointCloud.h:294
VectorT & points()
Provides access to the points in the point cloud.
Definition: PointCloud.h:121
const VectorT & points() const
Provides access to the points in the point cloud.
Definition: PointCloud.h:118
void buildVoxelGrid()
Builds a voxel grid for fast occupancy queries: A voxel is a grid cell.
Definition: PointCloud.h:159
void setLiveVoxelGrid(bool live)
Definition: PointCloud.h:97
void add(const Point3D< T > *pts, size_t count)
Add from array.
Definition: PointCloud.h:110
Represents a 3D point with x, y, z coordinates (in meters).
Definition: PointCloud.h:23