21#include <tf2/LinearMath/Transform.h>
23#include <tracetools/tracetools.h>
24#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
25#include <tf2_sensor_msgs/tf2_sensor_msgs.hpp>
27#include <rmw/qos_profiles.h>
28#include <rclcpp_components/register_node_macro.hpp>
33inline std::size_t pointFieldDatatypeSize(uint8_t datatype) {
34 using sensor_msgs::msg::PointField;
36 case PointField::INT8:
37 case PointField::UINT8:
39 case PointField::INT16:
40 case PointField::UINT16:
42 case PointField::INT32:
43 case PointField::UINT32:
44 case PointField::FLOAT32:
46 case PointField::FLOAT64:
67inline bool pointWithinRange(
68 float x,
float y,
float z,
float x_min,
float x_max,
float y_min,
float y_max,
float z_min,
float z_max) {
69 return x >= x_min && x <= x_max && y >= y_min && y <= y_max && z >= z_min && z <= z_max;
80template <
typename Byte>
81inline Byte* byteOffset(Byte* data, std::size_t offset) {
82 return std::next(data,
static_cast<std::ptrdiff_t
>(offset));
92inline float loadFloat(
const uint8_t* data, std::size_t offset) {
94 std::memcpy(&value, byteOffset(data, offset),
sizeof(value));
105inline void storeFloat(uint8_t* data, std::size_t offset,
float value) {
106 std::memcpy(byteOffset(data, offset), &value,
sizeof(value));
116 "Frame into which all input point clouds are transformed before fusion",
120 std::nullopt, std::nullopt, std::nullopt,
123 "Point-cloud topics to fuse",
127 std::nullopt, std::nullopt, std::nullopt,
128 "Must configure between 1 and " + std::to_string(
kMaxInputTopics) +
" topics");
131 "Transport hint for each input topic; unspecified entries use raw",
135 std::nullopt, std::nullopt, std::nullopt,
136 "Length must be zero or match input_topics; unspecified entries default to '" +
139 "Queue depth for approximate-time synchronization",
148 "Queue depth for the fused output publisher",
158 "Fields retained in the fused output; an empty list retains all input fields",
162 std::nullopt, std::nullopt, std::nullopt,
163 "Typical fields include: x, y, z, intensity, t, reflectivity, ring, ambient, range.");
165 "Fused timestamp selection: earliest, latest, mean, or input0",
169 std::nullopt, std::nullopt, std::nullopt,
173 "Runtime-reconfigurable maximum valid point count per input cloud; 0 disables the limit",
180 "0 = disabled; reasonable range is 0 to 10,000,000 points per input cloud");
182 "Runtime-reconfigurable backend selection; true uses CUDA and false uses CPU",
186 std::nullopt, std::nullopt, std::nullopt,
187 "Runtime changes apply between fusion batches.");
189 "Enable XYZ range filtering after transformation into target_frame",
193 std::nullopt, std::nullopt, std::nullopt,
194 "When false, no range filtering is applied.");
196 "Minimum x coordinate in target_frame to keep [m]",
203 "Must be less than range_limits.x_max.");
205 "Maximum x coordinate in target_frame to keep [m]",
212 "Must be greater than range_limits.x_min.");
214 "Minimum y coordinate in target_frame to keep [m]",
221 "Must be less than range_limits.y_max.");
223 "Maximum y coordinate in target_frame to keep [m]",
230 "Must be greater than range_limits.y_min.");
232 "Minimum z coordinate in target_frame to keep [m]",
239 "Must be less than range_limits.z_max.");
241 "Maximum z coordinate in target_frame to keep [m]",
248 "Must be greater than range_limits.z_min.");
251 "Maximum timestamp spread across a synchronized input batch in seconds",
255 0.0, std::nullopt, std::nullopt,
256 "Must be non-negative");
258 "Age penalty used by the approximate-time synchronizer",
265 "Valid range is [0, 100].");
272 cuda_context_ = std::make_unique<cuda::CudaTransformContext>();
274 RCLCPP_INFO(this->get_logger(),
"CUDA acceleration enabled");
276 RCLCPP_INFO(this->get_logger(),
"CUDA context initialized; using CPU backend by parameter");
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();
283 this->set_parameter(rclcpp::Parameter(
"use_cuda",
false));
286 RCLCPP_INFO(this->get_logger(),
"CUDA support not compiled, using CPU-only implementation");
288 this->set_parameter(rclcpp::Parameter(
"use_cuda",
false));
292 setup_timer_ = this->create_wall_timer(std::chrono::milliseconds(1), [
this]() {
302 const std::string& description,
303 const bool add_to_auto_reconfigurable_params,
304 const bool is_required,
305 const bool read_only,
306 const std::optional<double>& from_value,
307 const std::optional<double>& to_value,
308 const std::optional<double>& step_value,
309 const std::string& additional_constraints) {
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;
315 auto type = rclcpp::ParameterValue(param).get_type();
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};
329 RCLCPP_WARN(this->get_logger(),
330 "Parameter type of parameter '%s' does not support "
331 "specifying a range",
336 this->declare_parameter(name, type, param_desc);
339 param = this->get_parameter(name).get_value<T>();
340 std::stringstream ss;
341 ss <<
"Loaded parameter '" << name <<
"': ";
344 for (
const auto& element : param) ss << element << (&element != ¶m.back() ?
", " :
"");
349 RCLCPP_INFO_STREAM(this->get_logger(), ss.str());
350 }
catch (rclcpp::exceptions::ParameterUninitializedException&) {
352 RCLCPP_FATAL_STREAM(this->get_logger(),
"Missing required parameter '" << name <<
"', exiting");
355 std::stringstream ss;
356 ss <<
"Missing parameter '" << name <<
"', using default value: ";
359 for (
const auto& element : param) ss << element << (&element != ¶m.back() ?
", " :
"");
364 RCLCPP_WARN_STREAM(this->get_logger(), ss.str());
365 this->set_parameters({rclcpp::Parameter(name, rclcpp::ParameterValue(param))});
369 if (add_to_auto_reconfigurable_params) {
370 std::function<void(
const rclcpp::Parameter&)> setter = [¶m](
const rclcpp::Parameter& p) { param = p.get_value<T>(); };
376 std::unique_lock<std::shared_mutex> config_lock(
config_mutex_);
381 bool any_range_param =
false;
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;
420 rcl_interfaces::msg::SetParametersResult result;
421 result.successful =
false;
424 RCLCPP_ERROR(this->get_logger(),
"Rejecting parameter update: %s", result.reason.c_str());
428 if (prospective_use_cuda) {
430 if (!cuda_context_) {
431 rcl_interfaces::msg::SetParametersResult result;
432 result.successful =
false;
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());
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());
448 if (any_range_param) {
449 rcl_interfaces::msg::SetParametersResult result;
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) +
"). ";
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) +
"). ";
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) +
"). ";
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());
472 const bool previous_use_cuda =
use_cuda_;
473 for (
const auto& param : parameters) {
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());
484 RCLCPP_INFO(this->get_logger(),
"Fusion backend switched to %s",
use_cuda_ ?
"CUDA" :
"CPU");
487 rcl_interfaces::msg::SetParametersResult result;
488 result.successful =
true;
495template <std::
size_t N>
500 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2>;
505 using Policy = message_filters::sync_policies::
506 ApproximateTime<sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2>;
511 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
512 sensor_msgs::msg::PointCloud2,
513 sensor_msgs::msg::PointCloud2,
514 sensor_msgs::msg::PointCloud2>;
519 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
520 sensor_msgs::msg::PointCloud2,
521 sensor_msgs::msg::PointCloud2,
522 sensor_msgs::msg::PointCloud2,
523 sensor_msgs::msg::PointCloud2>;
528 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
529 sensor_msgs::msg::PointCloud2,
530 sensor_msgs::msg::PointCloud2,
531 sensor_msgs::msg::PointCloud2,
532 sensor_msgs::msg::PointCloud2,
533 sensor_msgs::msg::PointCloud2>;
538 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
539 sensor_msgs::msg::PointCloud2,
540 sensor_msgs::msg::PointCloud2,
541 sensor_msgs::msg::PointCloud2,
542 sensor_msgs::msg::PointCloud2,
543 sensor_msgs::msg::PointCloud2,
544 sensor_msgs::msg::PointCloud2>;
549 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
550 sensor_msgs::msg::PointCloud2,
551 sensor_msgs::msg::PointCloud2,
552 sensor_msgs::msg::PointCloud2,
553 sensor_msgs::msg::PointCloud2,
554 sensor_msgs::msg::PointCloud2,
555 sensor_msgs::msg::PointCloud2,
556 sensor_msgs::msg::PointCloud2>;
561 using Policy = message_filters::sync_policies::ApproximateTime<sensor_msgs::msg::PointCloud2,
562 sensor_msgs::msg::PointCloud2,
563 sensor_msgs::msg::PointCloud2,
564 sensor_msgs::msg::PointCloud2,
565 sensor_msgs::msg::PointCloud2,
566 sensor_msgs::msg::PointCloud2,
567 sensor_msgs::msg::PointCloud2,
568 sensor_msgs::msg::PointCloud2,
569 sensor_msgs::msg::PointCloud2>;
572template <std::
size_t N>
575template <std::
size_t N>
576using SyncType = message_filters::Synchronizer<SyncPolicy<N>>;
587template <std::size_t N, std::size_t... Is>
589 const std::vector<std::shared_ptr<point_cloud_transport::SubscriberFilter>>& subs,
590 std::index_sequence<Is...> ) {
592 sync.connectInput(*subs[Is]...);
602template <std::
size_t N>
615 tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
620 RCLCPP_WARN(this->get_logger(),
621 "'input_transport_hints' length (%zu) does not match "
622 "'input_topics' (%zu). Missing hints default to "
634 const std::string resolved = this->get_node_topics_interface()->resolve_topic_name(configured);
639 auto callback_group = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
640 rclcpp::SubscriptionOptions subscription_options;
641 subscription_options.callback_group = callback_group;
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());
650 RCLCPP_INFO(this->get_logger(),
"Configured %zu input subscriber callback groups for %zu input topics",
657 cloud_subscribers_.front()->registerCallback([
this](
const PointCloudMsg::ConstSharedPtr msg) {
658 std::vector<PointCloudMsg::ConstSharedPtr> batch;
660 batch.emplace_back(msg);
663 RCLCPP_INFO(this->get_logger(),
"Configured single-input mode for topic '%s'",
692 RCLCPP_FATAL(this->get_logger(),
"Unsupported number of input topics: %zu",
cloud_subscribers_.size());
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");
702 RCLCPP_INFO(this->get_logger(),
"Publishing to '%s'",
cloud_publisher_->getTopic().c_str());
706 std::vector<const void*> link_subs;
707 std::vector<const void*> link_pubs;
709 auto sub_base = sub_filter->getSubscriber().getSubscription();
711 link_subs.push_back(
static_cast<const void*
>(sub_base->get_subscription_handle().get()));
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()));
720 TRACETOOLS_TRACEPOINT(message_link_partial_sync, link_subs.data(), link_subs.size(), link_pubs.data(), link_pubs.size());
723template <std::
size_t N>
725 static_assert(N >= 2 && N <= 9,
"Supported synchronizer size is between 2 and 9");
731 auto sync = std::make_shared<Sync>(Policy(
static_cast<size_t>(
sync_queue_size_)));
738 sync->registerCallback([
this](
auto&&... msgs) {
739 std::vector<PointCloudMsg::ConstSharedPtr> batch;
740 batch.reserve(
sizeof...(msgs));
742 auto append = [&batch](
auto&& msg) {
743 using ArgT = std::decay_t<
decltype(msg)>;
744 if constexpr (std::is_same_v<ArgT, PointCloudMsg::ConstSharedPtr>) {
746 batch.emplace_back(std::forward<
decltype(msg)>(msg));
750 (append(std::forward<
decltype(msgs)>(msgs)), ...);
751 if (!batch.empty()) {
754 RCLCPP_WARN(this->get_logger(),
755 "ApproximateTime synchronizer yielded no "
756 "valid point clouds; skipping fusion.");
762 RCLCPP_INFO(this->get_logger(),
763 "Configured approximate time synchronizer for %zu inputs "
764 "(queue=%zu, max_dt=%.3f s, age_penalty=%.6f)",
775 std::shared_lock<std::shared_mutex> config_lock(
config_mutex_);
777 const auto callback_start = std::chrono::steady_clock::now();
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;
793 std::lock_guard<std::mutex> cuda_lock(cuda_context_mutex_);
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();
805 publishFusedCloud(std::move(cuda_result), timing, msgs.size(), cuda_valid_count, callback_start, processing_start,
806 processing_end,
"cuda_fusion_complete");
808 RCLCPP_WARN(this->get_logger(),
"CUDA processing failed");
815 std::size_t valid_count = 0;
816 const auto cpu_processing_start = std::chrono::steady_clock::now();
819 if (!fused_point_cloud) {
820 RCLCPP_WARN(this->get_logger(),
"All points are invalid, skipping fusion");
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");
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;
842 for (
const auto& pc_msg : msgs) {
844 RCLCPP_WARN(this->get_logger(),
845 "Received null point cloud pointer in "
846 "synchronized batch, skipping fusion");
850 const rclcpp::Time current_stamp(pc_msg->header.stamp);
852 earliest_stamp = current_stamp;
853 latest_stamp = current_stamp;
856 if (current_stamp < earliest_stamp) earliest_stamp = current_stamp;
857 if (current_stamp > latest_stamp) latest_stamp = current_stamp;
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;
874 const std::vector<PointCloudMsg::ConstSharedPtr>& msgs,
const FusionTiming& timing, std::size_t& valid_point_count)
const {
879 const auto& input0_msg = msgs.front();
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;
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;
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;
907 struct FieldCopyPlan {
908 const sensor_msgs::msg::PointField* source;
909 sensor_msgs::msg::PointField destination;
910 std::size_t byte_length;
914 std::vector<FieldCopyPlan> copy_plan;
915 std::vector<sensor_msgs::msg::PointField> fused_fields;
916 fused_fields.reserve(input0_fields.size());
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;
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();
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;
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;
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;
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;
970 copy_plan.push_back(plan);
971 fused_fields.push_back(plan.destination);
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.");
980 use_all_fields =
true;
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;
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;
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) {
1005 size_t cloud_size =
static_cast<size_t>(cloud->width) *
static_cast<size_t>(cloud->height);
1010 return sum + cloud_size;
1013 if (max_capacity == 0) {
1014 valid_point_count = 0;
1018 auto output = std::make_unique<PointCloudMsg>();
1019 output->header.frame_id = target_frame_;
1020 rclcpp::Time chosen_stamp;
1021 switch (output_stamp_mode_) {
1022 case OutputStampMode::Earliest:
1023 chosen_stamp = timing.earliest_stamp;
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);
1030 case OutputStampMode::Input0:
1031 chosen_stamp = timing.input0_stamp;
1033 case OutputStampMode::Latest:
1035 chosen_stamp = timing.latest_stamp;
1038 output->header.stamp = chosen_stamp;
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);
1046 uint8_t* dest_ptr = output->data.data();
1047 valid_point_count = 0;
1048 std::size_t skipped_inputs = 0;
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_);
1059 for (
const auto& msg : msgs) {
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());
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;
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());
1088 tf2::fromMsg(tf_stamped.transform, tf_transform);
1089 translation = tf_transform.getOrigin();
1090 rotation = tf_transform.getBasis();
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);
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);
1100 for (
const auto& plan : copy_plan) {
1101 std::memcpy(byteOffset(dest_ptr, plan.destination.offset), byteOffset(point_ptr, plan.source->offset),
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);
1112 dest_ptr = byteOffset(dest_ptr, fused_point_step);
1113 ++valid_point_count;
1116 if (fixed_points_per_input_cloud_ <= 0 ||
static_cast<size_t>(fixed_points_per_input_cloud_) >= total_points) {
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);
1124 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) {
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)) {
1132 emit_point(point_ptr, x, y, z,
false);
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());
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)) {
1146 emit_point(point_ptr, transformed_x, transformed_y, transformed_z,
true);
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;
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);
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);
1164 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) {
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)) {
1172 emit_point(point_ptr, x, y, z,
false);
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());
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)) {
1186 emit_point(point_ptr, transformed_x, transformed_y, transformed_z,
true);
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.");
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);
1201 output->is_dense =
true;
1205void PointCloudFusion::publishFusedCloud(PointCloudMsg::UniquePtr cloud,
1207 std::size_t input_count,
1208 std::size_t total_points,
1209 std::chrono::steady_clock::time_point callback_start,
1210 std::chrono::steady_clock::time_point processing_start,
1211 std::chrono::steady_clock::time_point processing_end,
1212 const char* event_name) {
1215 cloud_publisher_->publish(std::move(cloud));
1216 const auto publish_end = std::chrono::steady_clock::now();
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;
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 "
1228 event_name, input_count, total_points, e2e_duration_ms, prep_duration_ms, processing_duration_ms,
1232void PointCloudFusion::configureOutputStampMode(
const std::string& mode) {
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)); });
1237 if (lowered ==
"earliest") {
1238 output_stamp_mode_ = OutputStampMode::Earliest;
1239 }
else if (lowered ==
"mean" || lowered ==
"mid" || lowered ==
"midpoint") {
1240 output_stamp_mode_ = OutputStampMode::Mean;
1241 }
else if (lowered ==
"input0") {
1242 output_stamp_mode_ = OutputStampMode::Input0;
1243 }
else if (lowered ==
"latest") {
1244 output_stamp_mode_ = OutputStampMode::Latest;
1246 RCLCPP_WARN(this->get_logger(),
"Invalid output_stamp_mode '%s'; defaulting to 'earliest'.", mode.c_str());
1247 output_stamp_mode_ = OutputStampMode::Earliest;
1251void PointCloudFusion::validateInputTopicsParameter()
const {
1252 if (input_topics_.empty()) {
1253 RCLCPP_FATAL(this->get_logger(),
"No input topics configured (parameter 'input_topics'). Exiting");
1256 if (input_topics_.size() > kMaxInputTopics) {
1257 RCLCPP_FATAL(this->get_logger(),
1258 "Configured with %zu input topics, but only up to %zu inputs "
1260 input_topics_.size(), kMaxInputTopics);
1265void PointCloudFusion::validateRangeLimits() {
1267 if (range_limits_x_min_ >= range_limits_x_max_) {
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",
1271 range_limits_x_min_, range_limits_x_max_);
1274 if (range_limits_y_min_ >= range_limits_y_max_) {
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",
1278 range_limits_y_min_, range_limits_y_max_);
1281 if (range_limits_z_min_ >= range_limits_z_max_) {
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",
1285 range_limits_z_min_, range_limits_z_max_);
1289 range_limits_enable_ =
false;
1294PointCloudFusion::PointCloudMsg::UniquePtr PointCloudFusion::fusePointCloudBatchCUDA(
1295 const std::vector<PointCloudMsg::ConstSharedPtr>& msgs,
const FusionTiming& timing, std::size_t& valid_point_count)
const {
1296 if (msgs.empty() || !cuda_context_) {
1300 const auto& input0_msg = msgs.front();
1306 int x_offset = -1, y_offset = -1, z_offset = -1;
1307 for (
const auto& field : input0_msg->fields) {
1308 if (field.name ==
"x")
1309 x_offset = field.offset;
1310 else if (field.name ==
"y")
1311 y_offset = field.offset;
1312 else if (field.name ==
"z")
1313 z_offset = field.offset;
1316 if (x_offset < 0 || y_offset < 0 || z_offset < 0) {
1317 RCLCPP_WARN(this->get_logger(),
"Point cloud lacks x/y/z fields; skipping CUDA fusion");
1318 valid_point_count = 0;
1322 const size_t point_step = input0_msg->point_step;
1323 const auto& input0_fields = input0_msg->fields;
1324 const bool is_bigendian = input0_msg->is_bigendian;
1327 bool use_all_fields = output_fields_.empty();
1328 std::vector<sensor_msgs::msg::PointField> fused_fields;
1329 fused_fields.reserve(input0_fields.size());
1330 std::vector<cuda::CudaFieldCopy> copy_plan;
1332 std::size_t fused_point_step = point_step;
1333 int fused_x_offset = x_offset;
1334 int fused_y_offset = y_offset;
1335 int fused_z_offset = z_offset;
1337 if (!use_all_fields) {
1338 bool selection_valid =
true;
1339 fused_point_step = 0;
1340 fused_fields.clear();
1341 copy_plan.reserve(output_fields_.size());
1343 for (
const auto& requested_name : output_fields_) {
1345 std::find_if(input0_fields.begin(), input0_fields.end(),
1346 [&requested_name](
const sensor_msgs::msg::PointField& field) { return field.name == requested_name; });
1347 if (iter == input0_fields.end()) {
1348 RCLCPP_WARN(this->get_logger(),
1349 "Requested output field '%s' not present in incoming point "
1350 "cloud; publishing full field set instead.",
1351 requested_name.c_str());
1352 selection_valid =
false;
1356 const std::size_t datatype_size = pointFieldDatatypeSize(iter->datatype);
1357 if (datatype_size == 0) {
1358 RCLCPP_WARN(this->get_logger(),
1359 "Point field '%s' uses unsupported datatype %u; publishing "
1360 "full field set instead.",
1361 requested_name.c_str(),
static_cast<unsigned int>(iter->datatype));
1362 selection_valid =
false;
1366 cuda::CudaFieldCopy plan;
1367 plan.src_offset =
static_cast<int>(iter->offset);
1368 plan.dst_offset =
static_cast<int>(fused_point_step);
1369 plan.size =
static_cast<int>(datatype_size * iter->count);
1371 sensor_msgs::msg::PointField dest_field = *iter;
1372 dest_field.offset =
static_cast<uint32_t
>(fused_point_step);
1374 fused_point_step += plan.size;
1376 if (requested_name ==
"x")
1377 fused_x_offset = dest_field.offset;
1378 else if (requested_name ==
"y")
1379 fused_y_offset = dest_field.offset;
1380 else if (requested_name ==
"z")
1381 fused_z_offset = dest_field.offset;
1383 copy_plan.push_back(plan);
1384 fused_fields.push_back(dest_field);
1387 if (!selection_valid || fused_x_offset < 0 || fused_y_offset < 0 || fused_z_offset < 0) {
1388 if (selection_valid) {
1389 RCLCPP_ERROR(this->get_logger(),
1390 "Output field selection must include x, y, and z; "
1391 "publishing full field set instead.");
1393 use_all_fields =
true;
1395 fused_point_step = point_step;
1396 fused_fields.assign(input0_fields.begin(), input0_fields.end());
1397 fused_x_offset = x_offset;
1398 fused_y_offset = y_offset;
1399 fused_z_offset = z_offset;
1403 if (use_all_fields) {
1404 fused_fields.assign(input0_fields.begin(), input0_fields.end());
1405 fused_point_step = point_step;
1406 fused_x_offset = x_offset;
1407 fused_y_offset = y_offset;
1408 fused_z_offset = z_offset;
1416 cuda::CudaFieldCopy plan;
1417 plan.src_offset = 0;
1418 plan.dst_offset = 0;
1419 plan.size =
static_cast<int>(point_step);
1420 copy_plan.push_back(plan);
1425 size_t max_single_cloud_points = 0;
1426 for (
const auto& msg : msgs) {
1428 size_t cloud_size = msg->width * msg->height;
1429 max_single_cloud_points = std::max(max_single_cloud_points, cloud_size);
1433 if (max_single_cloud_points == 0) {
1434 valid_point_count = 0;
1440 size_t slot_size = max_single_cloud_points;
1443 const size_t num_inputs = msgs.size();
1444 size_t total_input_capacity = slot_size * num_inputs;
1447 if (!cuda_context_->resetBatch(total_input_capacity, slot_size, point_step, fused_point_step, x_offset, y_offset, z_offset,
1448 fused_x_offset, fused_y_offset, fused_z_offset, copy_plan,
1449 static_cast<float>(range_limits_x_min_),
static_cast<float>(range_limits_x_max_),
1450 static_cast<float>(range_limits_y_min_),
static_cast<float>(range_limits_y_max_),
1451 static_cast<float>(range_limits_z_min_),
static_cast<float>(range_limits_z_max_),
1452 range_limits_enable_)) {
1453 RCLCPP_ERROR(this->get_logger(),
"CUDA resetBatch failed");
1459 for (
size_t i = 0; i < msgs.size(); ++i) {
1460 const auto& msg = msgs[i];
1461 if (!msg || msg->point_step != point_step || msg->fields != input0_fields) {
1465 const bool apply_transform = (msg->header.frame_id != target_frame_);
1466 float rotation_matrix[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
1467 float translation[3] = {0, 0, 0};
1469 if (apply_transform) {
1471 auto tf_stamped = tf_buffer_->lookupTransform(target_frame_, msg->header.frame_id, msg->header.stamp,
1472 rclcpp::Duration::from_seconds(0.1));
1473 tf2::Transform tf_transform;
1474 tf2::fromMsg(tf_stamped.transform, tf_transform);
1475 auto rot = tf_transform.getBasis();
1476 auto trans = tf_transform.getOrigin();
1478 rotation_matrix[0] = rot[0][0];
1479 rotation_matrix[1] = rot[0][1];
1480 rotation_matrix[2] = rot[0][2];
1481 rotation_matrix[3] = rot[1][0];
1482 rotation_matrix[4] = rot[1][1];
1483 rotation_matrix[5] = rot[1][2];
1484 rotation_matrix[6] = rot[2][0];
1485 rotation_matrix[7] = rot[2][1];
1486 rotation_matrix[8] = rot[2][2];
1487 translation[0] = trans.x();
1488 translation[1] = trans.y();
1489 translation[2] = trans.z();
1490 }
catch (
const tf2::TransformException& ex) {
1491 RCLCPP_ERROR(this->get_logger(),
"CUDA: Cannot transform %s to %s: %s", msg->header.frame_id.c_str(),
1492 target_frame_.c_str(), ex.what());
1498 size_t num_points = msg->width * msg->height;
1499 const uint8_t* data_ptr = msg->data.data();
1502 const int desired_points = (fixed_points_per_input_cloud_ > 0) ?
static_cast<int>(fixed_points_per_input_cloud_) : 0;
1504 if (!cuda_context_->addCloud(data_ptr, num_points, rotation_matrix, translation, apply_transform, i, desired_points)) {
1505 RCLCPP_ERROR(this->get_logger(),
"CUDA addCloud failed");
1511 auto output = std::make_unique<PointCloudMsg>();
1512 output->header.frame_id = target_frame_;
1513 rclcpp::Time chosen_stamp;
1514 switch (output_stamp_mode_) {
1515 case OutputStampMode::Earliest:
1516 chosen_stamp = timing.earliest_stamp;
1518 case OutputStampMode::Mean: {
1519 const auto delta = timing.latest_stamp - timing.earliest_stamp;
1520 chosen_stamp = timing.earliest_stamp + rclcpp::Duration::from_nanoseconds(delta.nanoseconds() / 2);
1523 case OutputStampMode::Input0:
1524 chosen_stamp = timing.input0_stamp;
1526 case OutputStampMode::Latest:
1528 chosen_stamp = timing.latest_stamp;
1531 output->header.stamp = chosen_stamp;
1533 output->is_bigendian = is_bigendian;
1534 output->point_step = fused_point_step;
1535 output->fields = fused_fields;
1536 output->is_dense =
true;
1538 if (!cuda_context_->getBatchOutput(output->data, valid_point_count)) {
1539 RCLCPP_ERROR(this->get_logger(),
"CUDA getBatchOutput failed");
1543 if (valid_point_count == 0) {
1547 output->width = valid_point_count;
1548 output->row_step = output->point_step * output->width;
double range_limits_z_min_
static constexpr int32_t kMaxSyncQueueSize
void validateInputTopicsParameter() const
Validate that the configured input topic list is usable.
double max_time_diff_sec_
ROS parameters.
double range_limits_x_min_
static constexpr int32_t kStepSizeOutputQueueSize
double range_limits_y_max_
static constexpr int32_t kMinSyncQueueSize
static constexpr int32_t kMaxOutputQueueSize
static constexpr std::size_t kMaxInputTopics
static constexpr const char * kAllowedOutputStampModes
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 validateRangeLimits()
Validate configured XYZ range limits and disable filtering when invalid.
static constexpr double kMinRangeZ
int64_t fixed_points_per_input_cloud_
std::vector< std::string > output_fields_
void handleSynchronizedPointClouds(const std::vector< sensor_msgs::msg::PointCloud2::ConstSharedPtr > &msgs)
Process synchronized point clouds.
void configureOutputStampMode(const std::string &mode)
Parse and store the configured output timestamp mode.
std::string target_frame_
void declareAndLoadParameter(const std::string &name, T ¶m, 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.
bool range_limits_enable_
double range_limits_x_max_
void setup()
Sets up subscribers, publishers, etc. to configure the node.
rclcpp::TimerBase::SharedPtr setup_timer_
Timer to delay setup.
std::vector< std::string > input_topics_
OnSetParametersCallbackHandle::SharedPtr parameters_callback_
Callback handle for dynamic parameter reconfiguration.
std::shared_mutex config_mutex_
static constexpr const char * kDefaultTransportHint
std::string output_stamp_mode_param_
static constexpr double kMaxRangeXY
rcl_interfaces::msg::SetParametersResult parametersCallback(const std::vector< rclcpp::Parameter > ¶meters)
Validate and apply dynamic parameter updates.
static constexpr int32_t kMinOutputQueueSize
std::vector< std::string > input_transport_hints_
static constexpr int64_t kMaxFixedPointsPerInputCloud
static constexpr int32_t kStepSizeSyncQueueSize
std::shared_ptr< tf2_ros::Buffer > tf_buffer_
TF2 buffer and transform listener.
double range_limits_z_max_
std::shared_ptr< tf2_ros::TransformListener > tf_listener_
static constexpr int64_t kMinFixedPointsPerInputCloud
bool collectTimingInfo(const std::vector< PointCloudMsg::ConstSharedPtr > &msgs, FusionTiming &timing) const
Collect timestamp statistics for synchronized input clouds.
static constexpr double kMaxRangeZ
std::shared_ptr< void > synchronizer_
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.
static constexpr int64_t kStepSizeFixedPointsPerInputCloud
void setupSynchronizer()
Create the approximate-time synchronizer for a fixed number of input topics.
int64_t output_queue_size_
std::vector< std::tuple< std::string, std::function< void(const rclcpp::Parameter &)> > > auto_reconfigurable_params_
Auto-reconfigurable parameters for dynamic reconfiguration.
std::vector< rclcpp::CallbackGroup::SharedPtr > cloud_subscriber_callback_groups_
std::shared_ptr< point_cloud_transport::Publisher > cloud_publisher_
Publisher.
static constexpr double kMinRangeXY
std::vector< std::shared_ptr< point_cloud_transport::SubscriberFilter > > cloud_subscribers_
PointCloudFusion(const rclcpp::NodeOptions &options)
Constructor.
double range_limits_y_min_
void connectInputsImpl(SyncType< N > &sync, const std::vector< std::shared_ptr< point_cloud_transport::SubscriberFilter > > &subs, std::index_sequence< Is... >)
Connect subscriber filters to the synchronizer through an index sequence.
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.
constexpr bool is_vector_v
Timing metadata for one synchronized fusion batch.
rclcpp::Time input0_stamp
rclcpp::Time earliest_stamp
double max_dt_from_input0_sec
rclcpp::Time latest_stamp
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies:: ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy
message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 > Policy