point_cloud_fusion v1.4.0
Loading...
Searching...
No Matches
point_cloud_fusion::PointCloudFusion Class Reference

PointCloudFusion class. More...

#include <point_cloud_fusion.hpp>

Inheritance diagram for point_cloud_fusion::PointCloudFusion:

Classes

struct  FusionTiming
 Timing metadata for one synchronized fusion batch. More...
 

Public Member Functions

 PointCloudFusion (const rclcpp::NodeOptions &options)
 Constructor.
 

Private Types

enum class  OutputStampMode { Latest , Earliest , Mean , Input0 }
 
using PointCloudMsg = sensor_msgs::msg::PointCloud2
 

Private Member Functions

template<typename T >
void declareAndLoadParameter (const std::string &name, T &param, const std::string &description, const bool add_to_auto_reconfigurable_params=true, const bool is_required=false, const bool read_only=false, const std::optional< double > &from_value=std::nullopt, const std::optional< double > &to_value=std::nullopt, const std::optional< double > &step_value=std::nullopt, const std::string &additional_constraints="")
 Declares and loads a ROS parameter.
 
rcl_interfaces::msg::SetParametersResult parametersCallback (const std::vector< rclcpp::Parameter > &parameters)
 Validate and apply dynamic parameter updates.
 
void setup ()
 Sets up subscribers, publishers, etc. to configure the node.
 
void handleSynchronizedPointClouds (const std::vector< sensor_msgs::msg::PointCloud2::ConstSharedPtr > &msgs)
 Process synchronized point clouds.
 
template<std::size_t N>
void setupSynchronizer ()
 Create the approximate-time synchronizer for a fixed number of input topics.
 
bool collectTimingInfo (const std::vector< PointCloudMsg::ConstSharedPtr > &msgs, FusionTiming &timing) const
 Collect timestamp statistics for synchronized input clouds.
 
PointCloudMsg::UniquePtr fusePointCloudBatch (const std::vector< PointCloudMsg::ConstSharedPtr > &msgs, const FusionTiming &timing, std::size_t &valid_point_count) const
 Fuse a synchronized point-cloud batch using the CPU path.
 
void publishFusedCloud (PointCloudMsg::UniquePtr cloud, const FusionTiming &timing, std::size_t input_count, std::size_t total_points, std::chrono::steady_clock::time_point callback_start, std::chrono::steady_clock::time_point processing_start, std::chrono::steady_clock::time_point processing_end, const char *event_name)
 Publish a fused point cloud and emit tracing/timing diagnostics.
 
void configureOutputStampMode (const std::string &mode)
 Parse and store the configured output timestamp mode.
 
void validateInputTopicsParameter () const
 Validate that the configured input topic list is usable.
 
void validateRangeLimits ()
 Validate configured XYZ range limits and disable filtering when invalid.
 

Private Attributes

double max_time_diff_sec_ = 0.05
 ROS parameters.
 
double age_penalty_ = 0.1
 
int64_t sync_queue_size_ = 3
 
int64_t output_queue_size_ = 10
 
int64_t fixed_points_per_input_cloud_ = 0
 
bool range_limits_enable_ = false
 
double range_limits_x_min_ = -1000.0
 
double range_limits_x_max_ = 1000.0
 
double range_limits_y_min_ = -1000.0
 
double range_limits_y_max_ = 1000.0
 
double range_limits_z_min_ = -20.0
 
double range_limits_z_max_ = 20.0
 
bool use_cuda_ = true
 
OutputStampMode output_stamp_mode_ = OutputStampMode::Earliest
 
std::string output_stamp_mode_param_ = "earliest"
 
std::string target_frame_ = "base_link"
 
std::vector< std::string > output_fields_
 
std::vector< std::string > input_topics_
 
std::vector< std::string > input_transport_hints_
 
std::vector< std::tuple< std::string, std::function< void(const rclcpp::Parameter &)> > > auto_reconfigurable_params_
 Auto-reconfigurable parameters for dynamic reconfiguration.
 
std::shared_mutex config_mutex_
 
std::vector< std::shared_ptr< point_cloud_transport::SubscriberFilter > > cloud_subscribers_
 
std::vector< rclcpp::CallbackGroup::SharedPtr > cloud_subscriber_callback_groups_
 
std::shared_ptr< void > synchronizer_
 
OnSetParametersCallbackHandle::SharedPtr parameters_callback_
 Callback handle for dynamic parameter reconfiguration.
 
std::shared_ptr< point_cloud_transport::Publisher > cloud_publisher_
 Publisher.
 
std::shared_ptr< tf2_ros::Buffer > tf_buffer_
 TF2 buffer and transform listener.
 
std::shared_ptr< tf2_ros::TransformListener > tf_listener_
 
rclcpp::TimerBase::SharedPtr setup_timer_
 Timer to delay setup.
 

Static Private Attributes

static constexpr int32_t kMinSyncQueueSize = 1
 
static constexpr int32_t kMaxSyncQueueSize = 1000
 
static constexpr int32_t kStepSizeSyncQueueSize = 1
 
static constexpr int32_t kMinOutputQueueSize = 1
 
static constexpr int32_t kMaxOutputQueueSize = 1000
 
static constexpr int32_t kStepSizeOutputQueueSize = 1
 
static constexpr int64_t kMinFixedPointsPerInputCloud = 0
 
static constexpr int64_t kMaxFixedPointsPerInputCloud = 10000000
 
static constexpr int64_t kStepSizeFixedPointsPerInputCloud = 100
 
static constexpr std::size_t kMaxInputTopics = 9
 
static constexpr const char * kDefaultTransportHint = "raw"
 
static constexpr const char * kAllowedOutputStampModes = "latest, earliest, mean, input0"
 
static constexpr double kMinRangeXY = -1000.0
 
static constexpr double kMaxRangeXY = 1000.0
 
static constexpr double kMinRangeZ = -20.0
 
static constexpr double kMaxRangeZ = 20.0
 

Detailed Description

PointCloudFusion class.

Definition at line 45 of file point_cloud_fusion.hpp.

Member Typedef Documentation

◆ PointCloudMsg

using point_cloud_fusion::PointCloudFusion::PointCloudMsg = sensor_msgs::msg::PointCloud2
private

Definition at line 111 of file point_cloud_fusion.hpp.

Member Enumeration Documentation

◆ OutputStampMode

Constructor & Destructor Documentation

◆ PointCloudFusion()

point_cloud_fusion::PointCloudFusion::PointCloudFusion ( const rclcpp::NodeOptions & options)
explicit

Constructor.

Parameters
optionsnode options

Definition at line 114 of file point_cloud_fusion.cpp.

