Easy Navigation
Loading...
Searching...
No Matches
PointPerception.hpp
Go to the documentation of this file.
1// Copyright 2025 Intelligent Robotics Lab
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
25
26#ifndef EASYNAV_SENSORS_TYPES__POINTPERCEPTIONS_HPP_
27#define EASYNAV_SENSORS_TYPES__POINTPERCEPTIONS_HPP_
28
29#include <string>
30#include <vector>
31#include <optional>
32#include <mutex>
33
34#include "tf2/LinearMath/Transform.hpp"
35#include "pcl/point_cloud.h"
36#include "pcl/point_types.h"
37#include "pcl/PointIndices.h"
38
39#include "sensor_msgs/msg/laser_scan.hpp"
40#include "sensor_msgs/msg/point_cloud2.hpp"
41
42#include "rclcpp/time.hpp"
43#include "rclcpp_lifecycle/lifecycle_node.hpp"
44
48
49namespace std
50{
51
53template<>
54struct hash<std::tuple<int, int, int>>
55{
59 std::size_t operator()(const std::tuple<int, int, int> & key) const
60 {
61 std::size_t h1 = std::hash<int>()(std::get<0>(key));
62 std::size_t h2 = std::hash<int>()(std::get<1>(key));
63 std::size_t h3 = std::hash<int>()(std::get<2>(key));
64 return h1 ^ (h2 << 1) ^ (h3 << 2);
65 }
66};
67
68} // namespace std
69
70namespace easynav
71{
72
74{
75 pcl::PointCloud<pcl::PointXYZ> data;
76 std::string frame;
77 rclcpp::Time stamp;
78};
79
86{
87public:
89 static constexpr std::string_view default_group_ = "points";
90
94 static inline bool supports_msg_type(std::string_view t)
95 {
96 return t == "sensor_msgs/msg/LaserScan" ||
97 t == "sensor_msgs/msg/PointCloud2";
98 }
99
101 {
102 [[maybe_unused]] static const bool _ = [] {
104 [](const PointPerception & perception) {
105 std::ostringstream ret;
106 ret << "{ " << perception.stamp.seconds()
107 << " } PointPerception with " << perception.data.size()
108 << " points in frame [" << perception.frame_id
109 << "] with ts " << perception.stamp.seconds() << "\n";
110 return ret.str();
111 });
112 return true;
113 }();
114 }
115
117 {
118 std::lock_guard<std::mutex> lock(other.mutex_);
119 stamp = other.stamp;
120 frame_id = other.frame_id;
121 valid = other.valid;
122 new_data = other.new_data;
123
124 data = other.data;
129 buffer = other.buffer;
130 }
131
133 {
134 if (this == &other) {
135 return *this;
136 }
137
138 std::scoped_lock lock(mutex_, other.mutex_);
139 stamp = other.stamp;
140 frame_id = other.frame_id;
141 valid = other.valid;
142 new_data = other.new_data;
143
144 data = other.data;
149 buffer = other.buffer;
150
151 return *this;
152 }
153
155 pcl::PointCloud<pcl::PointXYZ> data;
156
158 pcl::PointCloud<pcl::PointXYZ> pending_cloud_;
159 std::string pending_frame_;
160 rclcpp::Time pending_stamp_;
161
164 pcl::PointCloud<pcl::PointXYZ> && cloud,
165 std::string && frame,
166 const rclcpp::Time & stamp)
167 {
168 std::lock_guard<std::mutex> lock(mutex_);
169 pending_cloud_ = std::move(cloud);
170 pending_frame_ = std::move(frame);
172 pending_available_ = true;
173 }
174
175
178 void resize(std::size_t size)
179 {
180 data.points.resize(size);
181 }
182
185 {
186 return buffer.latest_ref();
187 }
188
190 {
191 std::lock_guard<std::mutex> lock(mutex_);
192 // Access TF buffer singleton (already initialized somewhere with a clock)
193 auto tf_buffer_ptr = RTTFBuffer::getInstance();
194 auto & tf_buffer = *tf_buffer_ptr;
195 const auto tf_info = tf_buffer.get_tf_info();
196 const std::string & robot_frame = tf_info.robot_frame;
197
198 // ------------------------------------------------------------------
199 // 1. Push pending perception into the circular buffer exactly once.
200 // ------------------------------------------------------------------
201 if (pending_available_) {
202 PointPerceptionBufferType pending_item;
203 pending_item.data = std::move(pending_cloud_); // avoid deep copy
204 pending_item.frame = pending_frame_;
205 pending_item.stamp = pending_stamp_;
206
207 buffer.push(std::move(pending_item));
208 pending_available_ = false;
209 }
210
211 const std::size_t count = buffer.size();
212 if (count == 0) {
213 // No candidates at all: keep current visible state as is.
214 return;
215 }
216
217 // ------------------------------------------------------------------
218 // 2. Drain the circular buffer into a temporary vector so we can
219 // inspect all items and then rebuild the buffer.
220 // ------------------------------------------------------------------
221 std::vector<PointPerceptionBufferType> items;
222 items.reserve(count);
223
224 for (std::size_t i = 0; i < count; ++i) {
226 if (!buffer.pop(item)) {
227 break; // Defensive guard if pop() fails unexpectedly.
228 }
229 items.push_back(std::move(item));
230 }
231
232 if (items.empty()) {
233 // Nothing recovered from buffer: keep visible state untouched.
234 return;
235 }
236
237 // ------------------------------------------------------------------
238 // 3. Find indices:
239 // - newest_idx: newest perception by timestamp (regardless of TF),
240 // - newest_valid_idx: newest perception that has a valid TF.
241 //
242 // IMPORTANT: store only indices to avoid copying point clouds
243 // during the scan.
244 // ------------------------------------------------------------------
245 std::optional<std::size_t> newest_idx;
246 std::optional<std::size_t> newest_valid_idx;
247
248 for (std::size_t i = 0; i < items.size(); ++i) {
249 const auto & item = items[i];
250
251 // Track newest item overall (used when no TF is valid).
252 if (!newest_idx || item.stamp > items[*newest_idx].stamp) {
253 newest_idx = i;
254 }
255
256 bool has_tf = false;
257 try {
258 has_tf = tf_buffer.canTransform(
259 robot_frame,
260 item.frame,
261 tf2_ros::fromMsg(item.stamp),
262 tf2::durationFromSec(0.0));
263 } catch (...) {
264 // Any TF exception is treated as "no valid TF" for this item.
265 has_tf = false;
266 }
267
268 if (has_tf) {
269 if (!newest_valid_idx || item.stamp > items[*newest_valid_idx].stamp) {
270 newest_valid_idx = i;
271 }
272 }
273 }
274
275 // ------------------------------------------------------------------
276 // 4. Update visible state BEFORE moving items back into the buffer.
277 // This guarantees that `data` corresponds to:
278 // - the newest TF-valid item if any exists, otherwise
279 // - the newest item overall.
280 // ------------------------------------------------------------------
281 if (newest_valid_idx) {
282 const auto & sel = items[*newest_valid_idx];
283 data = sel.data; // single deep copy (intentional)
284 frame_id = sel.frame;
285 stamp = sel.stamp;
286 valid = true; // "valid" means "usable / not too old", not "TF ok"
287 new_data = true;
288 } else if (newest_idx) {
289 const auto & sel = items[*newest_idx];
290 data = sel.data; // single deep copy (intentional)
291 frame_id = sel.frame;
292 stamp = sel.stamp;
293 valid = true;
294 new_data = true;
295 } else {
296 // Defensive: should not happen because items is non-empty.
297 return;
298 }
299
300 // ------------------------------------------------------------------
301 // 5. Rebuild the circular buffer from scratch (move-only, no copies).
302 // ------------------------------------------------------------------
303 buffer.clear();
304
305 if (newest_valid_idx) {
306 // Keep the newest TF-valid item and any newer items (even if TF is not yet available).
307 const rclcpp::Time cutoff_stamp = items[*newest_valid_idx].stamp;
308
309 for (auto & item : items) {
310 if (item.stamp >= cutoff_stamp) {
311 buffer.push(std::move(item));
312 }
313 }
314 } else {
315 // No TF-valid items: keep everything in case TF arrives later.
316 for (auto & item : items) {
317 buffer.push(std::move(item));
318 }
319 }
320 }
321
322protected:
323 mutable std::mutex mutex_;
325};
326
333{
334public:
338 void on_initialize() override;
339
347 bool cycle_rt([[maybe_unused]] std::shared_ptr<NavState> nav_state) override;
348
349private:
351 std::shared_ptr<PointPerception> perception_data_ {nullptr};
352
354 rclcpp::SubscriptionBase::SharedPtr perception_sub_;
355};
356
360void convert(const sensor_msgs::msg::LaserScan & scan, pcl::PointCloud<pcl::PointXYZ> & pc);
361
365sensor_msgs::msg::PointCloud2 perception_to_rosmsg(const PointPerception & perception);
366
370sensor_msgs::msg::PointCloud2 points_to_rosmsg(const pcl::PointCloud<pcl::PointXYZ> & points);
371
375 std::vector<std::shared_ptr<PointPerception>>;
376
380rclcpp::Time get_latest_point_perceptions_stamp(const PointPerceptions & perceptions);
381
389{
390public:
393 struct VoxelKey
394 {
396 int x, y, z;
397
401 bool operator==(const VoxelKey & other) const
402 {
403 return x == other.x && y == other.y && z == other.z;
404 }
405 };
406
410 {
414 std::size_t operator()(const VoxelKey & key) const
415 {
416 std::size_t h1 = std::hash<int>{}(key.x);
417 std::size_t h2 = std::hash<int>{}(key.y);
418 std::size_t h3 = std::hash<int>{}(key.z);
419 return h1 ^ (h2 << 1) ^ (h3 << 2);
420 }
421 };
422
425 explicit PointPerceptionsOpsView(const PointPerceptions & perceptions);
426
433 explicit PointPerceptionsOpsView(const PointPerception & perception);
434
437 explicit PointPerceptionsOpsView(PointPerceptions && perceptions);
438
441
443
469 const std::vector<double> & min_bounds,
470 const std::vector<double> & max_bounds,
471 bool lazy_post_fuse = true);
472
476 PointPerceptionsOpsView & downsample(double resolution);
477
496 collapse(const std::vector<double> & collapse_dims, bool lazy = true);
497
500 pcl::PointCloud<pcl::PointXYZ> as_points() const;
501
505 const pcl::PointCloud<pcl::PointXYZ> & as_points(int idx) const;
506
518 PointPerceptionsOpsView & fuse(const std::string & target_frame, bool exact_time = false);
519
536 const std::string & target_frame,
537 rclcpp::Time & stamp,
538 bool exact_time = false);
539
555 add(
556 const pcl::PointCloud<pcl::PointXYZ> points,
557 const std::string & frame,
558 rclcpp::Time stamp);
559
562 const PointPerceptions & get_perceptions() const {return perceptions_;}
563
566 rclcpp::Time get_latest_stamp() const;
567
568private:
569 std::optional<PointPerceptions> owned_;
570 const PointPerceptions & perceptions_;
571 std::vector<pcl::PointIndices> indices_;
572
573 // Lazy fusion state
574 bool has_target_frame_ {false};
575 std::string target_frame_;
576 std::vector<tf2::Transform> tf_transforms_;
577 std::vector<bool> tf_valid_;
578
579 // Lazy collapse state
580 bool collapse_x_ {false};
581 bool collapse_y_ {false};
582 bool collapse_z_ {false};
583 float collapse_val_x_ {0.0f};
584 float collapse_val_y_ {0.0f};
585 float collapse_val_z_ {0.0f};
586
587 bool has_post_filter_ {false};
588 double post_min_[3] {0.0, 0.0, 0.0};
589 double post_max_[3] {0.0, 0.0, 0.0};
590 bool use_post_min_[3] {false, false, false};
591 bool use_post_max_[3] {false, false, false};
592
593 // Temporary storage for as_points(int)
594 mutable pcl::PointCloud<pcl::PointXYZ> tmp_single_cloud_;
595};
596
597} // namespace easynav
598
599#endif // EASYNAV_SENSORS_TYPES__POINTPERCEPTIONS_HPP_
Defines data structures and utilities for representing and processing sensor perceptions.
Fixed-size circular buffer, thread-safe (mutex-based), copyable.
Definition CircularBuffer.hpp:33
static void register_printer(std::function< std::string(const T &)> printer)
Registers a pretty-printer for type T used by debug_string().
Definition NavState.hpp:384
Abstract base class for representing a single sensor perception.
Definition Perceptions.hpp:44
bool valid
Whether the perception contains valid data.
Definition Perceptions.hpp:56
rclcpp::Time stamp
Timestamp of the perception (ROS time).
Definition Perceptions.hpp:49
bool new_data
Whether the data has changed since the last observation.
Definition Perceptions.hpp:59
std::string frame_id
Coordinate frame associated with the perception.
Definition Perceptions.hpp:52
Abstract base class for pluginlib-based sensor perception handlers.
Definition Perceptions.hpp:108
PerceptionHandler implementation for sensors producing point-based data.
Definition PointPerception.hpp:333
bool cycle_rt(std::shared_ptr< NavState > nav_state) override
Run one real-time sensor processing cycle.
Definition PointPerception.cpp:152
void on_initialize() override
Optional post-initialization hook for subclasses.
Definition PointPerception.cpp:92
Concrete perception class for 3D point cloud data.
Definition PointPerception.hpp:86
pcl::PointCloud< pcl::PointXYZ > pending_cloud_
Definition PointPerception.hpp:158
std::mutex mutex_
Definition PointPerception.hpp:323
static bool supports_msg_type(std::string_view t)
Checks if a ROS message type is supported by this perception.
Definition PointPerception.hpp:94
PointPerception()
Definition PointPerception.hpp:100
void set_pending_cloud(pcl::PointCloud< pcl::PointXYZ > &&cloud, std::string &&frame, const rclcpp::Time &stamp)
Stores a pending cloud atomically for later integration.
Definition PointPerception.hpp:163
CircularBuffer< PointPerceptionBufferType > buffer
Definition PointPerception.hpp:324
static constexpr std::string_view default_group_
Group identifier for point perceptions.
Definition PointPerception.hpp:89
std::string pending_frame_
Definition PointPerception.hpp:159
bool pending_available_
Definition PointPerception.hpp:157
pcl::PointCloud< pcl::PointXYZ > data
The 3D point cloud data associated with this perception.
Definition PointPerception.hpp:155
const PointPerceptionBufferType & get_last_perception() const
Retrieves the most recent buffered perception (independently of it has a valid TF) without removing i...
Definition PointPerception.hpp:184
PointPerception & operator=(const PointPerception &other)
Definition PointPerception.hpp:132
rclcpp::Time pending_stamp_
Definition PointPerception.hpp:160
PointPerception(const PointPerception &other)
Definition PointPerception.hpp:116
void resize(std::size_t size)
Resizes the internal point cloud storage.
Definition PointPerception.hpp:178
void integrate_pending_perceptions()
Definition PointPerception.hpp:189
PointPerceptionsOpsView & downsample(double resolution)
Downsamples each perception using a voxel grid.
Definition PointPerception.cpp:377
PointPerceptionsOpsView & add(const pcl::PointCloud< pcl::PointXYZ > points, const std::string &frame, rclcpp::Time stamp)
Adds a new perception to the current view.
Definition PointPerception.cpp:763
PointPerceptionsOpsView & fuse(const std::string &target_frame, bool exact_time=false)
Configures fusion of all perceptions into a common frame.
Definition PointPerception.cpp:619
PointPerceptionsOpsView(PointPerceptionsOpsView &&)=default
const PointPerceptions & get_perceptions() const
Provides a constant reference to the underlying perceptions container.
Definition PointPerception.hpp:562
pcl::PointCloud< pcl::PointXYZ > as_points() const
Retrieves all selected points across perceptions as a single concatenated cloud.
Definition PointPerception.cpp:489
PointPerceptionsOpsView(const PointPerceptions &perceptions)
Constructs a view over an external container of perceptions.
Definition PointPerception.cpp:213
rclcpp::Time get_latest_stamp() const
Retrieves the latest timestamp across all perceptions.
Definition PointPerception.cpp:819
PointPerceptionsOpsView & collapse(const std::vector< double > &collapse_dims, bool lazy=true)
Collapses dimensions to fixed values (for example, projection onto a plane).
Definition PointPerception.cpp:428
PointPerceptionsOpsView(const PointPerceptionsOpsView &)=delete
PointPerceptionsOpsView & operator=(const PointPerceptionsOpsView &)=delete
PointPerceptionsOpsView & filter(const std::vector< double > &min_bounds, const std::vector< double > &max_bounds, bool lazy_post_fuse=true)
Filters all point clouds by axis-aligned bounds.
Definition PointPerception.cpp:273
static RTTFBuffer * getInstance(Args &&... args)
Definition Singleton.hpp:31
Definition CircularBuffer.hpp:23
rclcpp::Time get_latest_point_perceptions_stamp(const PointPerceptions &perceptions)
Retrieves the latest timestamp among a set of point-based perceptions.
Definition PointPerception.cpp:796
sensor_msgs::msg::PointCloud2 points_to_rosmsg(const pcl::PointCloud< pcl::PointXYZ > &points)
Converts a PCL point cloud into a sensor_msgs::msg::PointCloud2 message.
Definition PointPerception.cpp:205
sensor_msgs::msg::PointCloud2 perception_to_rosmsg(const PointPerception &perception)
Converts a PointPerception into a sensor_msgs::msg::PointCloud2 message.
Definition PointPerception.cpp:195
void convert(const sensor_msgs::msg::LaserScan &scan, pcl::PointCloud< pcl::PointXYZ > &pc)
Converts a LaserScan message into a point cloud.
Definition PointPerception.cpp:163
std::vector< std::shared_ptr< PointPerception > > PointPerceptions
Alias for a vector of shared pointers to PointPerception objects.
Definition PointPerception.hpp:374
Definition PointPerception.hpp:50
Definition PointPerception.hpp:74
rclcpp::Time stamp
Definition PointPerception.hpp:77
pcl::PointCloud< pcl::PointXYZ > data
Definition PointPerception.hpp:75
std::string frame
Definition PointPerception.hpp:76
Hash functor for VoxelKey.
Definition PointPerception.hpp:410
std::size_t operator()(const VoxelKey &key) const
Computes the hash value.
Definition PointPerception.hpp:414
Discrete 3D voxel index used for downsampling.
Definition PointPerception.hpp:394
int y
Definition PointPerception.hpp:396
int z
Definition PointPerception.hpp:396
int x
Discrete coordinates (voxel indices).
Definition PointPerception.hpp:396
bool operator==(const VoxelKey &other) const
Equality comparison.
Definition PointPerception.hpp:401
std::size_t operator()(const std::tuple< int, int, int > &key) const
Computes the hash value.
Definition PointPerception.hpp:59