diff --git a/.github/workflows/r2r_galactic.yml b/.github/workflows/r2r_galactic.yml index 5c5f3d1..45a609f 100644 --- a/.github/workflows/r2r_galactic.yml +++ b/.github/workflows/r2r_galactic.yml @@ -18,4 +18,4 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker build . --file ./tests/Dockerfile_r2r_galactic --tag r2r_galactic - - run: docker run r2r_galactic cargo test --features r2r_msg + - run: docker run r2r_galactic cargo test --features r2r_msg,derive,nalgebra,rayon diff --git a/.github/workflows/r2r_humble.yml b/.github/workflows/r2r_humble.yml index 5373957..a88752c 100644 --- a/.github/workflows/r2r_humble.yml +++ b/.github/workflows/r2r_humble.yml @@ -18,4 +18,4 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker build . --file ./tests/Dockerfile_r2r_humble --tag r2r_humble - - run: docker run r2r_humble cargo test --features r2r_msg + - run: docker run r2r_humble cargo test --features r2r_msg,derive,nalgebra,rayon diff --git a/.github/workflows/r2r_iron.yml b/.github/workflows/r2r_iron.yml index 5de642d..ef3ab9d 100644 --- a/.github/workflows/r2r_iron.yml +++ b/.github/workflows/r2r_iron.yml @@ -18,4 +18,4 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker build . --file ./tests/Dockerfile_r2r_iron --tag r2r_iron - - run: docker run r2r_iron cargo test --features r2r_msg + - run: docker run r2r_iron cargo test --features r2r_msg,derive,nalgebra,rayon diff --git a/.github/workflows/rclrs_humble.yml b/.github/workflows/rclrs_humble.yml index 7ac4349..1c0f88b 100644 --- a/.github/workflows/rclrs_humble.yml +++ b/.github/workflows/rclrs_humble.yml @@ -18,4 +18,4 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker build . --file ./tests/Dockerfile_rclrs_humble --tag rclrs_humble - - run: docker run rclrs_humble cargo test + - run: docker run rclrs_humble cargo test --features derive,nalgebra,rayon diff --git a/.github/workflows/rclrs_iron.yml b/.github/workflows/rclrs_iron.yml index d9ac7cf..390419b 100644 --- a/.github/workflows/rclrs_iron.yml +++ b/.github/workflows/rclrs_iron.yml @@ -18,4 +18,4 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker build . --file ./tests/Dockerfile_rclrs_iron --tag rclrs_iron - - run: docker run rclrs_iron cargo test + - run: docker run rclrs_iron cargo test --features derive,nalgebra,rayon diff --git a/.github/workflows/rosrust_noetic.yml b/.github/workflows/rosrust_noetic.yml index 4662df4..40a5c12 100644 --- a/.github/workflows/rosrust_noetic.yml +++ b/.github/workflows/rosrust_noetic.yml @@ -105,4 +105,4 @@ jobs: - name: test run: | source /opt/ros/$ROS_DISTRO/setup.bash - cargo test --features rosrust_msg + cargo test --features rosrust_msg,derive,nalgebra,rayon diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f940b7..04c5cee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,4 +25,4 @@ jobs: - name: Linting run: cargo clippy --all-targets -- -D warnings - name: Tests - run: cargo test + run: cargo test --features derive,nalgebra,rayon diff --git a/Cargo.toml b/Cargo.toml index a70ded8..c347ffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,17 +7,48 @@ description = "Customizable conversions for working with sensor_msgs/PointCloud2 repository = "https://github.com/stelzo/ros_pointcloud2" license = "MIT" keywords = ["ros", "pointcloud2", "pointcloud", "message"] -categories = ["science::robotics", "encoding", "data-structures", "api-bindings"] +categories = [ + "science::robotics", + "encoding", + "data-structures", + "api-bindings", +] readme = "README.md" documentation = "https://docs.rs/ros_pointcloud2" homepage = "https://github.com/stelzo/ros_pointcloud2" -exclude = ["**/tests/**", "**/examples/**", "**/benches/**", "**/target/**", "**/build/**", "**/dist/**", "**/docs/**", "**/doc/**"] +exclude = [ + "**/tests/**", + "**/examples/**", + "**/benches/**", + "**/target/**", + "**/build/**", + "**/dist/**", + "**/docs/**", + "**/doc/**", +] [dependencies] rosrust_msg = { version = "0.1", optional = true } rosrust = { version = "0.9.11", optional = true } r2r = { version = "0.8.4", optional = true } +rayon = { version = "1", optional = true } +nalgebra = { version = "0", optional = true } +rpcl2_derive = { path = "./rpcl2_derive", optional = true } +type-layout = { version = "0.2", optional = true } + +[dev-dependencies] +rand = "0.8" +criterion = { version = "0.4", features = ["html_reports"] } + +[[bench]] +name = "roundtrip" +harness = false [features] rosrust_msg = ["dep:rosrust_msg", "dep:rosrust"] -r2r_msg = ["dep:r2r"] \ No newline at end of file +r2r_msg = ["dep:r2r"] +rayon = ["dep:rayon"] +derive = ["dep:rpcl2_derive", "dep:type-layout"] +nalgebra = ["dep:nalgebra"] + +default = ["rayon", "derive", "nalgebra"] \ No newline at end of file diff --git a/README.md b/README.md index 4bcd698..7143c6c 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,53 @@ +## !! Note !! +This library is currently in development for v1.0.0, for the documentation of v0.4.0 on crates.io, visit the [docs](https://docs.rs/ros_pointcloud2/0.4.0/ros_pointcloud2/). +

