diff --git a/Cargo.toml b/Cargo.toml
index f5de2b8..a70ded8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,9 +14,6 @@ homepage = "https://github.com/stelzo/ros_pointcloud2"
exclude = ["**/tests/**", "**/examples/**", "**/benches/**", "**/target/**", "**/build/**", "**/dist/**", "**/docs/**", "**/doc/**"]
[dependencies]
-mem_macros = "1.0.1"
-num-traits = "0.2.15"
-fallible-iterator = "0.3.0"
rosrust_msg = { version = "0.1", optional = true }
rosrust = { version = "0.9.11", optional = true }
r2r = { version = "0.8.4", optional = true }
diff --git a/README.md b/README.md
index 01d5766..4bcd698 100644
--- a/README.md
+++ b/README.md
@@ -5,58 +5,48 @@
-Providing a memory efficient way for message conversion while allowing user defined types without the cost of iterations.
-Instead of converting the entire cloud into a `Vec`, you get an iterable type that converts each point from the message on the fly.
+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 and support ROS1 and ROS2, it uses its own type for the message `ros_types::PointCloud2Msg`.
+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`.
+
+## Examples
```rust
use ros_pointcloud2::{
- fallible_iterator::FallibleIterator,
- pcl_utils::PointXYZ,
- ros_types::PointCloud2Msg,
- ConvertXYZ,
+ pcl_utils::PointXYZ, reader::ReaderXYZ, writer::WriterXYZ, PointCloud2Msg,
};
// Your points (here using the predefined type PointXYZ).
let cloud_points = vec![
- PointXYZ {
- x: 1.3,
- y: 1.6,
- z: 5.7,
- },
- PointXYZ {
- x: f32::MAX,
- y: f32::MIN,
- z: f32::MAX,
- },
+ PointXYZ {x: 91.486, y: -4.1, z: 42.0001,},
+ PointXYZ {x: f32::MAX, y: f32::MIN, z: f32::MAX,},
];
-let cloud_copy = cloud_points.clone(); // For checking equality later.
+// For equality test later
+let cloud_copy = cloud_points.clone();
-// Vector -> Converter -> Message
-let internal_msg: PointCloud2Msg = ConvertXYZ::try_from(cloud_points)
- .unwrap()
- .try_into()
- .unwrap();
+// 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 favorite ROS crate message type, we will use rosrust here.
-// let msg: rosrust_msg::sensor_msgs::PointCloud2 = internal_msg.into();
+// Convert to your ROS crate message type, we will use r2r here.
+// let msg: r2r::sensor_msgs::msg::PointCloud2 = internal_msg.into();
-// Back to this crate's message type.
+// Publish ...
+
+// ... now incoming from a topic.
// let internal_msg: PointCloud2Msg = msg.into();
-// Message -> Converter -> Vector
-let convert: ConvertXYZ = ConvertXYZ::try_from(internal_msg).unwrap();
-let new_cloud_points = convert
- .map(|point: PointXYZ| {
- // Insert your point business logic here
- // or use other methods like .for_each().
-
- // We are using a fallible iterator so we need to return a Result.
- Ok(point)
- })
- .collect::>()
- .unwrap(); // Handle point conversion or byte errors here.
+// 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
+
+ point
+ })
+ .collect::>(); // iterating points here O(n)
assert_eq!(new_cloud_points, cloud_copy);
```
@@ -95,141 +85,13 @@ Also, indicate the following dependencies to your linker inside the `package.xml
builtin_interfaces
```
-### Others
-To use `ros_pointcloud2` somewhere else, you can also implement the `Into` and `From` traits for `PointCloud2Msg`.
-
-Try to avoid cloning the `data: Vec` field.
-```rust
-use ros_pointcloud2::ros_types::PointCloud2Msg;
-
-struct YourROSPointCloud2 {} // Likely to be generated by your ROS crate.
-
-impl Into for PointCloud2Msg {
- fn into(self) -> YourROSPointCloud2 {
- todo!()
- }
-}
-
-impl From for PointCloud2Msg {
- fn from(msg: YourROSPointCloud2) -> Self {
- todo!()
- }
-}
-```
-
Please open an issue or PR if you want to see support for other crates.
-## Features
-
-- Easy to integrate into your favorite ROS1 or ROS2 crate
-- Custom point types
-- Predefined types for common conversions (compared to PCL)
- - PointXYZ
- - PointXYZI
- - PointXYZL
- - PointXYZRGB
- - PointXYZRGBA
- - PointXYZRGBL
- - PointXYZNormal
- - PointXYZINormal
- - PointXYZRGBNormal
-- 2D and 3D
-
-## Custom Types
-
-You can freely convert to your own point types without reiterating the entire cloud.
-
-You just need to implement some traits.
-```rust
-use ros_pointcloud2::mem_macros::size_of;
-use ros_pointcloud2::ros_types::PointCloud2Msg;
-use ros_pointcloud2::{ConversionError, Convert, MetaNames, PointConvertible, PointMeta};
-
-const DIM: usize = 3;
-const METADIM: usize = 4;
-
-#[derive(Debug, PartialEq, Clone)]
-struct CustomPoint {
- x: f32, // DIM 1
- y: f32, // DIM 2
- z: f32, // DIM 3
- r: u8, // METADIM 1
- g: u8, // METADIM 2
- b: u8, // METADIM 3
- a: u8, // METADIM 4
-}
-
-// Converting your custom point to the crate's internal representation
-impl From for ([f32; DIM], [PointMeta; METADIM]) {
- fn from(point: CustomPoint) -> Self {
- (
- [point.x, point.y, point.z],
- [
- PointMeta::new(point.r),
- PointMeta::new(point.g),
- PointMeta::new(point.b),
- PointMeta::new(point.a),
- ],
- )
- }
-}
-
-// The mappings for index of meta idx to field names. Example: 0 -> "r", 1 -> "g", 2 -> "b", 3 -> "a"
-impl MetaNames for CustomPoint {
- fn meta_names() -> [String; METADIM] {
- ["r", "g", "b", "a"].map(|s| s.to_string())
- }
-}
-
-// Converting crate's internal representation to your custom point
-impl TryFrom<([f32; DIM], [PointMeta; METADIM])> for CustomPoint {
- type Error = ConversionError;
- fn try_from(data: ([f32; DIM], [PointMeta; METADIM])) -> Result {
- Ok(Self {
- x: data.0[0],
- y: data.0[1],
- z: data.0[2],
- r: data.1[0].get()?,
- g: data.1[1].get()?,
- b: data.1[2].get()?,
- a: data.1[3].get()?,
- })
- }
-}
-
-impl PointConvertible for CustomPoint {}
-
-type MyConverter = Convert;
-
-// Your custom cloud (Vector)
-let custom_cloud = vec![
- CustomPoint {
- x: f32::MAX,
- y: f32::MIN,
- z: f32::MAX,
- r: u8::MAX,
- g: u8::MAX,
- b: u8::MAX,
- a: u8::MAX,
- },
-];
-
-// Cloud -> ROS message
-let custom_msg: PointCloud2Msg = MyConverter::try_from(custom_cloud)
- .unwrap()
- .try_into()
- .unwrap();
-
-// ROS message -> Cloud
-let to_custom_type = MyConverter::try_from(custom_msg).unwrap();
-```
-
## Future Work
- Benchmark vs PCL
-- Proper error passing to the iterator result (currently merged into `PointConversionError`)
-- remove allocations
-- introduce no-panic for maximum stability
- Add more predefined types
+- Optional derive macros for custom point implementations
+
## License
[MIT](https://choosealicense.com/licenses/mit/)
diff --git a/src/convert.rs b/src/convert.rs
new file mode 100644
index 0000000..c5d055f
--- /dev/null
+++ b/src/convert.rs
@@ -0,0 +1,361 @@
+use crate::*;
+
+/// Datatypes from the [PointField message](http://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/PointField.html).
+#[derive(Default, Clone, Debug, PartialEq, Copy)]
+pub enum FieldDatatype {
+ F32,
+ F64,
+ I32,
+ U8,
+ U16,
+ #[default]
+ U32,
+ I8,
+ I16,
+}
+
+impl FieldDatatype {
+ pub fn size(&self) -> usize {
+ match self {
+ FieldDatatype::U8 => 1,
+ FieldDatatype::U16 => 2,
+ FieldDatatype::U32 => 4,
+ FieldDatatype::I8 => 1,
+ FieldDatatype::I16 => 2,
+ FieldDatatype::I32 => 4,
+ FieldDatatype::F32 => 4,
+ FieldDatatype::F64 => 8,
+ }
+ }
+}
+
+/// Getter trait for the datatype of a field value.
+pub trait GetFieldDatatype {
+ fn field_datatype() -> FieldDatatype;
+}
+
+impl GetFieldDatatype for f32 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::F32
+ }
+}
+
+impl GetFieldDatatype for f64 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::F64
+ }
+}
+
+impl GetFieldDatatype for i32 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::I32
+ }
+}
+
+impl GetFieldDatatype for u8 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::U8
+ }
+}
+
+impl GetFieldDatatype for u16 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::U16
+ }
+}
+
+impl GetFieldDatatype for u32 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::U32
+ }
+}
+
+impl GetFieldDatatype for i8 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::I8
+ }
+}
+
+impl GetFieldDatatype for i16 {
+ fn field_datatype() -> FieldDatatype {
+ FieldDatatype::I16
+ }
+}
+
+impl TryFrom for FieldDatatype {
+ type Error = ConversionError;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 1 => Ok(FieldDatatype::I8),
+ 2 => Ok(FieldDatatype::U8),
+ 3 => Ok(FieldDatatype::I16),
+ 4 => Ok(FieldDatatype::U16),
+ 5 => Ok(FieldDatatype::I32),
+ 6 => Ok(FieldDatatype::U32),
+ 7 => Ok(FieldDatatype::F32),
+ 8 => Ok(FieldDatatype::F64),
+ _ => Err(ConversionError::UnsupportedFieldType),
+ }
+ }
+}
+
+impl From for u8 {
+ fn from(val: FieldDatatype) -> Self {
+ match val {
+ FieldDatatype::I8 => 1,
+ FieldDatatype::U8 => 2,
+ FieldDatatype::I16 => 3,
+ FieldDatatype::U16 => 4,
+ FieldDatatype::I32 => 5,
+ FieldDatatype::U32 => 6,
+ FieldDatatype::F32 => 7,
+ FieldDatatype::F64 => 8,
+ }
+ }
+}
+
+pub(crate) fn check_coord(
+ coord: Option,
+ fields: &[PointFieldMsg],
+ xyz_field_type: &FieldDatatype,
+) -> Result {
+ match coord {
+ Some(y_idx) => {
+ let field = &fields[y_idx];
+ if field.datatype != u8::from(*xyz_field_type) {
+ return Err(ConversionError::InvalidFieldFormat);
+ }
+ Ok(field.clone())
+ }
+ None => Err(ConversionError::NotEnoughFields),
+ }
+}
+
+/// Matching field names from each meta data per point to the PointField name.
+/// Always make sure to use the same order as in your conversion implementation to have a correct mapping.
+///
+/// This trait is needed to implement the `PointConvertible` trait.
+///
+/// # Example for full point conversion.
+/// ```
+/// use ros_pointcloud2::{Point, PointConvertible, MetaNames, size_of};
+///
+/// #[derive(Clone, Debug, PartialEq, Copy)]
+/// pub struct MyPointXYZI {
+/// pub x: f32,
+/// pub y: f32,
+/// pub z: f32,
+/// pub intensity: f32,
+/// }
+///
+/// impl MetaNames<1> for MyPointXYZI {
+/// fn meta_names() -> [&'static str; 1] {
+/// ["intensity"]
+/// }
+/// }
+/// ```
+pub trait MetaNames {
+ fn meta_names() -> [&'static str; METADIM];
+}
+
+/// This trait is used to convert a byte slice to a primitive type.
+/// All PointField types are supported.
+pub trait FromBytes: Default + Sized + Copy + GetFieldDatatype {
+ fn from_be_bytes(bytes: &[u8]) -> Self;
+ fn from_le_bytes(bytes: &[u8]) -> Self;
+
+ fn bytes(_: &Self) -> Vec;
+}
+
+impl FromBytes for i8 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0]])
+ }
+
+ #[inline]
+ fn bytes(x: &i8) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for i16 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0], bytes[1]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0], bytes[1]])
+ }
+
+ #[inline]
+ fn bytes(x: &i16) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for u16 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0], bytes[1]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0], bytes[1]])
+ }
+
+ #[inline]
+ fn bytes(x: &u16) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for u32 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+
+ #[inline]
+ fn bytes(x: &u32) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for f32 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+
+ #[inline]
+ fn bytes(x: &f32) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for i32 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
+ }
+
+ fn bytes(x: &i32) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for f64 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([
+ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
+ ])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([
+ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
+ ])
+ }
+
+ #[inline]
+ fn bytes(x: &f64) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+impl FromBytes for u8 {
+ #[inline]
+ fn from_be_bytes(bytes: &[u8]) -> Self {
+ Self::from_be_bytes([bytes[0]])
+ }
+
+ #[inline]
+ fn from_le_bytes(bytes: &[u8]) -> Self {
+ Self::from_le_bytes([bytes[0]])
+ }
+
+ #[inline]
+ fn bytes(x: &u8) -> Vec {
+ Vec::from(x.to_le_bytes())
+ }
+}
+
+#[derive(Default, Clone, Debug, PartialEq)]
+pub(crate) enum Endianness {
+ Big,
+
+ #[default]
+ Little,
+}
+
+#[inline(always)]
+pub(crate) fn load_loadable(
+ start_idx: usize,
+ data: &[u8],
+ endian: &Endianness,
+) -> T {
+ match endian {
+ Endianness::Big => T::from_be_bytes(load_bytes::(start_idx, data).as_slice()),
+ Endianness::Little => T::from_le_bytes(load_bytes::(start_idx, data).as_slice()),
+ }
+}
+
+/// Note: check if the data slice is long enough to load the bytes beforehand! Uses unsafe indexing.
+#[inline(always)]
+fn load_bytes(start_idx: usize, data: &[u8]) -> [u8; S] {
+ let mut target = [u8::default(); S];
+
+ debug_assert!(target.len() == S);
+ debug_assert!(data.len() >= start_idx + S);
+
+ let source = unsafe { data.get_unchecked(start_idx..start_idx + S) };
+ target
+ .iter_mut()
+ .zip(source.iter())
+ .for_each(|(target, source)| {
+ *target = *source;
+ });
+ target
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_bytes() {
+ i8::bytes(&1);
+ u8::bytes(&1);
+ i16::bytes(&1);
+ u16::bytes(&1);
+ i32::bytes(&1);
+ u32::bytes(&1);
+ f32::bytes(&1.0);
+ f64::bytes(&1.0);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index eff935d..dd868a1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,261 +1,210 @@
-#![doc = include_str!("../README.md")]
+//! A library to work with the PointCloud2 message type from ROS.
+//!
+//! ros_pointcloud2 mainly provides iterator-based [`ros_pointcloud2::reader`] and [`ros_pointcloud2::writer`] to interact
+//! with the internal message type [`ros_pointcloud2::PointCloud2Msg`] without forcing an iteration.
+//!
+//! For ROS interoperability, the message type generated by the respective crate must be converted to
+//! the [`ros_pointcloud2::PointCloud2Msg`] using the `From` trait,
+//! which is mostly a ownership transfer without copying the point data.
+//!
+//! There are implementations for the `r2r`, `rclrs` (ros2_rust) and `rosrust_msg` message types
+//! available in the feature flags. If you miss a message type, please open an issue or a PR.
+//! See the [`ros_pointcloud2::ros_types`] module for more information.
+//!
+//! Common point types like `PointXYZ` are provided in the [`ros_pointcloud2::pcl_utils`] module. You can implement any custom point type
+//! that can be described by the specification.
+//! See the [`ros_pointcloud2::Reader`] or [`ros_pointcloud2::Writer`] struct documentation for an example.
+//!
+//! # Example
+//! ```
+//! use ros_pointcloud2::{
+//! pcl_utils::PointXYZ, reader::ReaderXYZ, writer::WriterXYZ, PointCloud2Msg,
+//! };
+//!
+//! // Your points (here using the predefined type PointXYZ).
+//! let cloud_points = vec![
+//! PointXYZ {x: 9.0006, y: 42.0, z: -6.2,},
+//! PointXYZ {x: f32::MAX, y: f32::MIN,z: 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();
+//!
+//! // Publish ...
+//!
+//! // ... now incoming from a topic.
+//! // let internal_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
+//!
+//! point
+//! })
+//! .collect::>(); // iterating points here O(n)
+//!
+//! assert_eq!(new_cloud_points, cloud_copy);
+//! ```
+
+pub mod convert;
pub mod pcl_utils;
pub mod ros_types;
-use crate::pcl_utils::*;
-use crate::ros_types::{PointCloud2Msg, PointFieldMsg};
-use num_traits::Zero;
+/// Macro to get the size of a type at compile time. This is a convenience macro to avoid writing out the full std::mem::size_of::().
+/// Use it for your custom Reader and Writer implementations.
+/// # Example
+/// ```
+/// use ros_pointcloud2::{
+/// writer::Writer, size_of, pcl_utils::PointXYZ,
+/// };
+///
+/// type MyWriterXYZ = Writer;
+/// ```
+#[macro_export]
+macro_rules! size_of {
+ ($t:ty) => {
+ std::mem::size_of::<$t>()
+ };
+}
-#[macro_use]
-pub extern crate mem_macros;
+pub mod reader;
+pub mod writer;
-pub extern crate fallible_iterator;
+pub use convert::MetaNames;
+pub use ros_types::PointCloud2Msg;
-/// Errors that can occur when converting between PointCloud2 and a custom type.
+use crate::convert::{
+ FieldDatatype,
+ FromBytes,
+};
+
+use crate::ros_types::PointFieldMsg;
+
+/// All errors that can occur for creating Reader and Writer.
#[derive(Debug)]
pub enum ConversionError {
+ /// The coordinate field does not match the expected datatype.
InvalidFieldFormat,
+
+ /// Not enough meta or dimensional fields in the PointCloud2 message.
NotEnoughFields,
+
+ /// The dimensionality of the point inside the message is too high.
TooManyDimensions,
+
+ /// The field type is not supported by the ROS message description.
UnsupportedFieldType,
+
+ /// There are no points in the point cloud.
NoPoints,
+
+ /// The length of the byte buffer in the message does not match the expected length computed from the fields.
DataLengthMismatch,
- MetaIndexLengthMismatch,
- EndOfBuffer,
- PointConversionError,
- MetaDatatypeMismatch,
+}
+
+/// Internal point representation. It is used to store the coordinates and meta data of a point.
+/// In each iteration, the Reader will convert the internal representation to the desired point type.
+/// Implement the `From` traits for your point type to use the conversion as part of the [`PointConvertible`] trait.
+///
+/// # Example
+/// ```
+/// use ros_pointcloud2::Point;
+///
+/// #[derive(Clone, Debug, PartialEq, Copy)]
+/// pub struct MyPointXYZI {
+/// pub x: f32,
+/// pub y: f32,
+/// pub z: f32,
+/// pub intensity: f32,
+/// }
+///
+/// impl From for Point {
+/// fn from(point: MyPointXYZI) -> Self {
+/// Point {
+/// coords: [point.x, point.y, point.z],
+/// meta: [point.intensity.into()],
+/// }
+/// }
+/// }
+///
+/// impl From> for MyPointXYZI {
+/// fn from(point: Point) -> Self {
+/// Self {
+/// x: point.coords[0],
+/// y: point.coords[1],
+/// z: point.coords[2],
+/// intensity: point.meta[0].get(),
+/// }
+/// }
+/// }
+/// ```
+pub struct Point {
+ pub coords: [T; DIM],
+ pub meta: [PointMeta; METADIM],
}
/// Trait to convert a point to a tuple of coordinates and meta data.
/// Implement this trait for your point type to use the conversion.
-pub trait PointConvertible:
- TryInto<([T; DIM], [PointMeta; METADIM])>
- + TryFrom<([T; DIM], [PointMeta; METADIM])>
- + MetaNames
- + Clone
-where
- T: FromBytes,
-{
-}
-
-/// Datatypes from the [PointField message](http://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/PointField.html).
-#[derive(Clone, Debug, PartialEq, Copy)]
-pub enum FieldDatatype {
- F32,
- F64,
- I32,
- U8,
- U16,
- U32,
- I8,
- I16,
-}
-
-/// Getter trait for the datatype of a field value.
-pub trait GetFieldDatatype {
- fn field_datatype() -> FieldDatatype;
-}
-
-impl GetFieldDatatype for f32 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::F32
- }
-}
-
-impl GetFieldDatatype for f64 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::F64
- }
-}
-
-impl GetFieldDatatype for i32 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::I32
- }
-}
-
-impl GetFieldDatatype for u8 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::U8
- }
-}
-
-impl GetFieldDatatype for u16 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::U16
- }
-}
-
-impl GetFieldDatatype for u32 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::U32
- }
-}
-
-impl GetFieldDatatype for i8 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::I8
- }
-}
-
-impl GetFieldDatatype for i16 {
- fn field_datatype() -> FieldDatatype {
- FieldDatatype::I16
- }
-}
-
-fn convert_to_msg_code(geo_type: &FieldDatatype) -> u8 {
- match geo_type {
- FieldDatatype::I8 => 1,
- FieldDatatype::U8 => 2,
- FieldDatatype::I16 => 3,
- FieldDatatype::U16 => 4,
- FieldDatatype::I32 => 5,
- FieldDatatype::U32 => 6,
- FieldDatatype::F32 => 7,
- FieldDatatype::F64 => 8,
- }
-}
-
-fn convert_msg_code_to_type(code: u8) -> Result {
- match code {
- 7 => Ok(FieldDatatype::F32),
- 8 => Ok(FieldDatatype::F64),
- 5 => Ok(FieldDatatype::I32),
- 2 => Ok(FieldDatatype::U8),
- 4 => Ok(FieldDatatype::U16),
- 6 => Ok(FieldDatatype::U32),
- 1 => Ok(FieldDatatype::I8),
- 3 => Ok(FieldDatatype::I16),
- _ => Err(ConversionError::UnsupportedFieldType),
- }
-}
-
-fn check_coord(
- coord: Option,
- fields: &[PointFieldMsg],
- xyz_field_type: &FieldDatatype,
-) -> Result {
- match coord {
- Some(y_idx) => {
- let field = &fields[y_idx];
- if field.datatype != convert_to_msg_code(xyz_field_type) {
- return Err(ConversionError::InvalidFieldFormat);
- }
- Ok(field.clone())
- }
- None => Err(ConversionError::NotEnoughFields),
- }
-}
-
-/// Matching field names from each meta data per point to the PointField name.
-/// Always make sure to use the same order as in your Into<> implementation to have a correct mapping.
-pub trait MetaNames {
- fn meta_names() -> [String; METADIM];
-}
-
-/// The Convert struct is used to convert between a PointCloud2 message and a custom type.
-/// A custom type must implement the FromBytes trait and the Into trait.
-/// The Into trait is used to convert the custom type into a tuple of the coordinates and the meta data.
-/// The FromBytes trait is used to convert the bytes from the PointCloud2 message into the custom type.
-///
+///
/// # Example
/// ```
-/// use ros_pointcloud2::mem_macros::size_of;
-/// use ros_pointcloud2::{Convert, PointMeta};
-/// use ros_pointcloud2::ros_types::PointCloud2Msg;
-/// const DIM : usize = 3; // how many dimensions your point has (e.g. x, y, z)
-/// const METADIM : usize = 4; // how many meta fields you have (e.g. r, g, b, a)
-/// type MyConverter = Convert;
+/// use ros_pointcloud2::{Point, PointConvertible, MetaNames, size_of};
+///
+/// #[derive(Clone, Debug, PartialEq, Copy)]
+/// pub struct MyPointXYZI {
+/// pub x: f32,
+/// pub y: f32,
+/// pub z: f32,
+/// pub intensity: f32,
+/// }
+///
+/// impl From for Point {
+/// fn from(point: MyPointXYZI) -> Self {
+/// Point {
+/// coords: [point.x, point.y, point.z],
+/// meta: [point.intensity.into()],
+/// }
+/// }
+/// }
+///
+/// impl From> for MyPointXYZI {
+/// fn from(point: Point) -> Self {
+/// Self {
+/// x: point.coords[0],
+/// y: point.coords[1],
+/// z: point.coords[2],
+/// intensity: point.meta[0].get(),
+/// }
+/// }
+/// }
+///
+/// impl MetaNames<1> for MyPointXYZI {
+/// fn meta_names() -> [&'static str; 1] {
+/// ["intensity"]
+/// }
+/// }
+///
+/// impl PointConvertible for MyPointXYZI {}
/// ```
-pub struct Convert
+pub trait PointConvertible:
+ From> + Into> + MetaNames + Clone + 'static
where
T: FromBytes,
- C: PointConvertible,
{
- iteration: usize,
- coordinates: Vec,
- phantom_t: std::marker::PhantomData,
- data: Vec,
- point_step_size: usize,
- cloud_length: usize,
- offsets: Vec,
- big_endian: Endianness,
- xyz_field_type: FieldDatatype,
- meta: Vec<(String, FieldDatatype)>,
}
-pub type ConvertXYZ = Convert;
-pub type ConvertXYZI = Convert;
-pub type ConvertXYZNormal = Convert;
-pub type ConvertXYZRGB = Convert;
-pub type ConvertXYZRGBL = Convert;
-pub type ConvertXYZRGBA = Convert;
-pub type ConvertXYZRGBNormal = Convert;
-pub type ConvertXYZINormal = Convert;
-pub type ConvertXYZL = Convert;
-
-impl TryFrom>
- for Convert
-where
- T: FromBytes,
- C: PointConvertible,
-{
- type Error = ConversionError;
-
- /// Converts a vector of custom types into a Convert struct that implements the Iterator trait.
- ///
- /// # Example
- /// ```
- /// use ros_pointcloud2::{ConvertXYZ, ConversionError};
- /// use ros_pointcloud2::pcl_utils::PointXYZ;
- ///
- /// let cloud_points: Vec = vec![
- /// PointXYZ { x: 1.0, y: 2.0, z: 3.0 },
- /// PointXYZ { x: 4.0, y: 5.0, z: 6.0 },
- /// ];
- /// let convert: Result = ConvertXYZ::try_from(cloud_points);
- /// ```
- fn try_from(cloud: Vec) -> Result {
- let length = cloud.len();
-
- let mut meta: Vec<(String, FieldDatatype)> = vec![];
- let first_point = cloud.first().ok_or(ConversionError::NoPoints)?;
- let point_meta: ([T; DIM], [PointMeta; METADIM]) = match first_point.clone().try_into() {
- Ok(point_meta) => point_meta,
- Err(_) => {
- return Err(ConversionError::PointConversionError);
- }
- };
- let meta_names = C::meta_names();
- let mut point_step_size = DIM * SIZE;
- for (idx, value) in point_meta.1.iter().enumerate() {
- meta.push((
- meta_names
- .get(idx)
- .ok_or(ConversionError::MetaIndexLengthMismatch)?
- .clone(),
- value.datatype,
- ));
- point_step_size += datatype_size(&value.datatype);
- }
-
- Ok(Self {
- phantom_t: std::marker::PhantomData,
- iteration: usize::zero(),
- coordinates: cloud,
- data: Vec::new(),
- point_step_size,
- cloud_length: length,
- offsets: Vec::new(),
- big_endian: Endianness::Little,
- xyz_field_type: T::field_datatype(),
- meta,
- })
- }
-}
-
-/// Meta data representation for a point
+/// Metadata representation for a point.
///
/// This struct is used to store meta data in a fixed size byte buffer along the with the
/// datatype that is encoded so that it can be decoded later.
@@ -266,586 +215,122 @@ where
///
/// let original_data: f64 = 1.0;
/// let meta = PointMeta::new(original_data);
-/// let my_data: f64 = meta.get().unwrap();
+/// let my_data: f64 = meta.get();
/// ```
#[derive(Debug, Clone, Copy)]
pub struct PointMeta {
- bytes: [u8; size_of!(f64)],
+ bytes: [u8; std::mem::size_of::()],
datatype: FieldDatatype,
}
impl Default for PointMeta {
fn default() -> Self {
Self {
- bytes: [0; size_of!(f64)],
+ bytes: [u8::default(); std::mem::size_of::()],
datatype: FieldDatatype::F32,
}
}
}
impl PointMeta {
- /// Create a new PointMeta from a value
+ /// Create a new PointMeta from a value.
///
/// # Example
/// ```
/// let meta = ros_pointcloud2::PointMeta::new(1.0);
/// ```
+ #[inline(always)]
pub fn new(value: T) -> Self {
let raw_bytes = T::bytes(&value);
- let mut bytes = [0; size_of!(f64)];
- for (idx, byte) in raw_bytes.iter().enumerate() {
- bytes[idx] = *byte;
+ let mut bytes = [0; std::mem::size_of::()];
+ for (byte, save_byte) in raw_bytes.into_iter().zip(bytes.iter_mut()) {
+ *save_byte = byte;
}
+
Self {
bytes,
datatype: T::field_datatype(),
}
}
- fn new_from_buffer(
- data: &[u8],
- offset: usize,
- datatype: &FieldDatatype,
- ) -> Result {
- let size = datatype_size(datatype);
- let bytes = data
- .get(offset..offset + size)
- .ok_or(ConversionError::DataLengthMismatch)?;
- let mut bytes_array = [0; size_of!(f64)]; // 8 bytes as f64 is the largest type
- for (idx, byte) in bytes.iter().enumerate() {
- bytes_array[idx] = *byte;
+ #[inline(always)]
+ fn from_buffer(data: &[u8], offset: usize, datatype: &FieldDatatype) -> Self {
+ debug_assert!(data.len() >= offset + datatype.size());
+
+ let bytes = unsafe { data.get_unchecked(offset..offset + datatype.size()) };
+ let mut bytes_array = [0; std::mem::size_of::()]; // 8 bytes as f64 is the largest type
+ for (byte, save_byte) in bytes.iter().zip(bytes_array.iter_mut()) {
+ *save_byte = *byte;
}
- Ok(Self {
+
+ Self {
bytes: bytes_array,
datatype: *datatype,
- })
+ }
}
- /// Get the value from the PointMeta. It will return None if the datatype does not match.
+ /// Get the numeric value from the PointMeta description.
///
/// # Example
/// ```
/// let original_data: f64 = 1.0;
/// let meta = ros_pointcloud2::PointMeta::new(original_data);
- /// let my_data: f64 = meta.get().unwrap();
+ /// let my_data: f64 = meta.get();
/// ```
- pub fn get(&self) -> Result {
- if self.datatype != T::field_datatype() {
- return Err(ConversionError::MetaDatatypeMismatch);
- }
- let size = datatype_size(&T::field_datatype());
- if let Some(bytes) = self.bytes.get(0..size) {
- Ok(T::from_le_bytes(bytes))
- } else {
- Err(ConversionError::DataLengthMismatch) // self.bytes are not long enough, already handled while parsing
- }
+ pub fn get(&self) -> T {
+ let size = T::field_datatype().size();
+ let bytes = self
+ .bytes
+ .get(0..size)
+ .expect("Exceeds bounds of f64, which is the largest type");
+ T::from_le_bytes(bytes)
}
}
-impl TryFrom
- for Convert
-where
- T: FromBytes,
- C: PointConvertible,
-{
- type Error = ConversionError;
-
- /// Converts a ROS PointCloud2 message into a Convert struct that implements the Iterator trait.
- /// Iterate over the struct to get or use the points.
- ///
- /// # Example
- /// Since we do not have a ROS node here, we first create a PointCloud2 message and then convert back to a Convert struct.
- /// ```
- /// use ros_pointcloud2::ros_types::PointCloud2Msg;
- /// use ros_pointcloud2::ConvertXYZ;
- /// use ros_pointcloud2::pcl_utils::PointXYZ;
- ///
- /// let cloud_points: Vec = vec![
- /// PointXYZ { x: 1.0, y: 2.0, z: 3.0 },
- /// PointXYZ { x: 4.0, y: 5.0, z: 6.0 },
- /// ];
- /// let msg: PointCloud2Msg = ConvertXYZ::try_from(cloud_points).unwrap().try_into().unwrap();
- ///
- /// let convert: ConvertXYZ = ConvertXYZ::try_from(msg).unwrap(); // parse message
- /// ```
- fn try_from(cloud: PointCloud2Msg) -> Result {
- if cloud.fields.len() < DIM {
- return Err(ConversionError::NotEnoughFields);
- }
-
- let xyz_field_type = T::field_datatype();
-
- let mut has_x: Option = None;
- let mut has_y: Option = None;
- let mut has_z: Option = None;
-
- let mut meta_with_offsets: Vec<(String, FieldDatatype, usize)> =
- Vec::with_capacity(METADIM);
-
- let lower_meta_names = C::meta_names()
- .iter()
- .map(|x| x.to_lowercase())
- .collect::>();
- for (idx, field) in cloud.fields.iter().enumerate() {
- let lower_field_name = field.name.to_lowercase();
- match lower_field_name.as_str() {
- "x" => has_x = Some(idx),
- "y" => has_y = Some(idx),
- "z" => has_z = Some(idx),
- _ => {
- if lower_meta_names.contains(&lower_field_name) {
- meta_with_offsets.push((
- field.name.clone(),
- convert_msg_code_to_type(field.datatype)?,
- field.offset as usize,
- ));
- }
- }
- }
- }
-
- meta_with_offsets.sort_by(|a, b| a.2.cmp(&b.2));
- let meta_offsets: Vec = meta_with_offsets.iter().map(|x| x.2).collect();
- let meta: Vec<(String, FieldDatatype)> = meta_with_offsets
- .iter()
- .map(|x| (x.0.clone(), x.1))
- .collect();
-
- let x_field = check_coord(has_x, &cloud.fields, &xyz_field_type)?;
- let y_field = check_coord(has_y, &cloud.fields, &xyz_field_type)?;
-
- let mut offsets = vec![x_field.offset as usize, y_field.offset as usize];
-
- let z_field = check_coord(has_z, &cloud.fields, &xyz_field_type);
- match z_field {
- Ok(z_field) => {
- offsets.push(z_field.offset as usize);
- }
- Err(err) => match err {
- ConversionError::NotEnoughFields => {
- if DIM == 3 {
- return Err(ConversionError::NotEnoughFields);
- }
- }
- _ => return Err(err),
- },
- }
-
- let endian = if cloud.is_bigendian {
- Endianness::Big
- } else {
- Endianness::Little
- };
-
- if offsets.len() != DIM {
- return Err(ConversionError::NotEnoughFields);
- }
-
- offsets.extend(meta_offsets);
-
- if offsets.len() != DIM + METADIM {
- return Err(ConversionError::NotEnoughFields);
- }
-
- let point_step_size = cloud.point_step as usize;
- if point_step_size * cloud.width as usize * cloud.height as usize != cloud.data.len() {
- return Err(ConversionError::DataLengthMismatch);
- }
-
- if let Some(max_offset) = offsets.last() {
- if let Some(last_meta) = meta.last() {
- let size_with_last_meta = max_offset + datatype_size(&last_meta.1);
- if size_with_last_meta > point_step_size {
- return Err(ConversionError::DataLengthMismatch);
- }
- }
- }
-
- Ok(Self {
- phantom_t: std::marker::PhantomData,
- iteration: usize::zero(),
- coordinates: Vec::new(),
- data: cloud.data,
- point_step_size,
- cloud_length: cloud.width as usize * cloud.height as usize,
- offsets,
- big_endian: endian,
- xyz_field_type: T::field_datatype(),
- meta,
- })
+impl From for PointMeta {
+ fn from(value: f32) -> Self {
+ Self::new(value)
}
}
-fn datatype_size(datatype: &FieldDatatype) -> usize {
- match datatype {
- FieldDatatype::U8 => 1,
- FieldDatatype::U16 => 2,
- FieldDatatype::U32 => 4,
- FieldDatatype::I8 => 1,
- FieldDatatype::I16 => 2,
- FieldDatatype::I32 => 4,
- FieldDatatype::F32 => 4,
- FieldDatatype::F64 => 8,
+impl From for PointMeta {
+ fn from(value: f64) -> Self {
+ Self::new(value)
}
}
-impl TryInto
- for Convert
-where
- T: FromBytes,
- C: PointConvertible,
-{
- type Error = ConversionError;
-
- /// Convert the point cloud to a ROS message.
- /// First use the `try_from` method for initializing the conversion and parsing meta data.
- /// Then use the `try_into` method to do the actual conversion.
- ///
- /// # Example
- /// ```
- /// use ros_pointcloud2::ros_types::PointCloud2Msg;
- /// use ros_pointcloud2::ConvertXYZ;
- /// use ros_pointcloud2::pcl_utils::PointXYZ;
- ///
- /// let cloud_points: Vec = vec![
- /// PointXYZ { x: 1.0, y: 2.0, z: 3.0 },
- /// PointXYZ { x: 4.0, y: 5.0, z: 6.0 },
- /// ];
- /// let msg_out: PointCloud2Msg = ConvertXYZ::try_from(cloud_points).unwrap().try_into().unwrap();
- /// ```
- fn try_into(self) -> Result {
- let mut cloud = PointCloud2Msg::default();
-
- // Define the message fields
- let mut fields = Vec::new();
- if DIM > 3 {
- return Err(ConversionError::TooManyDimensions);
- }
-
- let datatype: u8 = convert_to_msg_code(&self.xyz_field_type);
-
- if DIM > 1 {
- fields.push(PointFieldMsg {
- name: "x".to_string(),
- offset: 0,
- datatype,
- count: 1,
- });
- fields.push(PointFieldMsg {
- name: "y".to_string(),
- offset: SIZE as u32,
- datatype,
- count: 1,
- });
- }
-
- if DIM == 3 {
- fields.push(PointFieldMsg {
- name: "z".to_string(),
- offset: 2 * SIZE as u32,
- datatype,
- count: 1,
- });
- }
-
- // meta data fields
- let first_point = self.coordinates.first().ok_or(ConversionError::NoPoints)?;
- let meta: ([T; DIM], [PointMeta; METADIM]) = match first_point.clone().try_into() {
- Ok(meta) => meta,
- Err(_) => return Err(ConversionError::PointConversionError),
- };
-
- let meta_names = C::meta_names();
-
- let sized_dim = DIM as u32 * SIZE as u32;
-
- for (idx, value) in meta.1.iter().enumerate() {
- let datatype: u8 = convert_to_msg_code(&value.datatype);
- let mut offset = sized_dim;
- for i in 0..idx {
- let datatype: u8 = convert_to_msg_code(&meta.1[i].datatype);
- let field_type = convert_msg_code_to_type(datatype)?;
- let field_size = datatype_size(&field_type);
- offset += field_size as u32;
- }
- fields.push(PointFieldMsg {
- name: meta_names[idx].to_string(),
- offset,
- datatype,
- count: 1,
- });
- }
-
- // calc all meta data points step size
- let mut step_size = 0;
- for field in fields.iter() {
- let field_type = convert_msg_code_to_type(field.datatype)?;
- let field_size = datatype_size(&field_type);
- step_size += field.count * field_size as u32;
- }
-
- cloud.fields = fields;
- cloud.point_step = step_size;
- cloud.row_step = self.coordinates.len() as u32 * cloud.point_step;
- cloud.height = 1;
- cloud.width = self.coordinates.len() as u32;
- cloud.is_bigendian = false;
- cloud.is_dense = true;
-
- for coords in self.coordinates {
- let coords_data: ([T; DIM], [PointMeta; METADIM]) = match coords.try_into() {
- Ok(meta) => meta,
- Err(_) => return Err(ConversionError::PointConversionError),
- };
- coords_data
- .0
- .iter()
- .for_each(|x| cloud.data.extend_from_slice(T::bytes(x).as_slice()));
- coords_data.1.iter().for_each(|meta| {
- let truncated_bytes = &meta.bytes[0..datatype_size(&meta.datatype)];
- cloud.data.extend_from_slice(truncated_bytes);
- });
- }
-
- Ok(cloud)
+impl From for PointMeta {
+ fn from(value: i32) -> Self {
+ Self::new(value)
}
}
-impl
- Convert
-where
- T: FromBytes,
- C: PointConvertible,
-{
- /// Convenience getter for the number of points in the cloud.
- pub fn len(&self) -> usize {
- self.cloud_length
- }
-
- pub fn is_empty(&self) -> bool {
- self.cloud_length == 0
+impl From for PointMeta {
+ fn from(value: u8) -> Self {
+ Self::new(value)
}
}
-impl
- fallible_iterator::FallibleIterator for Convert
-where
- T: FromBytes,
- C: PointConvertible,
-{
- type Item = C;
- type Error = ConversionError;
-
- /// Iterate over the data buffer and interpret the data as a point.
- fn next(&mut self) -> Result