diff --git a/CMakeLists.txt b/CMakeLists.txt
index 494d9723528425a5413069441c067276bcd6efa0..a79cf08d167397002ade8cbd28a2b5440ccbcb68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -46,6 +46,10 @@ armarx_find_package(PUBLIC RVO QUIET)
 armarx_find_package(PUBLIC teb_local_planner QUIET)
 armarx_find_package(PUBLIC teb_extension QUIET)
 
+# laser scanner feature extraction
+armarx_find_package(PUBLIC RobotComponents QUIET)
+armarx_find_package(PUBLIC PCL QUIET COMPONENTS io common)
+
 add_subdirectory(etc)
 
 # FetchContent_Declare(
diff --git a/source/armarx/navigation/components/CMakeLists.txt b/source/armarx/navigation/components/CMakeLists.txt
index efa6ec80e5a10ff6f1a4667c6c4792b29ef71f7f..e65951f6cdada3248f25aea846787eed6840de31 100644
--- a/source/armarx/navigation/components/CMakeLists.txt
+++ b/source/armarx/navigation/components/CMakeLists.txt
@@ -21,3 +21,5 @@ add_subdirectory(dynamic_scene_provider)
 add_subdirectory(human_simulator)
 
 add_subdirectory(navigation_skill_provider)
+
+add_subdirectory(laser_scanner_feature_extraction)
diff --git a/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.cpp b/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.cpp
index ab0d58102f6d3514940cabac0de4e8bea7d22f4f..5bab4b0deb3d568fde424ff1c32b6dc0bc47fdc2 100644
--- a/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.cpp
+++ b/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.cpp
@@ -35,13 +35,12 @@
 #include <RobotAPI/interface/ArViz/Elements.h>
 #include <RobotAPI/libraries/armem_robot/types.h>
 #include <RobotAPI/libraries/armem_robot_state/client/common/RobotReader.h>
-#include <RobotAPI/libraries/armem_vision/client/laser_scanner_features/Reader.h>
 
-#include <armarx/navigation/util/geometry.h>
 #include <armarx/navigation/algorithms/Costmap.h>
 #include <armarx/navigation/algorithms/spfa/ShortestPathFasterAlgorithm.h>
 #include <armarx/navigation/conversions/eigen.h>
 #include <armarx/navigation/memory/client/costmap/Reader.h>
+#include <armarx/navigation/util/geometry.h>
 
 // Include headers you only need in function definitions in the .cpp.
 
@@ -55,7 +54,6 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
 
     const std::string Component::defaultName = "dynamic_distance_to_obstacle_costmap_provider";
 
-
     armarx::PropertyDefinitionsPtr
     Component::createPropertyDefinitions()
     {
@@ -97,7 +95,6 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         return def;
     }
 
-
     Component::Component() :
         robotReader(memoryNameSystem()),
         costmapReader(memoryNameSystem()),
@@ -116,7 +113,6 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         // setDebugObserverBatchModeEnabled(true);
     }
 
-
     void
     Component::onConnectComponent()
     {
@@ -191,11 +187,8 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         // return false;
     }
 
-
-
-
     void
-    fillCostmap(const std::vector<armem::vision::LaserScannerFeatures>& features,
+    fillCostmap(const std::vector<memory::LaserScannerFeatures>& features,
                 algorithms::Costmap& costmap)
     {
 
@@ -257,7 +250,6 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         }
     }
 
-
     void
     Component::drawCostmap(const armarx::navigation::algorithms::Costmap& costmap,
                            const std::string& name,
@@ -325,7 +317,7 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
 
         const auto timestamp = armarx::core::time::Clock::Now();
 
-        const armem::vision::laser_scanner_features::client::Reader::Query query{
+        const memory::client::laser_scanner_features::Reader::Query query{
             .providerName = properties.laserScannerFeatures.providerName,
             .name = properties.laserScannerFeatures.name,
             .timestamp = timestamp};
@@ -405,33 +397,28 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         return navigationPlanningCostmap;
     }
 
-
     void
     Component::onDisconnectComponent()
     {
     }
 
-
     void
     Component::onExitComponent()
     {
     }
 
-
     std::string
     Component::getDefaultName() const
     {
         return Component::defaultName;
     }
 
-
     std::string
     Component::GetDefaultName()
     {
         return Component::defaultName;
     }
 
-
     ARMARX_REGISTER_COMPONENT_EXECUTABLE(Component, Component::GetDefaultName());
 
 } // namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_provider
diff --git a/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.h b/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.h
index 1837d8ac9cf76718fef5978213a5786235a50a08..92179ba82dc43eb2f989f8ea6cc92bae11307b4c 100644
--- a/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.h
+++ b/source/armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/Component.h
@@ -23,19 +23,18 @@
 
 #pragma once
 
-#include <ArmarXCore/core/services/tasks/PeriodicTask.h>
 #include <ArmarXCore/core/Component.h>
+#include <ArmarXCore/core/services/tasks/PeriodicTask.h>
 
-#include <RobotAPI/libraries/armem/client/forward_declarations.h>
-#include <RobotAPI/libraries/armem_robot_state/client/common/RobotReader.h>
-#include <RobotAPI/libraries/armem_vision/client/laser_scanner_features/Reader.h>
 #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
 #include <RobotAPI/libraries/armem/client.h>
+#include <RobotAPI/libraries/armem/client/forward_declarations.h>
+#include <RobotAPI/libraries/armem_robot_state/client/common/RobotReader.h>
 
+#include <armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/ComponentInterface.h>
 #include <armarx/navigation/memory/client/costmap/Reader.h>
 #include <armarx/navigation/memory/client/costmap/Writer.h>
-#include <armarx/navigation/components/dynamic_distance_to_obstacle_costmap_provider/ComponentInterface.h>
-
+#include <armarx/navigation/memory/client/laser_scanner_features/Reader.h>
 
 namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_provider
 {
@@ -114,7 +113,6 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
 
         static const std::string defaultName;
 
-
         // Private member variables go here.
 
 
@@ -141,6 +139,7 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
 
             int updatePeriodMs = 100;
         };
