Skip to content

Representation improvements: unknown space, incremental updates, descriptors, links, compact maps - #35

Merged
fmrico merged 11 commits into
rollingfrom
representation_improvements
Oct 15, 2026
Merged

fmrico merged 11 commits into
rollingfrom
representation_improvements

Conversation

@fmrico

@fmrico fmrico commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Closes #34

One commit per improvement:

  • Iterators over NavCels: box, disc, ring, polygon, by distance and segment walk. Regions grow over adjacency, so they never jump between floors. The geodesic metric uses Lazy Theta*.
  • Unknown values, int16 layers and log-odds occupancy. Each layer may declare its unknown value. .navmap moves to format version 2; version 1 files load through a migration, with a deprecation warning, and are rewritten in place. RViz2 draws unknown cells translucent grey.
  • Deep copy of layers. Copies of a NavMap used to share their layer buffers with the original.
  • Change tracking and DistanceField. Layers record which cells changed. DistanceField updates distances exactly when seeds are added or removed.
  • Descriptors: slope, roughness, step and clearance, as F32 layers.
  • SurfaceLink: links between NavCels that do not share an edge. They travel in messages and files, and RViz2 draws them.
  • Geometry ids and sparse layer updates. Every NavMap/NavMapLayer message carries the geometry id. Updates for another geometry are rejected. Layer updates send only the changed cells when they are few. Also fixes a stale geometry fingerprint.
  • zlib compression of .navmap (zlib is available on every ROS distro and in the Python standard library). Playground maps shrink to 20–41 %.
  • combine_layer: the costmap_2d operators (overwrite, true overwrite, max, max keeping unknown, min, saturating add). It respects each layer's unknown value and can record provenance in a U8 layer. Cost values move to navmap_core/Costs.hpp.
  • Fix: rays through a shared edge missed both NavCels. locate_navcel could then return the floor above.
  • simplify and navmap_simplify: a mesh simplification that keeps every layer value, the borders and the topology. Measured results:
    • 2D maps at 5 cm drop to 8–13 % of their NavCels, and the files become comparable to the .pgm;
    • the parking garage drops to 4–11 %, staying within millimetres of the original surface.

Tests cover each feature, including corrupt files, migration of version 1 files and the edge cases of each operator.

Merge order: EasyNavigation/easynav_plugins#122 must be merged first. With the edge fix, one obstacle-filter test (a point exactly on a grid vertex) marks a neighbouring NavCel.

fmrico added 11 commits October 15, 2026 09:05
navmap_core/Iterators.hpp: box (every surface, via the BVH), disc, ring,
polygon, distance order and segment walk over the NavCels. Regions around a
point grow from the NavCel under it through the mesh adjacency, so they never
jump to another floor. Distances are Euclidean (to the centroid) or geodesic
(Lazy Theta*: exact on planes, bends around borders). A reusable
TraversalScratch avoids allocations. Tests against brute force and a table in
navmap_bench.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
- Each layer can declare the value meaning "not observed" (NaN allowed):
  NavMap::set_layer_unknown() / is_unknown(). The unknown value, description
  and unit now travel in NavMapLayer and in the file; from_occupancy_grid
  declares NO_INFORMATION as the occupancy layer's unknown value.
- int16 layers (LayerType::I16, NavMapLayer::I16 / data_i16).
- navmap_core/Occupancy.hpp: log-odds occupancy with OctoMap's clamped update,
  in fixed point over an int16 layer, and a free / occupied / unknown state.
- .navmap file format version 2. Version 1 files (and files without header)
  load through frozen NavMapV1 / NavMapLayerV1 messages, with a deprecation
  warning, and are rewritten in place when writable. resample.py reads them
  too.
- RViz2: int16 layers (log-odds as occupancy greys), and cells holding a
  layer's unknown value in translucent grey; U8 layers unchanged.
- Fixes: add_layer() ignored the default value of a new layer, and integer
  conversions clamped negative values to 0.
