use crate::Tf2Error;
use crate::TransformStamped;
use crate::ffi::ffi;
use crate::ffi_utils::call_out;
use crate::transform::Transformable;
use ros_env::sensor_msgs;
crate::impl_has_header_for_ros2_msg!(sensor_msgs::msg::PointCloud2);
impl Transformable for sensor_msgs::msg::PointCloud2 {
fn apply_transform(&self, tf: &TransformStamped) -> Result<Self, Tf2Error> {
let ffi_in: ffi::Tf2PointCloud2 = self.into();
let ffi_out = call_out(|out| ffi::do_transform_pointcloud2(&ffi_in, &tf.to_ffi(), out))?;
Ok((ffi_out).into())
}
}
impl From<&sensor_msgs::msg::PointCloud2> for ffi::Tf2PointCloud2 {
fn from(pc: &sensor_msgs::msg::PointCloud2) -> Self {
ffi::Tf2PointCloud2 {
header: ffi::Tf2Header {
stamp: ffi::Tf2Time {
sec: pc.header.stamp.sec,
nanosec: pc.header.stamp.nanosec,
},
frame_id: pc.header.frame_id.clone(),
},
height: pc.height,
width: pc.width,
fields: pc
.fields
.iter()
.map(|f| ffi::Tf2PointField {
name: f.name.clone(),
offset: f.offset,
datatype: f.datatype,
count: f.count,
})
.collect(),
is_bigendian: pc.is_bigendian,
point_step: pc.point_step,
row_step: pc.row_step,
data: pc.data.clone(),
is_dense: pc.is_dense,
}
}
}
impl From<ffi::Tf2PointCloud2> for sensor_msgs::msg::PointCloud2 {
fn from(pc: ffi::Tf2PointCloud2) -> Self {
let mut out = sensor_msgs::msg::PointCloud2::default();
out.header.stamp.sec = pc.header.stamp.sec;
out.header.stamp.nanosec = pc.header.stamp.nanosec;
out.header.frame_id = pc.header.frame_id;
out.height = pc.height;
out.width = pc.width;
out.fields = pc
.fields
.into_iter()
.map(|f| sensor_msgs::msg::PointField {
name: f.name,
offset: f.offset,
datatype: f.datatype,
count: f.count,
})
.collect();
out.is_bigendian = pc.is_bigendian;
out.point_step = pc.point_step;
out.row_step = pc.row_step;
out.data = pc.data;
out.is_dense = pc.is_dense;
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::ffi;
const PF_FLOAT32: u8 = 7;
fn make_tf(
parent_frame: &str, child_frame: &str, translation: [f64; 3],
rotation_xyzw: [f64; 4],
stamp: (i32, u32),
) -> crate::TransformStamped {
let raw = ffi::Tf2TransformStamped {
stamp: ffi::Tf2Time {
sec: stamp.0,
nanosec: stamp.1,
},
parent_frame: parent_frame.to_string(),
child_frame: child_frame.to_string(),
translation_x: translation[0],
translation_y: translation[1],
translation_z: translation[2],
rotation_x: rotation_xyzw[0],
rotation_y: rotation_xyzw[1],
rotation_z: rotation_xyzw[2],
rotation_w: rotation_xyzw[3],
};
crate::TransformStamped::from_ffi(raw)
}
fn make_xyz_cloud(frame: &str, points: &[[f32; 3]]) -> sensor_msgs::msg::PointCloud2 {
let mut cloud = sensor_msgs::msg::PointCloud2::default();
cloud.header.frame_id = frame.to_string();
cloud.header.stamp.sec = 0;
cloud.header.stamp.nanosec = 0;
cloud.height = 1;
cloud.width = points.len() as u32;
cloud.is_bigendian = false;
cloud.is_dense = true;
let x = sensor_msgs::msg::PointField {
name: "x".to_string(),
offset: 0,
datatype: PF_FLOAT32,
count: 1,
};
let y = sensor_msgs::msg::PointField {
name: "y".to_string(),
offset: 4,
datatype: PF_FLOAT32,
count: 1,
};
let z = sensor_msgs::msg::PointField {
name: "z".to_string(),
offset: 8,
datatype: PF_FLOAT32,
count: 1,
};
cloud.fields = vec![x, y, z];
cloud.point_step = 12;
cloud.row_step = cloud.point_step * cloud.width;
cloud.data = Vec::with_capacity(cloud.row_step as usize);
for p in points {
cloud.data.extend_from_slice(&p[0].to_le_bytes());
cloud.data.extend_from_slice(&p[1].to_le_bytes());
cloud.data.extend_from_slice(&p[2].to_le_bytes());
}
cloud
}
fn read_xyz_cloud(cloud: &sensor_msgs::msg::PointCloud2) -> Vec<[f32; 3]> {
assert_eq!(cloud.point_step, 12, "test helper assumes point_step=12");
cloud
.data
.chunks_exact(12)
.map(|c| {
let x = f32::from_le_bytes(c[0..4].try_into().unwrap());
let y = f32::from_le_bytes(c[4..8].try_into().unwrap());
let z = f32::from_le_bytes(c[8..12].try_into().unwrap());
[x, y, z]
})
.collect()
}
fn assert_near(a: f32, b: f32, eps: f32, label: &str) {
if (a - b).abs() > eps {
panic!("{label}: {a} vs {b} (eps={eps})");
}
}
#[test]
fn transform_pointcloud2_translation_only() {
let tf = make_tf(
"map",
"lidar",
[10.0, -2.0, 0.5],
[0.0, 0.0, 0.0, 1.0], (123, 456),
);
let cloud_in = make_xyz_cloud("lidar", &[[1.0, 2.0, 3.0]]);
let cloud_out = cloud_in
.apply_transform(&tf)
.expect("transform should succeed");
assert_eq!(cloud_out.header.frame_id, "map");
assert_eq!(cloud_out.header.stamp.sec, 123);
assert_eq!(cloud_out.header.stamp.nanosec, 456);
let pts = read_xyz_cloud(&cloud_out);
assert_eq!(pts.len(), 1);
assert_near(pts[0][0], 11.0, 1e-5, "x");
assert_near(pts[0][1], 0.0, 1e-5, "y");
assert_near(pts[0][2], 3.5, 1e-5, "z");
}
#[test]
fn transform_pointcloud2_rotate_z_90deg() {
let s = (0.5f64).sqrt();
let tf = make_tf("map", "lidar", [0.0, 0.0, 0.0], [0.0, 0.0, s, s], (0, 0));
let cloud_in = make_xyz_cloud("lidar", &[[1.0, 0.0, 0.0]]);
let cloud_out = cloud_in
.apply_transform(&tf)
.expect("transform should succeed");
let pts = read_xyz_cloud(&cloud_out);
assert_eq!(pts.len(), 1);
assert_near(pts[0][0], 0.0, 1e-5, "x");
assert_near(pts[0][1], 1.0, 1e-5, "y");
assert_near(pts[0][2], 0.0, 1e-5, "z");
}
#[test]
fn transform_pointcloud2_missing_xyz_fields_returns_err() {
let tf = make_tf(
"map",
"lidar",
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
(0, 0),
);
let mut cloud = sensor_msgs::msg::PointCloud2::default();
cloud.header.frame_id = "lidar".to_string();
cloud.height = 1;
cloud.width = 1;
cloud.point_step = 12;
cloud.row_step = 12;
cloud.is_bigendian = false;
cloud.is_dense = true;
cloud.data = vec![0u8; 12];
let res = cloud.apply_transform(&tf);
assert!(
matches!(
res,
Err(crate::Tf2Error::InvalidArgument(_)) | Err(crate::Tf2Error::Other(_))
),
"expected an error for malformed PointCloud2, got: {res:?}"
);
}
}