ROS PointCloud2

-

Customizable conversions to and from the PointCloud2 ROS message.

+

A complete and versatile abstraction of PointCloud2.

-Providing an easy to use, generics defined, point-wise iterator abstraction over the byte buffer in `PointCloud2` to minimize iterations in your processing pipeline. +To keep the crate a general purpose library for the problem, it uses its own type for the message `PointCloud2Msg`. +ROS1 and ROS2 is supported with feature flags. -To keep the crate a general purpose library for the problem and support ROS1 and ROS2, it uses its own type for the message `PointCloud2Msg`. +Get started with the example below, check out the other use cases in the `examples` folder or see the [Documentation](https://docs.rs/ros_pointcloud2/1.0.0/ros_pointcloud2/) for a complete guide. + +## Quickstart + +The following example uses the iterator APIs. For memory bound pipelines, you may also try the `try_from_vec` or `try_into_vec` functions by enabling the `derive` feature. -## Examples ```rust -use ros_pointcloud2::{ - pcl_utils::PointXYZ, reader::ReaderXYZ, writer::WriterXYZ, PointCloud2Msg, -}; +use ros_pointcloud2::prelude::*; -// Your points (here using the predefined type PointXYZ). +// PointXYZ (and many others) are provided by the crate. let cloud_points = vec![ - PointXYZ {x: 91.486, y: -4.1, z: 42.0001,}, - PointXYZ {x: f32::MAX, y: f32::MIN, z: f32::MAX,}, + PointXYZ::new(91.486, -4.1, 42.0001), + PointXYZ::new(f32::MAX, f32::MIN, f32::MAX), ]; -// For equality test later -let cloud_copy = cloud_points.clone(); - -// Vector -> Writer -> Message. -// You can also just give the Vec or anything that implements `IntoIterator`. -let internal_msg: PointCloud2Msg = WriterXYZ::from(cloud_points.into_iter()) - .try_into() // iterating points here O(n) - .unwrap(); - -// Convert to your ROS crate message type, we will use r2r here. -// let msg: r2r::sensor_msgs::msg::PointCloud2 = internal_msg.into(); +// Give the Vec or anything that implements `IntoIterator`. +let in_msg = PointCloud2Msg::try_from_iter(cloud_points).unwrap(); +// Convert the ROS crate message type, we will use r2r here. +// let msg: r2r::sensor_msgs::msg::PointCloud2 = in_msg.into(); // Publish ... - // ... now incoming from a topic. -// let internal_msg: PointCloud2Msg = msg.into(); +// let in_msg: PointCloud2Msg = msg.into(); -// Message -> Reader -> your pipeline. The Reader implements the Iterator trait. -let reader = ReaderXYZ::try_from(internal_msg).unwrap(); -let new_cloud_points = reader - .map(|point: PointXYZ| { - // Some logic here +let new_pcl = in_msg.try_into_iter().unwrap() + .map(|point: PointXYZ| { // Define the type of point here. + // Some logic here ... - point - }) - .collect::>(); // iterating points here O(n) - -assert_eq!(new_cloud_points, cloud_copy); + point + }) + .collect::>(); ``` ## Integrations There are currently 3 integrations for common ROS crates. + - [rosrust_msg](https://github.com/adnanademovic/rosrust) - [![Tests](https://github.com/stelzo/ros_pointcloud2/actions/workflows/rosrust_noetic.yml/badge.svg)](https://github.com/stelzo/ros_pointcloud2/actions/workflows/rosrust_noetic.yml) - [r2r_msg](https://github.com/sequenceplanner/r2r) @@ -65,6 +59,7 @@ There are currently 3 integrations for common ROS crates. - [![Tests](https://github.com/stelzo/ros_pointcloud2/actions/workflows/rclrs_iron.yml/badge.svg)](https://github.com/stelzo/ros_pointcloud2/actions/workflows/rclrs_iron.yml) You can use `rosrust` and `r2r` by enabling the respective feature: + ```toml [dependencies] ros_pointcloud2 = { version = "*", features = ["r2r_msg"]} @@ -73,12 +68,16 @@ ros_pointcloud2 = { version = "*", features = ["rosrust_msg"]} ``` ### rclrs (ros2_rust) + Features do not work properly with `rcrls` because the messages are linked externally. You need to use tags instead: + ```toml [dependencies] ros_pointcloud2 = { git = "https://github.com/stelzo/ros_pointcloud2", tag = "v0.4.0_rclrs" } ``` + Also, indicate the following dependencies to your linker inside the `package.xml` of your package. + ```xml std_msgs sensor_msgs @@ -87,11 +86,6 @@ Also, indicate the following dependencies to your linker inside the `package.xml Please open an issue or PR if you want to see support for other crates. -## Future Work -- Benchmark vs PCL -- Add more predefined types -- Optional derive macros for custom point implementations - - ## License + [MIT](https://choosealicense.com/licenses/mit/) diff --git a/benches/roundtrip.rs b/benches/roundtrip.rs new file mode 100644 index 0000000..cf4945b --- /dev/null +++ b/benches/roundtrip.rs @@ -0,0 +1,164 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use ros_pointcloud2::prelude::*; + +use rand::Rng; + +pub fn generate_random_pointcloud(num_points: usize, min: f32, max: f32) -> Vec { + let mut rng = rand::thread_rng(); + let mut pointcloud = Vec::with_capacity(num_points); + for _ in 0..num_points { + let point = PointXYZ { + x: rng.gen_range(min..max), + y: rng.gen_range(min..max), + z: rng.gen_range(min..max), + }; + pointcloud.push(point); + } + pointcloud +} + +#[cfg(feature = "derive")] +fn roundtrip_vec(cloud: Vec) -> bool { + let orig_len = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_vec(cloud).unwrap(); + let total = internal_msg + .try_into_iter() + .unwrap() + .collect::>(); + orig_len == total.len() +} + +fn roundtrip(cloud: Vec) -> bool { + let orig_len = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_iter(cloud).unwrap(); + let total = internal_msg + .try_into_iter() + .unwrap() + .collect::>(); + orig_len == total.len() +} + +#[cfg(feature = "derive")] +fn roundtrip_filter_vec(cloud: Vec) -> bool { + let orig_len = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_vec(cloud).unwrap(); + let total = internal_msg + .try_into_iter() + .unwrap() + .filter(|point: &PointXYZ| { + (point.x.powi(2) + point.y.powi(2) + point.z.powi(2)).sqrt() < 1.9 + }) + .fold(PointXYZ::default(), |acc, point| PointXYZ { + x: acc.x + point.x, + y: acc.y + point.y, + z: acc.z + point.z, + }); + orig_len == total.x as usize +} + +fn roundtrip_filter(cloud: Vec) -> bool { + let orig_len = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_iter(cloud).unwrap(); + let total = internal_msg + .try_into_iter() + .unwrap() + .filter(|point: &PointXYZ| { + (point.x.powi(2) + point.y.powi(2) + point.z.powi(2)).sqrt() < 1.9 + }) + .fold(PointXYZ::default(), |acc, point| PointXYZ { + x: acc.x + point.x, + y: acc.y + point.y, + z: acc.z + point.z, + }); + orig_len == total.x as usize +} + +#[cfg(feature = "rayon")] +fn roundtrip_par(cloud: Vec) -> bool { + let orig_len = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_iter(cloud).unwrap(); + let total = internal_msg + .try_into_par_iter() + .unwrap() + .collect::>(); + orig_len != total.len() +} + +#[cfg(feature = "rayon")] +fn roundtrip_filter_par(cloud: Vec) -> bool { + let orig_len: usize = cloud.len(); + let internal_msg = PointCloud2Msg::try_from_iter(cloud).unwrap(); + let total = internal_msg + .try_into_par_iter() + .unwrap() + .filter(|point: &PointXYZ| { + (point.x.powi(2) + point.y.powi(2) + point.z.powi(2)).sqrt() < 1.9 + }) + .reduce(PointXYZ::default, |acc, point| PointXYZ { + x: acc.x + point.x, + y: acc.y + point.y, + z: acc.z + point.z, + }); + orig_len == total.x as usize +} + +fn roundtrip_benchmark(c: &mut Criterion) { + let cloud_points_500k = generate_random_pointcloud(500_000, f32::MIN / 2.0, f32::MAX / 2.0); + let cloud_points_1_5m = generate_random_pointcloud(1_500_000, f32::MIN / 2.0, f32::MAX / 2.0); + + c.bench_function("roundtrip 500k", |b| { + b.iter(|| { + black_box(roundtrip(cloud_points_500k.clone())); + }) + }); + + #[cfg(feature = "rayon")] + c.bench_function("roundtrip_par 500k", |b| { + b.iter(|| { + black_box(roundtrip_par(cloud_points_500k.clone())); + }) + }); + + #[cfg(feature = "derive")] + c.bench_function("roundtrip_vec 500k", |b| { + b.iter(|| { + black_box(roundtrip_vec(cloud_points_500k.clone())); + }) + }); + + c.bench_function("roundtrip_filter 500k", |b| { + b.iter(|| { + roundtrip_filter(black_box(cloud_points_500k.clone())); + }) + }); + + #[cfg(feature = "rayon")] + c.bench_function("roundtrip_filter_par 500k", |b| { + b.iter(|| { + roundtrip_filter_par(black_box(cloud_points_500k.clone())); + }) + }); + + c.bench_function("roundtrip_filter 1.5m", |b| { + b.iter(|| { + roundtrip_filter(black_box(cloud_points_1_5m.clone())); + }) + }); + + #[cfg(feature = "rayon")] + c.bench_function("roundtrip_filter_par 1.5m", |b| { + b.iter(|| { + black_box(roundtrip_filter_par(cloud_points_1_5m.clone())); + }) + }); + + #[cfg(feature = "derive")] + c.bench_function("roundtrip_filter_vec 1.5m", |b| { + b.iter(|| { + roundtrip_filter_vec(black_box(cloud_points_1_5m.clone())); + }) + }); +} + +criterion_group!(benches, roundtrip_benchmark); +criterion_main!(benches); diff --git a/examples/custom_enum_field_filter.rs b/examples/custom_enum_field_filter.rs new file mode 100644 index 0000000..264422b --- /dev/null +++ b/examples/custom_enum_field_filter.rs @@ -0,0 +1,148 @@ +/// This example demonstrates how to use a custom point with encoded metadata. +/// The use case is a segmentation point cloud where each point holds a label and we want to filter by it. +/// Since the datatypes for the PointCloud2 message are very limited, +/// we need to encode the enum into a supported type. +/// This needs some manual work to tell the library how to encode and decode the enum. +/// +/// Important Note: This example is only possible with disabled `derive` feature, +/// because the library (currently) does not know the size of your chosen supported type at compile time. +/// This makes direct copies impossible. +use ros_pointcloud2::prelude::*; + +#[derive(Debug, PartialEq, Clone, Default, Copy)] +enum Label { + #[default] + Human, + Deer, + Car, +} + +// Define a custom point with an enum (not supported by PointCloud2) +#[derive(Debug, PartialEq, Clone, Default)] +struct CustomPoint { + x: f32, + y: f32, + z: f32, + intensity: f32, + my_custom_label: Label, +} + +// For our convenience +impl From