Skip to content

Commit c262ec6

Browse files
authored
[filters] Add a filter accepting a functor to reduce boiler plate code for simple filters (#3890)
* Proposed lambda filter * move traits to common header * Adding tests * Make test more deterministic * Using multiple seeds * Using Typed Test * For older gtest * Adding more macros for older gtest
1 parent ba5ad23 commit c262ec6

File tree

6 files changed

+358
-2
lines changed

6 files changed

+358
-2
lines changed

common/include/pcl/type_traits.h

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141

4242
#include <cstddef> // for std::size_t
4343
#include <cstdint> // for std::uint8_t
44-
#include <string> // for std::string
44+
45+
#include <functional> // for std::function, needed till C++17
46+
#include <string> // for std::string
4547
#include <type_traits> // for std::false_type, std::true_type
4648

4749
namespace pcl
@@ -260,5 +262,33 @@ namespace pcl
260262
template <typename T> struct has_custom_allocator<T, void_t<typename T::_custom_allocator_type_trait>> : std::true_type {};
261263

262264
#endif
263-
}
264265

266+
/**
267+
* \todo: Remove in C++17
268+
*/
269+
#ifndef __cpp_lib_is_invocable
270+
// Implementation taken from: https://stackoverflow.com/a/51188325
271+
template <typename F, typename... Args>
272+
constexpr bool is_invocable_v =
273+
std::is_constructible<std::function<void(Args...)>,
274+
std::reference_wrapper<std::remove_reference_t<F>>>::value;
275+
276+
template <typename R, typename F, typename... Args>
277+
constexpr bool is_invocable_r_v =
278+
std::is_constructible<std::function<R(Args...)>,
279+
std::reference_wrapper<std::remove_reference_t<F>>>::value;
280+
#else
281+
using std::is_invocable_v;
282+
using std::is_invocable_r_v;
283+
#endif
284+
285+
/**
286+
* \todo: Remove in C++17
287+
*/
288+
#ifndef __cpp_lib_remove_cvref
289+
template <typename T>
290+
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
291+
#else
292+
using std::remove_cvref_t;
293+
#endif
294+
}