114 : Node("point_cloud_fusion", options) {
115 this->declareAndLoadParameter("target_frame", target_frame_, // name
116 "Frame into which all input point clouds are transformed before fusion", // description
117 false, // add_to_auto_reconfigurable_params
118 true, // is_required
119 true, // read_only
120 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
121 "Must be set."); // additional_constraints
122 this->declareAndLoadParameter("input_topics", input_topics_, // name
123 "Point-cloud topics to fuse", // description
124 false, // add_to_auto_reconfigurable_params
125 true, // is_required
126 true, // read_only
127 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
128 "Must configure between 1 and " + std::to_string(kMaxInputTopics) + " topics");
130 this->declareAndLoadParameter("input_transport_hints", input_transport_hints_, // name
131 "Transport hint for each input topic; unspecified entries use raw", // description
132 false, // add_to_auto_reconfigurable_params
133 false, // is_required
134 true, // read_only
135 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
136 "Length must be zero or match input_topics; unspecified entries default to '" +
137 std::string(kDefaultTransportHint) + "'."); // additional_constraints
138 this->declareAndLoadParameter("sync_queue_size", sync_queue_size_, // name
139 "Queue depth for approximate-time synchronization", // description
140 false, // add_to_auto_reconfigurable_params
141 false, // is_required
142 true, // read_only
143 kMinSyncQueueSize, // from_value
144 kMaxSyncQueueSize, // to_value
145 kStepSizeSyncQueueSize, // step_value
146 std::string("Must be >= ") + std::to_string(kMinSyncQueueSize)); // additional_constraints
147 this->declareAndLoadParameter("output_queue_size", output_queue_size_, // name
148 "Queue depth for the fused output publisher", // description
149 false, // add_to_auto_reconfigurable_params
150 false, // is_required
151 true, // read_only
152 kMinOutputQueueSize, // from_value
153 kMaxOutputQueueSize, // to_value
154 kStepSizeOutputQueueSize, // step_value
155 std::string("Must be >= ") + std::to_string(kMinOutputQueueSize)); // additional_constraints
157 "output_fields", output_fields_, // name
158 "Fields retained in the fused output; an empty list retains all input fields", // description
159 true, // add_to_auto_reconfigurable_params
160 false, // is_required
161 false, // read_only
162 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
163 "Typical fields include: x, y, z, intensity, t, reflectivity, ring, ambient, range."); // additional_constraints
164 this->declareAndLoadParameter("output_stamp_mode", output_stamp_mode_param_, // name
165 "Fused timestamp selection: earliest, latest, mean, or input0",
166 false, // add_to_auto_reconfigurable_params
167 false, // is_required
168 true, // read_only
169 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
170 std::string("Allowed values: ") + kAllowedOutputStampModes); // additional_constraints
171 // Allow user to optionally limit the per-cloud point count to a maximum
172 this->declareAndLoadParameter("fixed_points_per_input_cloud", fixed_points_per_input_cloud_,
173 "Runtime-reconfigurable maximum valid point count per input cloud; 0 disables the limit",
174 true, // add_to_auto_reconfigurable_params
175 false, // is_required
176 false, // read_only
177 kMinFixedPointsPerInputCloud, // from_value
180 "0 = disabled; reasonable range is 0 to 10,000,000 points per input cloud");
181 this->declareAndLoadParameter("use_cuda", use_cuda_, // name
182 "Runtime-reconfigurable backend selection; true uses CUDA and false uses CPU",
183 true, // add_to_auto_reconfigurable_params
184 false, // is_required
185 false, // read_only
186 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
187 "Runtime changes apply between fusion batches."); // additional_constraints
188 this->declareAndLoadParameter("range_limits.enable", range_limits_enable_, // name
189 "Enable XYZ range filtering after transformation into target_frame",
190 true, // add_to_auto_reconfigurable_params
191 false, // is_required
192 false, // read_only
193 std::nullopt, std::nullopt, std::nullopt, // from_value, to_value, step_value
194 "When false, no range filtering is applied."); // additional_constraints
195 this->declareAndLoadParameter("range_limits.x_min", range_limits_x_min_, // name
196 "Minimum x coordinate in target_frame to keep [m]", // description
197 true, // add_to_auto_reconfigurable_params
198 false, // is_required
199 false, // read_only
200 kMinRangeXY, // from_value
201 kMaxRangeXY, // to_value
202 std::nullopt, // step_value
203 "Must be less than range_limits.x_max."); // additional_constraints
204 this->declareAndLoadParameter("range_limits.x_max", range_limits_x_max_, // name
205 "Maximum x coordinate in target_frame to keep [m]", // description
206 true, // add_to_auto_reconfigurable_params
207 false, // is_required
208 false, // read_only
209 kMinRangeXY, // from_value
210 kMaxRangeXY, // to_value
211 std::nullopt, // step_value
212 "Must be greater than range_limits.x_min."); // additional_constraints
213 this->declareAndLoadParameter("range_limits.y_min", range_limits_y_min_, // name
214 "Minimum y coordinate in target_frame to keep [m]", // description
215 true, // add_to_auto_reconfigurable_params
216 false, // is_required
217 false, // read_only
218 kMinRangeXY, // from_value
219 kMaxRangeXY, // to_value
220 std::nullopt, // step_value
221 "Must be less than range_limits.y_max."); // additional_constraints
222 this->declareAndLoadParameter("range_limits.y_max", range_limits_y_max_, // name
223 "Maximum y coordinate in target_frame to keep [m]", // description
224 true, // add_to_auto_reconfigurable_params
225 false, // is_required
226 false, // read_only
227 kMinRangeXY, // from_value
228 kMaxRangeXY, // to_value
229 std::nullopt, // step_value
230 "Must be greater than range_limits.y_min."); // additional_constraints
231 this->declareAndLoadParameter("range_limits.z_min", range_limits_z_min_, // name
232 "Minimum z coordinate in target_frame to keep [m]", // description
233 true, // add_to_auto_reconfigurable_params
234 false, // is_required
235 false, // read_only
236 kMinRangeZ, // from_value
237 kMaxRangeZ, // to_value
238 std::nullopt, // step_value
239 "Must be less than range_limits.z_max."); // additional_constraints
240 this->declareAndLoadParameter("range_limits.z_max", range_limits_z_max_, // name
241 "Maximum z coordinate in target_frame to keep [m]", // description
242 true, // add_to_auto_reconfigurable_params
243 false, // is_required
244 false, // read_only
245 kMinRangeZ, // from_value
246 kMaxRangeZ, // to_value
247 std::nullopt, // step_value
248 "Must be greater than range_limits.z_min."); // additional_constraints
250 this->declareAndLoadParameter("max_time_diff_sec", max_time_diff_sec_, // name
251 "Maximum timestamp spread across a synchronized input batch in seconds",
252 false, // add_to_auto_reconfigurable_params
253 false, // is_required
254 true, // read_only
255 0.0, std::nullopt, std::nullopt, // from_value, to_value, step_value
256 "Must be non-negative"); // additional_constraints
257 this->declareAndLoadParameter("age_penalty", age_penalty_, // name
258 "Age penalty used by the approximate-time synchronizer",
259 false, // add_to_auto_reconfigurable_params
260 false, // is_required
261 true, // read_only
262 0.0, // from_value
263 100.0, // to_value
264 std::nullopt, // step_value
265 "Valid range is [0, 100]."); // additional_constraints
267
268#ifdef ENABLE_CUDA
269 // Keep the CUDA context available even when starting in CPU mode so the
270 // backend can be switched safely at runtime.
271 try {
272 cuda_context_ = std::make_unique<cuda::CudaTransformContext>();
273 if (use_cuda_) {
274 RCLCPP_INFO(this->get_logger(), "CUDA acceleration enabled");
275 } else {
276 RCLCPP_INFO(this->get_logger(), "CUDA context initialized; using CPU backend by parameter");
277 }
278 } catch (const std::exception& e) {
279 RCLCPP_ERROR(this->get_logger(), "Failed to initialize CUDA context: %s", e.what());
280 RCLCPP_WARN(this->get_logger(), "Falling back to CPU-only implementation");
281 cuda_context_.reset();
282 use_cuda_ = false;
283 this->set_parameter(rclcpp::Parameter("use_cuda", false));
284 }
285#else
286 RCLCPP_INFO(this->get_logger(), "CUDA support not compiled, using CPU-only implementation");
287 use_cuda_ = false;
288 this->set_parameter(rclcpp::Parameter("use_cuda", false));
289#endif
290
291 // run setup after constructor has finished to enable shared_from_this()
292 setup_timer_ = this->create_wall_timer(std::chrono::milliseconds(1), [this]() {
293 setup();
294 setup_timer_->cancel();
295 });
296}
void validateInputTopicsParameter() const
Validate that the configured input topic list is usable.
static constexpr int32_t kStepSizeOutputQueueSize
static constexpr std::size_t kMaxInputTopics
static constexpr const char * kAllowedOutputStampModes
void validateRangeLimits()
Validate configured XYZ range limits and disable filtering when invalid.
std::vector< std::string > output_fields_
void configureOutputStampMode(const std::string &mode)
Parse and store the configured output timestamp mode.
void declareAndLoadParameter(const std::string &name, T &param, const std::string &description, const bool add_to_auto_reconfigurable_params=true, const bool is_required=false, const bool read_only=false, const std::optional< double > &from_value=std::nullopt, const std::optional< double > &to_value=std::nullopt, const std::optional< double > &step_value=std::nullopt, const std::string &additional_constraints="")
Declares and loads a ROS parameter.
void setup()
Sets up subscribers, publishers, etc. to configure the node.
rclcpp::TimerBase::SharedPtr setup_timer_
Timer to delay setup.
static constexpr const char * kDefaultTransportHint
std::vector< std::string > input_transport_hints_
static constexpr int64_t kMaxFixedPointsPerInputCloud
static constexpr int32_t kStepSizeSyncQueueSize
static constexpr int64_t kMinFixedPointsPerInputCloud
static constexpr int64_t kStepSizeFixedPointsPerInputCloud

Member Function Documentation

◆ collectTimingInfo()

bool point_cloud_fusion::PointCloudFusion::collectTimingInfo ( const std::vector< PointCloudMsg::ConstSharedPtr > & msgs,
FusionTiming & timing ) const
private

Collect timestamp statistics for synchronized input clouds.

Parameters
msgsSynchronized input point clouds.
timingOutput timing metadata populated from the batch.
Returns
True if timing metadata could be collected for the batch.

Definition at line 829 of file point_cloud_fusion.cpp.

