diff --git a/.gitignore b/.gitignore
index 7874f4da8..23073ec0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,13 @@ docs/cpp_api
docs/_doxygen_cpp
docs/_doxygen_ros1
docs/_build
+
+# Local benchmark launch and monitoring utilities
+/interfaces/ros1/wavemap_ros/launch/benchmark_*.launch
+/interfaces/ros1/wavemap_ros/launch/benchmarks/
+/interfaces/ros1/wavemap_ros/scripts/monitor_wavemap_benchmark.py
+/interfaces/ros1/wavemap_ros/scripts/run_ouster_benchmark.py
+/interfaces/ros1/wavemap_ros/scripts/summarize_wavemap_benchmark.py
+
+# Local backup files
+*.orig
diff --git a/README.md b/README.md
index 737a112e6..96739124e 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,33 @@
-# Wavemap
-
-
-
-
+# Layered Wavemap
+
+
+
+
-
-[](https://www.youtube.com/live/ftQhK75Ri1E?si=9txTYyJ78wQuhyN-&t=733)
+
-## Hierarchical, multi-resolution volumetric mapping
+Layered Wavemap extends Wavemap's compressed multi-resolution occupancy map with typed environmental attributes. Continuous fields such as reflectivity, signal, near-infrared intensity, and RGB share the wavelet hierarchy with occupancy. Categorical fields such as semantic classes use aligned sparse side layers with lossless majority-with-exceptions compression.
+
+[Get started](docs/pages/tutorials/layered_wavemap.rst) · [Run the RGB demo](#try-the-layered-rgb-demo) · [Original Wavemap documentation](https://ethz-asl.github.io/wavemap/)
+
+[](https://youtu.be/l24QWAxm7Zw)
+
+*Watch the Layered Wavemap demo on YouTube.*
+
+## Try the layered RGB demo
+
+The synthetic forest demo needs no sensor data. On Ubuntu 20.04 with ROS Noetic:
+
+```bash
+cd ~/catkin_ws
+catkin build wavemap_all
+source devel/setup.bash
+roslaunch wavemap_ros synthetic_rgb_layered_map.launch
+```
+
+The launch file opens RViz and progressively builds a colored layered map. Docker setup, native installation, real Ouster usage, map persistence, and a layer-extension walkthrough are in the [Layered Wavemap guide](docs/pages/tutorials/layered_wavemap.rst).
+
+## Wavemap foundation
Wavemap achieves state-of-the-art memory and computational efficiency by combining Haar wavelet compression and a coarse-to-fine measurement integration scheme. Advanced measurement models allow it to attain exceptionally high recall rates on challenging obstacles like thin objects.
The framework is very flexible and supports several data structures, measurement integration methods, and sensor models out of the box. The ROS interface can, for example, easily be configured to fuse multiple sensor inputs, such as a LiDAR configured with a range of 20m and several depth cameras up to a resolution of 1cm, into a single multi-resolution occupancy grid map.
@@ -21,6 +41,7 @@ Wavemap provides [C++](https://ethz-asl.github.io/wavemap/pages/tutorials/cpp) a
The framework's documentation is available on [GitHub Pages](https://ethz-asl.github.io/wavemap/) for easy online access. A PDF version of each release’s documentation can also be found in the respective [release notes](https://github.com/ethz-asl/wavemap/releases).
### Table of contents
+* [Layered Wavemap: setup, use, and extension](docs/pages/tutorials/layered_wavemap.rst)
* [Installation](https://ethz-asl.github.io/wavemap/pages/installation)
* [Demos](https://ethz-asl.github.io/wavemap/pages/demos)
* [Tutorials](https://ethz-asl.github.io/wavemap/pages/tutorials)
diff --git a/docs/pages/tutorials/index.rst b/docs/pages/tutorials/index.rst
index 8858cef2a..f7badbdff 100644
--- a/docs/pages/tutorials/index.rst
+++ b/docs/pages/tutorials/index.rst
@@ -6,6 +6,7 @@ This section provides hands-on examples of how to use wavemap's C++ and Python A
:caption: Interfaces
:maxdepth: 1
+ layered_wavemap
cpp
python
ros1
diff --git a/docs/pages/tutorials/layered_wavemap.rst b/docs/pages/tutorials/layered_wavemap.rst
new file mode 100644
index 000000000..fa26da4fb
--- /dev/null
+++ b/docs/pages/tutorials/layered_wavemap.rst
@@ -0,0 +1,452 @@
+.. _layered-wavemap-tutorial:
+
+Layered Wavemap
+###############
+
+Layered Wavemap extends the original occupancy map with environmental
+attributes that use the same spatial coordinates. This guide takes you from a
+fresh checkout to a running RGB example, then shows how to use real point-cloud
+fields and define a new layer.
+
+What is stored
+**************
+
+Every layered map owns two related forms of storage:
+
+* Occupancy and wavelet-compatible continuous states share one wavelet octree.
+ Examples are reflectivity, signal strength, near-infrared intensity, and RGB.
+* Categorical or finite-state values use sparse side layers aligned to the
+ minimum-resolution voxel indices. The current discrete backend stores a
+ dominant value per block, observed offsets, and exact exceptions.
+
+Occupancy always remains part of the continuous voxel and is not listed in a
+user schema. Here, continuous and discrete classify the algebra of the stored
+state; all layers are still spatially voxelized.
+
+A layer is continuous when adding, subtracting, and scaling its stored state
+have a meaningful interpretation. It must also be possible to bound the state
+and measure the magnitude of a wavelet detail coefficient. Scalar sensor
+measurements, RGB vectors, and additive weighted-mean statistics satisfy these
+requirements and can share the Haar wavelet hierarchy with occupancy.
+
+A layer is discrete when interpolation or arithmetic would change its meaning.
+Semantic class IDs, object IDs, and Boolean flags are examples: averaging class
+1 and class 2 does not define a valid class. Their C++ representation does not
+determine the choice; an integer or floating-point value still belongs in a
+discrete layer when arithmetic on that value is semantically invalid. Such
+values use aligned side storage and do not participate in wavelet pruning.
+
+.. code-block:: text
+
+ posed point cloud
+ +-- XYZ ----------------> original Wavemap occupancy integrator
+ +-- point attributes ---> layer update policy
+ +-- continuous: shared wavelet voxel
+ +-- discrete: aligned side layer
+
+Requirements
+************
+
+The supported ROS setup is Ubuntu 20.04 with ROS Noetic. Docker is the fastest
+way to try the project when that environment is not already installed. Native
+installation is more convenient while changing and rebuilding the code.
+
+Docker quick start
+==================
+
+Install Docker and configure it for use without ``sudo``. Then clone this
+repository and build the ROS image from the local checkout:
+
+.. code-block:: bash
+
+ git clone https://github.com/GuilhermeCabaco/wavemap-digital-twin.git
+ cd wavemap-digital-twin
+ docker build --tag=wavemap_ros1 \
+ --file tooling/docker/ros1/full.Dockerfile .
+
+The full Dockerfile copies the current checkout into a Catkin workspace,
+installs its dependencies, and builds ``wavemap_all``. Downloading the upstream
+Wavemap image alone does not include this repository's layered extensions.
+
+The supplied runner forwards an X11 display for RViz. It expects a host data
+directory at ``/home/$USER/data``:
+
+.. code-block:: bash
+
+ mkdir -p /home/$USER/data
+ chmod +x tooling/scripts/run_in_docker.sh
+ ./tooling/scripts/run_in_docker.sh \
+ roslaunch wavemap_ros synthetic_rgb_layered_map.launch
+
+The runner is intended for Linux/X11. Other display systems require the Docker
+GUI forwarding appropriate to that platform.
+
+Native ROS Noetic installation
+==============================
+
+Install ROS Noetic, followed by the build tools:
+
+.. code-block:: bash
+
+ sudo apt update
+ sudo apt install git build-essential python3-rosdep python3-catkin-tools
+
+Create and configure a workspace:
+
+.. code-block:: bash
+
+ mkdir -p ~/catkin_ws/src
+ cd ~/catkin_ws
+ source /opt/ros/noetic/setup.bash
+ catkin init
+ catkin config --cmake-args -DCMAKE_BUILD_TYPE=Release
+
+Clone this repository into the source space:
+
+.. code-block:: bash
+
+ cd ~/catkin_ws/src
+ git clone https://github.com/GuilhermeCabaco/wavemap-digital-twin.git wavemap
+
+Initialize rosdep once, install dependencies, and build:
+
+.. code-block:: bash
+
+ sudo rosdep init
+ rosdep update
+ cd ~/catkin_ws
+ rosdep install -y --from-paths src --ignore-src
+ catkin build wavemap_all
+ source devel/setup.bash
+
+If rosdep is already initialized, continue with ``rosdep update``. Source
+``devel/setup.bash`` in each new terminal.
+
+Run the dataset-free RGB demo
+*****************************
+
+The synthetic demo constructs a small colored forest, publishes it as a
+layered map, and opens RViz:
+
+.. code-block:: bash
+
+ source ~/catkin_ws/devel/setup.bash
+ roslaunch wavemap_ros synthetic_rgb_layered_map.launch
+
+No bag, sensor driver, or localization system is needed. RViz subscribes to
+``/synthetic_rgb_layered_map/layered_map``. Use
+``Render voxels / Layer`` to switch between ``rgb`` and ``occupancy``. The node
+prints a completion message after inserting all voxels.
+
+Control the animation with:
+
+.. code-block:: bash
+
+ roslaunch wavemap_ros synthetic_rgb_layered_map.launch \
+ voxels_per_update:=300 update_period:=0.03
+
+Save the completed map with:
+
+.. code-block:: bash
+
+ roslaunch wavemap_ros synthetic_rgb_layered_map.launch \
+ save_path:=$HOME/synthetic_rgb.lwvmp
+
+After the completion message, stop the process with Ctrl-C. To inspect the file
+later, add a ``LayeredMap`` display in RViz, change ``Source`` from ``Topic`` to
+``File``, and select ``Load map from disk``. The built-in RGB schema handler
+recognizes the saved file.
+
+Use the Ouster integration as a reference
+******************************************
+
+The repository includes Ouster-specific applications as worked examples of a
+real layered sensor integration. No Ouster bag or live sensor stream is bundled
+with the repository, and the commands below do not start localization. They
+show how to launch the mapping side once a compatible point cloud and TF source
+are available. For another sensor or dataset, use the same structure and adapt
+the topic, field names, normalization ranges, projection model, and transforms.
+
+The example applications expect:
+
+* Point clouds on ``/ouster/points``.
+* A valid TF transform from each cloud frame to the configured map frame.
+* ``reflectivity`` values between 0 and 255.
+* ``intensity`` and ``ambient`` values between 0 and 65535 when selected.
+
+The all-layer reference application stores occupancy, reflectivity, signal,
+near infrared, and a geometrically derived ground/obstacle class. With a
+compatible live stream or bag already available, start it as follows:
+
+.. code-block:: bash
+
+ source ~/catkin_ws/devel/setup.bash
+ rosparam load \
+ $(rospack find wavemap_ros)/config/wavemap_ouster_all_layers_demo.yaml \
+ /wavemap
+ rosrun wavemap_ros signal_near_ir_class_layered_server __name:=wavemap
+
+In another terminal, run the sensor, localization, or bag playback that
+publishes the cloud and TF transforms. Open the supplied RViz configuration:
+
+.. code-block:: bash
+
+ source ~/catkin_ws/devel/setup.bash
+ rviz -d $(rospack find wavemap_ros)/config/rviz/layered_map.rviz
+
+Choose ``occupancy``, ``reflectivity``, ``signal``, ``near_ir``, or ``class``
+from the display's Layer property. Class 1 is ground and class 2 is obstacle.
+This is an example based on local elevation, not a learned semantic model.
+
+Available applications
+======================
+
+.. list-table::
+ :header-rows: 1
+ :widths: 45 55
+
+ * - Executable
+ - Additional layers
+ * - ``occupancy_layered_server``
+ - None; exercises the layered lifecycle
+ * - ``reflectivity_layered_server``
+ - reflectivity
+ * - ``reflectivity_class_layered_server``
+ - reflectivity and class
+ * - ``signal_layered_server``
+ - reflectivity and signal
+ * - ``signal_near_ir_layered_server``
+ - reflectivity, signal, and near infrared
+ * - ``signal_class_layered_server``
+ - reflectivity, signal, and class
+ * - ``signal_near_ir_class_layered_server``
+ - reflectivity, signal, near infrared, and class
+
+Hashed and chunked versions also exist for the occupancy and reflectivity
+examples. Their source is under
+``interfaces/ros1/wavemap_ros/app/ouster_layered``.
+
+Save and restore layered maps
+*****************************
+
+A layered server named ``/wavemap`` exposes these services:
+
+.. code-block:: bash
+
+ rosservice call /wavemap/save_layered_map \
+ "file_path: '$HOME/forest_map.lwvmp'"
+
+ rosservice call /wavemap/load_layered_map \
+ "file_path: '$HOME/forest_map.lwvmp'"
+
+ rosservice call /wavemap/layered_map_request_full
+
+The ``.lwvmp`` format stores its version, ordered layer schema, typed continuous
+wavelet map, and compressed discrete side layers. Loading rejects incompatible
+schemas and map geometry. Start the same typed server used to create the file
+before calling ``load_layered_map``. A successful load publishes the full map.
+
+Define and configure a layer
+****************************
+
+A layer is a compile-time type. This scalar example uses exponential smoothing
+with an observation weight of 0.2:
+
+.. code-block:: c++
+
+ using ReflectivityPolicy =
+ wavemap::layered::ExponentialScalarLayerUpdatePolicy<1, 5>;
+
+ struct ReflectivityLayer
+ : wavemap::layered::schema::ContinuousLayer<
+ float, ReflectivityPolicy> {
+ static constexpr std::string_view name = "reflectivity";
+ static constexpr wavemap::layered::ScalarLayerVisualization
+ visualization{{0.05f, 0.05f, 0.05f}, {1.f, 1.f, 1.f}};
+ };
+
+A class layer uses discrete storage because arithmetic on identifiers would not
+preserve their meaning:
+
+.. code-block:: c++
+
+ struct ClassLayer
+ : wavemap::layered::schema::DiscreteLayer<
+ int, wavemap::layered::ReplaceLayerUpdatePolicy> {
+ static constexpr std::string_view name = "class";
+ static constexpr std::array categories{
+ wavemap::layered::IntegerCategoryLabel{
+ 1, "Ground", {0.2f, 0.8f, 0.2f}},
+ wavemap::layered::IntegerCategoryLabel{
+ 2, "Obstacle", {0.9f, 0.2f, 0.15f}}};
+ };
+
+Combine the tags into a schema and derive the map type:
+
+.. code-block:: c++
+
+ using Schema = wavemap::layered::schema::LayerSchema<
+ ReflectivityLayer, ClassLayer>;
+ using Definition = wavemap::layered::LayeredMapDefinition;
+
+Occupancy is included automatically. Pass
+``wavemap::layered::HashedChunkedWaveletOctreeBackend`` as the second
+``LayeredMapDefinition`` template argument to select the chunked backend.
+
+Configure map geometry, pruning, bounds, and discrete grouping:
+
+.. code-block:: c++
+
+ wavemap::layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ config.discreteBlockHeight(2);
+ config.combinedPruningThreshold(1.f);
+
+A discrete block height of 2 groups 4 x 4 x 4 minimum-resolution positions. It
+changes compression grouping, not map resolution.
+
+Choose an update policy
+=======================
+
+Reusable policies live in
+``library/cpp/include/wavemap/layered/integration/layer_update_policy.h``. They
+include replacement, minimum, maximum, accumulation, exponential smoothing,
+confidence blending, logical operations, and a stateful weighted mean. These are
+provided defaults rather than a closed set. A user can define a custom policy
+without changing the map or integration pipeline.
+
+A stateless policy is a default-constructible callable that receives the current
+value and a ``LayerObservation`` and returns the next value. A stateful
+continuous policy follows the same pattern but receives and returns State:
+
+.. code-block:: c++
+
+ struct RateLimitedUpdate {
+ float operator()(
+ float current,
+ const wavemap::layered::LayerObservation& observation) const {
+ const float change = std::clamp(
+ observation.value - current, -0.1f, 0.1f);
+ return current + change;
+ }
+ };
+
+ struct HeightLayer
+ : wavemap::layered::schema::ContinuousLayer<
+ float, RateLimitedUpdate> {
+ static constexpr std::string_view name = "height";
+ };
+
+A discrete policy may instead accept ``std::optional`` as its current
+value so it can distinguish an unobserved voxel from an observed value. The
+compile-time schema checks the selected policy signature and reports an error if
+it is incompatible with the layer.
+
+For ordinary layers, Value and State are identical. Use
+``StatefulContinuousLayer`` when observations need a richer stored state. A
+custom wavelet-compatible state must provide:
+
+* ``ContinuousValueTraits`` operations for addition, subtraction, scaling,
+ thresholding, and magnitude.
+* A ``LayerStreamCodec`` for persistence.
+* A ``LayerStateConversion`` when State differs from public Value.
+* A ``LayerRosCodec`` when using the generic ROS converter.
+
+The RGB and weighted-mean types under
+``library/cpp/include/wavemap/layered/types`` are compact examples.
+
+Bind sensor data
+================
+
+Install the typed map with ``LayeredRosServerBuilder``, then register how sensor
+fields populate it. Numeric PointCloud2 fields can be normalized directly:
+
+.. code-block:: c++
+
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f,
+ wavemap::layered::EndpointRange{0.5f, 25.f});
+
+Other helpers include:
+
+* ``bindDirectPointField`` for values already in the layer's units.
+* ``bindComputedPointField`` for values derived from numeric fields.
+* ``bindGeometricClass`` for the included local-elevation classifier.
+* A custom endpoint adapter for structured or application-specific decoding.
+
+Bindings default to endpoint-only integration. Continuous layers may select an
+along-ray ``PointcloudLayerIntegrationMode``. Discrete fields are restricted to
+endpoints.
+
+Expose and visualize the application
+====================================
+
+Add the executable to ``interfaces/ros1/wavemap_ros/CMakeLists.txt``, link it to
+``wavemap_ros``, and rebuild:
+
+.. code-block:: bash
+
+ cd ~/catkin_ws
+ catkin build wavemap_ros wavemap_ros_conversions wavemap_rviz_plugin
+ source devel/setup.bash
+
+Topic visualization obtains schema and color metadata from the ``LayeredMap``
+message. Loading a custom ``.lwvmp`` directly in RViz additionally requires a
+typed ``LayeredMapFactory`` registration for the new schema.
+
+Verification and troubleshooting
+********************************
+
+Run the affected builds and tests after changing schemas, codecs, storage, or
+ROS conversions:
+
+.. code-block:: bash
+
+ cd ~/catkin_ws
+ catkin build wavemap wavemap_ros_conversions wavemap_ros \
+ wavemap_rviz_plugin
+ catkin run_tests wavemap wavemap_ros_conversions wavemap_ros \
+ wavemap_rviz_plugin
+ source /opt/ros/noetic/setup.bash
+ catkin_test_results --all build
+
+No map appears
+ Check the display topic, the fixed frame, TF availability, and whether the
+ node has published a map message.
+
+Point clouds are dropped
+ Verify cloud timestamps and frames, the map-to-sensor transform, and
+ ``max_wait_for_pose``. Motion undistortion needs usable per-point timing.
+
+An attribute remains empty
+ Inspect the PointCloud2 fields. Its name and numeric range must match the
+ registered binding.
+
+A saved map will not load
+ Use the same schema, backend, resolution, tree geometry, and discrete block
+ configuration that created it.
+
+Fine attribute detail disappears
+ Reduce the layer's pruning scale or increase its pruning weight. Continuous
+ fields contribute to one combined pruning decision.
+
+Relevant source locations
+*************************
+
+* ``library/cpp/include/wavemap/layered/schema``: declarations and bundles.
+* ``library/cpp/include/wavemap/layered/integration``: observations and update
+ policies.
+* ``library/cpp/include/wavemap/layered/map``: map types, backends, and storage.
+* ``library/cpp/include/wavemap/layered/io``: schema-aware persistence.
+* ``interfaces/ros1/wavemap_ros/app/ouster_layered``: complete applications.
+* ``interfaces/ros1/wavemap_ros_conversions``: ROS codecs.
+* ``interfaces/ros1/wavemap_rviz_plugin``: topic and file visualization.
diff --git a/interfaces/ros1/wavemap_msgs/CMakeLists.txt b/interfaces/ros1/wavemap_msgs/CMakeLists.txt
index 58dba387c..4a0505b05 100644
--- a/interfaces/ros1/wavemap_msgs/CMakeLists.txt
+++ b/interfaces/ros1/wavemap_msgs/CMakeLists.txt
@@ -2,15 +2,25 @@ cmake_minimum_required(VERSION 3.0.2)
project(wavemap_msgs)
# Load catkin and required dependencies to generate msgs
-find_package(catkin REQUIRED COMPONENTS message_generation std_msgs)
+find_package(catkin REQUIRED COMPONENTS geometry_msgs message_generation std_msgs)
# Declare message files to be built
add_message_files(FILES
+ DiscreteLayer.msg
+ DiscreteLayerCategory.msg
+ DiscreteLayerCell.msg
HashedBlock.msg
HashedBlocks.msg
HashedWaveletOctree.msg
HashedWaveletOctreeBlock.msg
Index3D.msg
+ LayerVisualization.msg
+ LayeredHashedWaveletOctree.msg
+ LayeredHashedWaveletOctreeBlock.msg
+ LayeredMap.msg
+ LayeredMapUpdate.msg
+ LayeredWaveletOctreeNode.msg
+ Layer.msg
Map.msg
OctreeNode.msg
WaveletOctree.msg
@@ -21,7 +31,7 @@ add_service_files(FILES
FilePath.srv)
# Generate the language-specific message and service files
-generate_messages(DEPENDENCIES std_msgs)
+generate_messages(DEPENDENCIES geometry_msgs std_msgs)
# Finish registering wavemap_msgs as a catkin pacakge
-catkin_package(CATKIN_DEPENDS message_runtime std_msgs)
+catkin_package(CATKIN_DEPENDS geometry_msgs message_runtime std_msgs)
diff --git a/interfaces/ros1/wavemap_msgs/msg/DiscreteLayer.msg b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayer.msg
new file mode 100644
index 000000000..5a997be87
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayer.msg
@@ -0,0 +1,18 @@
+# A compressed discrete voxel layer stored as parent cells with dominant values
+# and explicit exceptions.
+string name
+string value_type
+
+# Same meaning as DiscreteCompressionConfig.block_height:
+# 1 -> 2x2x2, 2 -> 4x4x4, etc.
+int32 block_height
+
+# Optional labels for integer category values. Values without an entry remain
+# valid and are displayed using their numeric value.
+DiscreteLayerCategory[] categories
+
+DiscreteLayerCell[] cells
+
+# Parent cells removed since the preceding update. Only used by incremental
+# LayeredMap messages; full snapshots implicitly remove all omitted cells.
+Index3D[] deleted_parent_indices
diff --git a/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCategory.msg b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCategory.msg
new file mode 100644
index 000000000..722744c46
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCategory.msg
@@ -0,0 +1,7 @@
+# Optional semantic label and schema-declared visualization color.
+int32 value
+string label
+bool has_color
+float32 r
+float32 g
+float32 b
diff --git a/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCell.msg b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCell.msg
new file mode 100644
index 000000000..0bd1cc0ef
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/DiscreteLayerCell.msg
@@ -0,0 +1,21 @@
+# One compressed parent cell in a discrete layer.
+# The parent_index identifies the compressed parent block.
+Index3D parent_index
+
+# Default value for all observed children unless an exception overrides it.
+# Exactly one value array is used, according to DiscreteLayer.value_type.
+int32 dominant_int32
+uint8 dominant_uint8
+
+# Child offsets observed inside this parent cell.
+# This keeps sparse unknown children unknown instead of implicitly assigning
+# them the dominant value.
+int32[] observed_offsets
+
+# Child offsets whose value differs from the dominant value.
+int32[] exception_offsets
+
+# Exception values aligned with exception_offsets. Exactly one array is used,
+# according to DiscreteLayer.value_type.
+int32[] exception_int32_values
+uint8[] exception_uint8_values
diff --git a/interfaces/ros1/wavemap_msgs/msg/Layer.msg b/interfaces/ros1/wavemap_msgs/msg/Layer.msg
new file mode 100644
index 000000000..8e5f5d41e
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/Layer.msg
@@ -0,0 +1,6 @@
+string name
+string type
+
+float32[] float32_values
+int32[] int32_values
+uint8[] uint8_values
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayerVisualization.msg b/interfaces/ros1/wavemap_msgs/msg/LayerVisualization.msg
new file mode 100644
index 000000000..0f836bb3c
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayerVisualization.msg
@@ -0,0 +1,10 @@
+# Optional visualization metadata declared by the layer schema.
+string name
+bool has_low_color
+float32 low_r
+float32 low_g
+float32 low_b
+bool has_high_color
+float32 high_r
+float32 high_g
+float32 high_b
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctree.msg b/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctree.msg
new file mode 100644
index 000000000..70d154e06
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctree.msg
@@ -0,0 +1,15 @@
+float32 min_cell_width
+float32 min_log_odds
+float32 max_log_odds
+
+int32 tree_height
+
+string[] layer_names
+string[] layer_types
+Layer[] layer_min_values
+Layer[] layer_max_values
+LayerVisualization[] layer_visualizations
+
+Index3D[] allocated_block_indices
+
+LayeredHashedWaveletOctreeBlock[] blocks
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctreeBlock.msg b/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctreeBlock.msg
new file mode 100644
index 000000000..b552eee9b
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayeredHashedWaveletOctreeBlock.msg
@@ -0,0 +1,6 @@
+Index3D root_node_offset
+
+float32 root_node_occupancy_scale_coefficient
+Layer[] root_node_layers
+
+LayeredWaveletOctreeNode[] nodes
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayeredMap.msg b/interfaces/ros1/wavemap_msgs/msg/LayeredMap.msg
new file mode 100644
index 000000000..01bd9de3d
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayeredMap.msg
@@ -0,0 +1,11 @@
+Header header
+
+# True for a complete snapshot. False for an incremental patch whose
+# continuous blocks and discrete parent cells replace only the included data.
+bool is_full_update
+
+# Existing Wavemap structured message for the continuous wavelet map.
+Map continuous_map
+
+# Additional non-wavelet discrete layers aligned with the same voxel index space.
+DiscreteLayer[] discrete_layers
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayeredMapUpdate.msg b/interfaces/ros1/wavemap_msgs/msg/LayeredMapUpdate.msg
new file mode 100644
index 000000000..1a003d8e6
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayeredMapUpdate.msg
@@ -0,0 +1,6 @@
+# A sensor-independent batch of spatial layer observations.
+# Every layer contains one value per position, in the same order.
+# Producers may include any subset of the configured map layers.
+std_msgs/Header header
+geometry_msgs/Point32[] positions
+Layer[] layers
diff --git a/interfaces/ros1/wavemap_msgs/msg/LayeredWaveletOctreeNode.msg b/interfaces/ros1/wavemap_msgs/msg/LayeredWaveletOctreeNode.msg
new file mode 100644
index 000000000..cc91939c7
--- /dev/null
+++ b/interfaces/ros1/wavemap_msgs/msg/LayeredWaveletOctreeNode.msg
@@ -0,0 +1,3 @@
+float32[8] occupancy_detail_coefficients
+Layer[] detail_layers
+uint8 allocated_children_bitset
diff --git a/interfaces/ros1/wavemap_msgs/msg/Map.msg b/interfaces/ros1/wavemap_msgs/msg/Map.msg
index 41db3abe1..67cea39cf 100644
--- a/interfaces/ros1/wavemap_msgs/msg/Map.msg
+++ b/interfaces/ros1/wavemap_msgs/msg/Map.msg
@@ -3,3 +3,4 @@ Header header
HashedBlocks[] hashed_blocks
WaveletOctree[] wavelet_octree
HashedWaveletOctree[] hashed_wavelet_octree
+LayeredHashedWaveletOctree[] layered_hashed_wavelet_octree
diff --git a/interfaces/ros1/wavemap_msgs/package.xml b/interfaces/ros1/wavemap_msgs/package.xml
index 72be093e5..48db555cb 100644
--- a/interfaces/ros1/wavemap_msgs/package.xml
+++ b/interfaces/ros1/wavemap_msgs/package.xml
@@ -12,6 +12,7 @@
catkin
+ geometry_msgs
message_generation
message_runtime
std_msgs
diff --git a/interfaces/ros1/wavemap_ros/CMakeLists.txt b/interfaces/ros1/wavemap_ros/CMakeLists.txt
index e90bc7449..e7a845abe 100644
--- a/interfaces/ros1/wavemap_ros/CMakeLists.txt
+++ b/interfaces/ros1/wavemap_ros/CMakeLists.txt
@@ -7,7 +7,7 @@ project(wavemap_ros)
find_package(OpenCV REQUIRED)
find_package(catkin REQUIRED COMPONENTS
wavemap wavemap_msgs wavemap_ros_conversions
- roscpp rosbag cv_bridge image_transport tf2_ros
+ roscpp rosbag message_filters cv_bridge image_transport tf2_ros
std_srvs sensor_msgs visualization_msgs)
# Register catkin package
@@ -16,7 +16,7 @@ catkin_package(
LIBRARIES ${PROJECT_NAME}
CATKIN_DEPENDS
wavemap wavemap_msgs wavemap_ros_conversions
- roscpp rosbag cv_bridge image_transport tf2_ros
+ roscpp rosbag message_filters cv_bridge image_transport tf2_ros
std_srvs sensor_msgs visualization_msgs)
@@ -53,6 +53,90 @@ add_executable(ros_server app/ros_server.cc)
set_wavemap_target_properties(ros_server)
target_link_libraries(ros_server ${PROJECT_NAME})
+add_executable(occupancy_layered_server
+ app/ouster_layered/occupancy_sensor_inputs.cc)
+set_wavemap_target_properties(occupancy_layered_server)
+target_include_directories(occupancy_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(occupancy_layered_server ${PROJECT_NAME})
+
+add_executable(occupancy_chunked_layered_server
+ app/ouster_layered/occupancy_chunked_sensor_inputs.cc)
+set_wavemap_target_properties(occupancy_chunked_layered_server)
+target_include_directories(occupancy_chunked_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(occupancy_chunked_layered_server ${PROJECT_NAME})
+
+add_executable(reflectivity_layered_server
+ app/ouster_layered/reflectivity_only_sensor_inputs.cc)
+set_wavemap_target_properties(reflectivity_layered_server)
+target_include_directories(reflectivity_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(reflectivity_layered_server ${PROJECT_NAME})
+
+add_executable(signal_layered_server
+ app/ouster_layered/signal_sensor_inputs.cc)
+set_wavemap_target_properties(signal_layered_server)
+target_include_directories(signal_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(signal_layered_server ${PROJECT_NAME})
+
+add_executable(signal_near_ir_layered_server
+ app/ouster_layered/signal_near_ir_sensor_inputs.cc)
+set_wavemap_target_properties(signal_near_ir_layered_server)
+target_include_directories(signal_near_ir_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(signal_near_ir_layered_server ${PROJECT_NAME})
+
+add_executable(signal_class_layered_server
+ app/ouster_layered/signal_class_sensor_inputs.cc)
+set_wavemap_target_properties(signal_class_layered_server)
+target_include_directories(signal_class_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(signal_class_layered_server ${PROJECT_NAME})
+
+add_executable(signal_near_ir_class_layered_server
+ app/ouster_layered/signal_near_ir_class_sensor_inputs.cc)
+set_wavemap_target_properties(signal_near_ir_class_layered_server)
+target_include_directories(signal_near_ir_class_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(signal_near_ir_class_layered_server ${PROJECT_NAME})
+
+add_executable(reflectivity_chunked_layered_server
+ app/ouster_layered/reflectivity_chunked_sensor_inputs.cc)
+set_wavemap_target_properties(reflectivity_chunked_layered_server)
+target_include_directories(reflectivity_chunked_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(reflectivity_chunked_layered_server ${PROJECT_NAME})
+
+add_executable(reflectivity_class_layered_server
+ app/ouster_layered/ouster_sensor_inputs.cc)
+set_wavemap_target_properties(reflectivity_class_layered_server)
+target_include_directories(reflectivity_class_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(reflectivity_class_layered_server ${PROJECT_NAME})
+
+add_executable(reflectivity_class_chunked_layered_server
+ app/ouster_layered/reflectivity_class_chunked_sensor_inputs.cc)
+set_wavemap_target_properties(reflectivity_class_chunked_layered_server)
+target_include_directories(reflectivity_class_chunked_layered_server PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(reflectivity_class_chunked_layered_server ${PROJECT_NAME})
+
+add_executable(inspect_ouster_benchmark_maps
+ app/ouster_layered/inspect_ouster_benchmark_maps.cc)
+set_wavemap_target_properties(inspect_ouster_benchmark_maps)
+target_include_directories(inspect_ouster_benchmark_maps PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app/ouster_layered)
+target_link_libraries(inspect_ouster_benchmark_maps ${PROJECT_NAME})
+
+add_executable(synthetic_rgb_layered_map_demo
+ app/synthetic_rgb_layered_map_demo.cc)
+set_wavemap_target_properties(synthetic_rgb_layered_map_demo)
+target_include_directories(synthetic_rgb_layered_map_demo PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/app)
+target_link_libraries(synthetic_rgb_layered_map_demo ${PROJECT_NAME})
+
add_executable(rosbag_processor app/rosbag_processor.cc)
set_wavemap_target_properties(rosbag_processor)
target_link_libraries(rosbag_processor ${PROJECT_NAME})
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/inspect_ouster_benchmark_maps.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/inspect_ouster_benchmark_maps.cc
new file mode 100644
index 000000000..60a4e43af
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/inspect_ouster_benchmark_maps.cc
@@ -0,0 +1,225 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "occupancy_map_layers.h"
+#include "reflectivity_map_layers.h"
+#include "reflectivity_only_map_layers.h"
+
+namespace occupancy_map = wavemap::examples::occupancy_map;
+namespace reflectivity_only_map = wavemap::examples::reflectivity_only_map;
+namespace reflectivity_map = wavemap::examples::reflectivity_map;
+
+namespace {
+struct OccupancyStats {
+ size_t blocks = 0u;
+ size_t leaves = 0u;
+ size_t observed = 0u;
+ float min = std::numeric_limits::max();
+ float max = std::numeric_limits::lowest();
+};
+
+template
+OccupancyStats occupancyStats(const MapT& map) {
+ OccupancyStats stats;
+ stats.blocks = map.getHashMap().size();
+ map.forEachLeaf([&](const wavemap::OctreeIndex&, float occupancy) {
+ ++stats.leaves;
+ stats.observed += 1e-5f < std::abs(occupancy);
+ stats.min = std::min(stats.min, occupancy);
+ stats.max = std::max(stats.max, occupancy);
+ });
+ return stats;
+}
+
+struct Comparison {
+ size_t samples = 0u;
+ size_t above_tolerance = 0u;
+ double absolute_error_sum = 0.;
+ float max_absolute_error = 0.f;
+};
+
+template
+Comparison compareDirection(const LhsMapT& lhs, const RhsMapT& rhs,
+ float tolerance = 1e-4f) {
+ Comparison comparison;
+ lhs.forEachLeaf([&](const wavemap::OctreeIndex& index, float lhs_value) {
+ const float error = std::abs(lhs_value - rhs.getCellValue(index));
+ ++comparison.samples;
+ comparison.above_tolerance += tolerance < error;
+ comparison.absolute_error_sum += error;
+ comparison.max_absolute_error =
+ std::max(comparison.max_absolute_error, error);
+ });
+ return comparison;
+}
+
+template
+void printComparison(const std::string& name, const LhsMapT& lhs,
+ const RhsMapT& rhs) {
+ const Comparison forward = compareDirection(lhs, rhs);
+ const Comparison reverse = compareDirection(rhs, lhs);
+ const auto print = [](const char* direction, const Comparison& comparison) {
+ std::cout << " " << direction << ": samples=" << comparison.samples
+ << " above_1e-4=" << comparison.above_tolerance
+ << " mean_abs="
+ << (comparison.samples
+ ? comparison.absolute_error_sum / comparison.samples
+ : 0.)
+ << " max_abs=" << comparison.max_absolute_error << "\n";
+ };
+ std::cout << " " << name << "\n";
+ print("forward", forward);
+ print("reverse", reverse);
+}
+
+template
+void printReflectivityStats(const std::string& name,
+ const typename DefinitionT::Map& map) {
+ size_t nonzero = 0u;
+ size_t out_of_bounds = 0u;
+ float min_value = std::numeric_limits::max();
+ float max_value = std::numeric_limits::lowest();
+ map.continuousMap().forEachVoxelLeaf(
+ [&](const wavemap::OctreeIndex&,
+ const typename DefinitionT::Voxel& voxel) {
+ const float value = voxel.data.template get();
+ nonzero += 1e-6f < std::abs(value);
+ out_of_bounds += value < 0.f || 1.f < value;
+ min_value = std::min(min_value, value);
+ max_value = std::max(max_value, value);
+ });
+ std::cout << " " << name << ": nonzero=" << nonzero
+ << " out_of_[0,1]=" << out_of_bounds
+ << " value_range=[" << min_value << ", " << max_value << "]\n";
+}
+
+void printOccupancyStats(const std::string& name,
+ const OccupancyStats& stats) {
+ std::cout << " " << name << ": blocks=" << stats.blocks
+ << " leaves=" << stats.leaves << " observed=" << stats.observed
+ << " range=[" << stats.min << ", " << stats.max << "]\n";
+}
+} // namespace
+
+int main(int argc, char** argv) {
+ if (argc != 5) {
+ std::cerr << "Usage: " << argv[0]
+ << " \n";
+ return 1;
+ }
+
+ wavemap::MapBase::Ptr original_base;
+ if (!wavemap::io::fileToMap(argv[1], original_base)) {
+ std::cerr << "Could not load A\n";
+ return 2;
+ }
+ auto original =
+ std::dynamic_pointer_cast(original_base);
+ if (!original) {
+ std::cerr << "A is not a HashedWaveletOctree\n";
+ return 2;
+ }
+
+ occupancy_map::Definition::Map occupancy(occupancy_map::makeMapConfig());
+ reflectivity_only_map::Definition::Map reflectivity(
+ reflectivity_only_map::makeMapConfig());
+ reflectivity_map::Definition::Map reflectivity_class(
+ reflectivity_map::makeMapConfig());
+ std::string error;
+ if (!occupancy_map::Definition::MapIo::load(argv[2], occupancy, &error)) {
+ std::cerr << "Could not load B: " << error << "\n";
+ return 2;
+ }
+ if (!reflectivity_only_map::Definition::MapIo::load(argv[3], reflectivity,
+ &error)) {
+ std::cerr << "Could not load C: " << error << "\n";
+ return 2;
+ }
+ if (!reflectivity_map::Definition::MapIo::load(argv[4], reflectivity_class,
+ &error)) {
+ std::cerr << "Could not load D: " << error << "\n";
+ return 2;
+ }
+
+ std::cout << "Occupancy statistics\n";
+ printOccupancyStats("A original", occupancyStats(*original));
+ printOccupancyStats("B layered occupancy",
+ occupancyStats(occupancy.continuousMap()));
+ printOccupancyStats("C reflectivity",
+ occupancyStats(reflectivity.continuousMap()));
+ printOccupancyStats("D reflectivity+class",
+ occupancyStats(reflectivity_class.continuousMap()));
+
+ std::cout << "Occupancy comparisons\n";
+ printComparison("A vs B", *original, occupancy.continuousMap());
+ printComparison("A vs C", *original, reflectivity.continuousMap());
+ printComparison("A vs D", *original, reflectivity_class.continuousMap());
+ printComparison("B vs C", occupancy.continuousMap(),
+ reflectivity.continuousMap());
+ printComparison("C vs D", reflectivity.continuousMap(),
+ reflectivity_class.continuousMap());
+
+ std::cout << "Reflectivity statistics\n";
+ printReflectivityStats(
+ "C reflectivity", reflectivity);
+ printReflectivityStats(
+ "D reflectivity", reflectivity_class);
+
+ const auto& class_layer =
+ reflectivity_class.discreteLayers().get();
+ size_t ground = 0u;
+ size_t obstacle = 0u;
+ for (const auto& [key, cell] : class_layer.cells()) {
+ (void)key;
+ for (const int offset : cell.observed_offsets) {
+ const auto exception = cell.exceptions.find(offset);
+ const int value = exception == cell.exceptions.end()
+ ? cell.dominant_value
+ : exception->second;
+ ground += value == static_cast(
+ wavemap::layered::GeometricClass::kGround);
+ obstacle += value == static_cast(
+ wavemap::layered::GeometricClass::kObstacle);
+ }
+ }
+ const size_t parent_count = class_layer.parentCount();
+ const size_t observed_count = class_layer.observedValueCount();
+ const size_t exception_count = class_layer.exceptionCount();
+ const size_t dominant_matching_count = observed_count - exception_count;
+ // Exact size of the version-1 discrete section for this single int layer:
+ // section marker, layer metadata/configuration, and all cell records.
+ const size_t serialized_discrete_bytes =
+ 67u + 32u * parent_count + 4u * observed_count +
+ 8u * exception_count;
+ std::cout << "Class statistics\n"
+ << " block_height=" << class_layer.config().block_height
+ << " block_side=" << class_layer.config().blockSideLength()
+ << " possible_children="
+ << class_layer.config().blockVoxelCount() << "\n"
+ << " parents=" << parent_count << " observed=" << observed_count
+ << " dominant_matching=" << dominant_matching_count
+ << " exceptions=" << exception_count << "\n"
+ << " exception_ratio="
+ << (observed_count ? static_cast(exception_count) /
+ static_cast(observed_count)
+ : 0.)
+ << " mean_observed_per_parent="
+ << (parent_count ? static_cast(observed_count) /
+ static_cast(parent_count)
+ : 0.)
+ << "\n"
+ << " serialized_discrete_bytes=" << serialized_discrete_bytes
+ << " ground=" << ground << " obstacle=" << obstacle << "\n";
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_chunked_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_chunked_sensor_inputs.cc
new file mode 100644
index 000000000..1e8f4072c
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_chunked_sensor_inputs.cc
@@ -0,0 +1,44 @@
+#include "occupancy_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::occupancy_map::chunked {
+
+using Definition = ChunkedDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& /*server*/,
+ ServerBuilder::Extension& /*extension*/) {
+ // Occupancy continues to use Wavemap's original point-cloud integration.
+}
+
+} // namespace wavemap::examples::occupancy_map::chunked
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::occupancy_map::chunked;
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::occupancy_map::makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO("Installed chunked layered occupancy-only benchmark.");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_map_layers.h b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_map_layers.h
new file mode 100644
index 000000000..d4ead448a
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_map_layers.h
@@ -0,0 +1,35 @@
+#ifndef WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_OCCUPANCY_MAP_LAYERS_H_
+#define WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_OCCUPANCY_MAP_LAYERS_H_
+
+#include
+#include
+#include
+
+namespace wavemap::examples::occupancy_map {
+
+// Occupancy is built into every layered map, so this schema intentionally has
+// no user-defined layers. It is benchmark variant B.
+using Schema = layered::schema::LayerSchema<>;
+template
+using DefinitionFor =
+ layered::LayeredMapDefinition;
+using Definition = DefinitionFor;
+using ChunkedDefinition =
+ DefinitionFor;
+
+template
+inline typename DefinitionT::Config makeMapConfig() {
+ layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.combinedPruningThreshold(1.f);
+ return config.build();
+}
+
+} // namespace wavemap::examples::occupancy_map
+
+#endif // WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_OCCUPANCY_MAP_LAYERS_H_
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_sensor_inputs.cc
new file mode 100644
index 000000000..e2a127bb3
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/occupancy_sensor_inputs.cc
@@ -0,0 +1,42 @@
+#include "occupancy_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::occupancy_map {
+
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& /*server*/,
+ ServerBuilder::Extension& /*extension*/) {
+ // Occupancy continues to use Wavemap's original point-cloud integration.
+}
+
+} // namespace wavemap::examples::occupancy_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::occupancy_map;
+ map::ServerBuilder::install(server, private_nh, server.worldFrame(),
+ map::makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO("Installed layered occupancy-only benchmark application (B).");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/ouster_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/ouster_sensor_inputs.cc
new file mode 100644
index 000000000..ead884360
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/ouster_sensor_inputs.cc
@@ -0,0 +1,61 @@
+#include "reflectivity_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::reflectivity_map { // Ouster mapping example
+
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ // The Ouster PointCloud2 stores reflectivity as a raw numeric field in
+ // [0, 255]. Decode it during the existing XYZ traversal, normalize it into
+ // the map's [0, 1] storage domain, and update occupied endpoints only.
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f,
+ layered::EndpointRange{0.5f, 25.f});
+
+ // This bag has no class field. Derive a small example taxonomy directly
+ // from each posed cloud: 1 = local ground and 2 = obstacle.
+ layered::LocalElevationClassifierConfig class_config;
+ class_config.grid_cell_width = 0.5f;
+ class_config.ground_tolerance = 0.2f;
+ class_config.obstacle_height = 0.35f;
+ class_config.min_points_per_cell = 3u;
+ server.bindGeometricClass(
+ extension, class_config, layered::EndpointRange{0.5f, 25.f});
+}
+
+} // namespace wavemap::examples::reflectivity_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::reflectivity_map;
+ int discrete_block_height = 0;
+ private_nh.param("discrete_block_height", discrete_block_height, 0);
+ map::ServerBuilder::install(server, private_nh, server.worldFrame(),
+ map::makeMapConfig(discrete_block_height),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed occupancy+reflectivity+class benchmark application "
+ "(D) with discrete block height %d.", discrete_block_height);
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_chunked_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_chunked_sensor_inputs.cc
new file mode 100644
index 000000000..484edd721
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_chunked_sensor_inputs.cc
@@ -0,0 +1,51 @@
+#include "reflectivity_only_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::reflectivity_only_map {
+namespace chunked {
+
+using Definition = ChunkedDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f,
+ layered::EndpointRange{0.5f, 25.f});
+}
+
+} // namespace chunked
+} // namespace wavemap::examples::reflectivity_only_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map =
+ wavemap::examples::reflectivity_only_map::chunked;
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::reflectivity_only_map::
+ makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed chunked occupancy+reflectivity application.");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_class_chunked_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_class_chunked_sensor_inputs.cc
new file mode 100644
index 000000000..f93eb68ed
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_class_chunked_sensor_inputs.cc
@@ -0,0 +1,58 @@
+#include "reflectivity_map_layers.h"
+
+#include
+
+#include
+#include
+#include
+
+namespace wavemap::examples::reflectivity_map::chunked {
+
+using Definition = ChunkedDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f,
+ layered::EndpointRange{0.5f, 25.f});
+
+ layered::LocalElevationClassifierConfig class_config;
+ class_config.grid_cell_width = 0.5f;
+ class_config.ground_tolerance = 0.2f;
+ class_config.obstacle_height = 0.35f;
+ class_config.min_points_per_cell = 3u;
+ server.bindGeometricClass(
+ extension, class_config, layered::EndpointRange{0.5f, 25.f});
+}
+
+} // namespace wavemap::examples::reflectivity_map::chunked
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::reflectivity_map::chunked;
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::reflectivity_map::
+ makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed chunked occupancy+reflectivity+class benchmark "
+ "application.");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_map_layers.h b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_map_layers.h
new file mode 100644
index 000000000..1a6eb88c1
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_map_layers.h
@@ -0,0 +1,70 @@
+#ifndef WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_MAP_LAYERS_H_
+#define WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_MAP_LAYERS_H_
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace wavemap::examples::reflectivity_map {
+
+using ReflectivityReplacePolicy = layered::ReplaceLayerUpdatePolicy;
+using ReflectivityExponentialPolicy =
+ layered::ExponentialScalarLayerUpdatePolicy<1, 5>;
+
+template
+struct ReflectivityLayerTemplate
+ : layered::schema::ContinuousLayer {
+ static constexpr layered::ScalarLayerVisualization visualization{
+ {0.05f, 0.05f, 0.05f}, {1.f, 1.f, 1.f}};
+ static constexpr std::string_view name = "reflectivity";
+};
+
+// Both policies are user-selectable. This example uses alpha=0.2 exponential
+// smoothing; changing this alias to ReflectivityReplacePolicy selects latest
+// observation replacement without changing the adapter or pipeline.
+using ReflectivityLayer =
+ ReflectivityLayerTemplate;
+
+struct ClassLayer : layered::schema::DiscreteLayer> {
+ static constexpr std::string_view name = "class";
+ static constexpr std::array categories{
+ layered::IntegerCategoryLabel{1, "Ground", {0.2f, 0.8f, 0.2f}},
+ layered::IntegerCategoryLabel{2, "Obstacle", {0.9f, 0.2f, 0.15f}}};
+};
+
+// Occupancy is built into the shared continuous voxel and is not repeated in
+// the user schema.
+using Schema = layered::schema::LayerSchema;
+template
+using DefinitionFor =
+ layered::LayeredMapDefinition;
+using Definition = DefinitionFor;
+using ChunkedDefinition =
+ DefinitionFor;
+
+template
+inline typename DefinitionT::Config makeMapConfig(
+ int discrete_block_height = 0) {
+ layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.template layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ config.discreteBlockHeight(discrete_block_height);
+ config.combinedPruningThreshold(1.f);
+ return config.build();
+}
+
+} // namespace wavemap::examples::reflectivity_map
+
+#endif // WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_MAP_LAYERS_H_
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_map_layers.h b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_map_layers.h
new file mode 100644
index 000000000..6fec5239d
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_map_layers.h
@@ -0,0 +1,36 @@
+#ifndef WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_ONLY_MAP_LAYERS_H_
+#define WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_ONLY_MAP_LAYERS_H_
+
+#include "reflectivity_map_layers.h"
+
+namespace wavemap::examples::reflectivity_only_map {
+
+using ReflectivityLayer = reflectivity_map::ReflectivityLayer;
+using Schema = layered::schema::LayerSchema;
+template
+using DefinitionFor =
+ layered::LayeredMapDefinition;
+using Definition = DefinitionFor;
+using ChunkedDefinition =
+ DefinitionFor;
+
+template
+inline typename DefinitionT::Config makeMapConfig() {
+ layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.template layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ config.combinedPruningThreshold(1.f);
+ return config.build();
+}
+
+} // namespace wavemap::examples::reflectivity_only_map
+
+#endif // WAVEMAP_EXAMPLES_DIGITAL_TWIN_OUSTER_REFLECTIVITY_ONLY_MAP_LAYERS_H_
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_sensor_inputs.cc
new file mode 100644
index 000000000..20d739d37
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/reflectivity_only_sensor_inputs.cc
@@ -0,0 +1,45 @@
+#include "reflectivity_only_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::reflectivity_only_map {
+
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f,
+ layered::EndpointRange{0.5f, 25.f});
+}
+
+} // namespace wavemap::examples::reflectivity_only_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::reflectivity_only_map;
+ map::ServerBuilder::install(server, private_nh, server.worldFrame(),
+ map::makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed occupancy+reflectivity benchmark application (C).");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/rgb_map_layers.h b/interfaces/ros1/wavemap_ros/app/ouster_layered/rgb_map_layers.h
new file mode 100644
index 000000000..b3e220bc5
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/rgb_map_layers.h
@@ -0,0 +1,43 @@
+#ifndef WAVEMAP_ROS_APP_OUSTER_LAYERED_RGB_MAP_LAYERS_H_
+#define WAVEMAP_ROS_APP_OUSTER_LAYERED_RGB_MAP_LAYERS_H_
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace wavemap::examples::rgb_map {
+
+struct RgbLayer
+ : layered::schema::ContinuousLayer<
+ layered::Rgb,
+ layered::ReplaceLayerUpdatePolicy> {
+ static constexpr std::string_view name = "rgb";
+};
+
+using Schema = layered::schema::LayerSchema;
+using Definition = layered::LayeredMapDefinition;
+
+inline Definition::Config makeMapConfig() {
+ layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.layer()
+ .storageBounds(layered::Rgb{0.f, 0.f, 0.f},
+ layered::Rgb{1.f, 1.f, 1.f})
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ config.combinedPruningThreshold(1.f);
+ return config.build();
+}
+
+} // namespace wavemap::examples::rgb_map
+
+#endif // WAVEMAP_ROS_APP_OUSTER_LAYERED_RGB_MAP_LAYERS_H_
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_class_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_class_sensor_inputs.cc
new file mode 100644
index 000000000..ace5a0168
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_class_sensor_inputs.cc
@@ -0,0 +1,61 @@
+#include "signal_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::signal_map::signal_class_map {
+
+using Definition = ClassDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ const layered::EndpointRange endpoint_range{0.5f, 25.f};
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f, endpoint_range);
+ server.bindNormalizedPointField(
+ extension, "intensity", 0.f, 65535.f, endpoint_range);
+ layered::LocalElevationClassifierConfig class_config;
+ class_config.grid_cell_width = 0.5f;
+ class_config.ground_tolerance = 0.2f;
+ class_config.obstacle_height = 0.35f;
+ class_config.min_points_per_cell = 3u;
+ server.bindGeometricClass(
+ extension, class_config, endpoint_range);
+}
+
+} // namespace wavemap::examples::signal_map::signal_class_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::signal_map::signal_class_map;
+ int discrete_block_height = 0;
+ private_nh.param("discrete_block_height", discrete_block_height, 0);
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::signal_map::makeMapConfigFor(
+ discrete_block_height),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed occupancy+reflectivity+signal+class benchmark "
+ "application (G) with discrete block height %d.",
+ discrete_block_height);
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_map_layers.h b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_map_layers.h
new file mode 100644
index 000000000..e06028016
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_map_layers.h
@@ -0,0 +1,86 @@
+#ifndef WAVEMAP_EXAMPLES_OUSTER_SIGNAL_MAP_LAYERS_H_
+#define WAVEMAP_EXAMPLES_OUSTER_SIGNAL_MAP_LAYERS_H_
+
+#include
+
+#include "reflectivity_map_layers.h"
+
+namespace wavemap::examples::signal_map {
+
+using ReflectivityLayer = reflectivity_map::ReflectivityLayer;
+using ClassLayer = reflectivity_map::ClassLayer;
+using SignalPolicy = layered::ExponentialScalarLayerUpdatePolicy<1, 5>;
+
+struct SignalLayer
+ : layered::schema::ContinuousLayer {
+ static constexpr layered::ScalarLayerVisualization visualization{
+ {0.02f, 0.04f, 0.20f}, {0.00f, 1.00f, 1.00f}};
+ static constexpr std::string_view name = "signal";
+};
+
+struct NearIrLayer
+ : layered::schema::ContinuousLayer {
+ static constexpr layered::ScalarLayerVisualization visualization{
+ {0.08f, 0.00f, 0.15f}, {1.00f, 0.85f, 0.00f}};
+ static constexpr std::string_view name = "near_ir";
+};
+
+// Occupancy remains built into every shared continuous voxel.
+using Schema =
+ layered::schema::LayerSchema;
+using NearIrSchema = layered::schema::LayerSchema<
+ ReflectivityLayer, SignalLayer, NearIrLayer>;
+using ClassSchema = layered::schema::LayerSchema<
+ ReflectivityLayer, SignalLayer, ClassLayer>;
+using NearIrClassSchema = layered::schema::LayerSchema<
+ ReflectivityLayer, SignalLayer, NearIrLayer, ClassLayer>;
+
+template
+using DefinitionFor =
+ layered::LayeredMapDefinition;
+using Definition = DefinitionFor;
+using NearIrDefinition = DefinitionFor;
+using ClassDefinition = DefinitionFor;
+using NearIrClassDefinition = DefinitionFor;
+
+template
+inline typename DefinitionT::Config makeMapConfigFor(
+ [[maybe_unused]] int discrete_block_height = 0) {
+ layered::LayeredMapConfigBuilder config;
+ config.map()
+ .minCellWidth(0.25f)
+ .occupancyLogOdds(-2.f, 4.f)
+ .treeHeight(7)
+ .onlyPruneBlocksIfUnusedFor(5.f);
+ config.occupancy().pruningScale(1e-3f).pruningWeight(1.f);
+ config.template layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ config.template layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ if constexpr (
+ DefinitionT::Schema::template containsContinuous) {
+ config.template layer()
+ .storageBounds(0.f, 1.f)
+ .pruningScale(1e-3f)
+ .pruningWeight(1.f);
+ }
+ if constexpr (
+ DefinitionT::Schema::template containsDiscrete) {
+ config.discreteBlockHeight(discrete_block_height);
+ }
+ config.combinedPruningThreshold(1.f);
+ return config.build();
+}
+
+inline Definition::Config makeMapConfig() {
+ return makeMapConfigFor();
+}
+
+} // namespace wavemap::examples::signal_map
+
+#endif // WAVEMAP_EXAMPLES_OUSTER_SIGNAL_MAP_LAYERS_H_
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_class_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_class_sensor_inputs.cc
new file mode 100644
index 000000000..839f1ea41
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_class_sensor_inputs.cc
@@ -0,0 +1,64 @@
+#include "signal_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::signal_map::near_ir_class_map {
+
+using Definition = NearIrClassDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ const layered::EndpointRange endpoint_range{0.5f, 25.f};
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f, endpoint_range);
+ server.bindNormalizedPointField(
+ extension, "intensity", 0.f, 65535.f, endpoint_range);
+ server.bindNormalizedPointField(
+ extension, "ambient", 0.f, 65535.f, endpoint_range);
+
+ layered::LocalElevationClassifierConfig class_config;
+ class_config.grid_cell_width = 0.5f;
+ class_config.ground_tolerance = 0.2f;
+ class_config.obstacle_height = 0.35f;
+ class_config.min_points_per_cell = 3u;
+ server.bindGeometricClass(
+ extension, class_config, endpoint_range, -kHalfPi, kHalfPi);
+}
+
+} // namespace wavemap::examples::signal_map::near_ir_class_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::signal_map::near_ir_class_map;
+ int discrete_block_height = 0;
+ private_nh.param("discrete_block_height", discrete_block_height, 0);
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::signal_map::makeMapConfigFor(
+ discrete_block_height),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed occupancy+reflectivity+signal+near_ir+class benchmark "
+ "application (H) with discrete block height %d.",
+ discrete_block_height);
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_sensor_inputs.cc
new file mode 100644
index 000000000..6186f445e
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_near_ir_sensor_inputs.cc
@@ -0,0 +1,50 @@
+#include "signal_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::signal_map::near_ir_map {
+
+using Definition = NearIrDefinition;
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ const layered::EndpointRange endpoint_range{0.5f, 25.f};
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f, endpoint_range);
+ server.bindNormalizedPointField(
+ extension, "intensity", 0.f, 65535.f, endpoint_range);
+ server.bindNormalizedPointField(
+ extension, "ambient", 0.f, 65535.f, endpoint_range);
+}
+
+} // namespace wavemap::examples::signal_map::near_ir_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::signal_map::near_ir_map;
+ map::ServerBuilder::install(
+ server, private_nh, server.worldFrame(),
+ wavemap::examples::signal_map::makeMapConfigFor(),
+ map::registerSensorInputs);
+ ROS_INFO("Installed occupancy+reflectivity+signal+near_ir benchmark application (F).");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_sensor_inputs.cc b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_sensor_inputs.cc
new file mode 100644
index 000000000..526d17a27
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/ouster_layered/signal_sensor_inputs.cc
@@ -0,0 +1,50 @@
+#include "signal_map_layers.h"
+
+#include
+
+#include
+#include
+
+namespace wavemap::examples::signal_map {
+
+using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+using ServerBuilder = LayeredRosServerBuilder;
+
+void registerSensorInputs(RosServer& server,
+ ServerBuilder::Extension& extension) {
+ const layered::EndpointRange endpoint_range{0.5f, 25.f};
+ server.bindNormalizedPointField(
+ extension, "reflectivity", 0.f, 255.f, endpoint_range);
+ // The bag's Ouster profile is SIG16. Preserve its full sensor-defined range
+ // in normalized storage rather than fitting bounds to this recording.
+ server.bindNormalizedPointField(
+ extension, "intensity", 0.f, 65535.f, endpoint_range);
+}
+
+} // namespace wavemap::examples::signal_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "wavemap_ros_server");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ ros::NodeHandle nh;
+ ros::NodeHandle nh_private("~");
+ wavemap::RosServer server(
+ nh, nh_private,
+ [](wavemap::RosServer& server, ros::NodeHandle private_nh) {
+ namespace map = wavemap::examples::signal_map;
+ map::ServerBuilder::install(server, private_nh, server.worldFrame(),
+ map::makeMapConfig(),
+ map::registerSensorInputs);
+ ROS_INFO(
+ "Installed occupancy+reflectivity+signal benchmark application "
+ "(E).");
+ });
+
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/app/synthetic_rgb_layered_map_demo.cc b/interfaces/ros1/wavemap_ros/app/synthetic_rgb_layered_map_demo.cc
new file mode 100644
index 000000000..800a9db68
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/app/synthetic_rgb_layered_map_demo.cc
@@ -0,0 +1,200 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include "ouster_layered/rgb_map_layers.h"
+
+namespace wavemap::examples::rgb_map {
+namespace {
+
+struct SceneVoxel {
+ Index3D index;
+ layered::Rgb color;
+};
+
+constexpr FloatingPoint kOccupiedLogOdds = 4.f;
+constexpr int kFloorCells = 52;
+constexpr int kPathHalfWidth = 3;
+
+const layered::Rgb kGrass{0.20f, 0.55f, 0.16f};
+const layered::Rgb kPath{0.46f, 0.27f, 0.12f};
+const layered::Rgb kTrunk{0.34f, 0.16f, 0.06f};
+
+void appendFloor(std::vector& scene) {
+ const int min_coordinate = -kFloorCells / 2;
+ const int max_coordinate = min_coordinate + kFloorCells;
+ scene.reserve(kFloorCells * kFloorCells);
+ for (int y = min_coordinate; y < max_coordinate; ++y) {
+ const int path_center =
+ static_cast(std::round(2.5 * std::sin(y / 9.0)));
+ for (int x = min_coordinate; x < max_coordinate; ++x) {
+ const bool is_path = std::abs(x - path_center) <= kPathHalfWidth;
+ scene.push_back({Index3D{x, y, 0}, is_path ? kPath : kGrass});
+ }
+ }
+}
+
+void appendTree(const Index2D& center, std::mt19937& generator,
+ std::vector& scene) {
+ for (int z = 1; z <= 8; ++z) {
+ for (int dx = 0; dx < 2; ++dx) {
+ for (int dy = 0; dy < 2; ++dy) {
+ scene.push_back(
+ {Index3D{center.x() + dx, center.y() + dy, z}, kTrunk});
+ }
+ }
+ }
+
+ std::uniform_real_distribution random_value(0.f, 1.f);
+ std::uniform_real_distribution green_variation(-0.10f, 0.10f);
+ std::unordered_set added_leaf_indices;
+ for (int dz = -2; dz <= 3; ++dz) {
+ for (int dx = -4; dx <= 5; ++dx) {
+ for (int dy = -4; dy <= 5; ++dy) {
+ const float nx = (static_cast(dx) - 0.5f) / 4.6f;
+ const float ny = (static_cast(dy) - 0.5f) / 4.6f;
+ const float nz = (static_cast(dz) - 0.5f) / 3.2f;
+ const float radius = nx * nx + ny * ny + nz * nz;
+ const float keep_probability =
+ std::clamp(1.25f - radius, 0.f, 0.92f);
+ if (random_value(generator) > keep_probability) {
+ continue;
+ }
+
+ const Index3D index{center.x() + dx, center.y() + dy, 9 + dz};
+ if (!added_leaf_indices.emplace(index).second) {
+ continue;
+ }
+ const float variation = green_variation(generator);
+ scene.push_back(
+ {index,
+ layered::Rgb{std::clamp(0.12f + 0.4f * variation, 0.f, 1.f),
+ std::clamp(0.48f + variation, 0.f, 1.f),
+ std::clamp(0.10f + 0.25f * variation, 0.f, 1.f)}});
+ }
+ }
+ }
+}
+
+std::vector makeScene() {
+ std::vector scene;
+ appendFloor(scene);
+
+ std::mt19937 generator(42u);
+ for (const Index2D& center : std::vector{
+ {-18, -17}, {-17, 14}, {-11, -4}, {12, -17}, {16, 13}, {10, 4}}) {
+ appendTree(center, generator, scene);
+ }
+ return scene;
+}
+
+} // namespace
+
+class SyntheticRgbLayeredMapDemo {
+ public:
+ SyntheticRgbLayeredMapDemo()
+ : private_nh_("~"), map_(makeMapConfig()), scene_(makeScene()) {
+ private_nh_.param("voxels_per_update", voxels_per_update_, 120);
+ private_nh_.param("update_period", update_period_, 0.08);
+ private_nh_.param("save_path", save_path_, std::string{});
+ voxels_per_update_ = std::max(1, voxels_per_update_);
+ update_period_ = std::max(0.01, update_period_);
+
+ publisher_ = private_nh_.advertise(
+ "layered_map", 1, true);
+ timer_ = private_nh_.createTimer(
+ ros::Duration(update_period_),
+ &SyntheticRgbLayeredMapDemo::insertNextVoxels, this);
+
+ ROS_INFO_STREAM("Synthetic RGB Layered Wavemap contains " << scene_.size()
+ << " voxels and will be revealed in batches of "
+ << voxels_per_update_ << ".");
+ }
+
+ private:
+ void insertNextVoxels(const ros::TimerEvent&) {
+ const size_t next_end = std::min(
+ next_voxel_ + static_cast(voxels_per_update_), scene_.size());
+ for (; next_voxel_ < next_end; ++next_voxel_) {
+ Definition::ContinuousLayers layers;
+ layers.get() = scene_[next_voxel_].color;
+ map_.continuousMap().setVoxelValue(
+ scene_[next_voxel_].index,
+ Definition::Voxel{kOccupiedLogOdds, layers});
+ }
+ publishMap();
+
+ if (next_voxel_ == scene_.size()) {
+ timer_.stop();
+ map_.continuousMap().threshold();
+ map_.continuousMap().prune();
+ publishMap();
+ saveMapIfRequested();
+ ROS_INFO("Synthetic RGB Layered Wavemap is complete.");
+ }
+ }
+
+ void publishMap() {
+ wavemap_msgs::LayeredMap message;
+ using RosConverter =
+ convert::DescriptorContinuousRosConverter;
+ if (!convert::layeredMapToRosMsg(
+ map_, "map", ros::Time::now(), message)) {
+ ROS_ERROR("Could not convert the synthetic RGB map to a ROS message.");
+ return;
+ }
+ publisher_.publish(message);
+ }
+
+ void saveMapIfRequested() {
+ if (save_path_.empty()) {
+ return;
+ }
+ const std::filesystem::path output_path(save_path_);
+ if (output_path.has_parent_path()) {
+ std::filesystem::create_directories(output_path.parent_path());
+ }
+ if (Definition::MapIo::save(output_path, map_)) {
+ ROS_INFO_STREAM("Saved synthetic RGB Layered Wavemap to "
+ << output_path);
+ } else {
+ ROS_ERROR_STREAM("Could not save synthetic RGB Layered Wavemap to "
+ << output_path);
+ }
+ }
+
+ ros::NodeHandle private_nh_;
+ Definition::Map map_;
+ std::vector scene_;
+ size_t next_voxel_ = 0u;
+ int voxels_per_update_ = 120;
+ double update_period_ = 0.08;
+ std::string save_path_;
+ ros::Publisher publisher_;
+ ros::Timer timer_;
+};
+
+} // namespace wavemap::examples::rgb_map
+
+int main(int argc, char** argv) {
+ ros::init(argc, argv, "synthetic_rgb_layered_map");
+ google::InitGoogleLogging(argv[0]);
+ google::InstallFailureSignalHandler();
+ FLAGS_alsologtostderr = true;
+ FLAGS_colorlogtostderr = true;
+
+ wavemap::examples::rgb_map::SyntheticRgbLayeredMapDemo demo;
+ ros::spin();
+ return 0;
+}
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_layered.yaml b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_layered.yaml
new file mode 100644
index 000000000..07fba6e37
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_layered.yaml
@@ -0,0 +1,43 @@
+general:
+ world_frame: "map"
+ logging_level: info
+ allow_reset_map_service: true
+
+# Layered map geometry and layer configuration are supplied by the selected
+# typed server executable.
+map_operations: []
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_chunked_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
+ enable_benchmark_metrics: true
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_layered.yaml b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_layered.yaml
new file mode 100644
index 000000000..56b0b7cc7
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_layered.yaml
@@ -0,0 +1,42 @@
+general:
+ world_frame: "map"
+ logging_level: debug
+ allow_reset_map_service: true
+
+# The layered map geometry is supplied by occupancy_map_layers.h.
+# Keep ROS serialization and periodic maintenance outside the integration timer.
+map_operations: []
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_chunked_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_original.yaml b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_original.yaml
new file mode 100644
index 000000000..b42a29b36
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_chunked_occupancy_original.yaml
@@ -0,0 +1,50 @@
+general:
+ world_frame: "map"
+ logging_level: info
+ allow_reset_map_service: true
+
+map:
+ type: hashed_chunked_wavelet_octree
+ min_cell_width: { meters: 0.25 }
+ min_log_odds: -2.0
+ max_log_odds: 4.0
+ tree_height: 7
+ only_prune_blocks_if_unused_for: { seconds: 5.0 }
+
+# Keep ROS serialization and periodic maintenance outside the integration timer.
+map_operations: []
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_chunked_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
+ enable_benchmark_metrics: true
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_layered.yaml b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_layered.yaml
new file mode 100644
index 000000000..ef08f851b
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_layered.yaml
@@ -0,0 +1,43 @@
+general:
+ world_frame: "map"
+ logging_level: info
+ allow_reset_map_service: true
+
+# Layered map geometry and layer configuration are supplied by the selected
+# typed server executable.
+map_operations: []
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
+ enable_benchmark_metrics: true
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_occupancy_original.yaml b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_occupancy_original.yaml
new file mode 100644
index 000000000..7af2180ba
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_hashed_occupancy_original.yaml
@@ -0,0 +1,50 @@
+general:
+ world_frame: "map"
+ logging_level: info
+ allow_reset_map_service: true
+
+map:
+ type: hashed_wavelet_octree
+ min_cell_width: { meters: 0.25 }
+ min_log_odds: -2.0
+ max_log_odds: 4.0
+ tree_height: 7
+ only_prune_blocks_if_unused_for: { seconds: 5.0 }
+
+# Keep ROS serialization and periodic maintenance outside the integration timer.
+map_operations: []
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
+ enable_benchmark_metrics: true
diff --git a/interfaces/ros1/wavemap_ros/config/benchmark_ouster_variants.csv b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_variants.csv
new file mode 100644
index 000000000..0a2e44b5e
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/benchmark_ouster_variants.csv
@@ -0,0 +1,19 @@
+variant,backend,layers,server_executable,param_file,output_directory,map_filename
+A,hashed,occupancy,ros_server,benchmark_ouster_hashed_occupancy_original.yaml,A_original,map.wvmp
+B,hashed,occupancy,occupancy_layered_server,benchmark_ouster_hashed_layered.yaml,B_layered_occupancy,map.lwvmp
+C,hashed,occupancy+reflectivity,reflectivity_layered_server,benchmark_ouster_hashed_layered.yaml,C_layered_reflectivity,map.lwvmp
+D,hashed,occupancy+reflectivity+class,reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class,map.lwvmp
+D_h1,hashed,occupancy+reflectivity+class (discrete block height 1),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h1,map.lwvmp
+D_h2,hashed,occupancy+reflectivity+class (discrete block height 2),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h2,map.lwvmp
+D_h3,hashed,occupancy+reflectivity+class (discrete block height 3),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h3,map.lwvmp
+D_h4,hashed,occupancy+reflectivity+class (discrete block height 4),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h4,map.lwvmp
+D_h5,hashed,occupancy+reflectivity+class (discrete block height 5),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h5,map.lwvmp
+D_h6,hashed,occupancy+reflectivity+class (discrete block height 6),reflectivity_class_layered_server,benchmark_ouster_hashed_layered.yaml,D_layered_reflectivity_class_h6,map.lwvmp
+E,hashed,occupancy+reflectivity+signal,signal_layered_server,benchmark_ouster_hashed_layered.yaml,E_layered_reflectivity_signal,map.lwvmp
+F,hashed,occupancy+reflectivity+signal+near_ir,signal_near_ir_layered_server,benchmark_ouster_hashed_layered.yaml,F_layered_reflectivity_signal_near_ir,map.lwvmp
+G,hashed,occupancy+reflectivity+signal+class,signal_class_layered_server,benchmark_ouster_hashed_layered.yaml,G_layered_reflectivity_signal_class,map.lwvmp
+H,hashed,occupancy+reflectivity+signal+near_ir+class,signal_near_ir_class_layered_server,benchmark_ouster_hashed_layered.yaml,H_layered_reflectivity_signal_near_ir_class,map.lwvmp
+chunked_A,chunked,occupancy,ros_server,benchmark_ouster_chunked_occupancy_original.yaml,chunked_A_original,map.wvmp
+chunked_B,chunked,occupancy,occupancy_chunked_layered_server,benchmark_ouster_chunked_layered.yaml,chunked_B_layered_occupancy,map.lwvmp
+chunked_C,chunked,occupancy+reflectivity,reflectivity_chunked_layered_server,benchmark_ouster_chunked_layered.yaml,chunked_C_layered_reflectivity,map.lwvmp
+chunked_D,chunked,occupancy+reflectivity+class,reflectivity_class_chunked_layered_server,benchmark_ouster_chunked_layered.yaml,chunked_D_layered_reflectivity_class,map.lwvmp
diff --git a/interfaces/ros1/wavemap_ros/config/rviz/layered_map.rviz b/interfaces/ros1/wavemap_ros/config/rviz/layered_map.rviz
new file mode 100644
index 000000000..1b46ef6a5
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/rviz/layered_map.rviz
@@ -0,0 +1,152 @@
+Panels:
+ - Class: rviz/Displays
+ Help Height: 138
+ Name: Displays
+ Property Tree Widget:
+ Expanded:
+ - /Global Options1
+ - /Status1
+ - /LayeredMap1
+ - /LayeredMap1/Render voxels1
+ - /LayeredMap1/Render voxels1/Layer color1
+ - /LayeredMap1/Render voxels1/Frame rate1
+ Splitter Ratio: 0.5
+ Tree Height: 1540
+ - Class: rviz/Selection
+ Name: Selection
+ - Class: rviz/Tool Properties
+ Expanded:
+ - /2D Pose Estimate1
+ - /2D Nav Goal1
+ - /Publish Point1
+ Name: Tool Properties
+ Splitter Ratio: 0.5886790156364441
+ - Class: rviz/Views
+ Expanded:
+ - /Current View1
+ Name: Views
+ Splitter Ratio: 0.5
+ - Class: rviz/Time
+ Name: Time
+ SyncMode: 0
+ SyncSource: ""
+Preferences:
+ PromptSaveOnExit: true
+Toolbars:
+ toolButtonStyle: 2
+Visualization Manager:
+ Class: ""
+ Displays:
+ - Alpha: 0.5
+ Cell Size: 1
+ Class: rviz/Grid
+ Color: 160; 160; 164
+ Enabled: true
+ Line Style:
+ Line Width: 0.029999999329447746
+ Value: Lines
+ Name: Grid
+ Normal Cell Count: 0
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Plane: XY
+ Plane Cell Count: 10
+ Reference Frame:
+ Value: true
+ - Class: wavemap_rviz_plugin/LayeredMap
+ Enabled: true
+ Name: LayeredMap
+ Queue Size: 10
+ Source: Topic
+ Render voxels:
+ Alpha: 1
+ Cell selector: Surface
+ Color mode: Height
+ Enable: true
+ Flat color: 0; 0; 255
+ Frame rate:
+ Max update time: 20
+ Layer: occupancy
+ Layer color:
+ Bool true color: 255; 12; 12
+ Scalar high color: 255; 255; 0
+ Scalar low color: 0; 0; 255
+ Scalar max: 0.2
+ Scalar min: 0
+ Show bool false: false
+ Log odds threshold: 0.0010000000474974513
+ Max log odds: 1e+06
+ Min log odds: 0.0010000000474974513
+ Termination height: 0
+ Unknown log odds: 9.999999747378752e-05
+ Topic: /wavemap/layered_map
+ Unreliable: false
+ Value: true
+ Enabled: true
+ Global Options:
+ Background Color: 48; 48; 48
+ Default Light: true
+ Fixed Frame: map
+ Frame Rate: 30
+ Name: root
+ Tools:
+ - Class: rviz/Interact
+ Hide Inactive Objects: true
+ - Class: rviz/MoveCamera
+ - Class: rviz/Select
+ - Class: rviz/FocusCamera
+ - Class: rviz/Measure
+ - Class: rviz/SetInitialPose
+ Theta std deviation: 0.2617993950843811
+ Topic: /initialpose
+ X std deviation: 0.5
+ Y std deviation: 0.5
+ - Class: rviz/SetGoal
+ Topic: /move_base_simple/goal
+ - Class: rviz/PublishPoint
+ Single click: true
+ Topic: /clicked_point
+ Value: true
+ Views:
+ Current:
+ Class: rviz/Orbit
+ Distance: 3.1961989402770996
+ Enable Stereo Rendering:
+ Stereo Eye Separation: 0.05999999865889549
+ Stereo Focal Distance: 1
+ Swap Stereo Eyes: false
+ Value: false
+ Field of View: 0.7853981852531433
+ Focal Point:
+ X: 0
+ Y: 0
+ Z: 0
+ Focal Shape Fixed Size: true
+ Focal Shape Size: 0.05000000074505806
+ Invert Z Axis: false
+ Name: Current View
+ Near Clip Distance: 0.009999999776482582
+ Pitch: 0.5203978419303894
+ Target Frame:
+ Yaw: 0.670401394367218
+ Saved: ~
+Window Geometry:
+ Displays:
+ collapsed: false
+ Height: 2022
+ Hide Left Dock: false
+ Hide Right Dock: false
+ QMainWindow State: 000000ff00000000fd000000040000000000000331000006f2fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b000000ab00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000abfb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000069000006f20000017800fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000014f000006f2fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a005600690065007700730100000069000006f20000012300fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e1000001970000000300000e7c00000056fc0100000002fb0000000800540069006d0065010000000000000e7c0000058200fffffffb0000000800540069006d00650100000000000004500000000000000000000009e4000006f200000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
+ Selection:
+ collapsed: false
+ Time:
+ collapsed: false
+ Tool Properties:
+ collapsed: false
+ Views:
+ collapsed: false
+ Width: 3708
+ X: 1860
+ Y: 64
diff --git a/interfaces/ros1/wavemap_ros/config/rviz/synthetic_rgb_layered_map.rviz b/interfaces/ros1/wavemap_ros/config/rviz/synthetic_rgb_layered_map.rviz
new file mode 100644
index 000000000..82e4b20c7
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/rviz/synthetic_rgb_layered_map.rviz
@@ -0,0 +1,106 @@
+Panels:
+ - Class: rviz/Displays
+ Name: Displays
+ Property Tree Widget:
+ Expanded:
+ - /Global Options1
+ - /RGB Layered Wavemap1
+ - /RGB Layered Wavemap1/Render voxels1
+ Splitter Ratio: 0.5
+ Tree Height: 650
+ - Class: rviz/Views
+ Name: Views
+Preferences:
+ PromptSaveOnExit: true
+Toolbars:
+ toolButtonStyle: 2
+Visualization Manager:
+ Class: ""
+ Displays:
+ - Alpha: 0.5
+ Cell Size: 1
+ Class: rviz/Grid
+ Color: 160; 160; 164
+ Enabled: true
+ Line Style:
+ Line Width: 0.03
+ Value: Lines
+ Name: Grid
+ Normal Cell Count: 0
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Plane: XY
+ Plane Cell Count: 40
+ Reference Frame:
+ Value: true
+ - Class: wavemap_rviz_plugin/LayeredMap
+ Enabled: true
+ Name: Layered Wavemap
+ Queue Size: 10
+ Source: Topic
+ Render voxels:
+ Alpha: 1
+ Cell selector: Band
+ Color mode: Flat
+ Enable: true
+ Flat color: 255; 255; 255
+ Frame rate:
+ Max update time: 20
+ Layer: rgb
+ Layer color:
+ Bool true color: 255; 12; 12
+ Scalar high color: 255; 255; 0
+ Scalar low color: 0; 0; 255
+ Scalar max: 1
+ Scalar min: 0
+ Show bool false: false
+ Max log odds: 1000000
+ Min log odds: 0.001
+ Termination height: 0
+ Unknown log odds: 0.0001
+ Topic: /synthetic_rgb_layered_map/layered_map
+ Unreliable: false
+ Value: true
+ Enabled: true
+ Global Options:
+ Background Color: 48; 48; 48
+ Default Light: true
+ Fixed Frame: map
+ Frame Rate: 30
+ Name: root
+ Tools:
+ - Class: rviz/Interact
+ Hide Inactive Objects: true
+ - Class: rviz/MoveCamera
+ - Class: rviz/Select
+ - Class: rviz/FocusCamera
+ - Class: rviz/Measure
+ Value: true
+ Views:
+ Current:
+ Class: rviz/Orbit
+ Distance: 20
+ Enable Stereo Rendering:
+ Stereo Eye Separation: 0.06
+ Stereo Focal Distance: 1
+ Swap Stereo Eyes: false
+ Value: false
+ Field of View: 0.785398
+ Focal Point:
+ X: 0
+ Y: 0
+ Z: 1
+ Focal Shape Fixed Size: true
+ Focal Shape Size: 0.05
+ Invert Z Axis: false
+ Name: Current View
+ Near Clip Distance: 0.01
+ Pitch: 0.65
+ Target Frame:
+ Yaw: 0.75
+ Saved: ~
+Window Geometry:
+ Height: 900
+ Width: 1440
diff --git a/interfaces/ros1/wavemap_ros/config/wavemap_ouster_all_layers_demo.yaml b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_all_layers_demo.yaml
new file mode 100644
index 000000000..cdbd78015
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_all_layers_demo.yaml
@@ -0,0 +1,51 @@
+general:
+ world_frame: "map"
+ logging_level: debug
+ allow_reset_map_service: true
+
+map_operations:
+ - type: threshold_map
+ once_every: { seconds: 2.0 }
+ - type: prune_map
+ once_every: { seconds: 10.0 }
+ - type: publish_map
+ once_every: { seconds: 0.5 }
+ max_num_blocks_per_msg: 50
+ topic: "layered_map"
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ # IG-LIO publishes one map-to-body pose per processed scan. This bag's
+ # Ouster t field is not usable as Wavemap-relative per-point time, so asking
+ # Wavemap for intermediate poses drops the cloud.
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
diff --git a/interfaces/ros1/wavemap_ros/config/wavemap_ouster_occupancy_only.yaml b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_occupancy_only.yaml
new file mode 100644
index 000000000..6e9791cfb
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_occupancy_only.yaml
@@ -0,0 +1,57 @@
+general:
+ world_frame: "map"
+ logging_level: debug
+ allow_reset_map_service: true
+
+# Original occupancy-only Wavemap data structure. No layered map is enabled.
+map:
+ type: hashed_wavelet_octree
+ min_cell_width: { meters: 0.25 }
+ min_log_odds: -2.0
+ max_log_odds: 4.0
+ tree_height: 7
+ only_prune_blocks_if_unused_for: { seconds: 5.0 }
+
+map_operations:
+ - type: threshold_map
+ once_every: { seconds: 2.0 }
+ - type: prune_map
+ once_every: { seconds: 10.0 }
+ - type: publish_map
+ once_every: { seconds: 0.5 }
+ max_num_blocks_per_msg: 50
+ topic: "map"
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
diff --git a/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_chunked_layered.yaml b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_chunked_layered.yaml
new file mode 100644
index 000000000..aee2b4188
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_chunked_layered.yaml
@@ -0,0 +1,48 @@
+general:
+ world_frame: "map"
+ logging_level: debug
+ allow_reset_map_service: true
+
+map_operations:
+ - type: threshold_map
+ once_every: { seconds: 2.0 }
+ - type: prune_map
+ once_every: { seconds: 10.0 }
+ - type: publish_map
+ once_every: { seconds: 0.5 }
+ max_num_blocks_per_msg: 50
+ topic: "layered_map"
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_chunked_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
diff --git a/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_layered.yaml b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_layered.yaml
new file mode 100644
index 000000000..cdbd78015
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/config/wavemap_ouster_reflectivity_layered.yaml
@@ -0,0 +1,51 @@
+general:
+ world_frame: "map"
+ logging_level: debug
+ allow_reset_map_service: true
+
+map_operations:
+ - type: threshold_map
+ once_every: { seconds: 2.0 }
+ - type: prune_map
+ once_every: { seconds: 10.0 }
+ - type: publish_map
+ once_every: { seconds: 0.5 }
+ max_num_blocks_per_msg: 50
+ topic: "layered_map"
+
+measurement_integrators:
+ ouster_os1:
+ projection_model:
+ type: ouster_projector
+ lidar_origin_to_beam_origin: { millimeters: 16.721 }
+ lidar_origin_to_sensor_origin_z_offset: { millimeters: 38.195 }
+ elevation:
+ num_cells: 128
+ min_angle: { degrees: -21.39 }
+ max_angle: { degrees: 21.25 }
+ azimuth:
+ num_cells: 1024
+ min_angle: { degrees: -180.0 }
+ max_angle: { degrees: 180.0 }
+ measurement_model:
+ type: continuous_beam
+ angle_sigma: { degrees: 0.1 }
+ range_sigma: { meters: 0.05 }
+ scaling_free: 0.2
+ scaling_occupied: 0.4
+ integration_method:
+ type: hashed_wavelet_integrator
+ min_range: { meters: 0.5 }
+ max_range: { meters: 25.0 }
+
+inputs:
+ - type: pointcloud_topic
+ topic_name: "/ouster/points"
+ topic_type: ouster
+ measurement_integrator_names: ouster_os1
+ # IG-LIO publishes one map-to-body pose per processed scan. This bag's
+ # Ouster t field is not usable as Wavemap-relative per-point time, so asking
+ # Wavemap for intermediate poses drops the cloud.
+ undistort_motion: false
+ topic_queue_length: 10
+ max_wait_for_pose: { seconds: 1.0 }
diff --git a/interfaces/ros1/wavemap_ros/include/wavemap_ros/inputs/pointcloud_endpoint_adapter.h b/interfaces/ros1/wavemap_ros/include/wavemap_ros/inputs/pointcloud_endpoint_adapter.h
new file mode 100644
index 000000000..f4a1aa556
--- /dev/null
+++ b/interfaces/ros1/wavemap_ros/include/wavemap_ros/inputs/pointcloud_endpoint_adapter.h
@@ -0,0 +1,347 @@
+#ifndef WAVEMAP_ROS_INPUTS_POINTCLOUD_ENDPOINT_ADAPTER_H_
+#define WAVEMAP_ROS_INPUTS_POINTCLOUD_ENDPOINT_ADAPTER_H_
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace wavemap {
+
+template
+struct PointFieldBinding {
+ std::string field_name;
+ FloatingPoint scale = 1.f;
+ FloatingPoint offset = 0.f;
+ ValueT min_value = std::numeric_limits::lowest();
+ ValueT max_value = std::numeric_limits::max();
+
+ explicit PointFieldBinding(
+ std::string field_name = {}, FloatingPoint scale = 1.f,
+ FloatingPoint offset = 0.f,
+ ValueT min_value = std::numeric_limits::lowest(),
+ ValueT max_value = std::numeric_limits::max())
+ : field_name(std::move(field_name)),
+ scale(scale),
+ offset(offset),
+ min_value(min_value),
+ max_value(max_value) {}
+
+ static PointFieldBinding normalized(std::string field_name,
+ FloatingPoint raw_min,
+ FloatingPoint raw_max) {
+ CHECK_LT(raw_min, raw_max);
+ const FloatingPoint scale = 1.f / (raw_max - raw_min);
+ return PointFieldBinding(std::move(field_name), scale,
+ -raw_min * scale, ValueT{0}, ValueT{1});
+ }
+};
+
+class DecodedPointcloudEndpointChannel {
+ public:
+ virtual ~DecodedPointcloudEndpointChannel() = default;
+ virtual bool integrate(
+ const PosedPointcloud<>& pointcloud,
+ const std::vector& sorted_points) const = 0;
+};
+
+// A message-local decoder. PointcloudTopicInput owns the outer point loop and
+// calls every prepared decoder once per point, so adding endpoint layers does
+// not add another traversal of the PointCloud2 message.
+class PointcloudEndpointDecoder {
+ public:
+ virtual ~PointcloudEndpointDecoder() = default;
+ virtual bool decodePoint(const sensor_msgs::PointCloud2& msg,
+ size_t point_offset) = 0;
+ virtual std::unique_ptr finish() = 0;
+};
+
+class PointcloudEndpointAdapter {
+ public:
+ virtual ~PointcloudEndpointAdapter() = default;
+ virtual std::unique_ptr prepare(
+ const sensor_msgs::PointCloud2& msg, size_t num_points) const = 0;
+};
+
+template
+class DecodedScalarEndpointChannel final
+ : public DecodedPointcloudEndpointChannel {
+ public:
+ using Callback =
+ std::function&, const std::vector&)>;
+
+ DecodedScalarEndpointChannel(std::vector values, Callback callback)
+ : values_(std::move(values)), callback_(std::move(callback)) {}
+
+ bool integrate(
+ const PosedPointcloud<>& pointcloud,
+ const std::vector& sorted_points)
+ const override {
+ if (values_.size() != pointcloud.size() ||
+ sorted_points.size() != pointcloud.size()) {
+ return false;
+ }
+ std::vector ordered_values(values_.size());
+ for (size_t index = 0u; index < sorted_points.size(); ++index) {
+ if (sorted_points[index].source_index >= values_.size()) {
+ return false;
+ }
+ ordered_values[index] = values_[sorted_points[index].source_index];
+ }
+ callback_(pointcloud, ordered_values);
+ return true;
+ }
+
+ private:
+ std::vector values_;
+ Callback callback_;
+};
+
+template
+class NumericPointcloudEndpointDecoder final
+ : public PointcloudEndpointDecoder {
+ public:
+ using Callback = typename DecodedScalarEndpointChannel::Callback;
+
+ NumericPointcloudEndpointDecoder(const sensor_msgs::PointField& field,
+ size_t num_points, FloatingPoint scale,
+ FloatingPoint offset, ValueT min_value,
+ ValueT max_value, Callback callback)
+ : field_(field),
+ scale_(scale),
+ offset_(offset),
+ min_value_(min_value),
+ max_value_(max_value),
+ callback_(std::move(callback)) {
+ values_.reserve(num_points);
+ }
+
+ bool decodePoint(const sensor_msgs::PointCloud2& msg,
+ size_t point_offset) override {
+ FloatingPoint raw = 0.f;
+ if (!convert::detail::readNumericPointField(msg, field_, point_offset,
+ raw) ||
+ !std::isfinite(raw)) {
+ return false;
+ }
+ const FloatingPoint converted = offset_ + scale_ * raw;
+ if (!std::isfinite(converted)) {
+ return false;
+ }
+ if constexpr (std::is_integral_v) {
+ if (converted != std::trunc(converted) ||
+ converted < static_cast(
+ std::numeric_limits::lowest()) ||
+ static_cast(std::numeric_limits::max()) <
+ converted) {
+ return false;
+ }
+ }
+ values_.emplace_back(
+ std::clamp(static_cast(converted), min_value_, max_value_));
+ return true;
+ }
+
+ std::unique_ptr finish() override {
+ return std::make_unique>(
+ std::move(values_), std::move(callback_));
+ }
+
+ private:
+ sensor_msgs::PointField field_;
+ FloatingPoint scale_;
+ FloatingPoint offset_;
+ ValueT min_value_;
+ ValueT max_value_;
+ Callback callback_;
+ std::vector