Fuse a synchronized point-cloud batch using the CPU path.
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
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();
931
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
999
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
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>();
1020 rclcpp::Time chosen_stamp;
1023 chosen_stamp = timing.earliest_stamp;
1024 break;
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
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
1072
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 {
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(),
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
1117
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
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.