829 {
830 if (msgs.empty()) {
831 return false;
832 }
833
834 const auto input0_stamp = rclcpp::Time(msgs.front()->header.stamp);
835 bool first_stamp = true;
836 double max_dt_from_input0_sec = 0.0;
837 rclcpp::Time earliest_stamp;
838 rclcpp::Time latest_stamp;
839
840 // Walk every cloud once to gather min/max stamps and the largest skew from
841 // input0.
842 for (const auto& pc_msg : msgs) {
843 if (!pc_msg) {
844 RCLCPP_WARN(this->get_logger(),
845 "Received null point cloud pointer in "
846 "synchronized batch, skipping fusion");
847 return false;
848 }
849
850 const rclcpp::Time current_stamp(pc_msg->header.stamp);
851 if (first_stamp) {
852 earliest_stamp = current_stamp;
853 latest_stamp = current_stamp;
854 first_stamp = false;
855 } else {
856 if (current_stamp < earliest_stamp) earliest_stamp = current_stamp;
857 if (current_stamp > latest_stamp) latest_stamp = current_stamp;
858 }
859
860 const double dt_sec = std::fabs((current_stamp - input0_stamp).seconds());
861 if (dt_sec > max_dt_from_input0_sec) {
862 max_dt_from_input0_sec = dt_sec;
863 }
864 }
865
866 timing.earliest_stamp = earliest_stamp;
867 timing.latest_stamp = latest_stamp;
868 timing.input0_stamp = input0_stamp;
869 timing.max_dt_from_input0_sec = max_dt_from_input0_sec;
870 return true;
871}

◆ configureOutputStampMode()

void point_cloud_fusion::PointCloudFusion::configureOutputStampMode ( const std::string & mode)
private

Parse and store the configured output timestamp mode.

Parameters
modeTimestamp mode parameter value.

Definition at line 1232 of file point_cloud_fusion.cpp.