- Example 04_unknown_and_occupancy with its launch file and RViz2 config.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
The copy constructor and the full-copy assignment copied the layer registry,
which holds shared pointers, so a copy shared every layer buffer with the
original: a write on either side reached the other (EasyNav filters copy the
map in and out of NavState every cycle). LayerRegistry copies are now deep,
through LayerViewBase::clone(). Moves are unchanged.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
- Layers record the NavCels changed since the last clear_changes(): single
  writes (layer_set, OccupancyLayer, mark_changed) once each, bulk writes
  (set_data, layer_clear, copies, messages, a new layer) as the whole layer.
- navmap_core/DistanceField.hpp: distance to seed cells and open edges, kept
  up to date from the seeds added and removed. With a finite max_distance it
  is exact (each seed paints the NavCels it reaches through adjacency within
  that distance; removing one repaints the seeds within twice the distance,
  found through a spatial index), and an update gives the same field as a
  full recomputation. changed() lists the NavCels whose distance changed.
- point_segment_distance moved to Geometry.hpp; resize_all handles int16.
- navmap_bench: full and incremental distance field timings.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
navmap_core/Descriptors.hpp: compute_descriptors() adds F32 layers slope
(deg), roughness (RMS distance to the plane fitted in a disc, m), step (height
span in a small disc, m) and clearance (free height to the next surface of the
map above, by an upward ray through the BVH; +inf if none). Discs grow over
the mesh adjacency, so floors never mix. They describe the surface only; a
planner decides how to weigh them.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
SurfaceLink (from, to, type, length, bidirectional) records connections such
as stairs, elevators or doors between NavCels that do not share an edge:
NavMap::add_link / remove_link / clear_links / links() / links_from(), with a
lazily rebuilt index. The map only records them; a planner decides whether a
robot can take one. Copies and assignments carry them. They travel in the
new NavMapLink[] links field of NavMap (file format version 2; version 1 files
have none), invalid ones are dropped on load, and RViz2 draws them coloured by
type (Draw Links).

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
- NavMap::geometry_id(): the geometry fingerprint, now also recomputed after
  add_vertex(), add_navcel() and rebuild_geometry_accels() (it went stale
  after a first query).
- NavMap and NavMapLayer messages carry geometry_id; NavMapLayer can carry
  indices for a sparse update (values for those NavCels only).
- navmap_ros::to_layer_update_msg(): sparse from the layer's change log when
  few NavCels changed, the full layer otherwise. from_msg rejects layer
  messages for another geometry and malformed sparse updates; RViz2 ignores
  the former and patches its cached layer with the latter.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
The version 2 envelope records the codec and both sizes. Files are
zlib-compressed by default (SaveOptions::compression, level 1-9);
playground maps shrink to 20-41 %. zlib is available on every ROS
distro and platform, and in the Python standard library, which
resample.py uses to read and write the same format. Corrupt or
truncated files are rejected.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
combine_layer() composes a source layer into a destination with the
costmap_2d operators (overwrite, true overwrite, max, max keeping the
unknown, min, saturating add). Each layer's declared unknown value is
respected, types may differ, the work can be limited to the changed
cells, and an optional U8 layer records which source set each cell.
The cost values move to navmap_core/Costs.hpp (navmap_ros keeps the
names). README: combining layers, and the save/load section now shows
the real API.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
The barycentric tolerance of ray_triangle_intersect was absolute
(1e-8), below float rounding for NavCels of a few decimetres or
more. A vertical ray through the edge between two NavCels could miss
both, and locate_navcel then fell back to the floor above. The
tolerance is now relative (1e-5).

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
simplify() merges NavCels by half-edge collapses, cheapest quadric
error first. A vertex goes only if all its NavCels share surface and
layer values, so every layer keeps its value at every point. Open edges
keep their shape, the topology is checked (link condition, no folds),
link ends survive, and quality, normal change and edge length are
bounded. Layers, surfaces, links and colors are carried over.

navmap_simplify simplifies a .navmap file. Playground maps drop to
21-23 % of their NavCels (occupancy grids) and 4-11 % (parking garage),
staying within millimetres of the original surface.

Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Representation improvements: unknown space, incremental updates, descriptors, links, compact maps

1 participant