Skip to content

Latest commit

 

History

History
339 lines (278 loc) · 13.8 KB

docusage.md

File metadata and controls

339 lines (278 loc) · 13.8 KB
@page docusage Occupancy map usage

Occupancy map usage

This section details general usage of an OccupancyMap including map generation and data access. The page is split into sections for CPU map access and GPU map generation.

CPU map usage

The OccupancyMap class is used to contain the occupancy map data. This class is always used at some level regardless of how the map is generated or populated. For example, the GpuMap wraps an OccupancyMap to support populating the map in GPU, but the underlying map object is the same.

The data within the map are stored in "chunks" (MapChunk) which represent contiguous blocks of voxel memory corresponding to fixed 3D regions of the map. Chunks are created as needed. This dense memory layout is what makes GPU update of the map possible. The additional overhead of using a dense memory layout is mitigated by using background compression (VoxelBlockCompressionQueue) of voxel data when not in use.

The map object stores the map data as well as the parametrisation of the map - for example the occupancy threshold (OccupancyMap::occupancyThresholdProbability()) and resolution (OccupancyMap::resolution()). The map object is also responsible for mapping between global coordinates and voxel keys (Key) via the OccupancyMap::voxelKey() function.

A map is created with a fixed resolution and fixed chunk size by specifying the voxel extents on each axis for a chunk. The following code creates an empty occupancy map at a resolution of 0.25 units per voxel and 32x32x32 voxels in each chunk.

ohm::OccupancyMap map(0.25, glm::u8vec3(32));

Data can be added to the map by using a RayMapper. Note that map object does not feature any methods for adding data to the map. The simplest mapper implementation is the RayMapperOccupancy. Using this mapper, origin/sample pairs (rays) are added to the map with the sample voxel occupancy increased and the occupancy for voxels along the ray decreased. This is shown in the code snipped below.

// A virtual utility class used in the examples to provide data for an occupancy map.
class DataProvider
{
public:
  virtual ~DataProvider() = default;
  // Get the next batch of rays to add to the map.
  // @param rays Populated with the next batch of origin/sample pairs (rays) to add to the map.
  // @return True while there are more rays to add.
  virtual bool nextBatch(std::vector<glm::dvec3> &rays) = 0;
};

void populateMap(DataProvider &provider)
{
  ohm::OccupancyMap map(0.25);          // Create a map
  ohm::RayMapperOccupancy mapper(&map); // Create a mapper for the map.
  std::vector<glm::dvec3> rays;         // Ray set storage.

  // While data are available.
  while (provider.nextBatch(rays))
  {
    // Add data to the map.
    mapper.integrateRays(rays.data(), rays.size());
  }
}

Data can then be queried about individual voxels using a combination of voxel keys and Voxel objects. The code below queries whether the voxel containing a given spatial position is occupied.

bool isVoxelAtPositionOccupied(const ohm::OccupancyMap &map, const glm::dvec3 &position)
{
  // First create a Voxel object which will reference the voxel occupancy data.
  // - We declare the template type as const float specifying that we expect voxel data to be a float per voxel.
  // - We use the map layout to resolve the map layer which contains occupancy data (more on this below).
  Voxel<const float> voxel_occupancy(&map, map.layout().occupancyLayer());

  // Validate that the Voxel is referencing a valid layer.
  if (!voxel_occupancy.isLayerValid())
  {
    // Layer is invalid - there is no occupancy layer.
    return false;
  }

  // Next resolve a key for the voxel containing position.
  const ohm::Key key = map.voxelKey(position);

  // Set the key for the Voxel object to reference.
  voxel_occupancy.setKey(key);

  // Ensure the key reference is valid within the map. An invalid voxel indicates the voxel has not been observed.
  if (!voxel_occupancy.isValid())
  {
    return false;
  }

  // Get the voxel occupancy value.
  float occupancy_value;
  // We use a read function to address C++ memory access standards.
  voxel_occupancy.read(&occupancy_value);

  // Finally check if the value is above the occupancy threshold.
  return occupancy_value >= map.occupancyThresholdValue();
}

The example above introduces several concepts including the map layout (MapLayout). The example demonstrates how to validate and access the data for a particular voxel layer (MapLayer), in this case the occupancy layer. Note that there are convenience functions for this available in the voxeloccupancy section.

The MapLayout