1232 {
1233 std::string lowered = mode;
1234 std::transform(lowered.begin(), lowered.end(), lowered.begin(),
1235 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
1236
1237 if (lowered == "earliest") {
1239 } else if (lowered == "mean" || lowered == "mid" || lowered == "midpoint") {
1241 } else if (lowered == "input0") {
1243 } else if (lowered == "latest") {
1245 } else {
1246 RCLCPP_WARN(this->get_logger(), "Invalid output_stamp_mode '%s'; defaulting to 'earliest'.", mode.c_str());
1248 }
1249}

◆ declareAndLoadParameter()

template<typename T >
void point_cloud_fusion::PointCloudFusion::declareAndLoadParameter ( const std::string & name,
T & param,
const std::string & description,
const bool add_to_auto_reconfigurable_params = true,
const bool is_required = false,
const bool read_only = false,
const std::optional< double > & from_value = std::nullopt,
const std::optional< double > & to_value = std::nullopt,
const std::optional< double > & step_value = std::nullopt,
const std::string & additional_constraints = "" )
private

Declares and loads a ROS parameter.

Parameters
namename
paramparameter variable to load into
descriptiondescription
add_to_auto_reconfigurable_paramsenable reconfiguration of parameter
is_requiredwhether failure to load parameter will stop node
read_onlyset parameter to read-only
from_valueparameter range minimum
to_valueparameter range maximum
step_valueparameter range step
additional_constraintsadditional constraints description

Definition at line 300 of file point_cloud_fusion.cpp.

309 {
310 rcl_interfaces::msg::ParameterDescriptor param_desc;
311 param_desc.description = description;
312 param_desc.additional_constraints = additional_constraints;
313 param_desc.read_only = read_only;
314
315 auto type = rclcpp::ParameterValue(param).get_type();
316
317 if (from_value.has_value() && to_value.has_value()) {
318 if constexpr (std::is_integral_v<T>) {
319 rcl_interfaces::msg::IntegerRange range;
320 range.set__from_value(static_cast<T>(from_value.value())).set__to_value(static_cast<T>(to_value.value()));
321 if (step_value.has_value()) range.set__step(static_cast<T>(step_value.value()));
322 param_desc.integer_range = {range};
323 } else if constexpr (std::is_floating_point_v<T>) {
324 rcl_interfaces::msg::FloatingPointRange range;
325 range.set__from_value(static_cast<T>(from_value.value())).set__to_value(static_cast<T>(to_value.value()));
326 if (step_value.has_value()) range.set__step(static_cast<T>(step_value.value()));
327 param_desc.floating_point_range = {range};
328 } else {
329 RCLCPP_WARN(this->get_logger(),
330 "Parameter type of parameter '%s' does not support "
331 "specifying a range",
332 name.c_str());
333 }
334 }
335
336 this->declare_parameter(name, type, param_desc);
337
338 try {
339 param = this->get_parameter(name).get_value<T>();
340 std::stringstream ss;
341 ss << "Loaded parameter '" << name << "': ";
342 if constexpr (is_vector_v<T>) {
343 ss << "[";
344 for (const auto& element : param) ss << element << (&element != &param.back() ? ", " : "");
345 ss << "]";
346 } else {
347 ss << param;
348 }
349 RCLCPP_INFO_STREAM(this->get_logger(), ss.str());
350 } catch (rclcpp::exceptions::ParameterUninitializedException&) {
351 if (is_required) {
352 RCLCPP_FATAL_STREAM(this->get_logger(), "Missing required parameter '" << name << "', exiting");
353 exit(EXIT_FAILURE);
354 } else {
355 std::stringstream ss;
356 ss << "Missing parameter '" << name << "', using default value: ";
357 if constexpr (is_vector_v<T>) {
358 ss << "[";
359 for (const auto& element : param) ss << element << (&element != &param.back() ? ", " : "");
360 ss << "]";
361 } else {
362 ss << param;
363 }
364 RCLCPP_WARN_STREAM(this->get_logger(), ss.str());
365 this->set_parameters({rclcpp::Parameter(name, rclcpp::ParameterValue(param))});
366 }
367 }
368
369 if (add_to_auto_reconfigurable_params) {
370 std::function<void(const rclcpp::Parameter&)> setter = [&param](const rclcpp::Parameter& p) { param = p.get_value<T>(); };
371 auto_reconfigurable_params_.push_back(std::make_tuple(name, setter));
372 }
373}
std::vector< std::tuple< std::string, std::function< void(const rclcpp::Parameter &)> > > auto_reconfigurable_params_
Auto-reconfigurable parameters for dynamic reconfiguration.

◆ fusePointCloudBatch()

PointCloudFusion::PointCloudMsg::UniquePtr point_cloud_fusion::PointCloudFusion::fusePointCloudBatch ( const std::vector< PointCloudMsg::ConstSharedPtr > & msgs,
const FusionTiming & timing,
std::size_t & valid_point_count ) const
private

Fuse a synchronized point-cloud batch using the CPU path.

Parameters
msgsSynchronized input point clouds.
timingTiming metadata for the batch.
valid_point_countNumber of valid points written to the output cloud.
Returns
Fused point cloud.

Definition at line 873 of file point_cloud_fusion.cpp.

874 {
875 if (msgs.empty()) {
876 return nullptr;
877 }
878
879 const auto& input0_msg = msgs.front();
880 if (!input0_msg) {
881 return nullptr;
882 }
883
884 std::optional<uint32_t> x_offset;
885 std::optional<uint32_t> y_offset;
886 std::optional<uint32_t> z_offset;
887 for (const auto& field : input0_msg->fields) {
888 if (field.name == "x") {
889 x_offset = field.offset;
890 } else if (field.name == "y") {
891 y_offset = field.offset;
892 } else if (field.name == "z") {
893 z_offset = field.offset;
894 }
895 }
896
897 if (!x_offset || !y_offset || !z_offset) {
898 RCLCPP_WARN(this->get_logger(), "Point cloud lacks x/y/z fields; skipping fusion for this batch.");
899 valid_point_count = 0;
900 return nullptr;
901 }
902
903 const size_t point_step = input0_msg->point_step;
904 const auto& input0_fields = input0_msg->fields;
905 const bool is_bigendian = input0_msg->is_bigendian;
906
907 struct FieldCopyPlan {
908 const sensor_msgs::msg::PointField* source;
909 sensor_msgs::msg::PointField destination;
910 std::size_t byte_length;
911 };
912
913 bool use_all_fields = output_fields_.empty();
914 std::vector<FieldCopyPlan> copy_plan;
915 std::vector<sensor_msgs::msg::PointField> fused_fields;
916 fused_fields.reserve(input0_fields.size());
917
918 std::size_t fused_point_step = point_step;
919 std::optional<uint32_t> fused_x_offset = x_offset;
920 std::optional<uint32_t> fused_y_offset = y_offset;
921 std::optional<uint32_t> fused_z_offset = z_offset;
922
923 if (!use_all_fields) {
924 fused_x_offset.reset();
925 fused_y_offset.reset();
926 fused_z_offset.reset();
927 bool selection_valid = true;
928 fused_point_step = 0;
929 fused_fields.clear();
930 copy_plan.reserve(output_fields_.size());
931
932 for (const auto& requested_name : output_fields_) {
933 auto iter =
934 std::find_if(input0_fields.begin(), input0_fields.end(),
935 [&requested_name](const sensor_msgs::msg::PointField& field) { return field.name == requested_name; });
936 if (iter == input0_fields.end()) {
937 RCLCPP_WARN(this->get_logger(),
938 "Requested output field '%s' not present in incoming point "
939 "cloud; publishing full field set instead.",
940 requested_name.c_str());
941 selection_valid = false;
942 break;
943 }
944
945 const std::size_t datatype_size = pointFieldDatatypeSize(iter->datatype);
946 if (datatype_size == 0) {
947 RCLCPP_WARN(this->get_logger(),
948 "Point field '%s' uses unsupported datatype %u; publishing "
949 "full field set instead.",
950 requested_name.c_str(), static_cast<unsigned int>(iter->datatype));
951 selection_valid = false;
952 break;
953 }
954
955 FieldCopyPlan plan;
956 plan.source = &(*iter);
957 plan.destination = *iter;
958 plan.destination.offset = static_cast<uint32_t>(fused_point_step);
959 plan.byte_length = datatype_size * static_cast<std::size_t>(iter->count);
960 fused_point_step += plan.byte_length;
961
962 if (requested_name == "x") {
963 fused_x_offset = plan.destination.offset;
964 } else if (requested_name == "y") {
965 fused_y_offset = plan.destination.offset;
966 } else if (requested_name == "z") {
967 fused_z_offset = plan.destination.offset;
968 }
969
970 copy_plan.push_back(plan);
971 fused_fields.push_back(plan.destination);
972 }
973
974 if (!selection_valid || !fused_x_offset || !fused_y_offset || !fused_z_offset) {
975 if (selection_valid) {
976 RCLCPP_ERROR(this->get_logger(),
977 "Output field selection must include x, y, and z; "
978 "publishing full field set instead.");
979 }
980 use_all_fields = true;
981 copy_plan.clear();
982 fused_point_step = point_step;
983 fused_fields.assign(input0_fields.begin(), input0_fields.end());
984 fused_x_offset = x_offset;
985 fused_y_offset = y_offset;
986 fused_z_offset = z_offset;
987 }
988 }
989
990 if (use_all_fields) {
991 fused_fields.assign(input0_fields.begin(), input0_fields.end());
992 fused_point_step = point_step;
993 fused_x_offset = x_offset;
994 fused_y_offset = y_offset;
995 fused_z_offset = z_offset;
996 }
997
998 // Reserve enough space once so the fusion loop only appends into a pre-sized
999 // buffer.
1000 const size_t max_capacity = std::accumulate(
1001 msgs.begin(), msgs.end(), static_cast<size_t>(0), [this](size_t sum, const PointCloudMsg::ConstSharedPtr& cloud) {
1002 if (!cloud) {
1003 return sum;
1004 }
1005 size_t cloud_size = static_cast<size_t>(cloud->width) * static_cast<size_t>(cloud->height);
1006 // If user set a cap, limit each cloud to that size
1008 cloud_size = std::min(cloud_size, static_cast<size_t>(fixed_points_per_input_cloud_));
1009 }
1010 return sum + cloud_size;
1011 });
1012
1013 if (max_capacity == 0) {
1014 valid_point_count = 0;
1015 return nullptr;
1016 }
1017
1018 auto output = std::make_unique<PointCloudMsg>();
1019 output->header.frame_id = target_frame_;
1020 rclcpp::Time chosen_stamp;
1021 switch (output_stamp_mode_) {
1023 chosen_stamp = timing.earliest_stamp;
1024 break;
1025 case OutputStampMode::Mean: {
1026 const auto delta = timing.latest_stamp - timing.earliest_stamp;
1027 chosen_stamp = timing.earliest_stamp + rclcpp::Duration::from_nanoseconds(delta.nanoseconds() / 2);
1028 break;
1029 }
1031 chosen_stamp = timing.input0_stamp;
1032 break;
1034 default:
1035 chosen_stamp = timing.latest_stamp;
1036 break;
1037 }
1038 output->header.stamp = chosen_stamp;
1039 output->height = 1;
1040 output->is_bigendian = is_bigendian;
1041 output->point_step = fused_point_step;
1042 output->fields = fused_fields;
1043 output->is_dense = true;
1044 output->data.resize(max_capacity * fused_point_step);
1045
1046 uint8_t* dest_ptr = output->data.data();
1047 valid_point_count = 0;
1048 std::size_t skipped_inputs = 0;
1049
1050 // Pre-cache range limits as float to avoid per-point double→float conversion.
1051 const bool check_range = range_limits_enable_;
1052 const float rl_x_min = static_cast<float>(range_limits_x_min_);
1053 const float rl_x_max = static_cast<float>(range_limits_x_max_);
1054 const float rl_y_min = static_cast<float>(range_limits_y_min_);
1055 const float rl_y_max = static_cast<float>(range_limits_y_max_);
1056 const float rl_z_min = static_cast<float>(range_limits_z_min_);
1057 const float rl_z_max = static_cast<float>(range_limits_z_max_);
1058
1059 for (const auto& msg : msgs) {
1060 if (!msg) {
1061 continue;
1062 }
1063
1064 if (msg->point_step != point_step || msg->fields != input0_fields) {
1065 RCLCPP_WARN(this->get_logger(), "Skipping point cloud '%s' due to incompatible field layout.",
1066 msg->header.frame_id.c_str());
1067 ++skipped_inputs;
1068 continue;
1069 }
1070
1071 // Cache the frame transform once per cloud to avoid repeated TF queries
1072 // inside the point loop.
1073 const bool apply_transform = msg->header.frame_id != target_frame_;
1074 tf2::Vector3 translation;
1075 tf2::Matrix3x3 rotation;
1076 if (apply_transform) {
1077 tf2::Transform tf_transform;
1078 geometry_msgs::msg::TransformStamped tf_stamped;
1079 try {
1080 tf_stamped = tf_buffer_->lookupTransform(target_frame_, msg->header.frame_id, msg->header.stamp,
1081 rclcpp::Duration::from_seconds(0.1));
1082 } catch (const tf2::TransformException& ex) {
1083 RCLCPP_ERROR(this->get_logger(), "Cannot transform point cloud from %s to %s: %s", msg->header.frame_id.c_str(),
1084 target_frame_.c_str(), ex.what());
1085 ++skipped_inputs;
1086 continue;
1087 }
1088 tf2::fromMsg(tf_stamped.transform, tf_transform);
1089 translation = tf_transform.getOrigin();
1090 rotation = tf_transform.getBasis();
1091 }
1092
1093 const auto* src_data = msg->data.data();
1094 const size_t total_points = static_cast<size_t>(msg->width) * static_cast<size_t>(msg->height);
1095
1096 auto emit_point = [&](const uint8_t* point_ptr, float x, float y, float z, bool overwrite_xyz) {
1097 if (use_all_fields) {
1098 std::memcpy(dest_ptr, point_ptr, point_step);
1099 } else {
1100 for (const auto& plan : copy_plan) {
1101 std::memcpy(byteOffset(dest_ptr, plan.destination.offset), byteOffset(point_ptr, plan.source->offset),
1102 plan.byte_length);
1103 }
1104 }
1105
1106 if (overwrite_xyz) {
1107 storeFloat(dest_ptr, *fused_x_offset, x);
1108 storeFloat(dest_ptr, *fused_y_offset, y);
1109 storeFloat(dest_ptr, *fused_z_offset, z);
1110 }
1111
1112 dest_ptr = byteOffset(dest_ptr, fused_point_step);
1113 ++valid_point_count;
1114 };
1115
1116 if (fixed_points_per_input_cloud_ <= 0 || static_cast<size_t>(fixed_points_per_input_cloud_) >= total_points) {
1117 // Fast path: when no downsampling is requested.
1118 for (size_t idx = 0; idx < total_points; ++idx) {
1119 const auto* point_ptr = byteOffset(src_data, idx * point_step);
1120 const float x = loadFloat(point_ptr, *x_offset);
1121 const float y = loadFloat(point_ptr, *y_offset);
1122 const float z = loadFloat(point_ptr, *z_offset);
1123
1124 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) {
1125 continue;
1126 }
1127
1128 if (!apply_transform) {
1129 if (check_range && !pointWithinRange(x, y, z, rl_x_min, rl_x_max, rl_y_min, rl_y_max, rl_z_min, rl_z_max)) {
1130 continue;
1131 }
1132 emit_point(point_ptr, x, y, z, false);
1133 continue;
1134 }
1135
1136 const tf2::Vector3 rotated = rotation * tf2::Vector3(x, y, z) + translation;
1137 const float transformed_x = static_cast<float>(rotated.x());
1138 const float transformed_y = static_cast<float>(rotated.y());
1139 const float transformed_z = static_cast<float>(rotated.z());
1140
1141 if (check_range && !pointWithinRange(transformed_x, transformed_y, transformed_z, rl_x_min, rl_x_max, rl_y_min, rl_y_max,
1142 rl_z_min, rl_z_max)) {
1143 continue;
1144 }
1145
1146 emit_point(point_ptr, transformed_x, transformed_y, transformed_z, true);
1147 }
1148 continue;
1149 }
1150
1151 // Downsample path: strided sampling for uniform spatial distribution.
1152 const size_t desired_points = static_cast<size_t>(fixed_points_per_input_cloud_);
1153 const double stride = static_cast<double>(total_points) / static_cast<double>(desired_points);
1154 const size_t num_samples = desired_points;
1155
1156 for (size_t i = 0; i < num_samples; ++i) {
1157 const size_t idx = std::min(static_cast<size_t>(static_cast<double>(i) * stride), total_points - 1);
1158
1159 const auto* point_ptr = byteOffset(src_data, idx * point_step);
1160 const float x = loadFloat(point_ptr, *x_offset);
1161 const float y = loadFloat(point_ptr, *y_offset);
1162 const float z = loadFloat(point_ptr, *z_offset);
1163
1164 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) {
1165 continue;
1166 }
1167
1168 if (!apply_transform) {
1169 if (check_range && !pointWithinRange(x, y, z, rl_x_min, rl_x_max, rl_y_min, rl_y_max, rl_z_min, rl_z_max)) {
1170 continue;
1171 }
1172 emit_point(point_ptr, x, y, z, false);
1173 continue;
1174 }
1175
1176 const tf2::Vector3 rotated = rotation * tf2::Vector3(x, y, z) + translation;
1177 const float transformed_x = static_cast<float>(rotated.x());
1178 const float transformed_y = static_cast<float>(rotated.y());
1179 const float transformed_z = static_cast<float>(rotated.z());
1180
1181 if (check_range && !pointWithinRange(transformed_x, transformed_y, transformed_z, rl_x_min, rl_x_max, rl_y_min, rl_y_max,
1182 rl_z_min, rl_z_max)) {
1183 continue;
1184 }
1185
1186 emit_point(point_ptr, transformed_x, transformed_y, transformed_z, true);
1187 }
1188 }
1189
1190 if (valid_point_count == 0) {
1191 if (skipped_inputs == msgs.size()) {
1192 RCLCPP_WARN(this->get_logger(), "Skipped all point clouds in synchronized batch; no data fused.");
1193 }
1194 return nullptr;
1195 }
1196
1197 output->width = valid_point_count;
1198 output->row_step = output->point_step * output->width;
1199 output->data.resize(valid_point_count * fused_point_step);
1200
1201 output->is_dense = true;
1202 return output;
1203}
std::shared_ptr< tf2_ros::Buffer > tf_buffer_
TF2 buffer and transform listener.