+
         Properties properties;
         /* Use a mutex if you access variables from different threads
          * (e.g. ice functions and RemoteGui_update()).
@@ -172,7 +171,7 @@ namespace armarx::navigation::components::dynamic_distance_to_obstacle_costmap_p
         memory::client::costmap::Reader costmapReader;
         memory::client::costmap::Writer costmapWriter;
 
-        armem::vision::laser_scanner_features::client::Reader laserScannerFeaturesReader;
+        memory::client::laser_scanner_features::Reader laserScannerFeaturesReader;
 
         std::optional<algorithms::Costmap> staticCostmap;
 
diff --git a/source/armarx/navigation/components/dynamic_scene_provider/Component.h b/source/armarx/navigation/components/dynamic_scene_provider/Component.h
index 11e0d65404a2c24d6179fe6327ee3e7b32fcc5c6..c70b0f6678d7822438197cf88e312bfe990758a5 100644
--- a/source/armarx/navigation/components/dynamic_scene_provider/Component.h
+++ b/source/armarx/navigation/components/dynamic_scene_provider/Component.h
@@ -34,10 +34,11 @@
 #include "RobotAPI/libraries/armem/client/plugins/PluginUser.h"
 #include "RobotAPI/libraries/armem/client/plugins/ReaderWriterPlugin.h"
 #include "RobotAPI/libraries/armem_robot_state/client/common/VirtualRobotReader.h"
-#include "RobotAPI/libraries/armem_vision/client/laser_scanner_features/Reader.h"
 
 #include "VisionX/libraries/armem_human/client/HumanPoseReader.h"
 
+#include <armarx/navigation/memory/client/laser_scanner_features/Reader.h>
+
 // #include <ArmarXCore/libraries/ArmarXCoreComponentPlugins/DebugObserverComponentPlugin.h>
 
 // #include <ArmarXGui/libraries/ArmarXGuiComponentPlugins/LightweightRemoteGuiComponentPlugin.h>
@@ -47,11 +48,10 @@
 #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
 
 #include <armarx/navigation/components/dynamic_scene_provider/ArVizDrawer.h>
+#include <armarx/navigation/components/dynamic_scene_provider/ComponentInterface.h>
 #include <armarx/navigation/human/HumanTracker.h>
 #include <armarx/navigation/memory/client/costmap/Reader.h>
 #include <armarx/navigation/memory/client/human/Writer.h>
-#include <armarx/navigation/components/dynamic_scene_provider/ComponentInterface.h>
-
 
 namespace armarx::navigation::components::dynamic_scene_provider
 {
@@ -123,7 +123,6 @@ namespace armarx::navigation::components::dynamic_scene_provider
     private:
         static const std::string defaultName;
 
-
         // Private member variables go here.
 
 
@@ -138,7 +137,6 @@ namespace armarx::navigation::components::dynamic_scene_provider
                 std::string name = ""; // all
             } laserScannerFeatures;
 
-
             struct
             {
                 std::string name = "Armar6";
@@ -155,6 +153,7 @@ namespace armarx::navigation::components::dynamic_scene_provider
 
             std::string humanPoseProvider = "AzureKinectPointCloudProvider";
         };
+
         Properties properties;
         /* Use a mutex if you access variables from different threads
          * (e.g. ice functions and RemoteGui_update()).
@@ -191,7 +190,7 @@ namespace armarx::navigation::components::dynamic_scene_provider
 
         ReaderWriterPlugin<armem::human::client::Reader>* humanPoseReaderPlugin = nullptr;
 
-        ReaderWriterPlugin<armem::vision::laser_scanner_features::client::Reader>*
+        ReaderWriterPlugin<memory::client::laser_scanner_features::Reader>*
             laserScannerFeaturesReaderPlugin = nullptr;
 
         ReaderWriterPlugin<armem::robot_state::VirtualRobotReader>* virtualRobotReaderPlugin =
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ad8b55c8d351fd1cb01b7d27a45b37466c347b96
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.cpp
@@ -0,0 +1,237 @@
+#include "ArVizDrawer.h"
+
+#include <iterator>
+#include <string>
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+
+#include <pcl/impl/point_types.hpp>
+#include <pcl/point_cloud.h>
+
+#include <SimoxUtility/color/Color.h>
+
+#include "RobotAPI/components/ArViz/Client/Client.h"
+#include "RobotAPI/components/ArViz/Client/Elements.h"
+#include "RobotAPI/components/ArViz/Client/elements/Color.h"
+#include "RobotAPI/components/ArViz/Client/elements/Line.h"
+#include "RobotAPI/components/ArViz/Client/elements/Path.h"
+#include "RobotAPI/components/ArViz/Client/elements/PointCloud.h"
+#include "RobotAPI/gui-plugins/RobotUnitPlugin/QWidgets/StyleSheets.h"
+
+#include "RobotComponents/libraries/cartographer/util/laser_scanner_conversion.h"
+
+#include "FeatureExtractor.h"
+#include "conversions/eigen.h"
+#include "conversions/pcl.h"
+#include "conversions/pcl_eigen.h"
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    void
+    ArVizDrawer::draw(const std::vector<Features>& features,
+                      const std::string& frame,
+                      const Eigen::Isometry3f& globalSensorPose)
+    {
+        // drawCircles(features, frame, globalSensorPose);
+        drawConvexHulls(features, frame, globalSensorPose);
+        drawEllipsoids(features, frame, globalSensorPose);
+        drawChains(features, frame, globalSensorPose);
+    }
+
+    void
+    ArVizDrawer::draw(const armem::laser_scans::LaserScanStamped& msg,
+                      const Eigen::Isometry3f& globalSensorPose,
+                      const simox::Color& color)
+    {
+        auto layer = arviz.layer("points_" + msg.header.frame);
+
+        const auto pointCloud = conversions::eigen2pcl(toCartesian<Eigen::Vector3f>(msg.data));
+
+        layer.add(viz::PointCloud("points_" + std::to_string(layer.size()))
+                      .pointCloud(pointCloud, viz::Color(color))
+                      .pointSizeInPixels(5)
+                      .pose(globalSensorPose));
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::draw(const std::string& layerName,
+                      const Circle& circle,
+                      const Eigen::Isometry3f& robotGlobalPose,
+                      const simox::Color& color)
+    {
+        auto layer = arviz.layer(layerName);
+
+        drawCircle(layer, circle, robotGlobalPose, color);
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::drawCircle(viz::Layer& layer,
+                            const Circle& circle,
+                            const Eigen::Isometry3f& globalSensorPose,
+                            const simox::Color& color)
+    {
+
+        const Eigen::Vector3f position =
+            globalSensorPose * Eigen::Vector3f(circle.center.x(), circle.center.y(), -1.F);
+
+        layer.add(viz::Ellipsoid("circle_" + std::to_string(layer.size()))
+                      .axisLengths(Eigen::Vector3f{circle.radius, circle.radius, 0.F})
+                      .position(position)
+                      .color(simox::Color::red(200, 100)));
+    }
+
+    void
+    ArVizDrawer::drawCircles(const std::vector<Features>& features,
+                             const std::string& frame,
+                             const Eigen::Isometry3f& globalSensorPose,
+                             const simox::Color& color)
+    {
+        auto layer = arviz.layer("circles_" + frame);
+
+        std::for_each(features.begin(),
+                      features.end(),
+                      [&](const Features& f)
+                      {
+                          if (not f.circle)
+                          {
+                              return;
+                          }
+                          drawCircle(layer, *f.circle, globalSensorPose, color);
+                      });
+
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::drawConvexHulls(const std::vector<Features>& features,
+                                 const std::string& frame,
+                                 const Eigen::Isometry3f& globalSensorPose)
+    {
+        auto layer = arviz.layer("convex_hulls_" + frame);
+
+        std::for_each(features.begin(),
+                      features.end(),
+                      [&](const Features& f)
+                      {
+                          if (not f.convexHull)
+                          {
+                              return;
+                          }
+                          drawConvexHull(
+                              layer, *f.convexHull, globalSensorPose, simox::Color::red(100, 80));
+                      });
+
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::draw(const std::string& layerName,
+                      const VirtualRobot::MathTools::ConvexHull2D& robotHull,
+                      const Eigen::Isometry3f& robotGlobalPose,
+                      const simox::Color& color)
+    {
+        auto layer = arviz.layer(layerName);
+
+        drawConvexHull(layer, robotHull, robotGlobalPose, color);
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::drawConvexHull(viz::Layer& layer,
+                                const VirtualRobot::MathTools::ConvexHull2D& hull,
+                                const Eigen::Isometry3f& globalSensorPose,
+                                const simox::Color& color)
+    {
+        const auto points = conversions::to3D(hull.vertices);
+
+        layer.add(viz::Polygon("convex_hull_" + std::to_string(layer.size()))
+                      .points(points)
+                      .color(color)
+                      .pose(globalSensorPose));
+    }
+
+    void
+    ArVizDrawer::drawEllipsoids(const std::vector<Features>& features,
+                                const std::string& frame,
+                                const Eigen::Isometry3f& globalSensorPose)
+    {
+        auto layer = arviz.layer("ellipsoids_" + frame);
+
+        std::for_each(features.begin(),
+                      features.end(),
+                      [&](const Features& f)
+                      {
+                          if (not f.ellipsoid)
+                          {
+                              return;
+                          }
+                          drawEllipsoid(layer, *f.ellipsoid, globalSensorPose);
+                      });
+
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::drawEllipsoid(viz::Layer& layer,
+                               const Ellipsoid& ellipsoid,
+                               const Eigen::Isometry3f& globalSensorPose)
+    {
+
+        const Eigen::Isometry3f pose = globalSensorPose * ellipsoid.pose;
+
+        layer.add(viz::Ellipsoid("ellipsoid_" + std::to_string(layer.size()))
+                      .axisLengths(conversions::to3D(ellipsoid.radii))
+                      .pose(pose)
+                      .color(simox::Color(255, 102, 0, 128))); // orange, but a bit more shiny
+    }
+
+    void
+    ArVizDrawer::drawChains(const std::vector<Features>& features,
+                            const std::string& frame,
+                            const Eigen::Isometry3f& globalSensorPose)
+    {
+        auto layer = arviz.layer("chains_" + frame);
+
+        std::for_each(features.begin(),
+                      features.end(),
+                      [&](const Features& f)
+                      {
+                          if (not f.chain)
+                          {
+                              return;
+                          }
+                          drawChain(layer, *f.chain, globalSensorPose);
+
+                          // const auto ellipsoids = f.linesAsEllipsoids(50);
+                          // for (const auto& ellipsoid : ellipsoids)
+                          // {
+                          //     drawEllipsoid(layer, ellipsoid, globalSensorPose);
+                          // }
+                      });
+
+        arviz.commit(layer);
+    }
+
+    void
+    ArVizDrawer::drawChain(viz::Layer& layer,
+                           const Points& chain,
+                           const Eigen::Isometry3f& globalSensorPose)
+    {
+        if (chain.size() < 2)
+        {
+            return;
+        }
+
+        const auto cloud = conversions::to3D(chain);
+
+        layer.add(viz::Path("chain_" + std::to_string(layer.size()))
+                      .points(cloud)
+                      .width(7.F)
+                      .color(viz::Color::blue())
+                      .pose(globalSensorPose));
+    }
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.h
new file mode 100644
index 0000000000000000000000000000000000000000..f1c51a4c3b865a52be36d29a755afd935619f504
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ArVizDrawer.h
@@ -0,0 +1,99 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <pcl/point_cloud.h>
+#include <pcl/point_types.h>
+
+#include <SimoxUtility/color/Color.h>
+#include <VirtualRobot/VirtualRobot.h>
+
+#include <RobotAPI/components/ArViz/Client/ScopedClient.h>
+
+#include "armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.h"
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    class ArVizDrawer
+    {
+    public:
+        using Points = std::vector<Eigen::Vector2f>;
+
+        ArVizDrawer(armarx::viz::Client& arviz) : arviz(arviz)
+        {
+        }
+
+        void draw(const std::vector<Features>& features,
+                  const std::string& frame,
+                  const Eigen::Isometry3f& globalSensorPose);
+
+        void draw(const armem::laser_scans::LaserScanStamped& msg,
+                  const Eigen::Isometry3f& globalSensorPose,
+                  const simox::Color& color);
+
+        void draw(const std::string& layerName,
+                  const Circle& circle,
+                  const Eigen::Isometry3f& robotGlobalPose,
+                  const simox::Color& color);
+
+        void draw(const std::string& layerName,
+                  const VirtualRobot::MathTools::ConvexHull2D& robotHull,
+                  const Eigen::Isometry3f& robotGlobalPose,
+                  const simox::Color& color = simox::Color::red(100, 80));
+
+    private:
+        void drawCircles(const std::vector<Features>& features,
+                         const std::string& frame,
+                         const Eigen::Isometry3f& globalSensorPose,
+                         const simox::Color& color);
+        void drawCircle(viz::Layer& layer,
+                        const Circle& circle,
+                        const Eigen::Isometry3f& globalSensorPose,
+                        const simox::Color& color);
+
+        void drawConvexHulls(const std::vector<Features>& features,
+                             const std::string& frame,
+                             const Eigen::Isometry3f& globalSensorPose);
+        void drawConvexHull(viz::Layer& layer,
+                            const VirtualRobot::MathTools::ConvexHull2D& hull,
+                            const Eigen::Isometry3f& globalSensorPose,
+                            const simox::Color& color);
+
+        void drawEllipsoids(const std::vector<Features>& features,
+                            const std::string& frame,
+                            const Eigen::Isometry3f& globalSensorPose);
+
+        void drawEllipsoid(viz::Layer& layer,
+                           const Ellipsoid& ellipsoid,
+                           const Eigen::Isometry3f& globalSensorPose);
+
+        void drawChains(const std::vector<Features>& features,
+                        const std::string& frame,
+                        const Eigen::Isometry3f& globalSensorPose);
+        void drawChain(viz::Layer& layer,
+                       const Points& chain,
+                       const Eigen::Isometry3f& globalSensorPose);
+
+        armarx::viz::Client arviz;
+    };
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/CMakeLists.txt b/source/armarx/navigation/components/laser_scanner_feature_extraction/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ede04e8d40a18b71e5e2009583de4d6d534dd887
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/CMakeLists.txt
@@ -0,0 +1,47 @@
+armarx_add_component(laser_scanner_feature_extraction
+    ICE_DEPENDENCIES
+        ArmarXCoreInterfaces
+        # RobotAPIInterfaces
+    SOURCES
+        Component.cpp
+        FeatureExtractor.cpp
+        ArVizDrawer.cpp
+        ScanClustering.cpp
+        ChainApproximation.cpp
+        #conversions
+        conversions/eigen.cpp
+        conversions/pcl.cpp
+        EnclosingEllipsoid.cpp
+        Path.cpp
+    HEADERS
+        Component.h
+        FeatureExtractor.h
+        ArVizDrawer.h
+        ScanClustering.h
+        ChainApproximation.h
+        EnclosingEllipsoid.h
+        Path.h
+        geometry.h
+        #conversions
+        conversions/eigen.h
+        conversions/pcl_eigen.h
+        conversions/opencv_eigen.h
+        conversions/opencv_pcl.h
+        conversions/pcl.h
+    DEPENDENCIES
+        # ArmarXCore
+        ArmarXCore
+        ArmarXCoreComponentPlugins  # For DebugObserver plugin.
+        ArmarXCoreLogging
+        # ArmarXGui
+        ## ArmarXGuiComponentPlugins  # For RemoteGui plugin.
+        # RobotAPI
+        ## RobotAPICore
+        ## RobotAPIInterfaces
+        RobotAPIComponentPlugins  # For ArViz and other plugins.
+        RobotComponentsInterfaces # For legacy LaserScannerFeatureExtraction topic
+        armarx_navigation::memory
+        armem_laser_scans
+    DEPENDENCIES_LEGACY
+        PCL
+)
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..905cf765c2c9f9c7c207f270c02b3d27b511992b
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.cpp
@@ -0,0 +1,214 @@
+#include "ChainApproximation.h"
+
+#include <iterator>
+#include <numeric>
+
+#include <Eigen/Geometry>
+
+#include "ArmarXCore/core/exceptions/LocalException.h"
+#include "ArmarXCore/core/exceptions/local/ExpressionException.h"
+
+namespace armarx
+{
+
+    ChainApproximation::ChainApproximation(const Points& points, const Params& params) :
+        points(points), params(params)
+    {
+        // fill indices
+        indices.resize(points.size());
+        std::iota(indices.begin(), indices.end(), 0);
+    }
+
+    ChainApproximation::ApproximationResult ChainApproximation::approximate()
+    {
+
+        int iterations = 0;
+
+        const auto maxIterConditionReached = [&]()
+        {
+            // inactive?
+            if (params.maxIterations <= 0)
+            {
+                return false;
+            }
+
+            return iterations >= params.maxIterations;
+        };
+
+        while (true)
+        {
+            if (maxIterConditionReached())
+            {
+                return ApproximationResult
+                {
+                    .condition  = ApproximationResult::TerminationCondition::IterationLimit,
+                    .iterations = iterations};
+            }
+
+            if (not approximateStep())
+            {
+                return ApproximationResult
+                {
+                    .condition       = ApproximationResult::TerminationCondition::Converged,
+                    .iterations      = iterations,
+                    .reductionFactor = 1.F - static_cast<float>(indices.size()) /
+                    static_cast<float>(points.size())};
+            }
+
+            iterations++;
+        }
+    }
+
+    ChainApproximation::Triplets ChainApproximation::getTriplets() const
+    {
+        const int nIndices = static_cast<int>(indices.size());
+
+        if (nIndices < 3)
+        {
+            return {};
+        }
+
+        Triplets triplets;
+        triplets.reserve(indices.size());
+
+        // Here, we iterate over all elements under consideration.
+        // The aim is to create a view - a sliding window - over the
+        // indices. i will always point to the centered element.
+
+        // the first element
+        triplets.emplace_back(indices.back(), indices.front(), indices.at(1));
+
+        // intermediate elements
+        for (int i = 1; i < (nIndices - 1); i++)
+        {
+            triplets.emplace_back(indices.at(i - 1), indices.at(i), indices.at(i + 1));
+        }
+
+        // the last element
+        triplets.emplace_back(indices.back(), indices.front(), indices.at(1));
+
+        return triplets;
+    }
+
+    std::vector<float>
+    ChainApproximation::computeDistances(const ChainApproximation::Triplets& triplets)
+    {
+        std::vector<float> distances;
+        distances.reserve(triplets.size());
+
+        std::transform(triplets.begin(),
+                       triplets.end(),
+                       std::back_inserter(distances),
+                       [&](const auto & triplet)
+        {
+            return computeDistance(triplet);
+        });
+
+        return distances;
+    }
+    float ChainApproximation::computeDistance(const ChainApproximation::Triplet& triplet) const
+    {
+        using Line = Eigen::ParametrizedLine<float, 2>;
+
+        const Eigen::Vector2f& ptBefore = points.at(triplet.a);
+        const Eigen::Vector2f& ptPivot  = points.at(triplet.b);
+        const Eigen::Vector2f& ptAfter  = points.at(triplet.c);
+
+        const auto line = Line::Through(ptBefore, ptAfter);
+        return line.distance(ptPivot);
+    }
+
+    bool ChainApproximation::approximateStep()
+    {
+        const size_t nIndices = indices.size();
+        if (nIndices <= 3)
+        {
+            return false;
+        }
+
+        const Triplets triplets            = getTriplets();
+        const std::vector<float> distances = computeDistances(triplets);
+
+        ARMARX_CHECK_EQUAL(triplets.size(), distances.size());
+        const int n = static_cast<int>(triplets.size());
+
+        std::vector<int> indicesToBeRemoved;
+
+        // TODO(fabian.reister): consider boundaries
+        for (int i = 1; i < n - 1; i++)
+        {
+            const auto& distance = distances.at(i);
+
+            // check distance criterion (necessary conditio)
+            if (distance >= params.distanceTh)
+            {
+                continue;
+            }
+
+            // better remove this element than those left and right (sufficient condition)
+            if (distance < std::min(distances.at(i - 1), distances.at(i + 1)))
+            {
+                indicesToBeRemoved.emplace_back(triplets.at(i).b);
+            }
+        }
+
+        // termination condition
+        if (indicesToBeRemoved.empty())
+        {
+            return false;
+        }
+
+        const auto isMatch = [&](const int& idx) -> bool
+        {
+            return std::find(indicesToBeRemoved.begin(), indicesToBeRemoved.end(), idx) !=
+            indicesToBeRemoved.end();
+        };
+
+        indices.erase(std::remove_if(indices.begin(), indices.end(), isMatch), indices.end());
+
+        return true;
+    }
+    ChainApproximation::Points ChainApproximation::approximatedChain() const
+    {
+        Points extractedPoints;
+        extractedPoints.reserve(indices.size());
+
+        std::transform(indices.begin(),
+                       indices.end(),
+                       std::back_inserter(extractedPoints),
+                       [&](const auto & idx)
+        {
+            return points.at(idx);
+        });
+
+        return extractedPoints;
+    }
+
+    std::ostream& detail::operator<<(std::ostream& str, const ApproximationResult& res)
+    {
+        using TerminationCondition = ApproximationResult::TerminationCondition;
+
+        const std::string condStr = [&res]() -> std::string
+        {
+            std::string repr;
+
+            switch (res.condition)
+            {
+                case TerminationCondition::Converged:
+                    repr = "Converged";
+                    break;
+                case TerminationCondition::IterationLimit:
+                    repr = "IterationLimit";
+                    break;
+            }
+            return repr;
+        }();
+
+        str << "ApproximationResult: ["
+            << "condition: " << condStr << " | "
+            << "iterations: " << res.iterations << " | "
+            << "reduction: " << res.reductionFactor * 100 << "%]";
+
+        return str;
+    }
+} // namespace armarx
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7b5e6cdaeb46a8909f4f572863e08c92751a649
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h
@@ -0,0 +1,97 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <Eigen/Core>
+
+namespace armarx
+{
+
+    namespace detail
+    {
+        struct ChainApproximationParams
+        {
+            float distanceTh; // [mm]
+
+            int maxIterations{-1};
+        };
+
+
+        struct ApproximationResult
+        {
+            enum class TerminationCondition
+            {
+                Converged,
+                IterationLimit
+            };
+            TerminationCondition condition;
+
+            int iterations;
+
+            float reductionFactor{0.F};
+        };
+
+        std::ostream& operator<<(std::ostream& str, const ApproximationResult& res);
+    } // namespace detail
+
+    class ChainApproximation
+    {
+    public:
+        using Point  = Eigen::Vector2f;
+        using Points = std::vector<Point>;
+
+        using Params = detail::ChainApproximationParams;
+        using ApproximationResult = detail::ApproximationResult;
+
+        ChainApproximation(const Points& points, const Params& params);
+
+        ApproximationResult approximate();
+
+        [[nodiscard]] Points approximatedChain() const;
+
+    private:
+        struct Triplet
+        {
+            Triplet(const int& a, const int& b, const int& c) : a(a), b(b), c(c) {}
+
+            int a;
+            int b;
+            int c;
+        };
+
+        using Triplets = std::vector<Triplet>;
+
+        Triplets getTriplets() const;
+
+        std::vector<float> computeDistances(const Triplets& triplets);
+        float computeDistance(const Triplet& triplet) const;
+
+        bool approximateStep();
+
+        Points points;
+
+        std::vector<int> indices;
+
+        const Params params;
+    };
+
+} // namespace armarx
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6c2f235b9f61f4482d5bc90fdf554e0985748fb0
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.cpp
@@ -0,0 +1,548 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    RobotComponents::ArmarXObjects::LaserScannerFeatureExtraction
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "Component.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <utility>
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+
+#include <IceUtil/Time.h>
+
+#include <SimoxUtility/algorithm/apply.hpp>
+#include <SimoxUtility/color/Color.h>
+#include <VirtualRobot/BoundingBox.h>
+#include <VirtualRobot/MathTools.h>
+
+#include "ArmarXCore/core/time/DateTime.h"
+#include "ArmarXCore/core/time/Duration.h"
+
+#include "RobotAPI/libraries/core/remoterobot/RemoteRobot.h"
+
+#include "ArVizDrawer.h"
+#include "FeatureExtractor.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/geometry.h"
+#include "conversions/eigen.h"
+#include "conversions/pcl_eigen.h"
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    armarx::PropertyDefinitionsPtr
+    LaserScannerFeatureExtraction::createPropertyDefinitions()
+    {
+        armarx::PropertyDefinitionsPtr def =
+            new ComponentPropertyDefinitions(getConfigIdentifier());
+
+        // Publish to a topic (passing the TopicListenerPrx).
+        def->topic(featuresTopic);
+
+        // // Add an optionalproperty.
+        def->optional(
+            properties.taskPeriodMs, "p.taskPeriodMs", "Update rate of the running task.");
+
+        def->optional(properties.robotHull.shape, "p.robotHull.shape", "Shape of the robot area.")
+            .map({std::make_pair("Rectangle", Properties::RobotHull::RECTANGLE),
+                  std::make_pair("Circle", Properties::RobotHull::CIRCLE)});
+        def->optional(properties.robotHull.radius,
+                      "p.robotHull.radius",
+                      "The radius of the robot when using the circle shape.");
+        def->optional(
+            properties.robotHull.robotConvexHullMargin,
+            "p.robotHull.robotConvexHullMargin",
+            "Parameter to increase the robot's convex hull when using the rectangle shape.");
+
+        def->optional(properties.cableFix.enabled,
+                      "p.cableFix.enabled",
+                      "Try to supress clusters belonging to the power supply cable.");
+        def->optional(properties.cableFix.cableAreaWidth,
+                      "p.cableFix.cableAreaWidth",
+                      "Width of the area where to search for the cable.");
+
+        def->optional(
+            properties.cableFix.maxAreaTh,
+            "p.cableFix.maxAreaTh",
+            "The cable will only be removed if the cluster area is below this threshold.");
+
+        def->optional(
+            properties.chainApproximationParams.distanceTh, "p.chainApproximation.distanceTh", "");
+        def->optional(properties.chainApproximationParams.maxIterations,
+                      "p.chainApproximation.maxIterations",
+                      "");
+
+        def->optional(properties.scanClusteringParams.angleThreshold,
+                      "p.scanClustering.angleThreshold",
+                      "Angular distance between consecutive points in laser scan for clustering.");
+        def->optional(properties.scanClusteringParams.distanceThreshold,
+                      "p.scanClustering.distanceThreshold",
+                      "Radial distance between consecutive points in laser scan for clustering.");
+        def->optional(properties.scanClusteringParams.maxDistance,
+                      "p.scanClustering.maxDistance",
+                      "Maximum radius around sensors to detect clusters.");
+
+        def->optional(properties.arviz.drawRawPoints,
+                      "p.arviz.drawRawPoints",
+                      "If true, the laser scans will be drawn.");
+
+        laserScannerFeatureWriter.registerPropertyDefinitions(def);
+
+        return def;
+    }
+
+    LaserScannerFeatureExtraction::LaserScannerFeatureExtraction() :
+        laserScannerFeatureWriter(memoryNameSystem())
+    {
+        addPlugin(laserScannerReaderPlugin);
+    }
+
+    void
+    LaserScannerFeatureExtraction::onInitComponent()
+    {
+        // Topics and properties defined above are automagically registered.
+
+        // Keep debug observer data until calling `sendDebugObserverBatch()`.
+        // (Requies the armarx::DebugObserverComponentPluginUser.)
+        // setDebugObserverBatchModeEnabled(true);
+    }
+
+    void
+    LaserScannerFeatureExtraction::onConnectComponent()
+    {
+        ARMARX_INFO << "Connecting";
+
+        frequencyReporterPublish = std::make_unique<FrequencyReporter>(
+            getDebugObserver(), "LaserScannerFeatureExtraction_publish");
+        frequencyReporterSubscribe = std::make_unique<FrequencyReporter>(
+            getDebugObserver(), "LaserScannerFeatureExtraction_subscribe");
+
+        arVizDrawer = std::make_unique<ArVizDrawer>(getArvizClient());
+
+        featureExtractor = std::make_unique<FeatureExtractor>(
+            properties.scanClusteringParams,
+            properties.chainApproximationParams,
+            [&](auto&&... args) { onFeatures(std::forward<decltype(args)>(args)...); });
+
+        ARMARX_INFO << "Connected";
+
+        robot = RemoteRobot::createLocalClone(getRobotStateComponent());
+        RemoteRobot::synchronizeLocalClone(robot, getRobotStateComponent());
+        switch (properties.robotHull.shape)
+        {
+            case Properties::RobotHull::RECTANGLE:
+            {
+                // initialize robot platform convex hull
+                const Eigen::Vector2f ptFrontLeft = conversions::to2D(
+                    robot->getRobotNode("PlatformCornerFrontLeft")->getPositionInRootFrame());
+                const Eigen::Vector2f ptFrontRight = conversions::to2D(
+                    robot->getRobotNode("PlatformCornerFrontRight")->getPositionInRootFrame());
+                const Eigen::Vector2f ptBackRight = conversions::to2D(
+                    robot->getRobotNode("PlatformCornerBackRight")->getPositionInRootFrame());
+                const Eigen::Vector2f ptBackLeft = conversions::to2D(
+                    robot->getRobotNode("PlatformCornerBackLeft")->getPositionInRootFrame());
+
+                const auto addMargin = [&](const Eigen::Vector2f& pt) -> Eigen::Vector2f
+                {
+                    Eigen::Vector2f ptWithMargin = pt;
+                    ptWithMargin.x() +=
+                        std::copysignf(properties.robotHull.robotConvexHullMargin, pt.x());
+                    ptWithMargin.y() +=
+                        std::copysignf(properties.robotHull.robotConvexHullMargin, pt.y());
+
+                    return ptWithMargin;
+                };
+
+                {
+                    std::vector<Eigen::Vector2f> hullPoints;
+                    hullPoints.push_back(addMargin(ptFrontLeft));
+                    hullPoints.push_back(addMargin(ptFrontRight));
+                    hullPoints.push_back(addMargin(ptBackRight));
+                    hullPoints.push_back(addMargin(ptBackLeft));
+
+                    robotHull = VirtualRobot::MathTools::createConvexHull2D(hullPoints);
+                }
+
+                // cable fix is only applicable when the robot shape is a rectangle
+                if (properties.cableFix.enabled)
+                {
+                    // the cable area and the robot hull overlap slightly. as all points must be within one region,
+                    // the cable area should start at approx. the position of the wheels
+
+                    std::vector<Eigen::Vector2f> cableAreaPoints;
+                    cableAreaPoints.emplace_back(addMargin(ptBackRight) +
+                                                 100 * Eigen::Vector2f::UnitY());
+                    cableAreaPoints.emplace_back(addMargin(ptBackLeft) +
+                                                 100 * Eigen::Vector2f::UnitY());
+                    cableAreaPoints.emplace_back(addMargin(ptBackRight) -
+                                                 properties.cableFix.cableAreaWidth *
+                                                     Eigen::Vector2f::UnitY());
+                    cableAreaPoints.emplace_back(addMargin(ptBackLeft) -
+                                                 properties.cableFix.cableAreaWidth *
+                                                     Eigen::Vector2f::UnitY());
+
+                    cableArea = VirtualRobot::MathTools::createConvexHull2D(cableAreaPoints);
+                }
+
+                break;
+            }
+            case Properties::RobotHull::CIRCLE:
+            {
+                // cable fix is not supported for circular robots
+                if (properties.cableFix.enabled)
+                {
+                    ARMARX_ERROR << "Cable fix is not supported for circular robots!";
+                }
+                const Eigen::Vector2f root =
+                    conversions::to2D(robot->getRootNode()->getPositionInRootFrame());
+                robotHull = root;
+                break;
+            }
+        }
+
+
+        laserScannerFeatureWriter.connect();
+
+        task = new PeriodicTask<LaserScannerFeatureExtraction>(
+            this,
+            &LaserScannerFeatureExtraction::runPeriodically,
+            properties.taskPeriodMs,
+            false,
+            "runningTask");
+        task->start();
+    }
+
+    memory::LaserScannerFeature
+    toArmemFeature(const Features& features)
+    {
+        memory::LaserScannerFeature armemFeature;
+
+        if (features.chain)
+        {
+            armemFeature.chain = features.chain.value();
+        }
+
+        if (features.circle)
+        {
+            armemFeature.circle = features.circle.value();
+        }
+
+        if (features.convexHull)
+        {
+            armemFeature.convexHull = features.convexHull->vertices;
+        }
+
+        if (features.ellipsoid)
+        {
+            armemFeature.ellipsoid = features.ellipsoid.value();
+        }
+
+        armemFeature.points = features.points;
+
+        return armemFeature;
+    }
+
+    memory::LaserScannerFeatures
+    toArmemFeatures(const std::vector<Features>& features,
+                    const Eigen::Isometry3f& global_T_sensor,
+                    const std::string& sensorFrame)
+    {
+        memory::LaserScannerFeatures armemFeatures;
+        armemFeatures.frame = sensorFrame;
+        armemFeatures.frameGlobalPose = global_T_sensor;
+
+
+        armemFeatures.features = simox::alg::apply(features, toArmemFeature);
+
+        return armemFeatures;
+    }
+
+    std::vector<Features>
+    removeInvalidFeatures(std::vector<Features> features)
+    {
+        const auto isInvalid = [](const Features& features) -> bool
+        {
+            if (features.points.size() < 2)
+            {
+                return true;
+            }
+
+            if (not features.convexHull.has_value())
+            {
+                return true;
+            }
+
+            // if(not features.circle.has_value())
+            // {
+            //     return true;
+            // }
+
+            // if(not features.ellipsoid.has_value())
+            // {
+            //     return true;
+            // }
+
+            return false;
+        };
+
+
+        features.erase(std::remove_if(features.begin(), features.end(), isInvalid), features.end());
+
+        return features;
+    }
+
+    void
+    LaserScannerFeatureExtraction::onFeatures(const armem::laser_scans::LaserScanStamped& data,
+                                              const std::vector<Features>& featuresFromExtractor)
+    {
+        ARMARX_DEBUG << "Publishing data";
+
+        // obtain sensor pose
+        RemoteRobot::synchronizeLocalCloneToTimestamp(
+            robot, getRobotStateComponent(), data.header.timestamp.toMicroSecondsSinceEpoch());
+        const Eigen::Isometry3f global_T_sensor(
+            robot->getRobotNode(data.header.frame)->getGlobalPose());
+        const Eigen::Isometry3f global_T_robot(robot->getRootNode()->getGlobalPose());
+        const Eigen::Isometry3f robot_T_sensor(
+            robot->getRobotNode(data.header.frame)->getPoseInRootFrame());
+
+        //Eigen::AlignedBox2f box;
+        //box.extend(pt1).extend(pt2).extend(pt3).extend(pt4);
+
+        const auto transformPoints =
+            [](const std::vector<Eigen::Vector2f>& points, const Eigen::Isometry3f& tf)
+        {
+            std::vector<Eigen::Vector2f> out;
+            out.reserve(points.size());
+
+            std::transform(points.begin(),
+                           points.end(),
+                           std::back_inserter(out),
+                           [&tf](const auto& pt)
+                           { return conversions::to2D(tf * conversions::to3D(pt)); });
+
+            return out;
+        };
+
+        const auto isPointInsideRobot = [&](const Eigen::Vector2f& pt) -> bool
+        {
+            if (std::holds_alternative<VirtualRobot::MathTools::ConvexHull2DPtr>(robotHull))
+            {
+                return VirtualRobot::MathTools::isInside(
+                    pt, std::get<VirtualRobot::MathTools::ConvexHull2DPtr>(robotHull));
+            }
+            else if (std::holds_alternative<Eigen::Vector2f>(robotHull))
+            {
+                return (std::get<Eigen::Vector2f>(robotHull) - pt).norm() <
+                       properties.robotHull.radius;
+            }
+            return false;
+        };
+
+        const auto isPointInsideCableArea = [&](const Eigen::Vector2f& pt) -> bool
+        { return VirtualRobot::MathTools::isInside(pt, cableArea); };
+
+        const auto isClusterInvalid = [&](const Features& features) -> bool
+        {
+            // either use convex hull (compact representation) or raw points as fallbacck
+            const auto points = [&]()
+            {
+                if (features.convexHull)
+                {
+                    return transformPoints(features.convexHull->vertices, robot_T_sensor);
+                }
+
+                return transformPoints(features.points, robot_T_sensor);
+            }();
+
+            const bool allPointsInsideRobot =
+                std::all_of(points.begin(), points.end(), isPointInsideRobot);
+            if (allPointsInsideRobot)
+            {
+                return true;
+            }
+
+            if (properties.cableFix.enabled)
+            {
+
+                const bool allPointsInCableArea =
+                    std::all_of(points.begin(), points.end(), isPointInsideCableArea);
+                if (allPointsInCableArea)
+                {
+                    if (geometry::ConvexHull(points).area() < properties.cableFix.maxAreaTh)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        };
+
+        const auto removeFeaturesOnRobotOrCable = [&](std::vector<Features> features)
+        {
+            features.erase(std::remove_if(features.begin(), features.end(), isClusterInvalid),
+                           features.end());
+            return features;
+        };
+
+        ARMARX_VERBOSE << featuresFromExtractor.size() << " features from extractor";
+
+        const auto features = removeFeaturesOnRobotOrCable(featuresFromExtractor);
+        ARMARX_VERBOSE << features.size() << " features without cable region";
+
+        const auto validFeatures = removeInvalidFeatures(features);
+        ARMARX_VERBOSE << validFeatures.size() << " valid features without cable region";
+
+        const auto armemFeatures =
+            toArmemFeatures(validFeatures, global_T_sensor, data.header.frame);
+
+        ARMARX_VERBOSE << "Reporting " << armemFeatures.features.size() << " features";
+
+        // report the features
+        publishFeatures(armemFeatures, data.header.timestamp);
+
+        // check if arviz should be triggered
+        const auto getOrCreateThrottler = [&]() -> Throttler&
+        {
+            const auto it = throttlers.find(data.header.frame);
+            if (it == throttlers.end())
+            {
+                throttlers.emplace(data.header.frame, Throttler(10.F));
+            }
+
+            return throttlers.at(data.header.frame);
+        };
+
+        if (getOrCreateThrottler().check(data.header.timestamp))
+        {
+            arVizDrawer->draw(features, data.header.frame, global_T_sensor);
+
+            if (std::holds_alternative<VirtualRobot::MathTools::ConvexHull2DPtr>(robotHull))
+            {
+                VirtualRobot::MathTools::ConvexHull2DPtr& convexHullPtr =
+                    std::get<VirtualRobot::MathTools::ConvexHull2DPtr>(robotHull);
+                if (convexHullPtr != nullptr)
+                {
+                    arVizDrawer->draw("robot_convex_hull",
+                                      *convexHullPtr,
+                                      global_T_robot,
+                                      simox::Color::azure(255, 80));
+                }
+            }
+            else if (std::holds_alternative<Eigen::Vector2f>(robotHull))
+            {
+                Eigen::Vector2f& root = std::get<Eigen::Vector2f>(robotHull);
+                arVizDrawer->draw("robot_circle_hull",
+                                  Circle{root, properties.robotHull.radius},
+                                  global_T_robot,
+                                  simox::Color::azure(255, 80));
+            }
+
+            if (cableArea != nullptr)
+            {
+                arVizDrawer->draw(
+                    "cable_area", *cableArea, global_T_robot, simox::Color::blue(255, 80));
+            }
+
+            if (properties.arviz.drawRawPoints)
+            {
+                arVizDrawer->draw(data, global_T_sensor, simox::Color::magenta(255, 100));
+            }
+        }
+
+        // some debugobserver reporting
+        frequencyReporterPublish->add(IceUtil::Time::now().toMicroSeconds());
+        setDebugObserverDatafield("numClusters", features.size());
+    }
+
+    void
+    LaserScannerFeatureExtraction::publishFeatures(const memory::LaserScannerFeatures& features,
+                                                   const armem::Time& timestamp)
+    {
+
+        // store in memory
+        laserScannerFeatureWriter.store(features, getName(), timestamp);
+
+        // legacy - topic
+        LineSegment2DChainSeq chains;
+        for (const auto& feature : features.features)
+        {
+            if (not feature.chain.empty())
+            {
+                LineSegment2DChain chain;
+                for (const auto& pt : feature.chain)
+                {
+                    chain.push_back(
+                        conversions::to2D(features.frameGlobalPose * conversions::to3D(pt)));
+                }
+                chains.push_back(chain);
+            }
+        }
+
+        featuresTopic->reportExtractedLineSegments(chains);
+    }
+
+    void
+    LaserScannerFeatureExtraction::onDisconnectComponent()
+    {
+        task->stop();
+    }
+
+    void
+    LaserScannerFeatureExtraction::onExitComponent()
+    {
+    }
+
+    std::string
+    LaserScannerFeatureExtraction::getDefaultName() const
+    {
+        return "LaserScannerFeatureExtraction";
+    }
+
+    void
+    LaserScannerFeatureExtraction::runPeriodically()
+    {
+        const armem::laser_scans::client::Reader::Query laserScanQuery{
+            .agent = robot->getName(), .timeRange = std::nullopt, .sensorList = {}};
+
+        const armem::laser_scans::client::Reader::Result laserScanResult =
+            laserScannerReaderPlugin->get().queryData(laserScanQuery);
+        ARMARX_CHECK_EQUAL(laserScanResult.status,
+                           armem::laser_scans::client::Reader::Result::Error)
+            << laserScanResult.errorMessage;
+
+        ARMARX_DEBUG << "Received laser scan data from " << laserScanResult.laserScans.size()
+                     << " sensors";
+
+        frequencyReporterSubscribe->add(IceUtil::Time::now().toMicroSeconds());
+
+        for (const auto& scan : laserScanResult.laserScans)
+        {
+            setDebugObserverDatafield(getDefaultName() + scan.header.frame, scan.data.size());
+            featureExtractor->onData(scan);
+        }
+    }
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.h
new file mode 100644
index 0000000000000000000000000000000000000000..fac5e42539378b10d5ea6c8fefa4751969100560
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/Component.h
@@ -0,0 +1,231 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    RobotComponents::ArmarXObjects::LaserScannerFeatureExtraction
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <variant>
+
+#include "ArmarXCore/core/util/Throttler.h"
+#include "ArmarXCore/libraries/logging/FrequencyReporter.h"
+#include <ArmarXCore/core/Component.h>
+#include <ArmarXCore/libraries/ArmarXCoreComponentPlugins/DebugObserverComponentPlugin.h>
+
+#include "RobotAPI/libraries/RobotAPIComponentPlugins/RobotStateComponentPlugin.h"
+#include "RobotAPI/libraries/armem/client/forward_declarations.h"
+#include "RobotAPI/libraries/armem/client/plugins/PluginUser.h"
+#include "RobotAPI/libraries/armem/client/plugins/ReaderWriterPlugin.h"
+#include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
+#include <RobotAPI/libraries/RobotAPIComponentPlugins/RobotStateComponentPlugin.h>
+#include <RobotAPI/libraries/armem_laser_scans/client/common/Reader.h>
+
+#include <RobotComponents/interface/components/LaserScannerFeatureExtraction/LaserScannerFeatureExtraction.h>
+
+#include "ArVizDrawer.h"
+#include "FeatureExtractor.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/geometry.h"
+#include <armarx/navigation/memory/client/laser_scanner_features/Writer.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    using LaserScannerFeaturesListenerPrx =
+        armarx::laser_scanner_feature_extraction::LaserScannerFeaturesListenerPrx;
+    using LineSegment2DChain = armarx::laser_scanner_feature_extraction::LineSegment2DChain;
+    using LineSegment2DChainSeq = armarx::laser_scanner_feature_extraction::LineSegment2DChainSeq;
+
+    // class FeatureExtractor;
+    // class ArVizDrawer;
+
+    /**
+     * @defgroup Component-LaserScannerFeatureExtraction LaserScannerFeatureExtraction
+     * @ingroup RobotComponents-Components
+     * A description of the component LaserScannerFeatureExtraction.
+     *
+     * @class LaserScannerFeatureExtraction
+     * @ingroup Component-LaserScannerFeatureExtraction
+     * @brief Brief description of class LaserScannerFeatureExtraction.
+     *
+     * Detailed description of class LaserScannerFeatureExtraction.
+     */
+    class LaserScannerFeatureExtraction :
+        virtual public armarx::Component,
+        virtual public armarx::DebugObserverComponentPluginUser
+        // , virtual public armarx::LightweightRemoteGuiComponentPluginUser
+        ,
+        virtual public armarx::ArVizComponentPluginUser,
+        virtual public RobotStateComponentPluginUser,
+        virtual public armem::ClientPluginUser
+
+    {
+    public:
+        LaserScannerFeatureExtraction();
+
+        /// @see armarx::ManagedIceObject::getDefaultName()
+        std::string getDefaultName() const override;
+
+    protected:
+        /// @see PropertyUser::createPropertyDefinitions()
+        armarx::PropertyDefinitionsPtr createPropertyDefinitions() override;
+
+        /// @see armarx::ManagedIceObject::onInitComponent()
+        void onInitComponent() override;
+
+        /// @see armarx::ManagedIceObject::onConnectComponent()
+        void onConnectComponent() override;
+
+        /// @see armarx::ManagedIceObject::onDisconnectComponent()
+        void onDisconnectComponent() override;
+
+        /// @see armarx::ManagedIceObject::onExitComponent()
+        void onExitComponent() override;
+
+        /* (Requires armarx::LightweightRemoteGuiComponentPluginUser.)
+        /// This function should be called once in onConnect() or when you
+        /// need to re-create the Remote GUI tab.
+        void createRemoteGuiTab();
+
+        /// After calling `RemoteGui_startRunningTask`, this function is
+        /// called periodically in a separate thread. If you update variables,
+        /// make sure to synchronize access to them.
+        void RemoteGui_update() override;
+        */
+
+    private:
+        void runPeriodically();
+
+        void onFeatures(const armem::laser_scans::LaserScanStamped& data,
+                        const std::vector<Features>& features);
+
+
+        void publishFeatures(const memory::LaserScannerFeatures& features,
+                             const armem::Time& timestamp);
+
+        // Private methods go here.
+
+        // Forward declare `Properties` if you used it before its defined.
+        // struct Properties;
+
+        /* (Requires the armarx::ArVizComponentPluginUser.)
+        /// Draw some boxes in ArViz.
+        void drawBoxes(const Properties& p, viz::Client& arviz);
+        */
+
+        // Private member variables go here.
+
+        /// Properties shown in the Scenario GUI.
+        struct Properties
+        {
+            int taskPeriodMs = 100;
+
+            //
+            struct RobotHull
+            {
+                enum Shape
+                {
+                    RECTANGLE,
+                    CIRCLE
+                };
+
+                Shape shape = RECTANGLE;
+                // for shape circle
+                float radius = 500.F; // [mm]
+                // for shape rectangle
+                float robotConvexHullMargin{50.F};
+            } robotHull;
+
+            //
+            struct CableFix
+            {
+                bool enabled = {true};
+                float cableAreaWidth{400};
+                float maxAreaTh{50 * 50};
+            } cableFix;
+
+            ScanClustering::Params scanClusteringParams{
+                .distanceThreshold = 30.F, // [mm]
+                // we know the scan resolution which produces a bit more than 1000 points
+                .angleThreshold = 2 * M_PIf32 / 1000.F * 5.F, // [rad]
+                .maxDistance = 3'000.F // [mm]
+            };
+
+            ChainApproximation::Params chainApproximationParams{
+                .distanceTh = 40.F // [mm]
+            };
+
+            struct ArViz
+            {
+                bool drawRawPoints = {true};
+            } arviz;
+        };
+
+        Properties properties;
+        /* Use a mutex if you access variables from different threads
+         * (e.g. ice functions and RemoteGui_update()).
+        std::mutex propertiesMutex;
+        */
+
+        /* (Requires the armarx::LightweightRemoteGuiComponentPluginUser.)
+        /// Tab shown in the Remote GUI.
+        struct RemoteGuiTab : armarx::RemoteGui::Client::Tab
+        {
+            armarx::RemoteGui::Client::LineEdit boxLayerName;
+            armarx::RemoteGui::Client::IntSpinBox numBoxes;
+
+            armarx::RemoteGui::Client::Button drawBoxes;
+        };
+        RemoteGuiTab tab;
+        */
+
+        /* (Requires the armarx::ArVizComponentPluginUser.)
+         * When used from different threads, an ArViz client needs to be synchronized.
+        /// Protects the arviz client inherited from the ArViz plugin.
+        std::mutex arvizMutex;
+        */
+
+        PeriodicTask<LaserScannerFeatureExtraction>::pointer_type task;
+
+        std::unique_ptr<FeatureExtractor> featureExtractor;
+        std::unique_ptr<ArVizDrawer> arVizDrawer;
+
+        std::unique_ptr<FrequencyReporter> frequencyReporterPublish;
+        std::unique_ptr<FrequencyReporter> frequencyReporterSubscribe;
+
+        std::unordered_map<std::string, Throttler> throttlers;
+
+        VirtualRobot::RobotPtr robot;
+
+        LaserScannerFeaturesListenerPrx featuresTopic;
+
+        std::variant<std::monostate, VirtualRobot::MathTools::ConvexHull2DPtr, Eigen::Vector2f>
+            robotHull;
+        VirtualRobot::MathTools::ConvexHull2DPtr cableArea;
+
+
+        armem::client::plugins::ReaderWriterPlugin<armem::laser_scans::client::Reader>*
+            laserScannerReaderPlugin = nullptr;
+
+
+        memory::client::laser_scanner_features::Writer laserScannerFeatureWriter;
+    };
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ComponentInterface.ice b/source/armarx/navigation/components/laser_scanner_feature_extraction/ComponentInterface.ice
new file mode 100644
index 0000000000000000000000000000000000000000..2c38043810363d0b8273a1e50e6234a2de2d9cae
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ComponentInterface.ice
@@ -0,0 +1,35 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * package    navigation::LaserScannerFeatureExtraction
+ * author     Tobias Gröger ( tobias dot groeger at student dot kit dot edu )
+ * date       2023
+ * copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *            GNU General Public License
+ */
+
+
+#pragma once
+
+
+module armarx {  module navigation {  module components {  module laser_scanner_feature_extraction
+{
+
+    interface ComponentInterface
+    {
+	// Define your interface here.
+    };
+
+};};};};
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..eea10eeb503434a73080dc9b1092b42a767293ba
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.cpp
@@ -0,0 +1,123 @@
+#include "EnclosingEllipsoid.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+#include <Eigen/src/Geometry/AngleAxis.h>
+
+#include <SimoxUtility/math/periodic/periodic_clamp.h>
+#include <VirtualRobot/math/Helpers.h>
+
+#include "ArmarXCore/core/exceptions/local/ExpressionException.h"
+#include "ArmarXCore/core/logging/Logging.h"
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    // Eigen::Affine2f Ellipsoid::pose() const noexcept
+    // {
+    //     Eigen::Affine2f pose;
+    //     pose.translation() = center;
+    //     pose.linear()      = Eigen::Rotation2Df(angle).toRotationMatrix();
+
+    //     return pose;
+    // }
+
+    // TODO(fabian.reister): use Eigen built-in stuff.
+    size_t
+    argmin(const Eigen::VectorXf& v)
+    {
+        const std::vector<float> vec(v.data(), v.data() + v.rows() * v.cols());
+        return std::distance(std::begin(vec), std::max_element(vec.begin(), vec.end()));
+    }
+
+    // https://github.com/minillinim/ellipsoid/blob/master/ellipsoid.py
+
+    EnclosingEllipsoid::EnclosingEllipsoid(const Points& points)
+    {
+        ARMARX_CHECK(compute(points));
+    }
+
+    bool
+    EnclosingEllipsoid::compute(const Points& points)
+    {
+        const float tolerance = 0.01;
+
+        const int N = static_cast<int>(points.size());
+        const int d = 2;
+
+        Eigen::MatrixXf P(N, d);
+
+        for (int i = 0; i < N; i++)
+        {
+            P.row(i) = points.at(i);
+        }
+
+        Eigen::MatrixXf Q(d + 1, N);
+        Q.topRows(d) = P.transpose();
+        Q.bottomRows(1).setOnes(); // last row
+
+        Eigen::MatrixXf Qt = Q.transpose();
+
+        float err = 1.F + tolerance;
+        Eigen::VectorXf u = (1.F / N) * Eigen::VectorXf::Ones(N);
+
+        // Khachiyan Algorithm
+        while (err > tolerance)
+        {
+            const Eigen::MatrixXf V = Q * (u.asDiagonal() * Qt);
+            const Eigen::VectorXf M = (Qt * (V.inverse() * Q)).diagonal();
+
+            const int j = static_cast<int>(argmin(M));
+            const float maximum = M(j);
+
+            const float stepSize = (maximum - d - 1.0) / ((d + 1.0) * (maximum - 1.0));
+
+            Eigen::VectorXf uNew = (1.0 - stepSize) * u;
+            uNew(j) += stepSize;
+
+            err = (uNew - u).norm();
+            u = uNew;
+        }
+
+        const Eigen::VectorXf center = P.transpose() * u;
+
+        Eigen::MatrixXf K(d, d);
+        for (int i = 0; i < d; i++)
+        {
+            for (int j = 0; j < d; j++)
+            {
+                K(i, j) = center(i) * center(j);
+            }
+        }
+
+        const Eigen::MatrixXf H = (P.transpose() * (u.asDiagonal() * P)) - K;
+        const Eigen::MatrixXf A = H.inverse() / d;
+
+        const Eigen::JacobiSVD svd(A, Eigen::ComputeThinV);
+
+        // angle
+        const Eigen::VectorXf v0 = svd.matrixV().col(0);
+        const float angle = math::Helpers::Angle(v0);
+
+        // radii
+        const Eigen::Vector2f radii = svd.singularValues().cwiseSqrt().cwiseInverse();
+
+        // As the singular values are sorted descending, the radii are sorted ascending.
+        // To fix this, the vector is reversed.
+        this->radii = radii.reverse();
+
+        // Thus, the angle does no longer match the radii. It has to be altered by 90°.
+        const float angle1 = simox::math::periodic_clamp(angle + M_PI_2f32, -M_PIf32, M_PIf32);
+
+        this->pose.setIdentity();
+        this->pose.translation().head<2>() = center;
+        this->pose.linear() =
+            Eigen::AngleAxisf(angle1, Eigen::Vector3f::UnitZ()).toRotationMatrix();
+
+        return true;
+    }
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.h
new file mode 100644
index 0000000000000000000000000000000000000000..277531e7ab181ad3e5a21572ce16e13eaae967de
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.h
@@ -0,0 +1,71 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+
+#include <armarx/navigation/memory/types.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    // struct Ellipsoid
+    // {
+    //     Eigen::Vector2f center;
+    //     float angle;
+
+    //     Eigen::Vector2f radii;
+
+    //     Eigen::Affine2f pose() const noexcept;
+    // };
+
+    using Ellipsoid = memory::Ellipsoid;
+
+    /**
+     * @brief Minimum volume enclosing ellipsoid (MVEE) for a set of points.
+     *
+     * See https://de.mathworks.com/matlabcentral/fileexchange/9542-minimum-volume-enclosing-ellipsoid
+     *
+     */
+    class EnclosingEllipsoid : public Ellipsoid
+    {
+    public:
+        using Point = Eigen::Vector2f;
+        using Points = std::vector<Point>;
+
+        EnclosingEllipsoid(const Points& points);
+
+    private:
+        /**
+         * @brief Computes the enclosing ellipsoid for the given points by using Khachiyan's Algorithm.
+         *
+         * The implementation is based on
+         * https://github.com/minillinim/ellipsoid/blob/master/ellipsoid.py
+         *
+         * @return true
+         * @return false
+         */
+        bool compute(const Points& points);
+    };
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d5c875ec258cc6bb58c8368287ce8cd206e65b07
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.cpp
@@ -0,0 +1,237 @@
+#include "FeatureExtractor.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <numeric>
+#include <optional>
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+#include <Eigen/src/Geometry/AngleAxis.h>
+#include <Eigen/src/Geometry/Transform.h>
+
+#include <pcl/PointIndices.h>
+#include <pcl/point_cloud.h>
+
+#include <opencv2/core/types.hpp>
+#include <opencv2/imgproc.hpp>
+
+#include <VirtualRobot/MathTools.h>
+#include <VirtualRobot/VirtualRobot.h>
+#include <VirtualRobot/math/Helpers.h>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/core/logging/Logging.h>
+
+#include <RobotComponents/libraries/cartographer/util/laser_scanner_conversion.h>
+
+#include "Path.h"
+#include "conversions/eigen.h"
+#include "conversions/opencv.h"
+#include "conversions/opencv_eigen.h"
+#include "conversions/opencv_pcl.h"
+#include "conversions/pcl_eigen.h"
+#include <armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h>
+#include <armarx/navigation/components/laser_scanner_feature_extraction/EnclosingEllipsoid.h>
+#include <armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    // Features
+
+    std::vector<Ellipsoid>
+    Features::linesAsEllipsoids(const float axisLength) const
+    {
+        if (not chain)
+        {
+            return {};
+        }
+
+        const auto asEllipsoid = [&](const Path::Segment& segment) -> Ellipsoid
+        {
+            const Eigen::Vector2f center = (segment.first + segment.second) / 2;
+            const Eigen::Vector2f v = segment.second - center;
+            const float angle = math::Helpers::Angle(v);
+            const float r = v.norm();
+
+            Eigen::Isometry3f globalPose = Eigen::Isometry3f::Identity();
+            globalPose.translation().head<2>() = center;
+            globalPose.linear() =
+                Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitZ()).toRotationMatrix();
+
+            return Ellipsoid{.pose = globalPose, .radii = Eigen::Vector2f{r, axisLength}};
+        };
+
+        const auto segments = Path{.points = *chain}.segments();
+
+        std::vector<Ellipsoid> ellipsoids;
+        std::transform(
+            segments.begin(), segments.end(), std::back_inserter(ellipsoids), asEllipsoid);
+
+        return ellipsoids;
+    }
+
+    // FeatureExtractor
+
+    FeatureExtractor::FeatureExtractor(const ScanClustering::Params& scanClusteringParams,
+                                       const ChainApproximation::Params& chainApproximationParams,
+                                       const Callback& callback) :
+        scanClusteringParams(scanClusteringParams),
+        chainApproximationParams(chainApproximationParams),
+        callback(callback)
+    {
+    }
+
+    void
+    FeatureExtractor::onData(const armem::laser_scans::LaserScanStamped& data)
+    {
+        ARMARX_DEBUG << "on data";
+        const auto clustersWithFeatures = features(data.data);
+
+        ARMARX_DEBUG << "callback";
+        callback(data, clustersWithFeatures);
+    }
+
+    std::vector<Features>
+    FeatureExtractor::features(const LaserScan& scan) const
+    {
+        const auto clusters = detectClusters(scan);
+
+        std::vector<Features> fs;
+        std::transform(clusters.begin(),
+                       clusters.end(),
+                       std::back_inserter(fs),
+                       [&](const LaserScan& cluster)
+                       {
+                           const auto points = toCartesian<Eigen::Vector2f>(cluster);
+                           const auto hull = convexHull(points);
+
+                           return Features{.convexHull = hull,
+                                           .circle = std::nullopt, //circle(points),
+                                           .ellipsoid = std::nullopt, //ellipsoid(hull),
+                                           .chain = chainApproximation(points, hull),
+                                           .points = points};
+                       });
+
+        return fs;
+    }
+
+    std::vector<LaserScan>
+    FeatureExtractor::detectClusters(const LaserScan& scan) const
+    {
+        ScanClustering clustering(scanClusteringParams);
+        return clustering.detectClusters(scan);
+    }
+
+    std::optional<VirtualRobot::MathTools::ConvexHull2D>
+    FeatureExtractor::convexHull(const Points& points) const
+    {
+
+        if (points.size() < 3) // no meaningful area
+        {
+            return std::nullopt;
+        }
+
+        try
+        {
+            return *VirtualRobot::MathTools::createConvexHull2D(points);
+        }
+        catch (std::exception& e)
+        {
+            ARMARX_WARNING << "Couldn't create convex hull: " << e.what();
+        }
+        return std::nullopt;
+    }
+
+    // Legacy OpenCV implementation. Didn't work as expected.
+    // Also, tries to fit an ellipsoid which is NOT the enclosing ellipsoid!
+
+    // std::optional<Ellipsoid> FeatureExtractor::ellipsoid(const PointCloud &pointCloud) const
+    // {
+    //     // OpenCV::fitEllipse requirement
+    //     // "There should be at least 5 points to fit the ellipse"
+    //     // => Too few points will cause algorithmic issues
+    //     if (pointCloud.size() < 100)
+    //     {
+    //         return std::nullopt;
+    //     }
+
+    //     const auto points2f = conversions::pcl2cv(pointCloud);
+    //     const auto points2i = conversions::cast<cv::Point2i>(points2f);
+
+    //     cv::RotatedRect rect = cv::fitEllipse(points2i);
+
+    //     Eigen::Affine2f pose;
+    //     pose.translation() = conversions::cv2eigen(rect.center);
+    //     pose.linear() =
+    //         Eigen::Rotation2Df(VirtualRobot::MathTools::deg2rad(rect.angle)).toRotationMatrix();
+
+    //     return Ellipsoid{.axisLengths = conversions::cv2eigen(rect.size), .pose = pose};
+    // }
+
+    std::optional<Ellipsoid>
+    FeatureExtractor::ellipsoid(
+        const std::optional<VirtualRobot::MathTools::ConvexHull2D>& hull) const
+    {
+
+        if (not hull)
+        {
+            return std::nullopt;
+        }
+
+        // TODO(fabian.reister): it might result in multiple ellipsoids if hull->segments is considered
+
+        // If there are not enough points, don't even try to fit an ellipse ...
+        if (hull->vertices.size() < 5)
+        {
+            return std::nullopt;
+        }
+
+        EnclosingEllipsoid mvee(hull->vertices);
+        return mvee;
+    }
+
+    std::optional<Circle>
+    FeatureExtractor::circle(const Points& points) const
+    {
+        // Too few points will cause algorithmic issues
+        if (points.size() < 5)
+        {
+            return std::nullopt;
+        }
+
+        const auto points2f = conversions::eigen2cv(points);
+        const auto points2i = conversions::cast<cv::Point2i>(points2f);
+
+        cv::Point2f center;
+        float radius = NAN;
+        cv::minEnclosingCircle(points2i, center, radius);
+
+        return Circle{.center = conversions::cv2eigen(center), .radius = radius};
+    }
+
+    std::optional<FeatureExtractor::Points>
+    FeatureExtractor::chainApproximation(
+        const Points& points,
+        const std::optional<VirtualRobot::MathTools::ConvexHull2D>& convexHull) const
+    {
+        if (not convexHull)
+        {
+            return std::nullopt;
+        }
+
+        ChainApproximation chApprx(points, chainApproximationParams);
+        const auto result = chApprx.approximate();
+
+        if (result.condition !=
+            ChainApproximation::ApproximationResult::TerminationCondition::Converged)
+        {
+            return std::nullopt;
+        }
+
+        return chApprx.approximatedChain();
+    }
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.h
new file mode 100644
index 0000000000000000000000000000000000000000..373fc7487b00f90bc6a3f9e40762886d3b57a95d
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.h
@@ -0,0 +1,91 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <optional>
+
+#include <pcl/point_types.h>
+
+#include <VirtualRobot/MathTools.h>
+
+#include <RobotAPI/libraries/armem_laser_scans/types.h>
+
+#include <RobotComponents/interface/components/GraspingManager/RobotPlacementInterface.h>
+
+#include "EnclosingEllipsoid.h"
+#include "ScanClustering.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/ChainApproximation.h"
+#include <armarx/navigation/memory/types.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+    using Circle = memory::Circle;
+
+    struct Features
+    {
+        using Points = std::vector<Eigen::Vector2f>;
+        using Chain = Points;
+
+        std::optional<VirtualRobot::MathTools::ConvexHull2D> convexHull;
+
+        std::optional<Circle> circle;
+        std::optional<Ellipsoid> ellipsoid;
+
+        std::optional<Chain> chain;
+
+        Points points;
+        std::vector<Ellipsoid> linesAsEllipsoids(float axisLength) const;
+    };
+
+    class FeatureExtractor
+    {
+    public:
+        using Points = std::vector<Eigen::Vector2f>;
+        using Callback = std::function<void(const armem::laser_scans::LaserScanStamped& data,
+                                            const std::vector<Features>& features)>;
+
+        FeatureExtractor(const ScanClustering::Params& scanClusteringParams,
+                         const ChainApproximation::Params& chainApproximationParams,
+                         const Callback& callback);
+
+        void onData(const armem::laser_scans::LaserScanStamped& data);
+
+    private:
+        std::vector<Features> features(const LaserScan& scan) const;
+
+        std::vector<LaserScan> detectClusters(const LaserScan& scan) const;
+
+        std::optional<VirtualRobot::MathTools::ConvexHull2D> convexHull(const Points& points) const;
+        std::optional<Ellipsoid>
+        ellipsoid(const std::optional<VirtualRobot::MathTools::ConvexHull2D>& hull) const;
+        std::optional<Circle> circle(const Points& circle) const;
+
+        std::optional<Points> chainApproximation(
+            const Points& points,
+            const std::optional<VirtualRobot::MathTools::ConvexHull2D>& convexHull) const;
+
+        const ScanClustering::Params scanClusteringParams;
+        const ChainApproximation::Params chainApproximationParams;
+
+        const Callback callback;
+    };
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d57cae5caa509a13b7271d43b9c1adb7e8ea377c
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.cpp
@@ -0,0 +1,24 @@
+#include "Path.h"
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    std::vector<Path::Segment>
+    Path::segments() const noexcept
+    {
+        if (points.size() <= 1)
+        {
+            return {};
+        }
+
+        std::vector<Segment> segments;
+        segments.reserve(static_cast<int>(points.size()) - 1);
+
+        for (int i = 0; i < static_cast<int>(points.size()) - 1; i++)
+        {
+            segments.emplace_back(points.at(i), points.at(i + 1));
+        }
+
+        return segments;
+    }
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.h
new file mode 100644
index 0000000000000000000000000000000000000000..2cf3a4134e580ee2e7558e42e5a36dff9fa8aae5
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/Path.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <vector>
+
+#include <Eigen/Core>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+
+    struct Path
+    {
+        std::vector<Eigen::Vector2f> points;
+
+        using Segment = std::pair<Eigen::Vector2f, Eigen::Vector2f>;
+
+        std::vector<Segment> segments() const noexcept;
+    };
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..32503968e0ba7e865cea541f25cc03c9a4bbde59
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.cpp
@@ -0,0 +1,84 @@
+#include "ScanClustering.h"
+
+#include <RobotAPI/interface/units/LaserScannerUnit.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+    ScanClustering::ScanClustering(const Params& params) : params(params)
+    {
+    }
+
+    bool
+    ScanClustering::add(const LaserScanStep& scanStep)
+    {
+        if (scan.empty())
+        {
+            scan.push_back(scanStep);
+            return true;
+        }
+
+        if (not supports(scanStep))
+        {
+            return false;
+        }
+
+        scan.push_back(scanStep);
+        return true;
+    }
+
+    std::vector<LaserScan>
+    ScanClustering::detectClusters(const LaserScan& scan)
+    {
+        const auto isInvalid = [&](const LaserScanStep& step) -> bool
+        { return step.distance > params.maxDistance; };
+
+        std::vector<LaserScan> clusters;
+        for (const auto& laserScanStep : scan)
+        {
+            // cluster finished
+            if (isInvalid(laserScanStep) or (not add(laserScanStep)))
+            {
+                if (not cluster().empty())
+                {
+                    clusters.push_back(cluster());
+                    clear();
+
+                    // work on a new cluster
+                    add(laserScanStep);
+                }
+            }
+        }
+
+        return clusters;
+    }
+
+    const LaserScan&
+    ScanClustering::cluster() const
+    {
+        return scan;
+    }
+
+    void
+    ScanClustering::clear()
+    {
+        scan.clear();
+    }
+
+    bool
+    ScanClustering::supports(const LaserScanStep& scanStep)
+    {
+        // OK to create a new cluster if it's empty
+        if (scan.empty())
+        {
+            return true;
+        }
+
+        const float distanceDiff = std::fabs(scanStep.distance - scan.back().distance);
+        const bool isWithinDistanceRange = distanceDiff < params.distanceThreshold;
+
+        const float angleDiff = std::fabs(scanStep.angle - scan.back().angle);
+        const bool isWithinAngleRange = angleDiff < params.angleThreshold;
+
+        return (isWithinDistanceRange and isWithinAngleRange);
+    }
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.h
new file mode 100644
index 0000000000000000000000000000000000000000..1159c2c648057ae5275a5989d90bf150f09a8893
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/ScanClustering.h
@@ -0,0 +1,71 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <RobotAPI/interface/units/LaserScannerUnit.h>
+
+namespace armarx::navigation::components::laser_scanner_feature_extraction
+{
+    namespace detail
+    {
+        struct ScanClusteringParams
+        {
+            // Clustering options to decide whether a point belongs to the cluster
+            float distanceThreshold; // [mm]
+            float angleThreshold; // [rad]
+
+            /// Range filter: only consider points that are closer than maxDistance
+            float maxDistance; // [mm]
+        };
+    } // namespace detail
+
+    class ScanClustering
+    {
+    public:
+        using Params = detail::ScanClusteringParams;
+        using Clusters = std::vector<LaserScan>;
+
+        ScanClustering(const Params& params);
+
+        /**
+        * @brief Performs cluster detection on a full laser scan.
+        *
+        * @param scan A full scan
+        * @return The input scan split into clusters.
+        */
+        Clusters detectClusters(const LaserScan& scan);
+
+    protected:
+        const LaserScan& cluster() const;
+
+        bool add(const LaserScanStep& scanStep);
+        bool supports(const LaserScanStep& scanStep);
+
+        void clear();
+
+    private:
+        LaserScan scan;
+
+        const Params params;
+    };
+
+} // namespace armarx::navigation::components::laser_scanner_feature_extraction
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..41af90f4e15b87cecec5d046697ba5897e9e8c5d
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.cpp
@@ -0,0 +1,23 @@
+#include "eigen.h"
+
+#include <algorithm>
+
+namespace armarx::conversions
+{
+
+    std::vector<Eigen::Vector3f> to3D(const std::vector<Eigen::Vector2f>& v)
+
+    {
+        std::vector<Eigen::Vector3f> v3;
+        v3.reserve(v.size());
+
+        std::transform(
+            v.begin(),
+            v.end(),
+            std::back_inserter(v3),
+            static_cast<Eigen::Vector3f (*)(const Eigen::Vector2f&)>(&to3D));
+
+        return v3;
+    }
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.h
new file mode 100644
index 0000000000000000000000000000000000000000..615d41c6d8e80a0f5ff2390079f3627d5281206a
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/eigen.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <Eigen/Core>
+#include <Eigen/Geometry>
+
+namespace armarx::conversions
+{
+
+    inline Eigen::Vector2f to2D(const Eigen::Vector3f& v2)
+    {
+        return Eigen::Vector2f{v2.x(), v2.y()};
+    }
+
+    inline Eigen::Vector3f to3D(const Eigen::Vector2f& v2)
+    {
+        return Eigen::Vector3f{v2.x(), v2.y(), 0.F};
+    }
+
+    inline Eigen::Isometry3f to3D(const Eigen::Affine2f& p2)
+    {
+        Eigen::Isometry3f pose            = Eigen::Isometry3f::Identity();
+        pose.linear().block<2, 2>(0, 0) = p2.linear();
+        pose.translation().head<2>()    = p2.translation();
+
+        return pose;
+    }
+
+    std::vector<Eigen::Vector3f> to3D(const std::vector<Eigen::Vector2f>& v);
+
+} // namespace armarx::conversions
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv.h
new file mode 100644
index 0000000000000000000000000000000000000000..ae12d627f1438237d4cc05069ba544ae3648073b
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv.h
@@ -0,0 +1,47 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <opencv2/core/types.hpp>
+
+namespace armarx::conversions
+{
+
+    // TODO(fabian.reister): this is a specialized method
+    template <typename CvT> CvT cast(const auto &pt) { return CvT(pt.x, pt.y); }
+
+    template <typename PointOutT, typename PointInT>
+    std::vector<PointOutT> cast(const std::vector<PointInT> &points)
+    {
+        std::vector<PointOutT> v;
+        v.reserve(points.size());
+
+        std::transform(points.begin(),
+                       points.end(),
+                       std::back_inserter(v),
+                       static_cast<PointOutT (*)(const PointInT &)>(cast));
+
+        return v;
+    }
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_eigen.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_eigen.h
new file mode 100644
index 0000000000000000000000000000000000000000..0e6b818219a8f143fc88f523800a850f275eba53
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_eigen.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <Eigen/Core>
+
+#include <opencv2/core/types.hpp>
+
+namespace armarx::conversions
+{
+    inline cv::Point2f eigen2cv(const Eigen::Vector2f& pt)
+    {
+        return {pt.x(), pt.y()};
+    }
+
+    template <typename EigenT, typename CvT = decltype(eigen2cv(EigenT()))>
+    auto eigen2cv(const std::vector<EigenT>& points)
+    {
+        std::vector<CvT> v;
+        v.reserve(points.size());
+
+        std::transform(
+            points.begin(), points.end(), std::back_inserter(v), static_cast<CvT(*)(const EigenT&)>(eigen2cv));
+
+        return v;
+    }
+
+    inline Eigen::Vector2f cv2eigen(const cv::Point2f& pt)
+    {
+        return Eigen::Vector2f{pt.x, pt.y};
+    }
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_pcl.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_pcl.h
new file mode 100644
index 0000000000000000000000000000000000000000..375f166f08b391a9462b7bb8fc2ebf7d8b7ff530
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/opencv_pcl.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <Eigen/Core>
+
+#include <pcl/point_cloud.h>
+#include <pcl/point_types.h>
+
+#include <opencv2/core/types.hpp>
+
+namespace armarx::conversions
+{
+
+    inline cv::Point2f pcl2cv(const pcl::PointXY& pt)
+    {
+        return cv::Point2f{pt.x, pt.y};
+    }
+    inline cv::Point3f pcl2cv(const pcl::PointXYZ& pt)
+    {
+        return cv::Point3f{pt.x, pt.y, pt.z};
+    }
+
+    template <typename PointT, typename CvT = decltype(pcl2cv(PointT()))>
+    auto pcl2cv(const pcl::PointCloud<PointT>& pointCloud)
+    {
+        std::vector<CvT> v;
+        v.reserve(pointCloud.points.size());
+
+        std::transform(pointCloud.points.begin(),
+                       pointCloud.points.end(),
+                       std::back_inserter(v),
+                       static_cast<CvT (*)(const PointT&)>(pcl2cv));
+
+        return v;
+    }
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c8ed47f21519ffcf26d40a55bd8a288118a0a25f
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.cpp
@@ -0,0 +1,27 @@
+#include "pcl.h"
+
+#include <algorithm>
+
+namespace armarx::conversions
+{
+    pcl::PointCloud<pcl::PointXYZ> to3D(const pcl::PointCloud<pcl::PointXY>& v)
+    {
+        pcl::PointCloud<pcl::PointXYZ> pc;
+        pc.points.reserve(v.size());
+
+        std::transform(
+            v.begin(),
+            v.end(),
+            std::back_inserter(pc),
+            static_cast<pcl::PointXYZ (*)(const pcl::PointXY&)>(&to3D));
+
+        pc.width    = v.height;
+        pc.height   = v.width;
+        pc.is_dense = v.is_dense;
+
+        pc.header = v.header;
+
+        return pc;
+    }
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.h
new file mode 100644
index 0000000000000000000000000000000000000000..2caf92b95541f850b0db004c78fa8751e8faf751
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <pcl/point_cloud.h>
+#include <pcl/point_types.h>
+
+namespace armarx::conversions
+{
+
+    inline pcl::PointXYZ to3D(const pcl::PointXY& v2)
+    {
+        return pcl::PointXYZ{v2.x, v2.y, 0.F};
+    }
+
+    pcl::PointCloud<pcl::PointXYZ> to3D(const pcl::PointCloud<pcl::PointXY>& v);
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl_eigen.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl_eigen.h
new file mode 100644
index 0000000000000000000000000000000000000000..b72277e033c4a51e374d0e9ba87cc12aad48b6b7
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/conversions/pcl_eigen.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <algorithm>
+
+#include <Eigen/Core>
+
+#include <pcl/point_cloud.h>
+#include <pcl/point_types.h>
+
+namespace armarx::conversions
+{
+    // pcl2eigen
+
+    inline Eigen::Vector2f pcl2eigen(const pcl::PointXY& pt)
+    {
+        return Eigen::Vector2f{pt.x, pt.y};
+    }
+    inline Eigen::Vector3f pcl2eigen(const pcl::PointXYZ& pt)
+    {
+        return Eigen::Vector3f{pt.x, pt.y, pt.z};
+    }
+
+    template <typename PointT,
+              typename EigenVector = decltype(pcl2eigen(PointT()))>
+    auto pcl2eigen(const pcl::PointCloud<PointT>& pointCloud)
+    -> std::vector<EigenVector>
+    {
+        std::vector<EigenVector> v;
+        v.reserve(pointCloud.points.size());
+
+        std::transform(pointCloud.points.begin(),
+                       pointCloud.points.end(),
+                       std::back_inserter(v),
+                       static_cast<EigenVector(*)(const PointT&)>(pcl2eigen));
+
+        return v;
+    }
+
+    // eigen2pcl
+
+    inline pcl::PointXY eigen2pcl(const Eigen::Vector2f& pt)
+    {
+        return pcl::PointXY{pt.x(), pt.y()};
+    }
+
+    inline pcl::PointXYZ eigen2pcl(const Eigen::Vector3f& pt)
+    {
+        return pcl::PointXYZ{pt.x(), pt.y(), 0.F};
+    }
+
+    template <typename EigenVector,
+              typename PointT = decltype(eigen2pcl(EigenVector()))>
+    auto eigen2pcl(const std::vector<EigenVector>& points)
+    -> pcl::PointCloud<PointT>
+    {
+        pcl::PointCloud<PointT> pointCloud;
+        pointCloud.points.reserve(points.size());
+
+        std::transform(points.begin(),
+                       points.end(),
+                       std::back_inserter(pointCloud.points),
+                       static_cast<PointT(*)(const EigenVector&)>(eigen2pcl));
+
+        pointCloud.width    = pointCloud.points.size();
+        pointCloud.height   = 1;
+        pointCloud.is_dense = true;
+
+        return pointCloud;
+    }
+
+} // namespace armarx::conversions
\ No newline at end of file
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/geometry.h b/source/armarx/navigation/components/laser_scanner_feature_extraction/geometry.h
new file mode 100644
index 0000000000000000000000000000000000000000..8b70a56da0dcd6ba78b76b3c3ec187872d66f0c1
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/geometry.h
@@ -0,0 +1,57 @@
+#pragma once
+
+// TODO To be moved to Simox after ROBDEKON demo
+
+#include <boost/geometry.hpp>
+#include <boost/geometry/algorithms/assign.hpp>
+#include <boost/geometry/algorithms/detail/within/interface.hpp>
+#include <boost/geometry/algorithms/distance.hpp>
+#include <boost/geometry/algorithms/make.hpp>
+#include <boost/geometry/geometries/point_xy.hpp>
+#include <boost/geometry/geometries/polygon.hpp>
+#include <boost/geometry/geometries/register/point.hpp>
+#include <boost/geometry/strategies/strategies.hpp>
+
+#include <VirtualRobot/MathTools.h>
+
+BOOST_GEOMETRY_REGISTER_POINT_2D(Eigen::Vector2f, float, cs::cartesian, x(), y())
+
+namespace armarx::geometry
+{
+    namespace bg = boost::geometry;
+
+    class ConvexHull
+    {
+    public:
+        using Point = bg::model::d2::point_xy<float>;
+
+        ConvexHull(const VirtualRobot::MathTools::ConvexHull2D& hull) : ConvexHull(hull.vertices)
+        {
+        }
+
+        ConvexHull(const std::vector<Eigen::Vector2f>& hull)
+        {
+            boost::geometry::assign_points(polygon, hull);
+        }
+
+        bool contains(const Eigen::Vector2f& point) const
+        {
+            return bg::within(Point(point.x(), point.y()), polygon);
+        }
+
+        float area() const
+        {
+            return static_cast<float>(bg::area(polygon));
+        }
+
+    private:
+        bg::model::polygon<Point> polygon;
+    };
+
+    // inline float area()
+    // {
+
+    //     return bg::area(polygon);
+    // }
+
+} // namespace armarx::geometry
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/test/CMakeLists.txt b/source/armarx/navigation/components/laser_scanner_feature_extraction/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3435a5f51ea072c80f48513c034d54f7c3d800fb
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+
+# Libs required for the tests
+SET(LIBS ${LIBS} ArmarXCore LaserScannerFeatureExtraction)
+ 
+armarx_add_test(LaserScannerFeatureExtractionTest LaserScannerFeatureExtractionTest.cpp "${LIBS}")
diff --git a/source/armarx/navigation/components/laser_scanner_feature_extraction/test/LaserScannerFeatureExtractionTest.cpp b/source/armarx/navigation/components/laser_scanner_feature_extraction/test/LaserScannerFeatureExtractionTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..db413af6aa13db704fc06decbeadba0f59151d19
--- /dev/null
+++ b/source/armarx/navigation/components/laser_scanner_feature_extraction/test/LaserScannerFeatureExtractionTest.cpp
@@ -0,0 +1,65 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    RobotComponents::ArmarXObjects::LaserScannerFeatureExtraction
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "ArmarXCore/core/logging/Logging.h"
+#include "armarx/navigation/components/laser_scanner_feature_extraction/FeatureExtractor.h"
+#include <boost/test/tools/old/interface.hpp>
+#define BOOST_TEST_MODULE RobotComponents::ArmarXObjects::LaserScannerFeatureExtraction
+
+#define ARMARX_BOOST_TEST
+
+#include <RobotComponents/Test.h>
+
+#include <pcl/point_types.h>
+
+#include "../EnclosingEllipsoid.h"
+
+#include <iostream>
+
+BOOST_AUTO_TEST_CASE(testExample)
+{
+    // Ground truth (GT) ellipsoid with params:
+    //  center = (0,0)
+    //  radii = (4,2)
+    //  angle = 0
+    armarx::laser_scanner_feature_extraction::FeatureExtractor::Points points;
+    points.push_back(Eigen::Vector2f{-4.F, 0});
+    points.push_back(Eigen::Vector2f{0, 2.F});
+    points.push_back(Eigen::Vector2f{4.F, 0});
+    points.push_back(Eigen::Vector2f{0, -2.F});
+
+    const Eigen::Vector2f radii(4, 2);
+
+    armarx::laser_scanner_feature_extraction::EnclosingEllipsoid ee(points);
+
+    // The computed ellipsoid must contain the GT ellipsoid
+    BOOST_CHECK_GE(ee.radii.x(), 4.F);
+    BOOST_CHECK_GE(ee.radii.y(), 2.F);
+
+    // ... but should not be too large
+    BOOST_CHECK_LE(ee.radii.x(), 4.1F);
+    BOOST_CHECK_LE(ee.radii.y(), 2.1F);
+
+    // The computed ellipsoid must be roughly at the position of the GT ellipsoid
+    BOOST_CHECK_LT(std::fabs(ee.pose.translation().x()), 0.1);
+    BOOST_CHECK_LT(std::fabs(ee.pose.translation().y()), 0.1);
+}
diff --git a/source/armarx/navigation/components/navigation_memory/Component.cpp b/source/armarx/navigation/components/navigation_memory/Component.cpp
index 22f674ca75fbe8e24770ad6cfb47d12efa814cee..e7f220c11398fcea3a8f6704e907f2552691ad3c 100644
--- a/source/armarx/navigation/components/navigation_memory/Component.cpp
+++ b/source/armarx/navigation/components/navigation_memory/Component.cpp
@@ -55,6 +55,7 @@
 #include <armarx/navigation/human/aron/Human.aron.generated.h>
 #include <armarx/navigation/graph/constants.h>
 #include <armarx/navigation/location/constants.h>
+#include <armarx/navigation/memory/aron/LaserScannerFeatures.aron.generated.h>
 #include <armarx/navigation/memory/constants.h>
 #include <armarx/navigation/rooms/aron/Room.aron.generated.h>
 
@@ -153,6 +154,11 @@ namespace armarx::navigation::components::navigation_memory
         workingMemory().addCoreSegment(memory::constants::HumanCoreSegmentName,
                                        navigation::human::arondto::Human::ToAronType());
 
+        workingMemory()
+            .addCoreSegment(memory::constants::LaserScannerFeaturesCoreSegment,
+                            navigation::memory::arondto::LaserScannerFeatures::ToAronType())
+            .setMaxHistorySize(properties.laserScannerFeaturesMaxHistorySize);
+
         // workingMemory().addCoreSegment(memory::constants::HumanGroupCoreSegmentName,
         //                                navigation::human::arondto::Human::ToAronType());
         workingMemory().addCoreSegment(navigation::rooms::coreSegmentID.coreSegmentName,
diff --git a/source/armarx/navigation/components/navigation_memory/Component.h b/source/armarx/navigation/components/navigation_memory/Component.h
index 77220afebd7eafae930ab19c7a436f35ee641d08..bc359652c277c5e1e5e6d9fe6ee2fbd041158ddb 100644
--- a/source/armarx/navigation/components/navigation_memory/Component.h
+++ b/source/armarx/navigation/components/navigation_memory/Component.h
@@ -99,6 +99,8 @@ namespace armarx::navigation::components::navigation_memory
         {
             std::string snapshotToLoad = "";
 
+            long laserScannerFeaturesMaxHistorySize = 20;
+
             struct LocationGraph
             {
                 bool visuLocations = true;
diff --git a/source/armarx/navigation/components/navigator/Component.cpp b/source/armarx/navigation/components/navigator/Component.cpp
index 3943986f1cccd1496240f844b0d3f08a8f714478..424e28511cb9974c05cb428e41884c857911de23 100644
--- a/source/armarx/navigation/components/navigator/Component.cpp
+++ b/source/armarx/navigation/components/navigator/Component.cpp
@@ -168,6 +168,7 @@ namespace armarx::navigation::components::navigator
                 .costmapReader = &costmapReaderPlugin->get(),
                 .virtualRobotReader = &virtualRobotReaderPlugin->get(),
                 .humanReader = &humanReaderPlugin->get(),
+                .laserScannerFeaturesReader = &laserScannerFeaturesReaderPlugin->get(),
                 .objectPoseClient = ObjectPoseClientPluginUser::getClient()};
 
             sceneProvider.emplace(srv, params.sceneCfg);
@@ -235,7 +236,6 @@ namespace armarx::navigation::components::navigator
         return "navigator";
     }
 
-
     void
     Component::createConfig(const aron::data::dto::DictPtr& stackConfig,
                             const std::string& callerId,
@@ -293,7 +293,8 @@ namespace armarx::navigation::components::navigator
     {
         ARMARX_TRACE;
 
-        ARMARX_INFO << "moveTo() requested by caller '" << callerId << "' with navigation mode `" << navigationMode << "`.";
+        ARMARX_INFO << "moveTo() requested by caller '" << callerId << "' with navigation mode `"
+                    << navigationMode << "`.";
         ARMARX_CHECK(navigators.count(callerId) > 0)
             << "Navigator config for caller `" << callerId << "` not registered!";
 
@@ -345,7 +346,6 @@ namespace armarx::navigation::components::navigator
                                        core::NavigationFrameNames.from_name(navigationMode));
     }
 
-
     void
     Component::moveTo2(const client::detail::Waypoints& waypoints,
                        const std::string& navigationMode,
@@ -363,13 +363,15 @@ namespace armarx::navigation::components::navigator
 
         navigators.at(callerId).moveTo(wps, core::NavigationFrameNames.from_name(navigationMode));
     }
-    
-    void Component::moveToLocation(const std::string& location,
-                         const std::string& callerId,
-                         const Ice::Current& c)
+
+    void
+    Component::moveToLocation(const std::string& location,
+                              const std::string& callerId,
+                              const Ice::Current& c)
     {
         ARMARX_TRACE;
-        ARMARX_INFO << "MoveToLocation `" << location << "` requested by caller '" << callerId << "'";
+        ARMARX_INFO << "MoveToLocation `" << location << "` requested by caller '" << callerId
+                    << "'";
 
         ARMARX_CHECK(navigators.count(callerId) > 0)
             << "Navigator config for caller `" << callerId << "` not registered!";
@@ -501,9 +503,12 @@ namespace armarx::navigation::components::navigator
                       "but won't execute it.");
 
         def->required(params.sceneCfg.robotName, "p.scene.robotName");
-        def->optional(params.sceneCfg.staticCostmapProviderName, "p.scene.staticCostmapProviderName");
+        def->optional(params.sceneCfg.staticCostmapProviderName,
+                      "p.scene.staticCostmapProviderName");
         def->optional(params.sceneCfg.staticCostmapName, "p.scene.staticCostmapName");
         def->optional(params.sceneCfg.humanProviderName, "p.scene.humanProviderName");
+        def->optional(params.sceneCfg.laserScannerFeaturesProviderName,
+                      "p.scene.laserScannerFeaturesProviderName");
 
         return def;
     }
diff --git a/source/armarx/navigation/components/navigator/Component.h b/source/armarx/navigation/components/navigator/Component.h
index aac593f1e2ef8bb2bf405bfa2dff65cffeff3e58..b8526d92dc78d976ee3e1a45686685fcc522d90c 100644
--- a/source/armarx/navigation/components/navigator/Component.h
+++ b/source/armarx/navigation/components/navigator/Component.h
@@ -51,11 +51,11 @@
 #include <RobotAPI/libraries/armem_vision/client/occupancy_grid/Reader.h>
 
 #include <armarx/control/client/ComponentPlugin.h>
-
-#include <armarx/navigation/memory/client/human/Reader.h>
 #include <armarx/navigation/components/navigator/RemoteGui.h>
 #include <armarx/navigation/core/types.h>
 #include <armarx/navigation/memory/client/graph/Reader.h>
+#include <armarx/navigation/memory/client/human/Reader.h>
+#include <armarx/navigation/memory/client/laser_scanner_features/Reader.h>
 #include <armarx/navigation/memory/client/parameterization/Reader.h>
 #include <armarx/navigation/memory/client/stack_result/Writer.h>
 #include <armarx/navigation/server/Navigator.h>
@@ -66,7 +66,6 @@
 #include <armarx/navigation/server/parameterization/MemoryParameterizationService.h>
 #include <armarx/navigation/server/scene_provider/SceneProvider.h>
 
-
 namespace armarx::navigation::components::navigator
 {
 
@@ -115,13 +114,13 @@ namespace armarx::navigation::components::navigator
                      const Ice::Current& c = Ice::emptyCurrent) override;
 
         void moveToLocation(const std::string& location,
-                     const std::string& callerId,
-                     const Ice::Current& c = Ice::emptyCurrent) override;
+                            const std::string& callerId,
+                            const Ice::Current& c = Ice::emptyCurrent) override;
 
         void updateMoveTo(const std::vector<Eigen::Matrix4f>& waypoints,
-                    const std::string& navigationMode,
-                    const std::string& callerId,
-                    const Ice::Current& c = Ice::emptyCurrent) override;
+                          const std::string& navigationMode,
+                          const std::string& callerId,
+                          const Ice::Current& c = Ice::emptyCurrent) override;
 
         void moveTowards(const Eigen::Vector3f& direction,
                          const std::string& navigationMode,
@@ -208,6 +207,8 @@ namespace armarx::navigation::components::navigator
             costmapReaderPlugin = nullptr;
         armem::client::plugins::ReaderWriterPlugin<memory::client::human::Reader>*
             humanReaderPlugin = nullptr;
+        armem::client::plugins::ReaderWriterPlugin<memory::client::laser_scanner_features::Reader>*
+            laserScannerFeaturesReaderPlugin = nullptr;
 
         // armem::vision::occupancy_grid::client::Reader occupancyGridReader;
 
diff --git a/source/armarx/navigation/core/DynamicScene.h b/source/armarx/navigation/core/DynamicScene.h
index 172957d00e1adb5b42a1bb47ca9fea7f80083b17..66f3170748191d475ff0bd2088f04c78a56fea15 100644
--- a/source/armarx/navigation/core/DynamicScene.h
+++ b/source/armarx/navigation/core/DynamicScene.h
@@ -24,6 +24,7 @@
 
 
 #include <armarx/navigation/human/types.h>
+#include <armarx/navigation/memory/types.h>
 
 namespace armarx::navigation::core
 {
@@ -31,6 +32,7 @@ namespace armarx::navigation::core
     struct DynamicScene
     {
         human::Humans humans;
+        std::vector<memory::LaserScannerFeatures> laserScannerFeatures;
     };
 
 } // namespace armarx::navigation::core
diff --git a/source/armarx/navigation/human/HumanTracker.h b/source/armarx/navigation/human/HumanTracker.h
index 1f27aed75375018ce613579ee07f4577564b4249..32c25cc4f3c050b54490f6f5e72e48a6d4ffa7a9 100644
--- a/source/armarx/navigation/human/HumanTracker.h
+++ b/source/armarx/navigation/human/HumanTracker.h
@@ -24,7 +24,6 @@
 
 #include <ArmarXCore/core/time.h>
 
-#include <RobotAPI/libraries/armem_vision/types.h>
 #include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h>
 
 #include <VisionX/libraries/armem_human/types.h>
@@ -33,11 +32,11 @@
 #include <armarx/navigation/human/HumanFilter.h>
 #include <armarx/navigation/human/HumanSystemModel.h>
 #include <armarx/navigation/human/types.h>
+#include <armarx/navigation/memory/types.h>
 
 namespace armarx::navigation::human
 {
-    using Cluster = armem::vision::LaserScannerFeature;
-
+    using Cluster = memory::LaserScannerFeature;
 
     /**
      * @brief The HumanTracker class can be used to track and filter multiple humans. It hides
@@ -115,6 +114,7 @@ namespace armarx::navigation::human
             // the old) velocity should be weighted when calculating the new velocity
             float velocityAlpha = 0.7;
         };
+
         /**
          * @brief HumanTracker::update Updates the tracked humans with the human measurements from a camera. When a
          * measurement is close enough to an existing tracked human, they are associated, otherwise a
diff --git a/source/armarx/navigation/memory/CMakeLists.txt b/source/armarx/navigation/memory/CMakeLists.txt
index c03f9a2b8fb622325659fd65c4ac9016fd22bfaf..c3910e067f645d1a066c478c81f702735c7afff8 100644
--- a/source/armarx/navigation/memory/CMakeLists.txt
+++ b/source/armarx/navigation/memory/CMakeLists.txt
@@ -1,6 +1,12 @@
+armarx_add_aron_library(memory_aron
+    ARON_FILES
+        aron/LaserScannerFeatures.xml
+)
+
 armarx_add_library(memory
     SOURCES
         #./memory.cpp
+        aron_conversions.cpp
         client/stack_result/Writer.cpp
         client/stack_result/Reader.cpp
         client/parameterization/Reader.cpp
@@ -11,10 +17,13 @@ armarx_add_library(memory
         client/costmap/Reader.cpp
         client/human/Reader.cpp
         client/human/Writer.cpp
+        client/laser_scanner_features/Reader.cpp
+        client/laser_scanner_features/Writer.cpp
         client/rooms/Reader.cpp
         # ./client/events/Reader.cpp
     HEADERS
         memory.h
+        aron_conversions.h
         client/stack_result/Writer.h
         client/stack_result/Reader.h
         client/parameterization/Reader.h
@@ -25,12 +34,16 @@ armarx_add_library(memory
         client/costmap/Reader.h
         client/human/Reader.h
         client/human/Writer.h
+        client/laser_scanner_features/Reader.h
+        client/laser_scanner_features/Writer.h
         client/rooms/Reader.h
         # ./client/events/Reader.h
+        types.h
     DEPENDENCIES
         ArmarXCoreInterfaces
         ArmarXCore
         armem
+        armarx_navigation::memory_aron
         armarx_navigation::core
         armarx_navigation::algorithms
         armarx_navigation::graph
diff --git a/source/armarx/navigation/memory/aron/LaserScannerFeatures.xml b/source/armarx/navigation/memory/aron/LaserScannerFeatures.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f78dea48892fab0e2bb8a477eaedc2425a484fc8
--- /dev/null
+++ b/source/armarx/navigation/memory/aron/LaserScannerFeatures.xml
@@ -0,0 +1,67 @@
+<!--Some fancy comment -->
+<?xml version="1.0" encoding="UTF-8" ?>
+<AronTypeDefinition>
+    <CodeIncludes>
+    </CodeIncludes>
+    <AronIncludes>
+    </AronIncludes>
+
+    <GenerateTypes>
+
+        <Object name='armarx::navigation::memory::arondto::Ellipsoid'>
+            <ObjectChild key='globalPose'>
+                <Pose />
+            </ObjectChild>
+            <ObjectChild key='radii'>
+                <Matrix rows="2" cols="1" type="float32" />
+            </ObjectChild>
+        </Object>
+
+        <Object name='armarx::navigation::memory::arondto::Circle'>
+            <ObjectChild key='center'>
+                <Matrix rows="2" cols="1" type="float32" />
+            </ObjectChild>
+            <ObjectChild key='radius'>
+                <float />
+            </ObjectChild>
+        </Object>
+
+        <Object name="armarx::navigation::memory::arondto::LaserScannerFeature">
+            <ObjectChild key="convexHull">
+                <List>
+                    <Matrix rows="2" cols="1" type="float32" />
+                </List>
+            </ObjectChild>
+            <ObjectChild key="circle">
+                <armarx::navigation::memory::arondto::Circle/>
+            </ObjectChild>
+            <ObjectChild key="ellipsoid">
+                <armarx::navigation::memory::arondto::Ellipsoid/>
+            </ObjectChild>
+            <!-- <ObjectChild key="chain">
+                <armarx::navigation::memory::arondto::Chain/>
+            </ObjectChild> -->
+            <ObjectChild key="points">
+                <List>
+                    <Matrix rows="2" cols="1" type="float32" />
+                </List>
+            </ObjectChild>
+        </Object>
+
+         <Object name="armarx::navigation::memory::arondto::LaserScannerFeatures">
+            <ObjectChild key="frame">
+                <String/>
+            </ObjectChild>
+            <ObjectChild key="frameGlobalPose">
+                <Pose/>
+            </ObjectChild>
+            <ObjectChild key="features">
+                <List>
+                    <armarx::navigation::memory::arondto::LaserScannerFeature />
+                </List>
+            </ObjectChild>
+        </Object>
+
+
+    </GenerateTypes>
+</AronTypeDefinition> 
diff --git a/source/armarx/navigation/memory/aron_conversions.cpp b/source/armarx/navigation/memory/aron_conversions.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bfc55c27da1c04e4ae9f3e35c8ac8c4559216d04
--- /dev/null
+++ b/source/armarx/navigation/memory/aron_conversions.cpp
@@ -0,0 +1,92 @@
+#include "aron_conversions.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <iterator>
+
+#include <RobotAPI/libraries/aron/common/aron_conversions.h>
+#include <RobotAPI/libraries/aron/converter/common/Converter.h>
+#include <RobotAPI/libraries/aron/core/data/variant/complex/NDArray.h>
+
+#include "types.h"
+#include <armarx/navigation/memory/aron/LaserScannerFeatures.aron.generated.h>
+
+namespace armarx::navigation::memory
+{
+
+
+    /************ toAron ************/
+
+    // auto toAron(const LaserScan& laserScan, aron::LaserScan& aronLaserScan)
+    // {
+    //     aronLaserScan.scan = toAron(laserScan);
+    // }
+
+
+    // LaserScannerFeatures
+
+    void
+    toAron(arondto::Circle& dto, const Circle& bo)
+    {
+        dto.center = bo.center;
+        dto.radius = bo.radius;
+    }
+
+    void
+    toAron(arondto::Ellipsoid& dto, const Ellipsoid& bo)
+    {
+        dto.globalPose = bo.pose.matrix();
+        dto.radii = bo.radii;
+    }
+
+    void
+    toAron(arondto::LaserScannerFeature& dto, const LaserScannerFeature& bo)
+    {
+        toAron(dto.circle, bo.circle);
+        dto.convexHull = bo.convexHull;
+        toAron(dto.ellipsoid, bo.ellipsoid);
+        dto.points = bo.points;
+    }
+
+    void
+    toAron(arondto::LaserScannerFeatures& dto, const LaserScannerFeatures& bo)
+    {
+        aron::toAron(dto.frame, bo.frame);
+        aron::toAron(dto.frameGlobalPose, bo.frameGlobalPose);
+        aron::toAron(dto.features, bo.features);
+    }
+
+    void
+    fromAron(const arondto::Circle& dto, Circle& bo)
+    {
+        bo.center = dto.center;
+        bo.radius = dto.radius;
+    }
+
+    void
+    fromAron(const arondto::Ellipsoid& dto, Ellipsoid& bo)
+    {
+        bo.pose = dto.globalPose;
+        bo.radii = dto.radii;
+    }
+
+    void
+    fromAron(const arondto::LaserScannerFeature& dto, LaserScannerFeature& bo)
+    {
+        bo.convexHull = dto.convexHull;
+        fromAron(dto.circle, bo.circle);
+        fromAron(dto.ellipsoid, bo.ellipsoid);
+
+        // bo.chain = dto.chain;
+        bo.points = dto.points;
+    }
+
+    void
+    fromAron(const arondto::LaserScannerFeatures& dto, LaserScannerFeatures& bo)
+    {
+        aron::fromAron(dto.frame, bo.frame);
+        aron::fromAron(dto.frameGlobalPose, bo.frameGlobalPose);
+        aron::fromAron(dto.features, bo.features);
+    }
+
+} // namespace armarx::navigation::memory
diff --git a/source/armarx/navigation/memory/aron_conversions.h b/source/armarx/navigation/memory/aron_conversions.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ad52e46393096a7b03c8e7a0898839a7d2eaf14
--- /dev/null
+++ b/source/armarx/navigation/memory/aron_conversions.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <RobotAPI/libraries/armem/core/Time.h>
+#include <RobotAPI/libraries/aron/converter/common/VectorConverter.h>
+#include <RobotAPI/libraries/aron/converter/eigen/EigenConverter.h>
+#include <RobotAPI/libraries/aron/core/data/variant/complex/NDArray.h>
+
+namespace armarx::navigation::memory
+{
+
+    struct LaserScannerFeatures;
+
+    namespace arondto
+    {
+        struct LaserScannerFeatures;
+    }
+
+    // LaserScannerFeatures
+    void toAron(arondto::LaserScannerFeatures& dto, const LaserScannerFeatures& bo);
+    void fromAron(const arondto::LaserScannerFeatures& dto, LaserScannerFeatures& bo);
+
+} // namespace armarx::navigation::memory
diff --git a/source/armarx/navigation/memory/client/human/Writer.h b/source/armarx/navigation/memory/client/human/Writer.h
index b224149c4aeb3c4858530f19ed051700060fc179..3a55ffc42e32b532d98a759e81cafa7033228beb 100644
--- a/source/armarx/navigation/memory/client/human/Writer.h
+++ b/source/armarx/navigation/memory/client/human/Writer.h
@@ -25,7 +25,6 @@
 #include <mutex>
 
 #include <RobotAPI/libraries/armem/client/util/SimpleWriterBase.h>
-#include <RobotAPI/libraries/armem_vision/types.h>
 
 #include <armarx/navigation/algorithms/Costmap.h>
 #include <armarx/navigation/human/types.h>
@@ -51,12 +50,12 @@ namespace armarx::navigation::memory::client::human
         ~Writer() override;
 
         bool store(const armarx::navigation::human::Humans& humans,
-                //    const std::string& name,
+                   //    const std::string& name,
                    const std::string& providerName,
                    const armem::Time& timestamp);
 
         bool store(const armarx::navigation::human::HumanGroups& groups,
-                //    const std::string& name,
+                   //    const std::string& name,
                    const std::string& providerName,
                    const armem::Time& timestamp);
 
diff --git a/source/armarx/navigation/memory/client/laser_scanner_features/Reader.cpp b/source/armarx/navigation/memory/client/laser_scanner_features/Reader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c7640bdc8af9beadb0132c2fcee5ca4a9260f081
--- /dev/null
+++ b/source/armarx/navigation/memory/client/laser_scanner_features/Reader.cpp
@@ -0,0 +1,219 @@
+#include "Reader.h"
+
+// STD / STL
+#include <algorithm>
+#include <cstring>
+#include <map>
+#include <optional>
+#include <ostream>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+// ICE
+#include <IceUtil/Handle.h>
+#include <IceUtil/Time.h>
+
+// Simox
+#include <SimoxUtility/algorithm/get_map_keys_values.h>
+
+// ArmarXCore
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/core/logging/LogSender.h>
+#include <ArmarXCore/core/logging/Logging.h>
+
+// RobotAPI Interfaces
+#include <RobotAPI/interface/armem/mns/MemoryNameSystemInterface.h>
+#include <RobotAPI/interface/armem/server/ReadingMemoryInterface.h>
+#include <RobotAPI/interface/units/LaserScannerUnit.h>
+
+// RobotAPI Aron
+#include <RobotAPI/libraries/aron/core/Exception.h>
+#include <RobotAPI/libraries/aron/core/data/variant/complex/NDArray.h>
+
+#include <armarx/navigation/memory/aron/LaserScannerFeatures.aron.generated.h>
+
+// RobotAPI Armem
+#include <RobotAPI/libraries/armem/client/Query.h>
+#include <RobotAPI/libraries/armem/client/Reader.h>
+#include <RobotAPI/libraries/armem/client/query/Builder.h>
+#include <RobotAPI/libraries/armem/client/query/selectors.h>
+#include <RobotAPI/libraries/armem/core/error.h>
+#include <RobotAPI/libraries/armem/core/wm/memory_definitions.h>
+#include <RobotAPI/libraries/armem/util/util.h>
+#include <RobotAPI/libraries/armem_laser_scans/aron/LaserScan.aron.generated.h>
+
+#include <armarx/navigation/memory/aron_conversions.h>
+
+namespace armarx::navigation::memory::client::laser_scanner_features
+{
+
+    Reader::Reader(armem::client::MemoryNameSystem& memoryNameSystem) :
+        memoryNameSystem(memoryNameSystem)
+    {
+    }
+
+    Reader::~Reader() = default;
+
+    void
+    Reader::registerPropertyDefinitions(armarx::PropertyDefinitionsPtr& def)
+    {
+        // ARMARX_DEBUG << "TransformReader: registerPropertyDefinitions";
+        // registerPropertyDefinitions(def);
+
+        const std::string prefix = propertyPrefix;
+
+        def->optional(properties.coreSegmentName,
+                      prefix + "CoreSegment",
+                      "Name of the mapping memory core segment to use.");
+
+        def->optional(properties.memoryName, prefix + "MemoryName");
+    }
+
+    void
+    Reader::connect()
+    {
+        // Wait for the memory to become available and add it as dependency.
+        ARMARX_IMPORTANT << "MappingDataReader: Waiting for memory '" << properties.memoryName
+                         << "' ...";
+        try
+        {
+            memoryReader =
+                memoryNameSystem.useReader(armem::MemoryID().withMemoryName(properties.memoryName));
+            ARMARX_IMPORTANT << "MappingDataReader: Connected to memory '" << properties.memoryName
+                             << "'";
+        }
+        catch (const armem::error::CouldNotResolveMemoryServer& e)
+        {
+            ARMARX_ERROR << e.what();
+            return;
+        }
+    }
+
+    armarx::armem::client::query::Builder
+    Reader::buildQuery(const Query& query) const
+    {
+        armarx::armem::client::query::Builder qb;
+
+        qb.coreSegments()
+            .withName(properties.coreSegmentName)
+            .providerSegments()
+            .withName(query.providerName)
+            .entities()
+            .all()
+            .snapshots()
+            .beforeOrAtTime(query.timestamp);
+
+        // auto entitySel = [&]()
+        // {
+        //     if (query.name.empty())
+        //     {
+        //         ARMARX_INFO << "querying all entities";
+        //         return sel.entities().all();
+        //     }
+        //     return sel.entities().withName(query.name);
+        // }();
+
+        // entitySel.snapshots().beforeOrAtTime(query.timestamp);
+
+        return qb;
+    }
+
+    std::vector<LaserScannerFeatures>
+    asFeaturesList(const armem::wm::ProviderSegment& providerSegment)
+    {
+        if (providerSegment.empty())
+        {
+            ARMARX_WARNING << "No entities!";
+            return {};
+        }
+
+        // const auto convert = [](const auto& aronLaserScanStamped,
+        //                         const wm::EntityInstance& ei) -> LaserScanStamped
+        // {
+        //     LaserScanStamped laserScanStamped;
+        //     fromAron(aronLaserScanStamped, laserScanStamped);
+
+        //     const auto ndArrayNavigator =
+        //         aron::data::NDArray::DynamicCast(ei.data()->getElement("scan"));
+
+        //     ARMARX_CHECK_NOT_NULL(ndArrayNavigator);
+
+        //     laserScanStamped.data = fromAron<LaserScanStep>(ndArrayNavigator);
+        //     ARMARX_IMPORTANT << "4";
+
+        //     return laserScanStamped;
+        // };
+
+        std::vector<LaserScannerFeatures> laserScannerFeatures;
+
+        // loop over all entities and their snapshots
+        providerSegment.forEachEntity(
+            [&](const armem::wm::Entity& entity)
+            {
+                // If we don't need this warning, we could directly iterate over the snapshots.
+                if (entity.empty())
+                {
+                    ARMARX_WARNING << "Empty history for " << entity.id();
+                }
+                ARMARX_DEBUG << "History size: " << entity.size();
+
+                entity.forEachInstance(
+                    [&](const armem::wm::EntityInstance& entityInstance)
+                    {
+                        if (const auto o =
+                                armem::tryCast<arondto::LaserScannerFeatures>(entityInstance))
+                        {
+                            LaserScannerFeatures& f = laserScannerFeatures.emplace_back();
+                            fromAron(o.value(), f);
+                        }
+                        return true;
+                    });
+                return true;
+            });
+
+        return laserScannerFeatures;
+    }
+
+    Reader::Result
+    Reader::queryData(const Query& query) const
+    {
+        const auto qb = buildQuery(query);
+
+        ARMARX_DEBUG << "[MappingDataReader] query ... ";
+
+        const armem::client::QueryResult qResult = memoryReader.query(qb.buildQueryInput());
+
+        ARMARX_DEBUG << "[MappingDataReader] result: " << qResult;
+
+        if (not qResult.success)
+        {
+            ARMARX_WARNING << "Failed to query data from memory: " << qResult.errorMessage;
+            return {.features = {},
+                    .status = Result::Status::Error,
+                    .errorMessage = qResult.errorMessage};
+        }
+
+        // now create result from memory
+        const armem::wm::ProviderSegment& providerSegment =
+            qResult.memory.getCoreSegment(properties.coreSegmentName)
+                .getProviderSegment(query.providerName);
+
+        const auto features = asFeaturesList(providerSegment);
+
+        // const auto laserScans = asLaserScans(providerSegment);
+        // std::vector<std::string> sensors;
+        // providerSegment.forEachEntity(
+        //     [&sensors](const wm::Entity& entity)
+        //     {
+        //         sensors.push_back(entity.name());
+        //         return true;
+        //     });
+
+        return {.features = features,
+                // .sensors = sensors,
+                .status = Result::Status::Success,
+                .errorMessage = ""};
+    }
+
+} // namespace armarx::navigation::memory::client::laser_scanner_features
diff --git a/source/armarx/navigation/memory/client/laser_scanner_features/Reader.h b/source/armarx/navigation/memory/client/laser_scanner_features/Reader.h
new file mode 100644
index 0000000000000000000000000000000000000000..4e08755e357910af451ba548d9617d3ac2b7e58b
--- /dev/null
+++ b/source/armarx/navigation/memory/client/laser_scanner_features/Reader.h
@@ -0,0 +1,114 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <ArmarXCore/core/application/properties/PropertyDefinitionContainer.h>
+#include <ArmarXCore/core/time/DateTime.h>
+
+#include <RobotAPI/libraries/armem/client.h>
+#include <RobotAPI/libraries/armem/client/MemoryNameSystem.h>
+#include <RobotAPI/libraries/armem/client/Reader.h>
+#include <RobotAPI/libraries/armem/client/query/Builder.h>
+
+#include <armarx/navigation/memory/constants.h>
+#include <armarx/navigation/memory/types.h>
+
+namespace armarx
+{
+    class ManagedIceObject;
+}
+
+namespace armarx::navigation::memory::client::laser_scanner_features
+{
+
+
+    /**
+    * @defgroup Component-ExampleClient ExampleClient
+    * @ingroup RobotAPI-Components
+    * A description of the component ExampleClient.
+    *
+    * @class ExampleClient
+    * @ingroup Component-ExampleClient
+    * @brief Brief description of class ExampleClient.
+    *
+    * Detailed description of class ExampleClient.
+    */
+    class Reader
+    {
+    public:
+        Reader(armem::client::MemoryNameSystem& memoryNameSystem);
+        virtual ~Reader();
+
+        void connect();
+
+        struct Query
+        {
+            std::string providerName;
+
+            std::string name; // sensor name
+
+            armarx::core::time::DateTime timestamp;
+
+            // std::vector<std::string> sensorList;
+        };
+
+        struct Result
+        {
+            std::vector<LaserScannerFeatures> features;
+
+            // std::vector<std::string> sensors;
+
+            enum Status
+            {
+                Error,
+                Success
+            } status;
+
+            std::string errorMessage;
+        };
+
+        Result queryData(const Query& query) const;
+
+        void registerPropertyDefinitions(armarx::PropertyDefinitionsPtr& def);
+
+
+    protected:
+        armarx::armem::client::query::Builder buildQuery(const Query& query) const;
+
+    private:
+        armem::client::MemoryNameSystem& memoryNameSystem;
+        armem::client::Reader memoryReader;
+
+        // Properties
+        struct Properties
+        {
+            std::string memoryName = memory::constants::NavigationMemoryName;
+            std::string coreSegmentName = memory::constants::LaserScannerFeaturesCoreSegment;
+        } properties;
+
+        const std::string propertyPrefix = "mem.nav.laser_scanner_features.";
+    };
+
+} // namespace armarx::navigation::memory::client::laser_scanner_features
diff --git a/source/armarx/navigation/memory/client/laser_scanner_features/Writer.cpp b/source/armarx/navigation/memory/client/laser_scanner_features/Writer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..425345e3e12131c82a40d7d970505db1e21ce95f
--- /dev/null
+++ b/source/armarx/navigation/memory/client/laser_scanner_features/Writer.cpp
@@ -0,0 +1,109 @@
+#include "Writer.h"
+
+#include <RobotAPI/libraries/armem/core/error.h>
+
+#include <armarx/navigation/memory/aron/LaserScannerFeatures.aron.generated.h>
+#include <armarx/navigation/memory/aron_conversions.h>
+
+namespace armarx::navigation::memory::client::laser_scanner_features
+{
+
+    Writer::Writer(armem::client::MemoryNameSystem& memoryNameSystem) :
+        memoryNameSystem(memoryNameSystem)
+    {
+    }
+
+    Writer::~Writer() = default;
+
+    void
+    Writer::registerPropertyDefinitions(armarx::PropertyDefinitionsPtr& def)
+    {
+        ARMARX_DEBUG << "LaserScansWriter: registerPropertyDefinitions";
+
+        const std::string prefix = propertyPrefix;
+
+        def->optional(properties.coreSegmentName,
+                      prefix + "CoreSegment",
+                      "Name of the mapping memory core segment to use.");
+
+        def->optional(properties.memoryName, prefix + "MemoryName");
+    }
+
+    void
+    Writer::connect()
+    {
+        // Wait for the memory to become available and add it as dependency.
+        ARMARX_IMPORTANT << "LaserScansWriter: Waiting for memory '" << properties.memoryName
+                         << "' ...";
+        try
+        {
+            memoryWriter =
+                memoryNameSystem.useWriter(armem::MemoryID().withMemoryName(properties.memoryName));
+            ARMARX_IMPORTANT << "MappingDataWriter: Connected to memory '" << properties.memoryName
+                             << "'";
+        }
+        catch (const armem::error::CouldNotResolveMemoryServer& e)
+        {
+            ARMARX_ERROR << e.what();
+            return;
+        }
+
+        ARMARX_IMPORTANT << "LaserScansWriter: Connected to memory '" << properties.memoryName;
+    }
+
+    bool
+    Writer::store(const LaserScannerFeatures& features,
+                  const std::string& providerName,
+                  const armem::Time& timestamp)
+    {
+        std::lock_guard g{memoryWriterMutex};
+
+        // const auto result = memoryWriter.addSegment(constants::memoryName,
+        //                                             constants::LaserScannerFeaturesCoreSegment);
+
+        // if (not result.success)
+        // {
+        //     ARMARX_ERROR << result.errorMessage;
+
+        //     // TODO(fabian.reister): message
+        //     return false;
+        // }
+
+        const auto entityID = armem::MemoryID()
+                                  .withMemoryName(properties.memoryName)
+                                  .withCoreSegmentName(properties.coreSegmentName)
+                                  .withProviderSegmentName(providerName)
+                                  .withEntityName(features.frame)
+                                  .withTimestamp(timestamp);
+
+        ARMARX_VERBOSE << "Memory id is " << entityID.str();
+
+        armem::EntityUpdate update;
+        update.entityID = entityID;
+
+        ARMARX_TRACE;
+
+        arondto::LaserScannerFeatures dto;
+        toAron(dto, features);
+
+        ARMARX_TRACE;
+
+        update.instancesData = {dto.toAron()};
+        update.timeCreated = timestamp;
+
+        ARMARX_DEBUG << "Committing " << update << " at time " << timestamp;
+
+        ARMARX_TRACE;
+        armem::EntityUpdateResult updateResult = memoryWriter.commit(update);
+
+        ARMARX_DEBUG << updateResult;
+
+        if (not updateResult.success)
+        {
+            ARMARX_ERROR << updateResult.errorMessage;
+        }
+
+        return updateResult.success;
+    }
+
+} // namespace armarx::navigation::memory::client::laser_scanner_features
diff --git a/source/armarx/navigation/memory/client/laser_scanner_features/Writer.h b/source/armarx/navigation/memory/client/laser_scanner_features/Writer.h
new file mode 100644
index 0000000000000000000000000000000000000000..bd4cad88956e5dc4d99d2f0c49ac105273439455
--- /dev/null
+++ b/source/armarx/navigation/memory/client/laser_scanner_features/Writer.h
@@ -0,0 +1,85 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    RobotAPI::ArmarXObjects::
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <mutex>
+
+#include <ArmarXCore/core/application/properties/PropertyDefinitionContainer.h>
+
+#include <RobotAPI/libraries/armem/client/MemoryNameSystem.h>
+#include <RobotAPI/libraries/armem/client/Writer.h>
+
+#include <armarx/navigation/memory/constants.h>
+#include <armarx/navigation/memory/types.h>
+
+namespace armarx::navigation::memory::client::laser_scanner_features
+{
+
+    /**
+    * @defgroup Component-ExampleClient ExampleClient
+    * @ingroup RobotAPI-Components
+    * A description of the component ExampleClient.
+    *
+    * @class ExampleClient
+    * @ingroup Component-ExampleClient
+    * @brief Brief description of class ExampleClient.
+    *
+    * Detailed description of class ExampleClient.
+    */
+    class Writer
+    {
+    public:
+        Writer(armem::client::MemoryNameSystem& memoryNameSystem);
+        virtual ~Writer();
+
+
+        void connect();
+
+        // MappingDataWriterInterface
+        /// to be called in Component::onConnectComponent
+        // void connect() override;
+
+        /// to be called in Component::addPropertyDefinitions
+        void registerPropertyDefinitions(armarx::PropertyDefinitionsPtr& def) /*override*/;
+
+        bool store(const LaserScannerFeatures& features,
+                   const std::string& providerName,
+                   const armem::Time& timestamp);
+
+    private:
+        armem::client::MemoryNameSystem& memoryNameSystem;
+        armem::client::Writer memoryWriter;
+
+        // Properties
+        struct Properties
+        {
+            std::string memoryName = memory::constants::NavigationMemoryName;
+            std::string coreSegmentName = memory::constants::LaserScannerFeaturesCoreSegment;
+        } properties;
+
+        std::mutex memoryWriterMutex;
+
+        const std::string propertyPrefix = "mem.nav.laser_scanner_features.";
+    };
+
+} // namespace armarx::navigation::memory::client::laser_scanner_features
diff --git a/source/armarx/navigation/memory/constants.h b/source/armarx/navigation/memory/constants.h
index 177f9730e047da14810c1d7ad5dcad2be60ef7ab..0df19cc8d2139d617f2283e806f39e42551b77c4 100644
--- a/source/armarx/navigation/memory/constants.h
+++ b/source/armarx/navigation/memory/constants.h
@@ -33,6 +33,7 @@ namespace armarx::navigation::memory::constants
     inline const std::string LocationCoreSegmentName = "Location";
     inline const std::string CostmapCoreSegmentName = "Costmap";
     inline const std::string HumanCoreSegmentName = "Human";
+    inline const std::string LaserScannerFeaturesCoreSegment = "LaserScannerFeatures";
 
     inline const std::string GlobalPlannerResultCoreSegment = "Results_GlobalPlanner";
     inline const std::string LocalPlannerResultCoreSegment = "Results_LocalPlanner";
diff --git a/source/armarx/navigation/memory/types.h b/source/armarx/navigation/memory/types.h
new file mode 100644
index 0000000000000000000000000000000000000000..73935f916f4a5a513f420106c608d43aa675422a
--- /dev/null
+++ b/source/armarx/navigation/memory/types.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Fabian Reister ( fabian dot reister at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <VirtualRobot/MathTools.h>
+
+#include <RobotAPI/interface/units/LaserScannerUnit.h>
+#include <RobotAPI/libraries/armem/core/Time.h>
+
+namespace armarx::navigation::memory
+{
+
+    struct Ellipsoid
+    {
+        Eigen::Isometry3f pose = Eigen::Isometry3f::Identity();
+
+        Eigen::Vector2f radii = Eigen::Vector2f::Zero();
+    };
+
+    struct Circle
+    {
+        Eigen::Vector2f center = Eigen::Vector2f::Zero();
+        float radius = 0.F;
+    };
+
+    struct LaserScannerFeature
+    {
+        using Points = std::vector<Eigen::Vector2f>;
+        using Chain = Points;
+
+        Points convexHull;
+
+        Circle circle;
+        Ellipsoid ellipsoid;
+
+        Chain chain;
+
+        Points points;
+    };
+
+    struct LaserScannerFeatures
+    {
+        // TODO(fabian.reister): framed pose
+        std::string frame;
+        Eigen::Isometry3f frameGlobalPose;
+
+        std::vector<LaserScannerFeature> features;
+
+
+        // std::vector<Ellipsoid> linesAsEllipsoids(float axisLength) const;
+    };
+
+
+} // namespace armarx::navigation::memory
diff --git a/source/armarx/navigation/server/scene_provider/SceneProvider.cpp b/source/armarx/navigation/server/scene_provider/SceneProvider.cpp
index 85e0d6236157edfd07c907367abafa3de69b7863..3fa2eb0179e30da481c3897e2f6dbc62cea8bcf9 100644
--- a/source/armarx/navigation/server/scene_provider/SceneProvider.cpp
+++ b/source/armarx/navigation/server/scene_provider/SceneProvider.cpp
@@ -14,9 +14,9 @@
 #include <armarx/navigation/memory/client/costmap/Reader.h>
 #include <armarx/navigation/memory/client/graph/Reader.h>
 #include <armarx/navigation/memory/client/human/Reader.h>
+#include <armarx/navigation/memory/client/laser_scanner_features/Reader.h>
 #include <armarx/navigation/util/util.h>
 
-
 namespace armarx::navigation::server::scene_provider
 {
 
@@ -124,11 +124,19 @@ namespace armarx::navigation::server::scene_provider
     core::DynamicScene
     SceneProvider::getDynamicScene(const DateTime& timestamp) const
     {
-        const memory::client::human::Reader::Query query{.providerName = config.humanProviderName,
-                                                         .timestamp = timestamp,
-                                                         .maxAge = Duration::MilliSeconds(500)};
-
-        return {.humans = srv.humanReader->queryHumans(query).humans};
+        const memory::client::human::Reader::Query queryHumans{
+            .providerName = config.humanProviderName,
+            .timestamp = timestamp,
+            .maxAge = Duration::MilliSeconds(500)};
+
+        const memory::client::laser_scanner_features::Reader::Query queryFeatures{
+            .providerName = config.laserScannerFeaturesProviderName,
+            .name = {},
+            .timestamp = timestamp};
+
+        return {.humans = srv.humanReader->queryHumans(queryHumans).humans,
+                .laserScannerFeatures =
+                    srv.laserScannerFeaturesReader->queryData(queryFeatures).features};
     }
 
     core::SceneGraph
diff --git a/source/armarx/navigation/server/scene_provider/SceneProvider.h b/source/armarx/navigation/server/scene_provider/SceneProvider.h
index 1f21e85ead9be31ca561a925e2bcceff0be53567..df8a9e77e861a730f8a3dedbcc75fd6d54f3300a 100644
--- a/source/armarx/navigation/server/scene_provider/SceneProvider.h
+++ b/source/armarx/navigation/server/scene_provider/SceneProvider.h
@@ -35,23 +35,29 @@
 #include <armarx/navigation/memory/client/human/Reader.h>
 #include <armarx/navigation/server/scene_provider/SceneProviderInterface.h>
 
-
 namespace armarx::navigation::memory::client
 {
     namespace graph
     {
         class Reader;
     }
+
     namespace costmap
     {
         class Reader;
     }
+
     namespace human
     {
         class Reader;
     }
-} // namespace armarx::navigation::memory::client
 
+    namespace laser_scanner_features
+    {
+        class Reader;
+    }
+
+} // namespace armarx::navigation::memory::client
 
 namespace armarx::navigation::server::scene_provider
 {
@@ -72,6 +78,8 @@ namespace armarx::navigation::server::scene_provider
 
             memory::client::human::Reader* humanReader;
 
+            memory::client::laser_scanner_features::Reader* laserScannerFeaturesReader;
+
             objpose::ObjectPoseClient objectPoseClient;
         };
 
@@ -83,6 +91,7 @@ namespace armarx::navigation::server::scene_provider
             std::string staticCostmapName = "distance_to_obstacles";
 
             std::string humanProviderName = "dynamic_scene_provider";
+            std::string laserScannerFeaturesProviderName = "LaserScannerFeatureExtraction";
         };
 
         SceneProvider(const InjectedServices& srv, const Config& config);