Skip to content

Latest commit

 

History

History
659 lines (520 loc) · 23.7 KB

File metadata and controls

659 lines (520 loc) · 23.7 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Cloudini is a high-performance pointcloud compression library with bindings for ROS, PCL, and WebAssembly. It implements a two-stage compression approach: custom encoding for pointcloud fields followed by general-purpose compression (LZ4/ZSTD).

Architecture

The project consists of three main components:

  • cloudini_lib/: Core compression library (C++20) with field encoders/decoders
  • cloudini_ros/: ROS2 integration with point_cloud_transport plugins and conversion utilities
  • cloudini_web/: WebAssembly interface for browser-based compression

Core Library (cloudini_lib)

Key components:

  • PointcloudEncoder: Multi-threaded encoder with double-buffering pattern for LZ4/ZSTD compression
  • PointcloudDecoder: Decoder supporting chunked decompression
  • Field Encoders/Decoders: Type-specific encoders for efficient field compression
    • FieldEncoderFloatN_Lossy: SIMD-optimized encoder for 3-4 consecutive FLOAT32 fields (typically XYZ/XYZRGB)
    • FieldEncoderInt: Delta + varint encoding for integer types
    • FieldEncoderFloat_XOR: XOR-based lossless float compression
    • FieldEncoderCopy: Pass-through for unsupported types

Threading Model:

  • Encoder uses a worker thread (compressionWorker()) for LZ4/ZSTD compression
  • Double-buffering pattern: main thread encodes fields while worker compresses previous chunk
  • Condition variables cv_ready_to_compress_ and cv_done_compressing_ coordinate work
  • CRITICAL: All shared state modifications (compression_done_, compressed_size_) MUST be protected by mutex_

ROS2 Integration (cloudini_ros)

Key Classes:

  1. CloudiniSubscriberPCL (cloudini_ros/include/cloudini_ros/cloudini_subscriber_pcl.hpp)

    • High-performance subscriber that converts CompressedPointCloud2 directly to pcl::PCLPointCloud2
    • Uses rclcpp::GenericSubscription for zero-copy raw DDS message access
    • Implements object pool pattern (max 4 objects) to avoid repeated allocations
    • Thread-safe with mutex-protected pool
    • Bypasses intermediate sensor_msgs::PointCloud2 conversion
  2. Topic Converter Node (cloudini_ros/src/topic_converter.cpp)

    • Converts between sensor_msgs/PointCloud2 and point_cloud_interfaces/CompressedPointCloud2
    • Supports both compression and decompression modes
    • Optimizations:
      • Skips processing when get_subscription_count() == 0
      • Auto-detects publisher QoS settings via adapt_request_to_offers()
      • Zero-copy via raw DDS message manipulation
  3. Point Cloud Transport Plugin (cloudini_ros/src/cloudini_publisher_plugin.cpp)

    • Integrates with ROS2 point_cloud_transport framework (similar to image_transport)
    • Automatically creates compressed topic variants (e.g., /points/cloudini)
    • Configurable via transport hints: TransportHints("cloudini")

Message Parsing Utilities (cloudini_lib/include/cloudini_lib/ros_msg_utils.hpp):

  • getDeserializedPointCloudMessage(): Parse raw DDS messages without full deserialization
  • convertPointCloud2ToCompressedCloud(): Convert + compress in single pass
  • convertCompressedCloudToPointCloud2(): Decompress + convert
  • toEncodingInfo(): Extract encoding metadata from point cloud

PCL Integration (cloudini_lib/include/cloudini_lib/pcl_conversion.hpp):

  • PCLPointCloudEncode(): Encode pcl::PCLPointCloud2 to Cloudini format
  • PCLPointCloudDecode(): Decode Cloudini format to pcl::PCLPointCloud2
  • Direct conversion without intermediate ROS message types

Build Commands

Core Library (Standalone)

cmake -B build_release -S cloudini_lib -DCMAKE_BUILD_TYPE=Release
cmake --build build_release --parallel

Debug Build with Sanitizers

