Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion docs/source/mp2p_icp_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -919,4 +919,80 @@ Filter: `FilterVoxelSlice`
.. rubric:: Before → After Screenshot

.. image:: voxel_slice_example.png
:alt: Screenshot showing point cloud before and after applying FilterVoxelSlice
:alt: Screenshot showing point cloud before and after applying FilterVoxelSlice

|

---

Filter: ``FilterVoxelSOR``
---------------------------

**Purpose:** Remove outlier points from point clouds using a voxel-based Statistical Outlier Removal (SOR) algorithm.
This filter partitions the point cloud into voxels and performs localized outlier detection within each voxel,
making it significantly faster than global SOR methods for large-scale point clouds while maintaining accuracy.

**How it works:** The algorithm divides the input point cloud into cubic voxels of a specified size. Within each voxel,
it computes the average distance from each point to its k-nearest neighbors. Points whose average distance exceeds
a threshold (mean + std_dev_mul × standard deviation) within their local voxel are classified as outliers.
This localized approach dramatically reduces computational cost by limiting KD-tree searches to small subsets of points.

**Key parameters:**

- ``voxel_size``: Size of each voxel cube in meters (default: 2.0). Larger voxels process more points together but may miss local outliers.
- ``mean_k``: Number of nearest neighbors to analyze for each point (default: 20). Higher values provide more robust statistics but increase computation time.
- ``std_dev_mul``: Standard deviation multiplier threshold (default: 2.0). Lower values remove more points; higher values are more permissive.
- ``input_layer``: Source point cloud layer to filter (default: ``raw``).
- ``output_layer_inliers``: Destination layer for filtered inlier points.
- ``output_layer_outliers``: Optional destination layer for detected outlier points (useful for debugging).
- ``use_tsl_robin_map``: Use optimized hash map for voxel storage (default: true, recommended for performance).

**Use cases:**

- Removing sensor noise and spurious measurements from lidar scans
- Cleaning point clouds before map building or registration
- Preprocessing for downstream algorithms sensitive to outliers
- Real-time filtering where computational efficiency is critical

.. dropdown:: YAML configuration example
:icon: code

.. code-block:: yaml

class_name: mp2p_icp_filters::FilterVoxelSOR
params:
input_layer: 'raw'
output_layer_inliers: 'filtered'
output_layer_outliers: 'outliers' # optional
voxel_size: 2.0 # meters
mean_k: 20 # neighbors to consider
std_dev_mul: 2.0 # threshold sensitivity
use_tsl_robin_map: true # performance optimization

.. dropdown:: C++ API example
:icon: code

.. code-block:: cpp

#include <mp2p_icp_filters/FilterVoxelSOR.h>

// Create and configure filter
auto filter = mp2p_icp_filters::FilterVoxelSOR::Create();
filter->params.input_layer = "raw";
filter->params.output_layer_inliers = "clean";
filter->params.voxel_size = 1.5f;
filter->params.mean_k = 25;
filter->params.std_dev_mul = 2.5;

// Apply filter to metric map
mp2p_icp::metric_map_t map;
// ... populate map with point cloud data ...
filter->filter(map);

// Access filtered results
auto clean_cloud = map.point_layer("clean");