◆ handleSynchronizedPointClouds()

void point_cloud_fusion::PointCloudFusion::handleSynchronizedPointClouds ( const std::vector< sensor_msgs::msg::PointCloud2::ConstSharedPtr > & msgs)
private

Process synchronized point clouds.

Parameters
msgsbatch of synchronized point clouds

Definition at line 768 of file point_cloud_fusion.cpp.

768 {
769 if (msgs.empty()) {
770 return;
771 }
772
773 // Protect runtime-configurable parameter reads against concurrent parameter
774 // updates.
775 std::shared_lock<std::shared_mutex> config_lock(config_mutex_);
776
777 const auto callback_start = std::chrono::steady_clock::now();
778
779 FusionTiming timing;
780 if (!collectTimingInfo(msgs, timing)) {
781 return;
782 }
783
784#ifdef ENABLE_CUDA
785 std::size_t cuda_valid_count = 0;
786 auto processing_start = std::chrono::steady_clock::time_point{};
787 auto processing_end = std::chrono::steady_clock::time_point{};
788 PointCloudMsg::UniquePtr cuda_result;
789 bool used_cuda = false;
790 {
791 // Guard shared CUDA pipeline state against concurrent synchronized
792 // callbacks.
793 std::lock_guard<std::mutex> cuda_lock(cuda_context_mutex_);
794 // Run either CPU or CUDA implementation based on parameter.
795 if (cuda_context_ && use_cuda_) {
796 used_cuda = true;
797 processing_start = std::chrono::steady_clock::now();
798 cuda_result = fusePointCloudBatchCUDA(msgs, timing, cuda_valid_count);
799 processing_end = std::chrono::steady_clock::now();
800 }
801 }
802
803 if (used_cuda) {
804 if (cuda_result) {
805 publishFusedCloud(std::move(cuda_result), timing, msgs.size(), cuda_valid_count, callback_start, processing_start,
806 processing_end, "cuda_fusion_complete");
807 } else {
808 RCLCPP_WARN(this->get_logger(), "CUDA processing failed");
809 }
810 return;
811 }
812#endif
813
814 // CPU-only path
815 std::size_t valid_count = 0;
816 const auto cpu_processing_start = std::chrono::steady_clock::now();
817 auto fused_point_cloud = fusePointCloudBatch(msgs, timing, valid_count);
818
819 if (!fused_point_cloud) {
820 RCLCPP_WARN(this->get_logger(), "All points are invalid, skipping fusion");
821 return;
822 }
823
824 const auto cpu_processing_end = std::chrono::steady_clock::now();
825 publishFusedCloud(std::move(fused_point_cloud), timing, msgs.size(), valid_count, callback_start, cpu_processing_start,
826 cpu_processing_end, "cpu_fusion_complete");
827}
PointCloudMsg::UniquePtr fusePointCloudBatch(const std::vector< PointCloudMsg::ConstSharedPtr > &msgs, const FusionTiming &timing, std::size_t &valid_point_count) const
Fuse a synchronized point-cloud batch using the CPU path.
bool collectTimingInfo(const std::vector< PointCloudMsg::ConstSharedPtr > &msgs, FusionTiming &timing) const
Collect timestamp statistics for synchronized input clouds.
void publishFusedCloud(PointCloudMsg::UniquePtr cloud, const FusionTiming &timing, std::size_t input_count, std::size_t total_points, std::chrono::steady_clock::time_point callback_start, std::chrono::steady_clock::time_point processing_start, std::chrono::steady_clock::time_point processing_end, const char *event_name)
Publish a fused point cloud and emit tracing/timing diagnostics.

◆ parametersCallback()

rcl_interfaces::msg::SetParametersResult point_cloud_fusion::PointCloudFusion::parametersCallback ( const std::vector< rclcpp::Parameter > & parameters)
private

Validate and apply dynamic parameter updates.

Parameters
parametersROS parameters requested for update.
Returns
Result indicating whether the update was accepted.

Definition at line 375 of file point_cloud_fusion.cpp.