cmake -B build_debug -S cloudini_lib -DCMAKE_BUILD_TYPE=Debug
cmake --build build_debug --parallel

ROS Build

colcon build --packages-select cloudini_lib cloudini_ros

WebAssembly Build

# Requires Emscripten
cmake -B build_wasm -S cloudini_lib -DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
cmake --build build_wasm

Web Interface

cd cloudini_web
npm install
npm run dev  # Development server
npm run build  # Production build

Testing

Core Library Tests

# Core library tests
cd build_debug && ctest

# Run specific benchmark
./build_release/benchmarks/pcd_benchmark

ROS2 Test Nodes

Test Publisher (cloudini_ros/test/test_plugin_publisher.cpp):

ros2 run cloudini_ros test_plugin_publisher --ros-args -p input_topic:=/points -p output_topic:=/points_pct
  • Subscribes to standard PointCloud2 topic
  • Publishes via point_cloud_transport, automatically creating compressed variants
  • Creates topics: /points_pct (raw) and /points_pct/cloudini (compressed)

Test Subscriber (cloudini_ros/test/test_plugin_subscriber.cpp):

ros2 run cloudini_ros test_plugin_subscriber --ros-args -p topic:=/points_pct
  • Uses point_cloud_transport to subscribe with transport hint "cloudini"
  • Manually converts to PCL format in callback

Direct PCL Subscriber (cloudini_ros/test/test_cloudini_subscriber.cpp):

ros2 run cloudini_ros test_cloudini_subscriber --ros-args -p topic:=/points/cloudini
  • Uses CloudiniSubscriberPCL for most efficient direct PCL conversion
  • Bypasses point_cloud_transport framework for minimal overhead

Topic Converter:

# Compress: sensor_msgs/PointCloud2 -> CompressedPointCloud2
ros2 run cloudini_ros cloudini_topic_converter --ros-args \
  -p compressing:=true \
  -p topic_input:=/points \
  -p topic_output:=/points/compressed \
  -p resolution:=0.001

# Decompress: CompressedPointCloud2 -> sensor_msgs/PointCloud2
ros2 run cloudini_ros cloudini_topic_converter --ros-args \
  -p compressing:=false \
  -p topic_input:=/points/compressed \
  -p topic_output:=/points/decompressed

Direct Compression Publisher (cloudini_ros/test/test_direct_publisher.cpp):

ros2 run cloudini_ros test_direct_publisher --ros-args \
  -p input_topic:=/points \
  -p output_topic:=/points/compressed \
  -p resolution:=0.001
  • Compresses PointCloud2 directly using SerializeCompressedPointCloud2
  • Publishes as serialized CompressedPointCloud2 via generic publisher
  • No topic_converter node needed

Benchmarking

MCAP Rosbag Conversion:

# Convert a ROS2 bag directory (creates <bag_dir>_encoded/ with .mcap + metadata.yaml)
./build_release/tools/cloudini_rosbag_converter -c -y -f DATA/my_bag/

# Convert a bare .mcap file (if sibling metadata.yaml exists, it is also transformed)
./build_release/tools/cloudini_rosbag_converter -c -y -f DATA/my_bag/my_bag_0.mcap

# Decode back to PointCloud2
./build_release/tools/cloudini_rosbag_converter -d -y -f DATA/my_bag_encoded/

Debugging with ROS2 CLI

# List available topics
ros2 topic list

# Check topic type
ros2 topic info /points/cloudini

# Monitor message rate
ros2 topic hz /points/cloudini

# Echo messages (won't work for compressed - binary data)
ros2 topic echo /points

Key Dependencies

  • Required: CMake 3.16+, C++20 compiler
  • Auto-downloaded via CPM: LZ4, ZSTD, cxxopts, benchmark, googletest
  • Optional: PCL (for pointcloud utilities), ROS 2 (for integration)
  • Web: Node.js 18+, Vite
  • ROS2 Packages:
    • rclcpp - Core ROS2 C++ API
    • sensor_msgs - Standard PointCloud2 messages
    • point_cloud_interfaces - CompressedPointCloud2 message definition
    • point_cloud_transport - Transport framework for point cloud plugins
    • pcl_conversions - PCL/ROS message conversion utilities (apt: ros-${ROS_DISTRO}-pcl-conversions)
    • pluginlib - Plugin loading framework