An OccupancyMap has an associated MapLayout which specifies the data available in the map and how that data are laid out. The MapLayout specifies a set of MapLayer objects and each layer identifies some data associated with each voxel. That is, each voxel may have multiple data types associated with it and for each data type the MapChunk stores a contiguous memory allocation for that data.

By default, an OccupancyMap contains a float layer which stores the occupancy value for each voxel. Maps constructed with the kVoxelMean flag also contain a layer which tracks a subvoxel position which represents an approximate mean value of all samples which have contributed to that voxel. This layer adds a VoxelMean structure for each voxel. The NdtMap also adds a covariance layer (CovarianceVoxel). Additional user data may also be added using the MapLayout and MapLayer API.

Voxel data should generally be accessed using the Voxel template class. This class is designed to handle referencing a particular voxel layer and to validate the data size against the voxel layer size. The Voxel template type is used to specify both the data type and read only vs read/write access.

For example, the code below shows how to access the VoxelMean data including some invalid access patterns.

void meanExample(ohm::OccupancyMap &map)
{
  // Manually resolve the voxel layer index. This is also available in ohm::MapLayout::meanLayer() .
  // Resolve by name. This name is also available as ohm::default_layer::meanLayerName() .
  const MapLayer *layer = map.layout().layer("mean");

  // Validate layer.
  if (!layer)
  {
    return;
  }

  // Create a Voxel object for read/write access to the layer.
  Voxel<VoxelMean> mean_rw(&map, layer->layerIndex());
  // Create a Voxel object for read only access. Note the template type is const
  Voxel<const VoxelMean> mean_read(&map, layer->layerIndex());

  // Query the voxel at the origin. This cannot create the voxel chunk.
  mean_read.setKey(map.voxelKey(glm::dvec3(0)));
  if (mean_read.isValid())
  {
    // read and report the number of points contributing to the mean.
    VoxelMean mean;
    mean_read.read(&mean);
    std::cout << "The voxel at the origin contains " << mean.count << " samples" << std::endl;
  }
  else
  {
    std::cout << "The voxel at the origin has not been created" << std::endl;
  }

  // We copy the mean_rw key from the mean_read object. This can be more efficient in tight loops
  // as some data lookups can be skipped.
  // This call can create the MapChunk for the key, whereas mean_read.setKey() could not.
  mean_rw.setKey(mean_read);
  // The validity check can only fail if mean_rw.isLayerValid() is false. Conversely, it will always be valid so long as
  // the map and layer references are valid.
  if (mean_rw.isValid())
  {
    // Read and report the number of points contributing to the mean.
    VoxelMean mean;
    mean_rw.read(&mean);
    std::cout << "The voxel at the origin contains " << mean.count << " samples" << std::endl;
    // Reset the number of samples to zero.
    mean.count = 0;
    mean_rw.write(mean);
  }
}

NDT map

The NdtMap is an extension of the OccupancyMap which adds normal distribution transforms semantics. This adds a covariance representation to each voxel which can be used to represent a "surfel" within the voxel. See CovarianceVoxel for more details. The NdtMap should always be populated (in CPU) using the RayMapperNdt.

Iterating a map

Once a map has been populated, it is possible to iterate the voxels in the map using either a using range based for loop over the map or using a OccupancyMap::iterator. In either case, this will iterate the chunks (MapChunk) in the map in an undefined order and iterate each voxel within the chunk. Iteration of voxels within a chunk starts from the chunk's MapChunk::firstValidKey() which is maintained as the first voxel in the chunk memory which has been touched.

Iterating with a range based for loop or dereferencing the OccupancyMap::iterator yields a Key for the current voxel. The data associated with the voxel must be resolved using the Voxel template class. The OccupancyMap::iterator has additional, non-standard iterator functions which provide access to the target MapChunk and OccupancyMap. Below is an example of iterating a map.

// A structure detailing some map statistics
struct MapStats
{
  // Smallest point count for an occupied voxel.
  unsigned min_point_count{0};
  // Largest point count for an occupied voxel.
  unsigned max_point_count{0};
  // Total number of samples contributing to the map.
  uint_64 total_sample_count{0};
  // Average point count for an occupied voxel.
  unsigned average_point_count{0};
  // Number of occupied voxels.
  unsigned occupied_voxel_count{0};
  // Number of free voxels.
  unsigned free_voxel_count{0};
};