375 {
376 std::unique_lock<std::shared_mutex> config_lock(config_mutex_);
377
378 // Pre-validate interdependent and runtime-sensitive parameters before
379 // applying any changes.
380 // Build the prospective state: current values overridden by incoming changes.
381 bool any_range_param = false;
382 bool prospective_use_cuda = use_cuda_;
383 int64_t prospective_fixed_points_per_input_cloud = fixed_points_per_input_cloud_;
384 double prospective_x_min = range_limits_x_min_;
385 double prospective_x_max = range_limits_x_max_;
386 double prospective_y_min = range_limits_y_min_;
387 double prospective_y_max = range_limits_y_max_;
388 double prospective_z_min = range_limits_z_min_;
389 double prospective_z_max = range_limits_z_max_;
390
391 for (const auto& param : parameters) {
392 const auto& name = param.get_name();
393 if (name == "fixed_points_per_input_cloud") {
394 prospective_fixed_points_per_input_cloud = param.as_int();
395 } else if (name == "use_cuda") {
396 prospective_use_cuda = param.as_bool();
397 } else if (name == "range_limits.x_min") {
398 prospective_x_min = param.as_double();
399 any_range_param = true;
400 } else if (name == "range_limits.x_max") {
401 prospective_x_max = param.as_double();
402 any_range_param = true;
403 } else if (name == "range_limits.y_min") {
404 prospective_y_min = param.as_double();
405 any_range_param = true;
406 } else if (name == "range_limits.y_max") {
407 prospective_y_max = param.as_double();
408 any_range_param = true;
409 } else if (name == "range_limits.z_min") {
410 prospective_z_min = param.as_double();
411 any_range_param = true;
412 } else if (name == "range_limits.z_max") {
413 prospective_z_max = param.as_double();
414 any_range_param = true;
415 }
416 }
417
418 if (prospective_fixed_points_per_input_cloud < kMinFixedPointsPerInputCloud ||
419 prospective_fixed_points_per_input_cloud > kMaxFixedPointsPerInputCloud) {
420 rcl_interfaces::msg::SetParametersResult result;
421 result.successful = false;
422 result.reason = "fixed_points_per_input_cloud must be in [" + std::to_string(kMinFixedPointsPerInputCloud) + ", " +
423 std::to_string(kMaxFixedPointsPerInputCloud) + "]";
424 RCLCPP_ERROR(this->get_logger(), "Rejecting parameter update: %s", result.reason.c_str());
425 return result;
426 }
427
428 if (prospective_use_cuda) {
429#ifdef ENABLE_CUDA
430 if (!cuda_context_) {
431 rcl_interfaces::msg::SetParametersResult result;
432 result.successful = false;
433 result.reason =
434 "CUDA backend is unavailable because CUDA context "
435 "initialization failed";
436 RCLCPP_ERROR(this->get_logger(), "Rejecting parameter update: %s", result.reason.c_str());
437 return result;
438 }
439#else
440 rcl_interfaces::msg::SetParametersResult result;
441 result.successful = false;
442 result.reason = "CUDA backend is unavailable because CUDA support was not compiled";
443 RCLCPP_ERROR(this->get_logger(), "Rejecting parameter update: %s", result.reason.c_str());
444 return result;
445#endif
446 }
447
448 if (any_range_param) {
449 rcl_interfaces::msg::SetParametersResult result;
450 std::string reason;
451 if (prospective_x_min >= prospective_x_max) {
452 reason += "range_limits.x_min (" + std::to_string(prospective_x_min) + ") must be less than range_limits.x_max (" +
453 std::to_string(prospective_x_max) + "). ";
454 }
455 if (prospective_y_min >= prospective_y_max) {
456 reason += "range_limits.y_min (" + std::to_string(prospective_y_min) + ") must be less than range_limits.y_max (" +
457 std::to_string(prospective_y_max) + "). ";
458 }
459 if (prospective_z_min >= prospective_z_max) {
460 reason += "range_limits.z_min (" + std::to_string(prospective_z_min) + ") must be less than range_limits.z_max (" +
461 std::to_string(prospective_z_max) + "). ";
462 }
463 if (!reason.empty()) {
464 result.successful = false;
465 result.reason = reason;
466 RCLCPP_ERROR(this->get_logger(), "Rejecting range_limits parameter update: %s", reason.c_str());
467 return result;
468 }
469 }
470
471 // All validations passed — apply changes.
472 const bool previous_use_cuda = use_cuda_;
473 for (const auto& param : parameters) {
474 for (auto& auto_reconfigurable_param : auto_reconfigurable_params_) {
475 if (param.get_name() == std::get<0>(auto_reconfigurable_param)) {
476 std::get<1>(auto_reconfigurable_param)(param);
477 RCLCPP_INFO(this->get_logger(), "Reconfigured parameter '%s' to: %s", param.get_name().c_str(),
478 param.value_to_string().c_str());
479 break;
480 }
481 }
482 }
483 if (use_cuda_ != previous_use_cuda) {
484 RCLCPP_INFO(this->get_logger(), "Fusion backend switched to %s", use_cuda_ ? "CUDA" : "CPU");
485 }
486
487 rcl_interfaces::msg::SetParametersResult result;
488 result.successful = true;
489
490 return result;
491}

◆ publishFusedCloud()

void point_cloud_fusion::PointCloudFusion::publishFusedCloud ( PointCloudMsg::UniquePtr cloud,
const FusionTiming & timing,
std::size_t input_count,
std::size_t total_points,
std::chrono::steady_clock::time_point callback_start,
std::chrono::steady_clock::time_point processing_start,
std::chrono::steady_clock::time_point processing_end,
const char * event_name )
private

Publish a fused point cloud and emit tracing/timing diagnostics.

Parameters
cloudFused point cloud to publish.
timingTiming metadata for the synchronized input batch.
input_countNumber of input clouds in the batch.
total_pointsNumber of points before filtering.
callback_startTime when the synchronized callback started.
processing_startTime when fusion processing started.
processing_endTime when fusion processing ended.
event_nameTrace event name for the selected backend.

Definition at line 1205 of file point_cloud_fusion.cpp.

1212 {
1213 // Publish the fused cloud and emit a compact timing summary for
1214 // observability.
1215 cloud_publisher_->publish(std::move(cloud));
1216 const auto publish_end = std::chrono::steady_clock::now();
1217
1218 const double prep_duration_ms = std::chrono::duration<double, std::milli>(processing_start - callback_start).count();
1219 const double processing_duration_ms = std::chrono::duration<double, std::milli>(processing_end - processing_start).count();
1220 const double publish_duration_ms = std::chrono::duration<double, std::milli>(publish_end - processing_end).count();
1221 const double e2e_duration_ms = prep_duration_ms + processing_duration_ms + publish_duration_ms;
1222 const double batch_dt_ms = (timing.latest_stamp - timing.earliest_stamp).seconds() * 1000.0;
1223
1224 RCLCPP_DEBUG(this->get_logger(),
1225 "%s inputs=%zu points=%zu e2e_ms=%.3f prep_ms=%.3f "
1226 "process_ms=%.3f publish_ms=%.3f batch_dt_ms=%.3f "
1227 "max_dt_ms=%.3f",
1228 event_name, input_count, total_points, e2e_duration_ms, prep_duration_ms, processing_duration_ms,
1229 publish_duration_ms, batch_dt_ms, timing.max_dt_from_input0_sec * 1000.0);
1230}
std::shared_ptr< point_cloud_transport::Publisher > cloud_publisher_
Publisher.

◆ setup()

void point_cloud_fusion::PointCloudFusion::setup ( )
private

Sets up subscribers, publishers, etc. to configure the node.

Definition at line 609 of file point_cloud_fusion.cpp.