filters/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ set(incs
5858
"include/pcl/${SUBSYS_NAME}/extract_indices.h"
5959
"include/pcl/${SUBSYS_NAME}/filter.h"
6060
"include/pcl/${SUBSYS_NAME}/filter_indices.h"
61+
"include/pcl/${SUBSYS_NAME}/functor_filter.h"
6162
"include/pcl/${SUBSYS_NAME}/passthrough.h"
6263
"include/pcl/${SUBSYS_NAME}/shadowpoints.h"
6364
"include/pcl/${SUBSYS_NAME}/project_inliers.h"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* SPDX-License-Identifier: BSD-3-Clause
3+
*
4+
* Point Cloud Library (PCL) - www.pointclouds.org
5+
* Copyright (c) 2020-, Open Perception
6+
*
7+
* All rights reserved
8+
*/
9+
10+
#pragma once
11+
12+
#include <pcl/filters/filter_indices.h>
13+
#include <pcl/type_traits.h> // for is_invocable
14+
15+
namespace pcl {
16+
template <typename PointT, typename Function>
17+
constexpr static bool is_functor_for_filter_v =
18+
pcl::is_invocable_r_v<bool,
19+
Function,
20+
const pcl::remove_cvref_t<pcl::PointCloud<PointT>>&,
21+
pcl::index_t>;
22+
23+
/**
24+
* \brief Filter point clouds and indices based on a functor passed in the ctor
25+
* \ingroup filters
26+
*/
27+
template <typename PointT, typename Functor>
28+
class FunctorFilter : public FilterIndices<PointT> {
29+
using Base = FilterIndices<PointT>;
30+
using PCLBase = pcl::PCLBase<PointT>;
31+
32+
public:
33+
using FunctorT = Functor;
34+
// using in type would complicate signature
35+
static_assert(is_functor_for_filter_v<PointT, FunctorT>,
36+
"Functor signature must be similar to `bool(const PointCloud<PointT>&, "
37+
"index_t)`");
38+
39+
protected:
40+
using Base::extract_removed_indices_;
41+
using Base::filter_name_;
42+
using Base::negative_;
43+
using Base::removed_indices_;
44+
using PCLBase::indices_;
45+
using PCLBase::input_;
46+
47+
// need to hold a value because functors can only be copy or move constructed
48+
FunctorT functor_;
49+
50+
public:
51+
/** \brief Constructor.
52+
* \param[in] extract_removed_indices Set to true if you want to be able to
53+
* extract the indices of points being removed (default = false).
54+
*/
55+
FunctorFilter(FunctorT functor, bool extract_removed_indices = false)
56+
: Base(extract_removed_indices), functor_(std::move(functor))
57+
{
58+
filter_name_ = "functor_filter";
59+
}
60+
61+
const FunctorT&
62+
getFunctor() const noexcept
63+
{
64+
return functor_;
65+
}
66+
67+
FunctorT&
68+
getFunctor() noexcept
69+
{
70+
return functor_;
71+
}
72+
73+
/**
74+
* \brief Filtered results are indexed by an indices array.
75+
* \param[out] indices The resultant indices.
76+
*/
77+
void
78+
applyFilter(Indices& indices) override
79+
{
80+
indices.clear();
81+
indices.reserve(input_->size());
82+
if (extract_removed_indices_) {
83+
removed_indices_->clear();
84+
removed_indices_->reserve(input_->size());
85+
}
86+
87+
for (const auto index : *indices_) {
88+
// functor returns true for points that should be selected
89+
if (negative_ != functor_(*input_, index)) {
90+
indices.push_back(index);
91+
}
92+
else if (extract_removed_indices_) {
93+
removed_indices_->push_back(index);
94+
}
95+
}
96+
}
97+
};
98+
} // namespace pcl

test/filters/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ PCL_ADD_TEST(filters_morphological test_morphological
2121
FILES test_morphological.cpp
2222
LINK_WITH pcl_gtest pcl_common pcl_filters)
2323

24+
PCL_ADD_TEST(filters_functor test_filters_functor
25+
FILES test_functor_filter.cpp
26+
LINK_WITH pcl_gtest pcl_common pcl_filters)
27+
2428
PCL_ADD_TEST(filters_local_maximum test_filters_local_maximum
2529
FILES test_local_maximum.cpp
2630
LINK_WITH pcl_gtest pcl_common pcl_filters pcl_search pcl_octree)

test/filters/test_functor_filter.cpp

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* SPDX-License-Identifier: BSD-3-Clause
3+
*
4+
* Point CLoud Library (PCL) - www.pointclouds.org
5+
* Copyright (c) 2020-, Open Perception
6+
*
7+
* All rights reserved
8+
*/
9+
10+
#include <pcl/common/generate.h>
11+
#include <pcl/filters/functor_filter.h>
12+
#include <pcl/test/gtest.h>
13+
#include <pcl/point_types.h>
14+
15+
#include <random>
16+
17+
using namespace pcl;
18+
19+
TEST(FunctorFilterTrait, CheckCompatibility)
20+
{
21+
const auto copy_all = [](PointCloud<PointXYZ>, index_t) { return 0; };
22+
EXPECT_TRUE((is_functor_for_filter_v<PointXYZ, decltype(copy_all)>));
23+
24+
const auto ref_all = [](PointCloud<PointXYZ>&, index_t&) { return 0; };
25+
EXPECT_FALSE((is_functor_for_filter_v<PointXYZ, decltype(ref_all)>));
26+
27+
const auto ref_cloud = [](PointCloud<PointXYZ>&, index_t) { return 0; };
28+
EXPECT_FALSE((is_functor_for_filter_v<PointXYZ, decltype(ref_cloud)>));
29+
30+
const auto const_ref_cloud = [](const PointCloud<PointXYZ>&, index_t) { return 0; };
31+
EXPECT_TRUE((is_functor_for_filter_v<PointXYZ, decltype(const_ref_cloud)>));
32+
33+
const auto const_ref_all = [](const PointCloud<PointXYZ>&, const index_t&) {
34+
return 0;
35+
};
36+
EXPECT_TRUE((is_functor_for_filter_v<PointXYZ, decltype(const_ref_all)>));
37+
}
38+
39+
struct FunctorFilterRandom : public testing::TestWithParam<std::uint32_t> {
40+
void
41+
SetUp() override
42+
{
43+
cloud = make_shared<PointCloud<PointXYZ>>();
44+
45+
std::uint32_t seed = GetParam();
46+
common::CloudGenerator<PointXYZ, common::UniformGenerator<float>> generator{
47+
{-10., 0., seed}};
48+
generator.fill(20, 20, negative_cloud);
49+
generator.setParameters({0., 10., seed});
50+
generator.fill(10, 10, positive_cloud);
51+
*cloud = negative_cloud + positive_cloud;
52+
}
53+
54+
shared_ptr<PointCloud<PointXYZ>> cloud;
55+
PointCloud<PointXYZ> out_cloud, negative_cloud, positive_cloud;
56+
};
57+
58+
TEST_P(FunctorFilterRandom, functioning)
59+
{
60+
61+
const auto lambda = [](const PointCloud<PointXYZ>& cloud, index_t idx) {
62+
const auto& pt = cloud[idx];
63+
return (pt.getArray3fMap() < 0).all();
64+
};
65+
66+
for (const auto& keep_removed : {true, false}) {
67+
FunctorFilter<PointXYZ, decltype(lambda)> filter{lambda, keep_removed};
68+
filter.setInputCloud(cloud);
69+
const auto removed_size = filter.getRemovedIndices()->size();
70+
71+
filter.setNegative(false);
72+
filter.filter(out_cloud);
73+
74+
EXPECT_EQ(out_cloud.size(), negative_cloud.size());
75+
if (keep_removed) {
76+
EXPECT_EQ(filter.getRemovedIndices()->size(), positive_cloud.size());
77+
}
78+
else {
79+
EXPECT_EQ(filter.getRemovedIndices()->size(), removed_size);
80+
}
81+
82+
filter.setNegative(true);
83+
filter.filter(out_cloud);
84+
85+
EXPECT_EQ(out_cloud.size(), positive_cloud.size());
86+
if (keep_removed) {
87+
EXPECT_EQ(filter.getRemovedIndices()->size(), negative_cloud.size());
88+
}
89+
else {
90+
EXPECT_EQ(filter.getRemovedIndices()->size(), removed_size);
91+
}
92+
}
93+
}
94+
95+
INSTANTIATE_TEST_SUITE_P(RandomSeed,
96+
FunctorFilterRandom,
97+
testing::Values(123, 456, 789));
98+
99+
namespace type_test {
100+
int
101+
free_func(const PointCloud<PointXYZ>&, const index_t& idx)
102+
{
103+
return idx % 2;
104+
}
105+
106+
static const auto lambda_func = [](const PointCloud<PointXYZ>& cloud, index_t idx) {
107+
return free_func(cloud, idx);
108+
};
109+
110+
struct StaticFunctor {
111+
static int
112+
functor(PointCloud<PointXYZ> cloud, index_t idx)
113+
{
114+
return free_func(cloud, idx);
115+
}
116+
int
117+
operator()(PointCloud<PointXYZ> cloud, index_t idx)
118+
{
119+
return free_func(cloud, idx);
120+
}
121+
};
122+
struct StaticFunctorConst {
123+
int
124+
operator()(PointCloud<PointXYZ> cloud, index_t idx) const
125+
{
126+
return free_func(cloud, idx);
127+
}
128+
};
129+
130+
using LambdaT = decltype(lambda_func);
131+
using StdFunctorBoolT = std::function<bool(PointCloud<PointXYZ>, index_t)>;
132+
using StdFunctorIntT = std::function<int(PointCloud<PointXYZ>, index_t)>;
133+
using FreeFuncT = decltype(free_func)*;
134+
using StaticFunctorT = decltype(StaticFunctor::functor)*;
135+
using NonConstFuntorT = StaticFunctor;
136+
using ConstFuntorT = StaticFunctorConst;
137+
138+
template <typename FunctorT>
139+
struct Helper {};
140+
141+
#define HELPER_MACRO(TYPE, VALUE) \
142+
template <> \
143+
struct Helper<TYPE> { \
144+
using type = TYPE; \
145+
static type value; \
146+
}; \
147+
TYPE Helper<TYPE>::value = VALUE
148+
149+
HELPER_MACRO(LambdaT, lambda_func);
150+
HELPER_MACRO(StdFunctorBoolT, lambda_func);
151+
HELPER_MACRO(StdFunctorIntT, lambda_func);
152+
HELPER_MACRO(FreeFuncT, free_func);
153+
HELPER_MACRO(StaticFunctorT, StaticFunctor::functor);
154+
HELPER_MACRO(NonConstFuntorT, {});
155+
HELPER_MACRO(ConstFuntorT, {});
156+
157+
#undef HELPER_MACRO
158+
159+
using types = ::testing::Types<LambdaT,
160+
StdFunctorBoolT,
161+
StdFunctorIntT,
162+
FreeFuncT,
163+
StaticFunctorT,
164+
NonConstFuntorT,
165+
ConstFuntorT>;
166+
} // namespace type_test
167+
168+
template <typename T>
169+
struct FunctorFilterFunctor : public ::testing::Test {
170+
void
171+
SetUp() override
172+
{
173+
cloud.resize(2);
174+
}
175+
PointCloud<PointXYZ> cloud;
176+
};
177+
TYPED_TEST_SUITE_P(FunctorFilterFunctor);
178+
179+
TYPED_TEST_P(FunctorFilterFunctor, type_check)
180+
{
181+
using FunctorT = TypeParam;
182+
const auto& functor = type_test::Helper<FunctorT>::value;
183+
184+
FunctorFilter<PointXYZ, FunctorT> filter_lambda{functor};
185+
EXPECT_EQ(filter_lambda.getFunctor()(this->cloud, 0), 0);
186+
EXPECT_EQ(filter_lambda.getFunctor()(this->cloud, 1), 1);
187+
}
188+
189+
REGISTER_TYPED_TEST_SUITE_P(FunctorFilterFunctor, type_check);
190+
INSTANTIATE_TYPED_TEST_SUITE_P(pcl, FunctorFilterFunctor, type_test::types);
191+
192+
int
193+
main(int argc, char** argv)
194+
{
195+
testing::InitGoogleTest(&argc, argv);
196+
return RUN_ALL_TESTS();
197+
}

test/include/pcl/test/gtest.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@
5454
#define TYPED_TEST_SUITE TYPED_TEST_CASE
5555
#endif
5656

57+
/**
58+
* \brief Macro choose between TYPED_TEST_CASE_P and TYPED_TEST_SUITE_P depending on the GTest version
59+
*
60+
* \ingroup test
61+
*/
62+
#if !defined(TYPED_TEST_SUITE_P)
63+
#define TYPED_TEST_SUITE_P TYPED_TEST_CASE_P
64+
#endif
65+
5766
/**
5867
* \brief Macro choose between INSTANTIATE_TEST_CASE_P and INSTANTIATE_TEST_SUITE_P depending on the GTest version
5968
*
@@ -63,3 +72,20 @@
6372
#define INSTANTIATE_TEST_SUITE_P INSTANTIATE_TEST_CASE_P
6473
#endif
6574

75+
/**
76+
* \brief Macro choose between INSTANTIATE_TYPED_TEST_CASE_P and INSTANTIATE_TYPED_TEST_SUITE_P depending on the GTest version
77+
*
78+
* \ingroup test
79+
*/
80+
#if !defined(INSTANTIATE_TYPED_TEST_SUITE_P)
81+
#define INSTANTIATE_TYPED_TEST_SUITE_P INSTANTIATE_TYPED_TEST_CASE_P
82+
#endif
83+
84+
/**
85+
* \brief Macro choose between REGISTER_TYPED_TEST_CASE_P and REGISTER_TYPED_TEST_SUITE_P depending on the GTest version
86+
*
87+
* \ingroup test
88+
*/
89+
#if !defined(REGISTER_TYPED_TEST_SUITE_P)
90+
#define REGISTER_TYPED_TEST_SUITE_P REGISTER_TYPED_TEST_CASE_P
91+
#endif

0 commit comments

Comments
 (0)