Efficient data transport using binary datatype - ashBabu/Utilities GitHub Wiki
In ROS, the data that are heavy like pointclouds are encoded as binary. An example is
ros2 topic echo /livox_mid360/points --once
header:
stamp:
sec: 16
nanosec: 450000000
frame_id: livox_frame
height: 1000
width: 1000
fields:
- name: x
offset: 0
datatype: 7
count: 1
- name: y
offset: 4
datatype: 7
count: 1
- name: z
offset: 8
datatype: 7
count: 1
- name: intensity
offset: 16
datatype: 7
count: 1
- name: ring
offset: 24
datatype: 4
count: 1
is_bigendian: false
point_step: 32
row_step: 32000
data:
- 110
- 106
- 22
In the above ROS 2, the datatype numbers are constants.
Datatype 7: FLOAT32 (4 bytes each)
Datatype 4: UINT16 (2 bytes each)
The point_step: 32 represents that every single point in the pointcloud takes up exactly 32 bytes of memory.
| Bytes | Field | Type | Size (Bytes) | Python |
|---|---|---|---|---|
| 0 | x | FLOAT32 | 4 | f |
| 4 | y | FLOAT32 | 4 | f |
| 8 | z | FLOAT32 | 4 | f |
| 12-15 | Padding | - | 4 | 4x |
| 16 | intensity | FLOAT32 | 4 | f |
| 20-23 | Padding | - | 4 | 4x |
| 24 | ring | UINT16 | 2 | H |
| 26-31 | Padding | - | 6 | 6x |
| Code | C Type | Python Type | Size (Bytes) |
|---|---|---|---|
| f | float | float | 4 |
| d | double | float | 8 |
| H | unsigned short | integer | 2 |
| I | unsigned int | integer | 4 |
| x | pad byte | no value | 1 |
is_bigendign:false (Little Endian) is represented as < in python.
Since it is little endian and for x, y, z, starting at 0, 4, and 8, we need <fff. Then 4 padding makes it <fff4x, followed by intesity, 4 padding, u16 ring and 6 padding makes it <fff4xf4xH6x.
In Python, this would be
import struct
# Let's say 'raw_point' is a chunk of 32 bytes from your data
# we convert the list of numbers into actual bytes
point_bytes = bytes(raw_point)
# Now we unpack using our map
x, y, z, intensity, ring = struct.unpack('<fff4xf4xH6x', point_bytes)
print(f"X: {x}, Y: {y}, Z: {z}, Intensity: {intensity}, Ring: {ring}")
Using Numpy, this will be
import numpy as np
timestamp = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
point_dtype = np.dtype({
'names': ['x', 'y', 'z', 'intensity', ...],
'formats': ['f4', 'f4', 'f4', 'f4', 'u2'],
'offsets': [0, 4, 8, 16, 24],
'itemsize': msg.point_step # 32
})
raw_data = msg.data
full_cloud = np.frombuffer(raw_data, dtype=point_dtype)
xyz = np.stack([full_cloud['x'], full_cloud['y'], full_cloud['z']], axis=1) # take out just x, y, z
mask = np.all(np.isfinite(xyz), axis=1) # remove NANs and INFs
xyz = xyz[mask] # Array of valid numbers only
#### Optional slicing if pointcloud has millions of points ##########
n = xyz.shape[0]
MAX_POINTS = 100_000
if n > MAX_POINTS:
step_size = n // MAX_POINTS
xyz = xyz[::step_size][:MAX_POINTS]
n = xyz.shape[0]
#############################################################
rotation_matrix = self.get_rotxyz('x', -90)
points = xyz @ rotation_matrix.T
###
If any operations are performed on the data, it needs to be converted
back to contiguous array
###
points = np.ascontiguousarray(points, dtype=np.float32)
header = struct.pack("<dI", timestamp, points.shape[0])
payload = header + points.tobytes()
A note on the header
header = struct.pack("<dI", timestamp, points.shape[0])
This line is essentially creating a custom binary header for your network packet (data to be sent).
| Character | Meaning | Data Type | Size |
|---|---|---|---|
| < | Little Endian | Standard byte order for Intel/AMD processors | 0 bytes |
| d | Double | 64-bit Floating Point (the timestamp) | 8 bytes |
| I | Unsigned Int | 32-bit Integer (the number of points n) | 4 bytes |
header becomes a bytes object exactly 12 bytes long (8 for the double + 4 for the int)
In the aiortc server, this can be unpacked as
const timestamp = dataView.getFloat64(0, true); // Read the 'd'
const numPoints = dataView.getUint32(8, true); // Read the 'I'