# cloudini
A Rust implementation of the [Cloudini](https://github.com/facontidavide/cloudini) point cloud compression format.
Cloudini uses a two-stage pipeline:
1. **Field encoding** — type-aware per-field compression (delta + varint for integers, quantisation for floats, XOR for lossless floats).
2. **Block compression** — the encoded stream is compressed with LZ4 or ZSTD.
Typical results on Lidar geometry (XYZ @ 1 mm + intensity):
| Lossy + ZSTD | ~34% |
| Lossy + LZ4 | ~40% |
## Quickstart
```toml
[dependencies]
cloudini = "0.1.0"
```
```rust
use cloudini::ros::{CompressExt, CompressionConfig, PointCloud2Msg};
use ros_pointcloud2::points::PointXYZI;
let resolution = 0.001_f32; // 1 mm
// Build a PointCloud2Msg from a vec of typed points (simulated Lidar scan)
let points: Vec<PointXYZI> = (0..200)
.map(|i| {
let t = i as f32 * 0.05;
PointXYZI::new(t.sin() * 5.0, t.cos() * 5.0, t * 0.1, i as f32)
})
.collect();
let cloud = PointCloud2Msg::try_from_vec(points.clone())?;
let raw_size = cloud.data.len();
// Compress — 1 mm lossy + ZSTD
let compressed = cloud.compress(CompressionConfig::lossy_zstd(resolution))?;
// compressed is a CompressedPointCloud2 — ship it over DDS, store to disk, etc.
assert!(compressed.compressed_data.len() < raw_size);
// Decompress — no metadata needed, it is embedded in the buffer
let restored = compressed.decompress()?;
// Each float component is within resolution / 2 of the original
let tol = resolution / 2.0 + 1e-6;
let restored_points: Vec<PointXYZI> = restored.try_into_iter()?.collect();
for (orig, rest) in points.iter().zip(restored_points.iter()) {
assert!((orig.x - rest.x ).abs() <= tol);
assert!((orig.y - rest.y ).abs() <= tol);
assert!((orig.z - rest.z ).abs() <= tol);
assert!((orig.intensity - rest.intensity).abs() <= tol);
}
```
### Compression presets
| `CompressionConfig::lossy_zstd(res)` | Lossy | ZSTD | `res / 2` |
| `CompressionConfig::lossy_lz4(res)` | Lossy | LZ4 | `res / 2` |
| `CompressionConfig::lossless_zstd()` | Lossless (XOR) | ZSTD | 0 |
| `CompressionConfig::lossless_lz4()` | Lossless (XOR) | LZ4 | 0 |
`CompressionConfig::default()` is `lossy_zstd(0.001)` — suitable for most
Lidar use cases.
## Raw
You can disable the `ros_pointcloud2` convenience wrapper by setting `no-default-features = true` and use the underlying enoder and decoder.
### Encode
```rust
use cloudini::{
CompressionOption, EncodingInfo, EncodingOptions, FieldType, PointField,
PointcloudEncoder,
};
// Describe the point layout.
// point_step = sum of all field sizes = 4+4+4+1 = 13 bytes
let info = EncodingInfo {
fields: vec![
PointField { name: "x".into(), offset: 0, field_type: FieldType::Float32, resolution: Some(0.001) },
PointField { name: "y".into(), offset: 4, field_type: FieldType::Float32, resolution: Some(0.001) },
PointField { name: "z".into(), offset: 8, field_type: FieldType::Float32, resolution: Some(0.001) },
PointField { name: "intensity".into(), offset: 12, field_type: FieldType::Uint8, resolution: None },
],
width: 1000, // number of points
height: 1, // 1 for unorganised clouds
point_step: 13,
encoding_opt: EncodingOptions::Lossy,
compression_opt: CompressionOption::Zstd,
..EncodingInfo::default()
};
// raw_cloud: width * height * point_step bytes of packed point data
let encoder = PointcloudEncoder::new(info);
let compressed: Vec<u8> = encoder.encode(&raw_cloud)?;
// compressed is a self-contained buffer: header + chunk data
```
### Decode
```rust
use cloudini::PointcloudDecoder;
let decoder = PointcloudDecoder::new();
let (info, decoded): (_, Vec<u8>) = decoder.decode(&compressed)?;
// decoded.len() == info.width * info.height * info.point_step
// For lossy float fields the max error per component is resolution / 2
```
---
## Encoding modes
| `None` | raw copy | raw copy | raw copy |
| `Lossy` | quantise → delta → varint | raw copy | delta → varint |
| `Lossless` | raw copy (f32) / XOR (f64) | XOR with prev bits | delta → varint |
> **Lossy float error**: `max_error = resolution / 2`
> e.g. `resolution: Some(0.001)` → at most 0.5 mm error per component.
---
## Compression backends
| `None` | field encoding only, no block compression |
| `Lz4` | fast (good for real-time), moderate ratio |
| `Zstd` | higher ratio, higher CPU cost |
---
## Field types
| `Int8` / `Uint8` | 1 byte | always copied verbatim |
| `Int16` / `Uint16` | 2 bytes | delta + varint |
| `Int32` / `Uint32` | 4 bytes | delta + varint |
| `Int64` / `Uint64` | 8 bytes | delta + varint |
| `Float32` | 4 bytes | lossy (with resolution) or copy |
| `Float64` | 8 bytes | lossy (with resolution) or XOR lossless |
---
## Format compatibility
Encoded buffers start with `CLOUDINI_V03\n` followed by a null-terminated YAML
header and then the compressed chunks. The format is compatible with the
original C++ Cloudini library.