MapStats collectMapStats(const ohm::OccupancyMap &map)
{
  MapStats stats;

  // Setup voxel data.
  ohm::Voxel<const float> occupancy(&map, map.layout().occupancyLayer());
  ohm::Voxel<ohm::VoxelMean> mean(&map, map.layout().meanLayer());

  ohm::VoxelMean mean_value;

  for (auto iter = map.begin(); iter != map.end(); ++iter)
  {
    // Set the key for all voxel data accessors in one call.
    // Note: passing the iterator to setVoxelKey() instead of the key is more efficient as the MapChunk can be copied
    // from the iterator. A range based for loop will miss this minor efficiency gain.
    ohm::setVoxelKey(iter, occupancy, mean);
    if (occupancy.isValid()) // Should always be true - we are only iterating known voxels.
    {
      if (ohm::isOccupied(occupancy))
      {
        // Increment occupied voxels.
        ++stats.occupied_voxel_count;
        mean.read(&mean_value);

        stats.total_sample_count += mean_value.count;
        stats.min_point_count =
          (stats.occupied_voxel_count > 1) ? std::min(stats.min_point_count, mean_value.count) : mean_value.count;
        stats.max_point_count = std::max(stats.min_point_count, mean_value.count);
      }
      else if (ohm::isFree(voxel))
      {
        ++stats.free_voxel_count;
      }
    }
  }

  // Finalise average.
  if (stats.occupied_voxel_count)
  {
    stats.average_point_count = unsigned(stats.total_sample_count / stats.occupied_voxel_count);
  }

  return stats;
}

GPU map

GPU support is implemented in the ohmgpucuda and ohmgpuocl libraries, using CUDA and OpenCL respectively. While these are technically optional libraries, they are the focus of the ohm innovation. These libraries have the same API backed by the associated GPU SDK. The SDK selection is forced at compile time and cannot be switched at runtime.

The ohm GPU API introduces the GpuMap class, which is both a wrapper for an OccupancyMap and the RayMapper implementation which should be used to update the map. The code below shows how to use the GpuMap to populate an OccupancyMap.

void populateGpuMap(DataProvider &provider)
{
  ohm::OccupancyMap map(0.1);   // Create a map
  ohm::GpuMap gpu_map(&map);    // Create a GPU map
  std::vector<glm::dvec3> rays; // Ray set storage.

  // While data are available.
  while (provider.nextBatch(rays))
  {
    // Add data to the map.
    gpu_map.integrateRays(rays.data(), rays.size());
  }

  // Synchronise GPU data back to the CPU map memory.
  gpu_map.syncVoxels();
}

Note the call to gpu_map.syncVoxels(). The GpuMap does not automatically sync GPU changes back to CPU memory. This call ensures data synchronisation and allows read access to the map on CPU following this call.

The GpuMap relies on a GpuCache which keeps a copy of relevant MapChunk data in GPU memory. This cache keeps recently accessed chunks in GPU memory and will move data back to CPU memory once the cache is full and new chunks need to be modified. Changes in CPU will mark the chunk as dirty and the GpuCache will upload the updated data from CPU, however there is no resolution mechanism for merging simultaneous changes on CPU and GPU.

For optimal performance the number of rays given to each GpuMap::integrateRays() call may need to be tuned to the current platform. Batch sizes of 2048 or 4096 are recommended. Small batch sizes will be slower than performing the same update in CPU.

Note that the GpuMap implementation will update the VoxelMean for maps which have a VoxelMean layer.

GPU NDT Map

The GpuNdtMap extends the GpuMap adding the NDT semantics to the GPU update. This GpuNdtMap must be used in place of the GpuMap object if NDT semantics are required. The NDT update is notably more expensive than the base GPU update.

void populateGpuNdtMap(DataProvider &provider)
{
  ohm::OccupancyMap map(0.1);   // Create a map
  ohm::GpuNdtMap ndt_map(&map); // Create GPU map with NDT support
  std::vector<glm::dvec3> rays; // Ray set storage.

  // While data are available.
  while (provider.nextBatch(rays))
  {
    // Add data to the map.
    gpu_map.integrateRays(rays.data(), rays.size());
  }

  // Synchronise GPU data back to the CPU map memory.
  gpu_map.syncVoxels();
}