**Performance notes:** The voxel-based approach provides substantial speedup over global SOR, especially for large point clouds (>1M points).
Typical speedup factors range from 5-20× depending on point cloud density and voxel size. The ``use_tsl_robin_map`` option enables
further optimization using a high-performance hash map implementation.
2 changes: 2 additions & 0 deletions mp2p_icp_filters/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ set(LIB_SRCS
src/FilterRenameLayer.cpp
src/FilterSOR.cpp
src/FilterVoxelSlice.cpp
src/FilterVoxelSOR.cpp
src/Generator.cpp
src/GeneratorEdgesFromCurvature.cpp
src/GeneratorEdgesFromRangeImage.cpp
Expand Down Expand Up @@ -63,6 +64,7 @@ set(LIB_PUBLIC_HDRS
include/mp2p_icp_filters/FilterRenameLayer.h
include/mp2p_icp_filters/FilterSOR.h
include/mp2p_icp_filters/FilterVoxelSlice.h
include/mp2p_icp_filters/FilterVoxelSOR.h
include/mp2p_icp_filters/Generator.h
include/mp2p_icp_filters/GeneratorEdgesFromCurvature.h
include/mp2p_icp_filters/GeneratorEdgesFromRangeImage.h
Expand Down
2 changes: 1 addition & 1 deletion mp2p_icp_filters/include/mp2p_icp_filters/FilterSOR.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class FilterSOR : public mp2p_icp_filters::FilterBase
DEFINE_MRPT_OBJECT(FilterSOR, mp2p_icp_filters)

public:
FilterSOR();
FilterSOR() = default;

void filter(mp2p_icp::metric_map_t& inOut) const override;

Expand Down
89 changes: 89 additions & 0 deletions mp2p_icp_filters/include/mp2p_icp_filters/FilterVoxelSOR.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* _
_ __ ___ ___ | | __ _
| '_ ` _ \ / _ \| |/ _` | Modular Optimization framework for
| | | | | | (_) | | (_| | Localization and mApping (MOLA)
|_| |_| |_|\___/|_|\__,_| https://github.com/MOLAorg/mola

A repertory of multi primitive-to-primitive (MP2P) ICP algorithms
and map building tools. mp2p_icp is part of MOLA.

Copyright (C) 2018-2026 Jose Luis Blanco, University of Almeria,
and individual contributors.
SPDX-License-Identifier: BSD-3-Clause
*/

/**
* @file FilterVoxelSOR.h
* @brief Voxel-based Statistical Outlier Removal (SOR) filter.
* @author Jose Luis Blanco Claraco
* @date Jan 21, 2026
*/
Comment thread
jlblancoc marked this conversation as resolved.

#pragma once

#include <mp2p_icp/metricmap.h>
#include <mp2p_icp_filters/FilterBase.h>
#include <mrpt/maps/CPointsMap.h>

namespace mp2p_icp_filters
{
/**
* @brief Voxel-based Statistical Outlier Removal (SOR) filter.
*
* This filter partitions the point cloud into voxels. Inside each voxel, it
* computes the average distance of each point to its k-nearest neighbors.
* Points are considered outliers if their average distance is significantly
* higher than the mean of the distances within that specific voxel.
*
* This is much faster than global SOR for large clouds as the KD-tree searches
* are localized and restricted to a small subset of points.
*
* When TBB is available, voxel processing is automatically parallelized for
* improved performance on multi-core systems.
*
* \ingroup mp2p_icp_filters_grp
*/
class FilterVoxelSOR : public mp2p_icp_filters::FilterBase
{
DEFINE_MRPT_OBJECT(FilterVoxelSOR, mp2p_icp_filters)
public:
FilterVoxelSOR();

// See docs in FilterBase
void filter(mp2p_icp::metric_map_t& inOut) const override;

struct Parameters
{
void load_from_yaml(const mrpt::containers::yaml& c);

std::string input_layer = mp2p_icp::metric_map_t::PT_LAYER_RAW;
std::string output_layer_inliers;
std::string output_layer_outliers;

/** Size of the voxel for local processing [meters]. */
float voxel_size = 2.0f;

/** Number of neighbors to analyze for each point locally. */
uint32_t mean_k = 20;

/** Standard deviation multiplier threshold. */
double std_dev_mul = 2.0;

/** Whether to use tsl::robin_map for voxel storage (faster in general) */
bool use_tsl_robin_map = true;

/** Grain size for TBB parallelization (number of voxels per thread block).
* Larger values reduce overhead but may cause load imbalance.
* Only used if TBB is available at build time.
*/
size_t parallelization_grain_size = 10;
};

/** Algorithm parameters */
Parameters params;

protected:
void initialize_filter(const mrpt::containers::yaml& c) override;
};

} // namespace mp2p_icp_filters
4 changes: 3 additions & 1 deletion mp2p_icp_filters/src/FilterBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ FilterPipeline mp2p_icp_filters::filter_pipeline_from_yaml(

const auto sClass = e.at("class_name").as<std::string>();
auto o = mrpt::rtti::classFactory(sClass);
ASSERT_(o);
ASSERTMSG_(
o,
mrpt::format("Cannot create object of class `%s`, is it registered?", sClass.c_str()));

auto f = std::dynamic_pointer_cast<FilterBase>(o);
ASSERTMSG_(
Expand Down
24 changes: 17 additions & 7 deletions mp2p_icp_filters/src/FilterSOR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,6 @@ IMPLEMENTS_MRPT_OBJECT(FilterSOR, mp2p_icp_filters::FilterBase, mp2p_icp_filters

using namespace mp2p_icp_filters;

FilterSOR::FilterSOR()
{
#if MRPT_VERSION < 0x020f04
throw std::runtime_error("FilterSOR requires MRPT >= 2.15.4");
#endif
}

void FilterSOR::Parameters::load_from_yaml(const mrpt::containers::yaml& c)
{
MCP_LOAD_REQ(c, input_pointcloud_layer);
Expand All @@ -62,6 +55,10 @@ void FilterSOR::initialize_filter(const mrpt::containers::yaml& c)

void FilterSOR::filter(mp2p_icp::metric_map_t& inOut) const
{
#if MRPT_VERSION < 0x020f04
throw std::runtime_error("FilterSOR requires MRPT >= 2.15.4");
#endif
Comment thread
jlblancoc marked this conversation as resolved.

auto pcPtr = inOut.layer<mrpt::maps::CPointsMap>(params.input_pointcloud_layer);
ASSERT_(pcPtr);
const auto& pc = *pcPtr;
Expand Down Expand Up @@ -159,49 +156,62 @@ void FilterSOR::filter(mp2p_icp::metric_map_t& inOut) const
mrpt::math::meanAndStd(avg_distances, mean_dist, std_dev);
const double threshold = mean_dist + params.std_dev_mul * std_dev;

size_t num_inliers = 0;
size_t num_outliers = 0;

// 4. Pass 2: Dispatch points to output layers
for (size_t i = 0; i < N; ++i)
{
const bool isInlier = (avg_distances[i] <= threshold);
if (isInlier)
{
#if MRPT_VERSION >= 0x020f03 // 2.15.3
++num_inliers;
if (outInliers)
{
outInliers->insertPointFrom(i, *ctxI);
}
}
else
{
++num_outliers;
if (outOutliers)
{
outOutliers->insertPointFrom(i, *ctxO);
}
#elif MRPT_VERSION >= 0x020f00 // 2.15.0
++num_inliers;
if (outInliers)
{
outInliers->insertPointFrom(pc, i, *ctxI);
}
}
else
{
++num_outliers;
if (outOutliers)
{
outOutliers->insertPointFrom(pc, i, *ctxO);
}
#else
++num_inliers;
if (outInliers)
{
outInliers->insertPointFrom(pc, i);
}
}
else
{
++num_outliers;
if (outOutliers)
{
outOutliers->insertPointFrom(pc, i);
}
#endif
}
}

// Debug stats:
MRPT_LOG_DEBUG_FMT(
"FilterSOR: Total points: %zu, Inliers: %zu, Outliers: %zu", N, num_inliers, num_outliers);
}
Loading