API Usage Patterns

Using CloudiniSubscriberPCL (Most Efficient)

#include <cloudini_ros/cloudini_subscriber_pcl.hpp>

auto node = std::make_shared<rclcpp::Node>("my_node");

// Create subscriber with callback
auto subscriber = std::make_shared<cloudini_ros::CloudiniSubscriberPCL>(
    node,  // Can also use node->shared_from_this() if inside node class
    "/points/cloudini",  // Topic name
    [](const pcl::PCLPointCloud2::Ptr& cloud) {
        // Process PCL cloud - object is from pool, will be returned when lambda exits
        std::cout << "Received cloud with " << cloud->width * cloud->height << " points\n";
    },
    rclcpp::QoS(10)  // Optional QoS settings
);

rclcpp::spin(node);

IMPORTANT: Do NOT call shared_from_this() in node constructors - use raw this pointer instead, or call from init() method after node construction.

Using Point Cloud Transport

#include <point_cloud_transport/point_cloud_transport.hpp>

// Publisher side
auto pct = std::make_shared<point_cloud_transport::PointCloudTransport>(node);
auto pub = pct->advertise("/points", 10);
// Automatically creates /points/cloudini compressed topic

pub.publish(cloud_msg);

// Subscriber side with transport hint
auto transport_hint = std::make_shared<point_cloud_transport::TransportHints>("cloudini");
auto sub = pct->subscribe("/points", 10, callback, {}, transport_hint.get());

Raw DDS Message Access Pattern

#include <rclcpp/generic_subscription.hpp>
#include <cloudini_lib/ros_msg_utils.hpp>

// Generic subscription for zero-copy access
auto callback = [](std::shared_ptr<rclcpp::SerializedMessage> msg) {
    const auto& input_msg = msg->get_rcl_serialized_message();
    const Cloudini::ConstBufferView raw_dds_msg(input_msg.buffer, input_msg.buffer_length);

    // Parse without full deserialization
    auto pc_info = cloudini_ros::getDeserializedPointCloudMessage(raw_dds_msg);

    // Process compressed data directly
    // pc_info.data contains the cloudini-compressed payload
};

auto subscription = node->create_generic_subscription(
    topic, "point_cloud_interfaces/msg/CompressedPointCloud2",
    rclcpp::QoS(10), callback
);

Resolution Configuration

#include <cloudini_lib/ros_msg_utils.hpp>

// Create encoding info from PointCloud2
auto pc_info = cloudini_ros::getDeserializedPointCloudMessage(raw_msg);
auto encoding_info = cloudini_ros::toEncodingInfo(pc_info);

// Apply resolution profile (quantization for lossy compression)
cloudini_ros::applyResolutionProfile(
    cloudini_ros::ResolutionProfile{},  // Default profile
    encoding_info.fields,
    0.001  // 1mm resolution
);

// Convert with custom encoding
std::vector<uint8_t> output;
cloudini_ros::convertPointCloud2ToCompressedCloud(pc_info, encoding_info, output);

Direct Compression (Without Topic Converter)

#include <cloudini_ros/conversion_utils.hpp>
#include <rclcpp/rclcpp.hpp>

// Setup: create a generic publisher for CompressedPointCloud2
auto publisher = node->create_generic_publisher(
    "/points/compressed",
    "point_cloud_interfaces/msg/CompressedPointCloud2",
    rclcpp::QoS(10));

// Compress a PointCloud2 message
std::vector<uint8_t> buffer;
Cloudini::SerializeCompressedPointCloud2(pcd_msg, 0.001, buffer);