609 {
610 // callback for dynamic parameter configuration
612 this->add_on_set_parameters_callback(std::bind(&PointCloudFusion::parametersCallback, this, std::placeholders::_1));
613
614 // create transform buffer and listener
615 tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
616 tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
617
618 // validate inputs
619 if (!input_transport_hints_.empty() && input_transport_hints_.size() != input_topics_.size()) {
620 RCLCPP_WARN(this->get_logger(),
621 "'input_transport_hints' length (%zu) does not match "
622 "'input_topics' (%zu). Missing hints default to "
623 "'%s'",
625 }
626
627 // create subscribers
628 cloud_subscribers_.clear();
630 cloud_subscribers_.reserve(input_topics_.size());
632 for (size_t i = 0; i < input_topics_.size(); ++i) {
633 const std::string configured = input_topics_[i];
634 const std::string resolved = this->get_node_topics_interface()->resolve_topic_name(configured);
635 const std::string hint = (i < input_transport_hints_.size() && !input_transport_hints_[i].empty())
637 : std::string(kDefaultTransportHint);
638
639 auto callback_group = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
640 rclcpp::SubscriptionOptions subscription_options;
641 subscription_options.callback_group = callback_group;
642
643 auto subscriber = std::make_shared<point_cloud_transport::SubscriberFilter>();
644 subscriber->subscribe(this->shared_from_this(), resolved, hint, rmw_qos_profile_default, subscription_options);
645 RCLCPP_INFO(this->get_logger(), "Subscribed to '%s' (hint=%s)", subscriber->getTopic().c_str(), hint.c_str());
646 cloud_subscriber_callback_groups_.push_back(std::move(callback_group));
647 cloud_subscribers_.push_back(std::move(subscriber));
648 }
649
650 RCLCPP_INFO(this->get_logger(), "Configured %zu input subscriber callback groups for %zu input topics",
652
653 synchronizer_.reset();
654
655 // configure synchronization or direct passthrough
656 if (cloud_subscribers_.size() == 1) {
657 cloud_subscribers_.front()->registerCallback([this](const PointCloudMsg::ConstSharedPtr msg) {
658 std::vector<PointCloudMsg::ConstSharedPtr> batch;
659 batch.reserve(1);
660 batch.emplace_back(msg);
662 });
663 RCLCPP_INFO(this->get_logger(), "Configured single-input mode for topic '%s'",
664 cloud_subscribers_.front()->getTopic().c_str());
665 } else if (cloud_subscribers_.size() <= 9) {
666 switch (cloud_subscribers_.size()) {
667 case 2:
669 break;
670 case 3:
672 break;
673 case 4:
675 break;
676 case 5:
678 break;
679 case 6:
681 break;
682 case 7:
684 break;
685 case 8:
687 break;
688 case 9:
690 break;
691 default:
692 RCLCPP_FATAL(this->get_logger(), "Unsupported number of input topics: %zu", cloud_subscribers_.size());
693 exit(EXIT_FAILURE);
694 }
695 }
696
697 // create publisher
698 point_cloud_transport::PointCloudTransport pct(this->shared_from_this());
699 std::string point_cloud_topic_name = this->get_node_topics_interface()->resolve_topic_name("~/point_cloud");
700 cloud_publisher_ = std::make_shared<point_cloud_transport::Publisher>(
701 pct.advertise(point_cloud_topic_name, static_cast<uint32_t>(output_queue_size_)));
702 RCLCPP_INFO(this->get_logger(), "Publishing to '%s'", cloud_publisher_->getTopic().c_str());
703
704 // Annotate message links for tracing: Each publisher (for raw and compressed
705 // point clouds) depends an all input point clouds.
706 std::vector<const void*> link_subs;
707 std::vector<const void*> link_pubs;
708 for (const auto& sub_filter : cloud_subscribers_) {
709 auto sub_base = sub_filter->getSubscriber().getSubscription();
710 if (sub_base) {
711 link_subs.push_back(static_cast<const void*>(sub_base->get_subscription_handle().get()));
712 }
713 }
714 if (cloud_publisher_) {
715 std::map<std::string, rclcpp::PublisherBase::SharedPtr> pubs_base = cloud_publisher_->getPublishers();
716 for (const auto& [transport, pub_base] : pubs_base) {
717 link_pubs.push_back(static_cast<const void*>(pub_base->get_publisher_handle().get()));
718 }
719 }
720 TRACETOOLS_TRACEPOINT(message_link_partial_sync, link_subs.data(), link_subs.size(), link_pubs.data(), link_pubs.size());
721}
void handleSynchronizedPointClouds(const std::vector< sensor_msgs::msg::PointCloud2::ConstSharedPtr > &msgs)
Process synchronized point clouds.
OnSetParametersCallbackHandle::SharedPtr parameters_callback_
Callback handle for dynamic parameter reconfiguration.
rcl_interfaces::msg::SetParametersResult parametersCallback(const std::vector< rclcpp::Parameter > &parameters)
Validate and apply dynamic parameter updates.
std::shared_ptr< tf2_ros::TransformListener > tf_listener_
void setupSynchronizer()
Create the approximate-time synchronizer for a fixed number of input topics.
std::vector< rclcpp::CallbackGroup::SharedPtr > cloud_subscriber_callback_groups_
std::vector< std::shared_ptr< point_cloud_transport::SubscriberFilter > > cloud_subscribers_

◆ setupSynchronizer()

template<std::size_t N>
void point_cloud_fusion::PointCloudFusion::setupSynchronizer ( )
private

Create the approximate-time synchronizer for a fixed number of input topics.

Template Parameters
NNumber of synchronized point-cloud inputs.

Definition at line 724 of file point_cloud_fusion.cpp.

724 {
725 static_assert(N >= 2 && N <= 9, "Supported synchronizer size is between 2 and 9");
726
727 using Policy = detail::SyncPolicy<N>;
728 using Sync = detail::SyncType<N>;
729
730 // Instantiate ApproximateTime policy tuned to the active input count.
731 auto sync = std::make_shared<Sync>(Policy(static_cast<size_t>(sync_queue_size_)));
732
733 // Wire the configured subscribers into the synchronizer slots.
735
736 sync->setMaxIntervalDuration(rclcpp::Duration::from_seconds(max_time_diff_sec_));
737 sync->setAgePenalty(age_penalty_);
738 sync->registerCallback([this](auto&&... msgs) {
739 std::vector<PointCloudMsg::ConstSharedPtr> batch;
740 batch.reserve(sizeof...(msgs));
741 // Extract only valid PointCloud2 pointers from the variadic callback.
742 auto append = [&batch](auto&& msg) {
743 using ArgT = std::decay_t<decltype(msg)>;
744 if constexpr (std::is_same_v<ArgT, PointCloudMsg::ConstSharedPtr>) {
745 if (msg) {
746 batch.emplace_back(std::forward<decltype(msg)>(msg));
747 }
748 }
749 };
750 (append(std::forward<decltype(msgs)>(msgs)), ...);
751 if (!batch.empty()) {
753 } else {
754 RCLCPP_WARN(this->get_logger(),
755 "ApproximateTime synchronizer yielded no "
756 "valid point clouds; skipping fusion.");
757 }
758 });
759
760 synchronizer_ = sync;
761
762 RCLCPP_INFO(this->get_logger(),
763 "Configured approximate time synchronizer for %zu inputs "
764 "(queue=%zu, max_dt=%.3f s, age_penalty=%.6f)",
765 static_cast<size_t>(N), static_cast<size_t>(sync_queue_size_), max_time_diff_sec_, age_penalty_);
766}
typename SyncPolicyTraits< N >::Policy SyncPolicy
message_filters::Synchronizer< SyncPolicy< N > > SyncType
void connectInputs(SyncType< N > &sync, const std::vector< std::shared_ptr< point_cloud_transport::SubscriberFilter > > &subs)
Connect all subscriber filters to the synchronizer for N inputs.

◆ validateInputTopicsParameter()

void point_cloud_fusion::PointCloudFusion::validateInputTopicsParameter ( ) const
private

Validate that the configured input topic list is usable.

Definition at line 1251 of file point_cloud_fusion.cpp.

1251 {
1252 if (input_topics_.empty()) {
1253 RCLCPP_FATAL(this->get_logger(), "No input topics configured (parameter 'input_topics'). Exiting");
1254 exit(EXIT_FAILURE);
1255 }
1256 if (input_topics_.size() > kMaxInputTopics) {
1257 RCLCPP_FATAL(this->get_logger(),
1258 "Configured with %zu input topics, but only up to %zu inputs "
1259 "are supported",
1261 exit(EXIT_FAILURE);
1262 }
1263}

◆ validateRangeLimits()

void point_cloud_fusion::PointCloudFusion::validateRangeLimits ( )
private

Validate configured XYZ range limits and disable filtering when invalid.

Definition at line 1265 of file point_cloud_fusion.cpp.

1265 {
1266 bool valid = true;
1268 RCLCPP_ERROR(this->get_logger(),
1269 "range_limits.x_min (%.3f) must be less than "
1270 "range_limits.x_max (%.3f); disabling range filtering",
1272 valid = false;
1273 }
1275 RCLCPP_ERROR(this->get_logger(),
1276 "range_limits.y_min (%.3f) must be less than "
1277 "range_limits.y_max (%.3f); disabling range filtering",
1279 valid = false;
1280 }
1282 RCLCPP_ERROR(this->get_logger(),
1283 "range_limits.z_min (%.3f) must be less than "
1284 "range_limits.z_max (%.3f); disabling range filtering",
1286 valid = false;
1287 }
1288 if (!valid) {
1289 range_limits_enable_ = false;
1290 }
1291}

Member Data Documentation

◆ age_penalty_

double point_cloud_fusion::PointCloudFusion::age_penalty_ = 0.1
private

Definition at line 216 of file point_cloud_fusion.hpp.

◆ auto_reconfigurable_params_

std::vector<std::tuple<std::string, std::function<void(const rclcpp::Parameter&)> > > point_cloud_fusion::PointCloudFusion::auto_reconfigurable_params_
private

Auto-reconfigurable parameters for dynamic reconfiguration.

Definition at line 240 of file point_cloud_fusion.hpp.

◆ cloud_publisher_

std::shared_ptr<point_cloud_transport::Publisher> point_cloud_fusion::PointCloudFusion::cloud_publisher_
private

Publisher.

Definition at line 255 of file point_cloud_fusion.hpp.

◆ cloud_subscriber_callback_groups_

std::vector<rclcpp::CallbackGroup::SharedPtr> point_cloud_fusion::PointCloudFusion::cloud_subscriber_callback_groups_
private

Definition at line 244 of file point_cloud_fusion.hpp.

◆ cloud_subscribers_

std::vector<std::shared_ptr<point_cloud_transport::SubscriberFilter> > point_cloud_fusion::PointCloudFusion::cloud_subscribers_
private

Definition at line 243 of file point_cloud_fusion.hpp.

◆ config_mutex_

std::shared_mutex point_cloud_fusion::PointCloudFusion::config_mutex_
mutableprivate

Definition at line 241 of file point_cloud_fusion.hpp.

◆ fixed_points_per_input_cloud_

int64_t point_cloud_fusion::PointCloudFusion::fixed_points_per_input_cloud_ = 0
private

Definition at line 221 of file point_cloud_fusion.hpp.

◆ input_topics_

std::vector<std::string> point_cloud_fusion::PointCloudFusion::input_topics_
private

Definition at line 234 of file point_cloud_fusion.hpp.

◆ input_transport_hints_

std::vector<std::string> point_cloud_fusion::PointCloudFusion::input_transport_hints_
private

Definition at line 235 of file point_cloud_fusion.hpp.

◆ kAllowedOutputStampModes

const char* point_cloud_fusion::PointCloudFusion::kAllowedOutputStampModes = "latest, earliest, mean, input0"
staticconstexprprivate

Definition at line 206 of file point_cloud_fusion.hpp.

◆ kDefaultTransportHint

const char* point_cloud_fusion::PointCloudFusion::kDefaultTransportHint = "raw"
staticconstexprprivate

Definition at line 205 of file point_cloud_fusion.hpp.

◆ kMaxFixedPointsPerInputCloud

int64_t point_cloud_fusion::PointCloudFusion::kMaxFixedPointsPerInputCloud = 10000000
staticconstexprprivate

Definition at line 202 of file point_cloud_fusion.hpp.

◆ kMaxInputTopics

std::size_t point_cloud_fusion::PointCloudFusion::kMaxInputTopics = 9
staticconstexprprivate

Definition at line 204 of file point_cloud_fusion.hpp.

◆ kMaxOutputQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kMaxOutputQueueSize = 1000
staticconstexprprivate

Definition at line 199 of file point_cloud_fusion.hpp.

◆ kMaxRangeXY

double point_cloud_fusion::PointCloudFusion::kMaxRangeXY = 1000.0
staticconstexprprivate

Definition at line 208 of file point_cloud_fusion.hpp.

◆ kMaxRangeZ

double point_cloud_fusion::PointCloudFusion::kMaxRangeZ = 20.0
staticconstexprprivate

Definition at line 210 of file point_cloud_fusion.hpp.

◆ kMaxSyncQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kMaxSyncQueueSize = 1000
staticconstexprprivate

Definition at line 196 of file point_cloud_fusion.hpp.

◆ kMinFixedPointsPerInputCloud

int64_t point_cloud_fusion::PointCloudFusion::kMinFixedPointsPerInputCloud = 0
staticconstexprprivate

Definition at line 201 of file point_cloud_fusion.hpp.

◆ kMinOutputQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kMinOutputQueueSize = 1
staticconstexprprivate

Definition at line 198 of file point_cloud_fusion.hpp.

◆ kMinRangeXY

double point_cloud_fusion::PointCloudFusion::kMinRangeXY = -1000.0
staticconstexprprivate

Definition at line 207 of file point_cloud_fusion.hpp.

◆ kMinRangeZ

double point_cloud_fusion::PointCloudFusion::kMinRangeZ = -20.0
staticconstexprprivate

Definition at line 209 of file point_cloud_fusion.hpp.

◆ kMinSyncQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kMinSyncQueueSize = 1
staticconstexprprivate

Definition at line 195 of file point_cloud_fusion.hpp.

◆ kStepSizeFixedPointsPerInputCloud

int64_t point_cloud_fusion::PointCloudFusion::kStepSizeFixedPointsPerInputCloud = 100
staticconstexprprivate

Definition at line 203 of file point_cloud_fusion.hpp.

◆ kStepSizeOutputQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kStepSizeOutputQueueSize = 1
staticconstexprprivate

Definition at line 200 of file point_cloud_fusion.hpp.

◆ kStepSizeSyncQueueSize

int32_t point_cloud_fusion::PointCloudFusion::kStepSizeSyncQueueSize = 1
staticconstexprprivate

Definition at line 197 of file point_cloud_fusion.hpp.

◆ max_time_diff_sec_

double point_cloud_fusion::PointCloudFusion::max_time_diff_sec_ = 0.05
private

ROS parameters.

Definition at line 215 of file point_cloud_fusion.hpp.

◆ output_fields_

std::vector<std::string> point_cloud_fusion::PointCloudFusion::output_fields_
private

Definition at line 233 of file point_cloud_fusion.hpp.

◆ output_queue_size_

int64_t point_cloud_fusion::PointCloudFusion::output_queue_size_ = 10
private

Definition at line 218 of file point_cloud_fusion.hpp.

◆ output_stamp_mode_

OutputStampMode point_cloud_fusion::PointCloudFusion::output_stamp_mode_ = OutputStampMode::Earliest
private

Definition at line 230 of file point_cloud_fusion.hpp.

◆ output_stamp_mode_param_

std::string point_cloud_fusion::PointCloudFusion::output_stamp_mode_param_ = "earliest"
private

Definition at line 231 of file point_cloud_fusion.hpp.

◆ parameters_callback_

OnSetParametersCallbackHandle::SharedPtr point_cloud_fusion::PointCloudFusion::parameters_callback_
private

Callback handle for dynamic parameter reconfiguration.

Definition at line 250 of file point_cloud_fusion.hpp.

◆ range_limits_enable_

bool point_cloud_fusion::PointCloudFusion::range_limits_enable_ = false
private

Definition at line 222 of file point_cloud_fusion.hpp.

◆ range_limits_x_max_

double point_cloud_fusion::PointCloudFusion::range_limits_x_max_ = 1000.0
private

Definition at line 224 of file point_cloud_fusion.hpp.

◆ range_limits_x_min_

double point_cloud_fusion::PointCloudFusion::range_limits_x_min_ = -1000.0
private

Definition at line 223 of file point_cloud_fusion.hpp.

◆ range_limits_y_max_

double point_cloud_fusion::PointCloudFusion::range_limits_y_max_ = 1000.0
private

Definition at line 226 of file point_cloud_fusion.hpp.

◆ range_limits_y_min_

double point_cloud_fusion::PointCloudFusion::range_limits_y_min_ = -1000.0
private

Definition at line 225 of file point_cloud_fusion.hpp.

◆ range_limits_z_max_

double point_cloud_fusion::PointCloudFusion::range_limits_z_max_ = 20.0
private

Definition at line 228 of file point_cloud_fusion.hpp.

◆ range_limits_z_min_

double point_cloud_fusion::PointCloudFusion::range_limits_z_min_ = -20.0
private

Definition at line 227 of file point_cloud_fusion.hpp.

◆ setup_timer_

rclcpp::TimerBase::SharedPtr point_cloud_fusion::PointCloudFusion::setup_timer_
private

Timer to delay setup.

Definition at line 266 of file point_cloud_fusion.hpp.

◆ sync_queue_size_

int64_t point_cloud_fusion::PointCloudFusion::sync_queue_size_ = 3
private

Definition at line 217 of file point_cloud_fusion.hpp.

◆ synchronizer_

std::shared_ptr<void> point_cloud_fusion::PointCloudFusion::synchronizer_
private

Definition at line 245 of file point_cloud_fusion.hpp.

◆ target_frame_

std::string point_cloud_fusion::PointCloudFusion::target_frame_ = "base_link"
private

Definition at line 232 of file point_cloud_fusion.hpp.

◆ tf_buffer_

std::shared_ptr<tf2_ros::Buffer> point_cloud_fusion::PointCloudFusion::tf_buffer_
private

TF2 buffer and transform listener.

Definition at line 260 of file point_cloud_fusion.hpp.

◆ tf_listener_

std::shared_ptr<tf2_ros::TransformListener> point_cloud_fusion::PointCloudFusion::tf_listener_
private

Definition at line 261 of file point_cloud_fusion.hpp.

◆ use_cuda_

bool point_cloud_fusion::PointCloudFusion::use_cuda_ = true
private

Definition at line 229 of file point_cloud_fusion.hpp.


The documentation for this class was generated from the following files: