# Case study: filter a Point Cloud faster [Point Cloud Library (PCL)](https://pointclouds.org/) is a very popular library used in robotics, autonomous vehicles and 3D perception in general. Its contribution to this fields is **huge**. ![](img/pcl.jpg) It is a behemoth with more than [12K commits and 5K Github stars](https://github.com/PointCloudLibrary/pcl). With about [400 contributors](https://github.com/PointCloudLibrary/pcl/graphs/contributors) you would think that it is hard to find opportunities for optimization in plain sight, even more inside the most frequently used functions. **Me**: Muahahahaha (Evil laugh) ## The ConditionalRemoval filter I bet that anyone that has been playing around with PCL has used [pcl::ConditionalRemoval](https://pcl-tutorials.readthedocs.io/en/latest/remove_outliers.html) at least once. From the [official tutorial](https://pcl-tutorials.readthedocs.io/en/latest/remove_outliers.html): ```C++ // slightly changed for clarity using namespace pcl; auto range_cond = std::make_shared (); range_cond->addComparison ( std::make_shared("z", ComparisonOps::GT, 0.0)); range_cond->addComparison ( std::make_shared("z", ComparisonOps::LT, 1.0))); // build the filter ConditionalRemoval condition_removal; condition_removal.setCondition (range_cond); condition_removal.setInputCloud (input_cloud); // apply filter condition_removal.filter (*cloud_filtered); ``` > Basically, we create the condition which a given point must satisfy for it to remain in our PointCloud. > In this example, we use add two comparisons to the condition: greater than (GT) 0.0 and less than (LT) 1.0. > This condition is then used to build the filter. Let me rephrase it for people that are not familiar with PCL: - An object is created that says: "the Z value of the point must be greater than 0.0". - Another object is created saying: "the Z value of the point must be less than 1.0". - They are both added to a `ConditionAnd`. - We tell `ConditionalRemoval` to use this combined condition. - Apply the filter to an input cloud to create a filtered one. Now think that the usal Point Cloud has a number of points in the order of **tens of thousands** or more. Think about it: ![](img/think_about_it.jpg) Seriously, take some minutes to think how, given a **vector** of points that looks like this: ```C++ // oversimplified, not the actual implementation struct PointXYZ{ float x; float y; float z; }; ``` You want to create another point cloud with all the points that pass this condition: 0.0 < point.z < 1.0 I mean, if you ask **me**, this is what I would do, because I am not that smart: ```C++ auto cloud_filtered = std::make_shared>(); for (const auto& point: input_cloud->points) { if( point.z > 0.0 && point.z < 1.0 ) { cloud_filtered->push_back( point ); } } ``` This is what we will call the **"naive filter"**. Before showing you a benchmark that will knock your socks off (don't worry, I will), I must admit that it is an **unfair comparison** because the `pcl` filters do many more checks when processing your data to prevent weird corner cases. But do not forget that we expressed our conditions like this: ```C++ pcl::FieldComparison ("z", pcl::ComparisonOps::GT, 0.0))); pcl::FieldComparison ("z", pcl::ComparisonOps::LT, 1.0))); ``` If you think about it, there **must be** some kind of parser "somewhere". The easiest implementation of a parser is of course a `switch` statement, but no one would ever do that for **each** of these trillion points... [Oh, snap!](https://github.com/PointCloudLibrary/pcl/blob/pcl-1.11.0/filters/include/pcl/filters/impl/conditional_removal.hpp#L98-L127) Indeed, 2 switch statements called for each point of the cloud. Summarizing: the very fact that these function tries to be "too clever" using these "composable rules", means that the implementation is **inherently slow**. There is nothing we can do to save them. Nevertheles, we can replace them ;) ## Davide, give me speed AND expressive code. Sure thing, my friend! Since `pcl::FieldComparison` is intrinsically broken (so are all the other Conditions in the library), because of their `switch`statements, let me write my own pcl::Condition (must be derived from `pcl::ConditionBase`) like this: ```C++ template class GenericCondition : public pcl::ConditionBase { public: typedef std::shared_ptr> Ptr; typedef std::shared_ptr> ConstPtr; typedef std::function FunctorT; GenericCondition(FunctorT evaluator): pcl::ConditionBase(),_evaluator( evaluator ) {} virtual bool evaluate (const PointT &point) const { // just delegate ALL the work to the injected std::function return _evaluator(point); } private: FunctorT _evaluator; }; ``` That is literally **all** the code you need, no omissions. I am simply wrapping a `std::function` inside `pcl::ConditionBase`. Nothing else. This is the **old** code: ```C++ auto range_cond = std::make_shared (); range_cond->addComparison ( std::make_shared("z", ComparisonOps::GT, 0.0)); range_cond->addComparison ( std::make_shared("z", ComparisonOps::LT, 1.0))); ``` And this is the **new** one, where my condition is expressed in plain old code: ```C++ auto range_cond = std::make_shared>( [](const PointXYZ& point){ return point.z > 0.0 && point.z < 1.0; }); ``` The rest of the code is unchanged!!! ![](img/beautiful.jpg) ## Let's talk about speed You may find the code to replicate my tests [here](https://github.com/facontidavide/CPP_Optimizations_Diary/tree/master/cpp/pcl_conditional_removal.cpp). These are the benchmarks based on my sample cloud and 4 filters (upper and lower bound in X and Y): ``` ------------------------------------------------------------- Benchmark Time CPU Iterations ------------------------------------------------------------- PCL_Filter 1403083 ns 1403084 ns 498 Naive_Filter 107418 ns 107417 ns 6586 PCL_Filter_Generic 668223 ns 668191 ns 1069 ``` Your results may change a lot according to the number of conditions and the size of the point cloud. But the lessons to learn are: - The "naive" filter might be an option in many cases and it is blazing fast. - The "safe" `pcl::ConditionalRemoval` can still be used if you just ditch the builtin `pcl::Conditions` and use instead the much more concise and readable `GenericCondition`.