// Publish using zero-copy pointer swap
rclcpp::SerializedMessage ser_msg;
ser_msg.get_rcl_serialized_message().buffer = buffer.data();
ser_msg.get_rcl_serialized_message().buffer_length = buffer.size();
publisher->publish(ser_msg);
// NOTE: null out ser_msg buffer before destruction to prevent double-free
ser_msg.get_rcl_serialized_message().buffer = nullptr;
ser_msg.get_rcl_serialized_message().buffer_length = 0;

Performance Optimization Patterns

Object Pool Pattern (CloudiniSubscriberPCL)

CloudiniSubscriberPCL implements an object pool to avoid repeated PCL cloud allocations:

pcl::PCLPointCloud2::Ptr CloudiniSubscriberPCL::acquireCloudFromPool() {
    pcl::PCLPointCloud2* raw_ptr = nullptr;

    {
        std::lock_guard<std::mutex> lock(pool_mutex_);
        if (!cloud_pool_.empty()) {
            raw_ptr = cloud_pool_.back();
            cloud_pool_.pop_back();
        } else {
            raw_ptr = new pcl::PCLPointCloud2();
        }
    }

    // Custom deleter returns to pool
    return pcl::PCLPointCloud2::Ptr(raw_ptr, [this](pcl::PCLPointCloud2* ptr) {
        std::lock_guard<std::mutex> lock(pool_mutex_);
        if (cloud_pool_.size() < MAX_POOL_SIZE) {
            ptr->data.clear();  // Clear but keep capacity
            ptr->fields.clear();
            cloud_pool_.push_back(ptr);
        } else {
            delete ptr;
        }
    });
}

Benefits: ~75% reduction in allocations during steady-state operation.

Skip Processing When No Subscribers

void callback(std::shared_ptr<rclcpp::SerializedMessage> msg) {
    // Skip expensive processing if no one is listening
    if (publisher_->get_subscription_count() == 0) {
        return;
    }

    // Proceed with compression/decompression
    // ...
}

Zero-Copy via Raw DDS Access

Instead of deserializing to sensor_msgs::PointCloud2, parse the raw DDS buffer:

// AVOID THIS (requires full deserialization + copy):
void callback(const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
    auto pcl_cloud = std::make_shared<pcl::PCLPointCloud2>();
    pcl_conversions::toPCL(*msg, *pcl_cloud);
    // ...
}

// PREFER THIS (zero-copy):
void callback(std::shared_ptr<rclcpp::SerializedMessage> msg) {
    const auto& raw = msg->get_rcl_serialized_message();
    const Cloudini::ConstBufferView view(raw.buffer, raw.buffer_length);
    auto pc_info = cloudini_ros::getDeserializedPointCloudMessage(view);
    // Access fields directly from buffer without intermediate copies
}

Development Notes

  • Uses CPM for dependency management - set CPM_SOURCE_CACHE environment variable to avoid re-downloading
  • Supports SSE4.1 optimizations on x86_64
  • Address sanitizer enabled automatically in Debug builds on GCC/Clang
  • Custom intrinsics for SIMD operations in include/cloudini_lib/intrinsics.hpp
  • Field encoders support both lossy (quantized floats) and lossless compression modes

Threading Safety Guidelines

CRITICAL: When modifying shared state accessed by multiple threads:

  1. Always acquire mutex before modifying condition variable predicates
  2. Release mutex before calling notify_one() or notify_all()
  3. Use std::lock_guard for automatic RAII-style locking

Example from PointcloudEncoder::compressionWorker():

// CORRECT:
{
    std::lock_guard<std::mutex> lock(mutex_);
    compressed_size_ += chunk_size + sizeof(uint32_t);
    compression_done_ = true;
}
cv_done_compressing_.notify_one();  // Notify after releasing lock

// WRONG - DATA RACE:
compressed_size_ += chunk_size;  // No mutex!
compression_done_ = true;         // Race condition!
cv_done_compressing_.notify_one();

ROS2 Node Lifecycle Safety

IMPORTANT: Do NOT call shared_from_this() in constructors:

// WRONG - crashes with bad_weak_ptr:
MyNode::MyNode() : Node("my_node") {
    subscriber_ = std::make_shared<CloudiniSubscriberPCL>(
        this->shared_from_this(),  // BAD - shared_ptr not yet constructed
        "/topic", callback
    );
}

// CORRECT Option 1 - Use raw pointer:
MyNode::MyNode() : Node("my_node") {
    subscriber_ = std::make_shared<CloudiniSubscriberPCL>(
        this,  // GOOD - raw pointer
        "/topic", callback
    );
}

// CORRECT Option 2 - Separate init():
MyNode::MyNode() : Node("my_node") {}

void MyNode::init() {
    subscriber_ = std::make_shared<CloudiniSubscriberPCL>(
        this->shared_from_this(),  // GOOD - called after construction
        "/topic", callback
    );
}

Common Issues and Fixes

Issue: Timeout waiting for compression to complete

Symptom: Exception "Timeout waiting for compression to complete" during encoding.

Cause: Data race or lost notification in multi-threaded compression worker.

Fix: Ensure all modifications to compression_done_ and compressed_size_ are protected by mutex. Fixed in recent commit to cloudini_lib/src/cloudini.cpp.

Issue: Point cloud transport subscriber not receiving messages

Symptom: Subscriber callback never fires despite topic being published.

Cause: Topic naming mismatch. Point cloud transport expects base topic name (e.g., /points), not the compressed variant (e.g., /points/cloudini).

Solution:

  • Publisher: Use base topic name with advertise("/points", 10)
  • Subscriber: Use base topic + transport hint subscribe("/points", 10, callback, {}, TransportHints("cloudini"))

Issue: PCL conversions header not found

Symptom: Compilation error pcl_conversions/pcl_conversions.h: No such file or directory

Solution:

  1. Install package: sudo apt install ros-${ROS_DISTRO}-pcl-conversions
  2. Add to CMakeLists.txt:
    find_package(pcl_conversions REQUIRED)
    target_link_libraries(target ${pcl_conversions_TARGETS})
  3. Export in package.xml: <depend>pcl_conversions</depend>

Issue: Generic subscription crashes with deserialization error

Symptom: Runtime crash when accessing serialized message buffer.

Cause: Incorrect topic type string or attempting to deserialize incompatible message.

Solution: Verify topic type matches exactly:

ros2 topic info /topic_name  # Check actual type

Use correct type string:

  • "sensor_msgs/msg/PointCloud2" (not sensor_msgs::msg::PointCloud2)
  • "point_cloud_interfaces/msg/CompressedPointCloud2"

Tools

  • cloudini_rosbag_converter: Convert MCAP rosbags between compressed/uncompressed pointclouds. Accepts bag directories or bare .mcap files; generates a transformed metadata.yaml when one is present
  • mcap_cutter: Extract portions of MCAP files
  • pcd_benchmark: Benchmark compression on PCD files
  • run_encoder.sh: Batch processing script for test data

File Structure Reference

cloudini_lib/

include/cloudini_lib/
├── cloudini.hpp              # Core encoder/decoder classes
├── encoding_utils.hpp        # Buffer views, encode/decode helpers
├── field_encoder.hpp         # Field-specific encoder implementations
├── field_decoder.hpp         # Field-specific decoder implementations
├── intrinsics.hpp            # SIMD intrinsics wrappers (Vector4f, Vector4i)
├── pcl_conversion.hpp        # PCL ↔ Cloudini conversion
└── ros_msg_utils.hpp         # ROS2 message parsing/conversion utilities

src/
├── cloudini.cpp              # PointcloudEncoder/Decoder implementation
├── field_encoder.cpp         # Encoder implementations (FloatN, Int, XOR)
├── field_decoder.cpp         # Decoder implementations
├── pcl_conversion.cpp        # PCL integration
└── ros_msg_utils.cpp         # ROS message utilities

tools/
├── cloudini_rosbag_converter # MCAP conversion tool
└── mcap_cutter              # MCAP slicing utility

cloudini_ros/

include/cloudini_ros/
└── cloudini_subscriber_pcl.hpp  # High-performance PCL subscriber with object pool

src/
├── topic_converter.cpp          # Standalone compression/decompression node
├── cloudini_subscriber_pcl.cpp  # PCL subscriber implementation
├── cloudini_publisher_plugin.cpp # point_cloud_transport publisher plugin
└── cloudini_subscriber_plugin.cpp # point_cloud_transport subscriber plugin

test/
├── test_cloudini_subscriber.cpp # Direct CloudiniSubscriberPCL usage example
├── test_direct_publisher.cpp    # Direct compression publisher example
├── test_plugin_publisher.cpp    # point_cloud_transport publisher example
└── test_plugin_subscriber.cpp   # point_cloud_transport subscriber example

Quick Reference

Compression Modes

  1. None (EncodingOptions::NONE + CompressionOption::NONE)

    • No encoding, no compression (for debugging)
  2. Lossless (EncodingOptions::LOSSLESS + CompressionOption::LZ4/ZSTD)

    • Delta encoding for integers
    • XOR encoding for floats
    • LZ4 or ZSTD compression
  3. Lossy (EncodingOptions::LOSSY + CompressionOption::LZ4/ZSTD)

    • Quantized floats based on resolution parameter
    • SIMD-optimized for XYZ/XYZRGB fields
    • Highest compression ratio
    • Typical resolution: 0.001 (1mm) for geometry

QoS Profile Auto-Detection

topic_converter.cpp uses adapt_request_to_offers() to auto-detect publisher QoS:

  • Reliability: Uses RELIABLE if all publishers are RELIABLE, else BEST_EFFORT
  • Durability: Uses TRANSIENT_LOCAL if all publishers are TRANSIENT_LOCAL (for latched topics), else VOLATILE

Typical Compression Ratios

Based on empirical data:

  • Lossy (1mm resolution): 5-10x compression
  • Lossless: 2-4x compression
  • Depends on: Point cloud structure, field types, data entropy

Message Flow Diagram

Standard ROS2 Workflow:
┌─────────────────┐
│ Sensor/Driver   │
│ PointCloud2     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐      ┌──────────────────┐
│ point_cloud_    │─────▶│ cloudini plugin  │
│ transport       │      │ (compress)       │
└─────────────────┘      └────────┬─────────┘
                                  │
                                  ▼
                         ┌─────────────────┐
                         │ Compressed      │
                         │ PointCloud2     │
                         └────────┬────────┘
                                  │
                                  ▼
┌─────────────────┐      ┌──────────────────┐
│ CloudiniSub     │◀─────│ Generic Sub      │
│ PCL (fastest)   │      │ (zero-copy DDS)  │
└─────────────────┘      └──────────────────┘

Alternative (Manual Conversion):
┌─────────────────┐
│ Sensor          │
│ PointCloud2     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ topic_converter │ (standalone node)
│ compressing=true│
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Compressed      │
│ PointCloud2     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ CloudiniSub     │
│ PCL             │
└─────────────────┘

Performance Tips

  1. Use CloudiniSubscriberPCL for PCL workflows - bypasses intermediate ROS conversions
  2. Enable intra-process comms when publisher/subscriber in same process:
    rclcpp::NodeOptions options;
    options.use_intra_process_comms(true);
    auto node = std::make_shared<MyNode>(options);
  3. Set resolution based on needs:
    • Indoor robotics: 0.001 (1mm)
    • Outdoor/autonomous driving: 0.01 (1cm)
    • Architecture/mapping: 0.0001 (0.1mm)
  4. Use ZSTD for maximum compression (slower), LZ4 for real-time (faster)
  5. Monitor with ros2 topic hz and ros2 topic bw to verify compression gains

Recent Fixes (as of 2025)

  • Threading fix in PointcloudEncoder: Protected compression_done_ and compressed_size_ with mutex to prevent data races and deadlocks
  • Object pool optimization: Added to CloudiniSubscriberPCL for ~75% allocation reduction
  • Subscriber-aware processing: topic_converter now skips processing when no subscribers present
  • QoS auto-detection: topic_converter automatically adapts to publisher QoS settings
  • pcl_conversions linking: Added to CMakeLists.txt for proper PCL integration