diff --git a/examples/components/example_client/Component.cpp b/examples/components/example_client/Component.cpp
index 1110c342da3693c7868daa14a741bca2e0f67711..8d776c194c6d6ecafc9522a77211d0dd54330f24 100644
--- a/examples/components/example_client/Component.cpp
+++ b/examples/components/example_client/Component.cpp
@@ -189,6 +189,7 @@ namespace armarx::navigation::components::example_client
             client::NavigationStackConfig()
                 .general({} /*{.maxVel = VelocityLimits{.linear = 400 , .angular = 0.1}}*/)
                 .globalPlanner(global_planning::SPFAParams())
+                .localPlanner(local_planning::TimedElasticBandsParams())
                 .trajectoryController(
                     traj_ctrl::local::TrajectoryFollowingControllerParams())); // FIXME
 
@@ -289,6 +290,7 @@ namespace armarx::navigation::components::example_client
             client::NavigationStackConfig()
                 .general({} /*{.maxVel = VelocityLimits{.linear = 400 , .angular = 0.1}}*/)
                 .globalPlanner(global_planning::SPFAParams())
+                .localPlanner(local_planning::TimedElasticBandsParams())
                 .trajectoryController(
                     traj_ctrl::local::TrajectoryFollowingControllerParams())); // FIXME
 
@@ -366,6 +368,7 @@ namespace armarx::navigation::components::example_client
             client::NavigationStackConfig()
                 .general({} /*{.maxVel = VelocityLimits{.linear = 400 , .angular = 0.1}}*/)
                 .globalPlanner(global_planning::Point2PointParams())
+                .localPlanner(local_planning::TimedElasticBandsParams())
                 .trajectoryController(
                     traj_ctrl::local::TrajectoryFollowingControllerParams())); // FIXME
 
diff --git a/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp b/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp
index 9ddc696aba96611d4b09cbc79757c80da3f62c00..ed8a9f439046eb2983a5baffdc81fb2da5faeef1 100644
--- a/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp
+++ b/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp
@@ -37,17 +37,17 @@
 
 #include "armarx/navigation/components/dynamic_scene_provider/ArVizDrawer.h"
 #include <armarx/navigation/core/basic_types.h>
+#include <armarx/navigation/human/types.h>
 #include <armarx/navigation/memory/client/costmap/Reader.h>
 #include <armarx/navigation/util/util.h>
 
-
 namespace armarx::navigation::components::dynamic_scene_provider
 {
 
     const std::string Component::defaultName = "dynamic_scene_provider";
 
 
-    Component::Component()
+    Component::Component() : humanGrouper(HumanGrouper::GroupingSettings())
     {
         addPlugin(humanPoseReaderPlugin);
         addPlugin(laserScannerFeaturesReaderPlugin);
@@ -55,6 +55,7 @@ namespace armarx::navigation::components::dynamic_scene_provider
         addPlugin(costmapReaderPlugin);
         addPlugin(occupancyGridReaderPlugin);
         addPlugin(humanWriterPlugin);
+        addPlugin(humanGroupWriterPlugin);
     }
 
     armarx::PropertyDefinitionsPtr
@@ -189,6 +190,54 @@ namespace armarx::navigation::components::dynamic_scene_provider
         return Component::defaultName;
     }
 
+    using Human = armarx::navigation::human::Human;
+
+    void
+    armarx::navigation::components::dynamic_scene_provider::Component::addTestHumans(
+        std::vector<Human>& humans)
+    {
+        DateTime timestamp = Clock::Now();
+
+        ARMARX_INFO << "For testing purposes, we will add another 3 humans!";
+        core::Pose2D p1 = core::Pose2D::Identity();
+        p1.linear() = Eigen::Rotation2Df(1.25 * M_PI).toRotationMatrix();
+        p1.translation() = Eigen::Vector2f(1000, 500);
+        Human h1 = {
+            .pose = p1, .linearVelocity = Eigen::Vector2f(0, 0), .detectionTime = timestamp};
+        core::Pose2D p2 = core::Pose2D::Identity();
+        p2.linear() = Eigen::Rotation2Df(-0.25 * M_PI).toRotationMatrix();
+        p2.translation() = Eigen::Vector2f(1000, -500);
+        Human h2 = {
+            .pose = p2, .linearVelocity = Eigen::Vector2f(0, 0), .detectionTime = timestamp};
+
+
+        // Create third human that swings in a sine wave
+
+        double f_1_s = 0.25f;
+        double omega_1_s = 2 * M_PI * f_1_s;
+        double t_s = timestamp.toDurationSinceEpoch().toSecondsDouble();
+        double sinArg = std::fmod(t_s * omega_1_s, 2 * M_PI);
+        double amplitude_mm = 1000;
+
+        core::Pose2D p3 = core::Pose2D::Identity();
+        p3.linear() = Eigen::Rotation2Df(M_PI_2).toRotationMatrix();
+        // in mm
+        p3.translation() = Eigen::Vector2f(2500 + amplitude_mm * sin(sinArg), 0);
+        Human h3 = {.pose = p3,
+                    // linear velocity in mm/s
+                    .linearVelocity = Eigen::Vector2f(amplitude_mm * omega_1_s * cos(sinArg), 0),
+                    .detectionTime = timestamp};
+
+        humans.emplace_back(h1);
+        humans.emplace_back(h2);
+
+        // periodically toggle third human for showing group of 2 vs group of 3
+        //        if ((timestamp.toSecondsSinceEpoch() / 5) % 2 == 0)
+        //        {
+        humans.emplace_back(h3);
+        // }
+    }
+
     void
     Component::runPeriodically()
     {
@@ -242,6 +291,9 @@ namespace armarx::navigation::components::dynamic_scene_provider
         // Laser scanner features
         //
 
+        ARMARX_INFO << "Not querying laser scanner features for testing purposes (no "
+                       "robot available)";
+
         /*
         TIMING_START(READ_LASERSCANNER_FROM_MEMORY);
 
@@ -251,13 +303,13 @@ namespace armarx::navigation::components::dynamic_scene_provider
             .name = properties.laserScannerFeatures.name,
             .timestamp = timestamp};
 
-        const armem::vision::laser_scanner_features::client::Reader::Result laserFeaturesResult =
-            laserScannerFeaturesReaderPlugin->get().queryData(laserFeaturesQuery);
+                const armem::vision::laser_scanner_features::client::Reader::Result laserFeaturesResult =
+                    laserScannerFeaturesReaderPlugin->get().queryData(laserFeaturesQuery);
 
-        ARMARX_CHECK_EQUAL(laserFeaturesResult.status,
-                           armem::vision::laser_scanner_features::client::Reader::Result::Success);
+                ARMARX_CHECK_EQUAL(laserFeaturesResult.status,
+                                   armem::vision::laser_scanner_features::client::Reader::Result::Success);
 
-        ARMARX_INFO << laserFeaturesResult.features.size() << " clusters/features";
+                ARMARX_INFO << laserFeaturesResult.features.size() << " clusters/features";
 
         TIMING_END_COMMENT_STREAM(
             READ_LASERSCANNER_FROM_MEMORY, "Timer: reading laserscanner from memory", ARMARX_VERBOSE);
@@ -394,6 +446,9 @@ namespace armarx::navigation::components::dynamic_scene_provider
                                   ARMARX_VERBOSE);
 
 
+        ARMARX_INFO << "Not running tracker with lasersensors because they are not available for "
+                       "the current tests.";
+
         /*
         ARMARX_INFO << "Running human tracker with lasersensor measurements";
 
@@ -425,18 +480,40 @@ namespace armarx::navigation::components::dynamic_scene_provider
 
         //TODO use clusters for obstacle creation
 
-        std::vector<human::Human> humans = humanTracker.getTrackedHumans();
+        humanTracker.update(human::HumanTracker::CameraMeasurement{
+            .detectionTime = timestamp, .humanPoses = humanPoseResult.humanPoses});
+        using Human = armarx::navigation::human::Human;
+        std::vector<Human> humans = humanTracker.getTrackedHumans();
+
+        // Add some testing humans
+        addTestHumans(humans);
 
+        humanWriterPlugin->get().store(humans, getName(), timestamp);
 
-        if(not humans.empty())
+        if (not humans.empty())
         {
             ARMARX_INFO << "Detected " << humans.size() << " humans";
+            for (Human& human : humans)
+            {
+                human.proxemicZones = proxemics.createProxemicZones(human);
+            }
             humanWriterPlugin->get().store(humans, getName(), timestamp);
         }
 
+        ARMARX_INFO << "Providing grouper with new humans";
+
+        humanGrouper.updateHumans(humans);
+
+        ARMARX_INFO << "Running human grouper";
+        using HumanGroup = armarx::navigation::human::HumanGroup;
+        std::vector<HumanGroup> groups = humanGrouper.getCurrentGroups();
+
+        ARMARX_INFO << "Detected " << groups.size() << " human groups";
+
+        humanGroupWriterPlugin->get().store(groups, getName(), timestamp);
 
         TIMING_END_COMMENT_STREAM(
-            WRITE_BACK_HUMANS, "Timer: write humans to memory", ARMARX_VERBOSE);
+            WRITE_BACK_HUMANS, "Timer: write humans and human groups to memory", ARMARX_VERBOSE);
 
 
         TIMING_END_COMMENT_STREAM(
diff --git a/source/armarx/navigation/components/dynamic_scene_provider/Component.h b/source/armarx/navigation/components/dynamic_scene_provider/Component.h
index aec652dd6438b528ed939e80379715d34e795f60..42a109c6555a3ba6e7384c9ac6165996bf4dc6ed 100644
--- a/source/armarx/navigation/components/dynamic_scene_provider/Component.h
+++ b/source/armarx/navigation/components/dynamic_scene_provider/Component.h
@@ -51,6 +51,9 @@
 #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>
+#include <armarx/navigation/human/HumanGrouper.h>
+#include <armarx/navigation/human/ProxemicZoneCreator.h>
+#include <armarx/navigation/memory/client/human_group/Writer.h>
 
 
 namespace armarx::navigation::components::dynamic_scene_provider
@@ -119,6 +122,8 @@ namespace armarx::navigation::components::dynamic_scene_provider
 
         VirtualRobot::RobotPtr robot = nullptr;
 
+        using Human = armarx::navigation::human::Human;
+        void addTestHumans(std::vector<Human>& humans);
 
     private:
         static const std::string defaultName;
@@ -204,8 +209,13 @@ namespace armarx::navigation::components::dynamic_scene_provider
 
         ReaderWriterPlugin<memory::client::human::Writer>* humanWriterPlugin = nullptr;
 
+        ReaderWriterPlugin<memory::client::human_group::Writer>* humanGroupWriterPlugin = nullptr;
+
 
         human::HumanTracker humanTracker;
+        human::ProxemicZoneCreator proxemics;
+        using HumanGrouper = human::HumanGrouper;
+        HumanGrouper humanGrouper;
     };
 
 } // namespace armarx::navigation::components::dynamic_scene_provider
diff --git a/source/armarx/navigation/components/navigation_memory/Component.cpp b/source/armarx/navigation/components/navigation_memory/Component.cpp
index dcf4b7ad14653de4fe1dbdc83e56fd69fa591452..e2cc5737e3991c3b9fbd4898ad144e22e93abc02 100644
--- a/source/armarx/navigation/components/navigation_memory/Component.cpp
+++ b/source/armarx/navigation/components/navigation_memory/Component.cpp
@@ -51,8 +51,8 @@
 #include <armarx/navigation/core/aron/Location.aron.generated.h>
 #include <armarx/navigation/core/aron/Trajectory.aron.generated.h>
 #include <armarx/navigation/core/aron/Twist.aron.generated.h>
-#include <armarx/navigation/human/aron/Human.aron.generated.h>
 #include <armarx/navigation/graph/constants.h>
+#include <armarx/navigation/human/aron/Human.aron.generated.h>
 #include <armarx/navigation/location/constants.h>
 #include <armarx/navigation/memory/constants.h>
 
@@ -133,8 +133,8 @@ namespace armarx::navigation::components::navigation_memory
         workingMemory().addCoreSegment(memory::constants::HumanCoreSegmentName,
                                        navigation::human::arondto::Human::ToAronType());
 
-        // workingMemory().addCoreSegment(memory::constants::HumanGroupCoreSegmentName,
-        //                                navigation::human::arondto::Human::ToAronType());
+        workingMemory().addCoreSegment(memory::constants::HumanGroupCoreSegmentName,
+                                       navigation::human::arondto::HumanGroup::ToAronType());
 
         if (not properties.snapshotToLoad.empty())
         {
@@ -421,11 +421,13 @@ namespace armarx::navigation::components::navigation_memory
     void
     Component::visuRun()
     {
-        memory::Visu visu{arviz,
-                          workingMemory().getCoreSegment(navigation::location::coreSegmentID),
-                          workingMemory().getCoreSegment(navigation::graph::coreSegmentID),
-                          workingMemory().getCoreSegment(memory::constants::CostmapCoreSegmentName),
-                          workingMemory().getCoreSegment(memory::constants::HumanCoreSegmentName)};
+        memory::Visu visu{
+            arviz,
+            workingMemory().getCoreSegment(navigation::location::coreSegmentID),
+            workingMemory().getCoreSegment(navigation::graph::coreSegmentID),
+            workingMemory().getCoreSegment(memory::constants::CostmapCoreSegmentName),
+            workingMemory().getCoreSegment(memory::constants::HumanCoreSegmentName),
+            workingMemory().getCoreSegment(memory::constants::HumanGroupCoreSegmentName)};
 
 
         Properties::LocationGraph p;
@@ -458,6 +460,9 @@ namespace armarx::navigation::components::navigation_memory
             // Humans
             visu.drawHumans(layers, p.visuHumans, p.visuTransparent);
 
+            // Groups
+            visu.drawHumanGroups(layers, p.visuHumanGroups);
+
             arviz.commit(layers);
 
             metronome.waitForNextTick();
@@ -580,7 +585,7 @@ namespace armarx::navigation::components::navigation_memory
 
     bool
     Component::storeLocationGraph(const armarx::data::PackagePath& packagePath,
-                                         const Ice::Current& current)
+                                  const Ice::Current& current)
     {
         armem::server::wm::CoreSegment& locationCoreSegment =
             workingMemory().getCoreSegment(navigation::location::coreSegmentID);
@@ -651,4 +656,4 @@ namespace armarx::navigation::components::navigation_memory
     }
 
 
-}  // namespace armarx::navigation::components::navigation_memory
+} // namespace armarx::navigation::components::navigation_memory
diff --git a/source/armarx/navigation/components/navigation_memory/Component.h b/source/armarx/navigation/components/navigation_memory/Component.h
index b1b1a1b682e645c33ab1daef65c0cb10587a0796..619be519aef6dd3be5ed205ce3042d10aa975a8c 100644
--- a/source/armarx/navigation/components/navigation_memory/Component.h
+++ b/source/armarx/navigation/components/navigation_memory/Component.h
@@ -105,6 +105,7 @@ namespace armarx::navigation::components::navigation_memory
                 bool visuGraphEdges = true;
                 bool visuCostmaps = true;
                 bool visuHumans = true;
+                bool visuHumanGroups = true;
 
                 bool visuTransparent = false;
 
diff --git a/source/armarx/navigation/components/navigation_memory/Visu.cpp b/source/armarx/navigation/components/navigation_memory/Visu.cpp
index 882deb59ae697593a36325bd0b3b8fff5625f9eb..7a8390d151d13ab837f22fd8a3debd8c5c1856e4 100644
--- a/source/armarx/navigation/components/navigation_memory/Visu.cpp
+++ b/source/armarx/navigation/components/navigation_memory/Visu.cpp
@@ -15,6 +15,7 @@
  *
  * @package    Navigation::ArmarXObjects::NavigationMemory
  * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @author     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
  * @date       2021
  * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
  *             GNU General Public License
@@ -53,12 +54,14 @@ namespace armarx::navigation::memory
                const armem::server::wm::CoreSegment& locSegment,
                const armem::server::wm::CoreSegment& graphSegment,
                const armem::server::wm::CoreSegment& costmapSegment,
-               const armem::server::wm::CoreSegment& humanSegment) :
+               const armem::server::wm::CoreSegment& humanSegment,
+               const armem::server::wm::CoreSegment& humanGroupSegment) :
         arviz(arviz),
         locSegment(locSegment),
         graphSegment(graphSegment),
         costmapSegment(costmapSegment),
         humanSegment(humanSegment),
+        humanGroupSegment(humanGroupSegment),
         visu(std::make_unique<graph::GraphVisu>())
     {
     }
@@ -137,94 +140,124 @@ namespace armarx::navigation::memory
         }
     }
 
-    namespace
+
+    void
+    Visu::visualize(const algorithms::Costmap& costmap, viz::Layer& layer, const std::string& name)
     {
+        const auto cmap = simox::color::cmaps::viridis();
+        const float vmax = costmap.getGrid().array().maxCoeff();
 
-        void
-        visualize(const algorithms::Costmap& costmap, viz::Layer& layer, const std::string& name)
+        const auto asColor = [&cmap, &vmax](const float distance) -> viz::data::Color
         {
-            const auto cmap = simox::color::cmaps::viridis();
-            const float vmax = costmap.getGrid().array().maxCoeff();
+            const auto color = cmap.at(distance, 0.F, vmax);
+            return {color.a, color.r, color.g, color.b};
+        };
 
-            const auto asColor = [&cmap, &vmax](const float distance) -> viz::data::Color
-            {
-                const auto color = cmap.at(distance, 0.F, vmax);
-                return {color.a, color.r, color.g, color.b};
-            };
+        const std::int64_t cols = costmap.getGrid().cols();
+        const std::int64_t rows = costmap.getGrid().rows();
 
-            const std::int64_t cols = costmap.getGrid().cols();
-            const std::int64_t rows = costmap.getGrid().rows();
+        auto mesh = viz::Mesh(name);
 
-            auto mesh = viz::Mesh(name);
+        std::vector<std::vector<Eigen::Vector3f>> vertices;
+        std::vector<std::vector<viz::data::Color>> colors;
 
-            std::vector<std::vector<Eigen::Vector3f>> vertices;
-            std::vector<std::vector<viz::data::Color>> colors;
+        for (int r = 0; r < rows; r++)
+        {
+            auto& verticesRow = vertices.emplace_back(cols);
+            auto& colorsRow = colors.emplace_back(cols);
 
-            for (int r = 0; r < rows; r++)
+            for (int c = 0; c < cols; c++)
             {
-                auto& verticesRow = vertices.emplace_back(cols);
-                auto& colorsRow = colors.emplace_back(cols);
-
-                for (int c = 0; c < cols; c++)
-                {
-                    verticesRow.at(c) = conv::to3D(costmap.toPositionGlobal({r, c}));
-                    colorsRow.at(c) = asColor(costmap.getGrid()(r, c));
-                }
+                verticesRow.at(c) = conv::to3D(costmap.toPositionGlobal({r, c}));
+                colorsRow.at(c) = asColor(costmap.getGrid()(r, c));
             }
+        }
 
-            mesh.grid2D(vertices, colors);
+        mesh.grid2D(vertices, colors);
 
-            layer.add(mesh);
-        }
+        layer.add(mesh);
+    }
 
-        void
-        visualize(const human::Humans& humans, viz::Layer& layer, const bool visuTransparent)
-        {
+    void
+    Visu::visualize(const human::Humans& humans, viz::Layer& layer, const bool visuTransparent)
+    {
 
-            const Eigen::Translation3f human_T_mmm(Eigen::Vector3f{0, 0, 1000});
+        const Eigen::Translation3f human_T_mmm(Eigen::Vector3f{0, 0, 1000});
+
+        ARMARX_INFO << "Visualizing " << humans.size() << " humans";
+        for (const auto& human : humans)
+        {
+            viz::Robot mmm(std::to_string(layer.size()));
+            mmm.file("RobotAPI", "RobotAPI/robots/MMM/mmm.xml");
+            mmm.pose(conv::to3D(human.pose) * human_T_mmm);
+            mmm.scale(1.7); // 1.7m
+            mmm.overrideColor(viz::Color::orange(255, visuTransparent ? 100 : 255));
+            layer.add(mmm);
+
+            core::Pose pose3d = conv::to3D(human.pose);
+            pose3d.translation() += Eigen::Vector3f{0, 0, 1000};
+            auto arrow = viz::Arrow(std::to_string(layer.size()))
+                             .pose(pose3d)
+                             .length(200)
+                             .color(simox::Color::red());
+            layer.add(arrow);
 
-            ARMARX_INFO << "Visualizing " << humans.size() << " humans";
-            for (const auto& human : humans)
             {
-                // viz::Cylinder cylinder(std::to_string(layer.size()));
-                // cylinder.fromTo(conv::to3D(human.pose.translation()),
-                //                 conv::to3D(human.pose.translation()) + Eigen::Vector3f{0, 0, 10});
+                auto arrow = viz::Arrow(std::to_string(layer.size()))
+                                 .fromTo(pose3d.translation(),
+                                         pose3d.translation() + conv::to3D(human.linearVelocity))
+                                 .color(simox::Color::blue());
+                arrow.width(10);
+                layer.add(arrow);
+            }
+        }
+    }
 
+    void
+    Visu::visualizeProxemicZones(const human::Humans& humans, viz::Layer& layer)
+    {
+        for (const auto& human : humans)
+        {
+            auto proxemicZones = human.proxemicZones;
 
-                // cylinder.color(simox::Color::orange());
-                // cylinder.radius(300);
-                // layer.add(cylinder);
+            int i = 0;
+            for (const auto& proxemicZone : proxemicZones)
+            {
+                const Eigen::Vector3f axisLength(
+                    proxemicZone.shape.a, proxemicZone.shape.b, 10.f - i);
+                const core::Pose pose3d = conv::to3D(proxemicZone.pose);
 
+                viz::Ellipsoid zoneEllipse("proxemicZone_" + std::to_string(visualizationIndex));
+                zoneEllipse.pose(pose3d);
+                zoneEllipse.axisLengths(axisLength);
+                zoneEllipse.color(PROXEMIC_ZONE_COLOR[i % PROXEMIC_ZONE_COLOR.size()]);
 
-                viz::Robot mmm(std::to_string(layer.size()));
-                mmm.file("RobotAPI", "RobotAPI/robots/MMM/mmm.xml");
-                mmm.pose(conv::to3D(human.pose) * human_T_mmm);
-                mmm.scale(1.7); // 1.7m
-                mmm.overrideColor(viz::Color::orange(255, visuTransparent ? 100 : 255));
-                layer.add(mmm);
+                layer.add(zoneEllipse);
 
+                i++;
+            }
+        }
+    }
 
-                core::Pose pose3d = conv::to3D(human.pose);
-                pose3d.translation() += Eigen::Vector3f{0, 0, 1000};
-                auto arrow = viz::Arrow(std::to_string(layer.size()))
-                                 .pose(pose3d)
-                                 .length(200)
-                                 .color(simox::Color::red());
-                layer.add(arrow);
+    void
+    Visu::visualize(const human::HumanGroups& humanGroups, viz::Layer& layer)
+    {
+        for (const auto& humanGroup : humanGroups)
+        {
+            viz::Polygon polygon(std::to_string(layer.size()));
 
-                {
-                    auto arrow =
-                        viz::Arrow(std::to_string(layer.size()))
-                            .fromTo(pose3d.translation(),
-                                    pose3d.translation() + conv::to3D(human.linearVelocity))
-                            .color(simox::Color::blue());
-                    arrow.width(10);
-                    layer.add(arrow);
-                }
+            const std::vector<Eigen::Vector2f> verts2D = humanGroup.shape.vertices;
+            std::vector<Eigen::Vector3f> verts3D;
+            for (const auto& v2d : verts2D)
+            {
+                verts3D.emplace_back(Eigen::Vector3f(v2d.x(), v2d.y(), 50));
             }
+            polygon.points(verts3D);
+            polygon.color(simox::Color::kit_yellow());
+            polygon.lineWidth(10);
+            layer.add(polygon);
         }
-
-    } // namespace
+    }
 
     void
     Visu::drawCostmaps(std::vector<viz::Layer>& layers, bool enabled)
@@ -292,11 +325,13 @@ namespace armarx::navigation::memory
                             [&namedProviderHumans](
                                 const armarx::armem::wm::EntityInstance& instance)
                             {
+                                ARMARX_TRACE;
                                 const auto dto =
                                     navigation::human::arondto::Human::FromAron(instance.data());
-
+                                ARMARX_TRACE;
                                 navigation::human::Human human;
                                 fromAron(dto, human);
+                                ARMARX_TRACE;
 
                                 namedProviderHumans[instance.id().providerSegmentName].emplace_back(
                                     std::move(human));
@@ -307,7 +342,52 @@ namespace armarx::navigation::memory
         for (const auto& [providerName, humans] : namedProviderHumans)
         {
             viz::Layer& layer = layers.emplace_back(arviz.layer("humans_" + providerName));
+            viz::Layer& proxZoneLayer =
+                layers.emplace_back(arviz.layer("proxemic_zones_" + providerName));
             visualize(humans, layer, visuTransparent);
+            visualizeProxemicZones(humans, proxZoneLayer);
+        }
+    }
+
+    void
+    Visu::drawHumanGroups(std::vector<viz::Layer>& layers, bool enabled)
+    {
+        if (not enabled)
+        {
+            return;
+        }
+
+        std::map<std::string, navigation::human::HumanGroups> namedProviderHumanGroups;
+
+        humanGroupSegment.doLocked(
+            [&]()
+            {
+                using namespace armem::server;
+
+                humanGroupSegment.forEachEntity(
+                    [&](const wm::Entity& entity)
+                    {
+                        namedProviderHumanGroups[entity.id().providerSegmentName];
+                        entity.getLatestSnapshot().forEachInstance(
+                            [&namedProviderHumanGroups](
+                                const armarx::armem::wm::EntityInstance& instance)
+                            {
+                                const auto dto = navigation::human::arondto::HumanGroup::FromAron(
+                                    instance.data());
+
+                                navigation::human::HumanGroup humanGroup;
+                                fromAron(dto, humanGroup);
+
+                                namedProviderHumanGroups[instance.id().providerSegmentName]
+                                    .emplace_back(std::move(humanGroup));
+                            });
+                    });
+            });
+
+        for (const auto& [providerName, humanGroups] : namedProviderHumanGroups)
+        {
+            viz::Layer& layer = layers.emplace_back(arviz.layer("humanGroups_" + providerName));
+            visualize(humanGroups, layer);
         }
     }
 
diff --git a/source/armarx/navigation/components/navigation_memory/Visu.h b/source/armarx/navigation/components/navigation_memory/Visu.h
index d965feccd52b78b895ded9ebaf1683ff64c95319..835823d9868337c65d98ab1ec146f73a9817eac0 100644
--- a/source/armarx/navigation/components/navigation_memory/Visu.h
+++ b/source/armarx/navigation/components/navigation_memory/Visu.h
@@ -31,6 +31,7 @@
 #include <RobotAPI/libraries/armem/core/forward_declarations.h>
 
 #include "armarx/navigation/algorithms/Costmap.h"
+#include "armarx/navigation/human/types.h"
 
 
 namespace armarx::navigation::graph
@@ -48,7 +49,8 @@ namespace armarx::navigation::memory
              const armem::server::wm::CoreSegment& locSegment,
              const armem::server::wm::CoreSegment& graphSegment,
              const armem::server::wm::CoreSegment& costmapSegment,
-             const armem::server::wm::CoreSegment& humanSegment);
+             const armem::server::wm::CoreSegment& humanSegment,
+             const armem::server::wm::CoreSegment& humanGroupSegment);
         ~Visu();
 
 
@@ -56,6 +58,7 @@ namespace armarx::navigation::memory
         void drawGraphs(std::vector<viz::Layer>& layers, bool enabled);
         void drawCostmaps(std::vector<viz::Layer>& layers, bool enabled);
         void drawHumans(std::vector<viz::Layer>& layers, bool enabled, bool visuTransparent);
+        void drawHumanGroups(std::vector<viz::Layer>& layers, bool enabled);
 
 
         viz::ScopedClient arviz;
@@ -64,8 +67,21 @@ namespace armarx::navigation::memory
         const armem::server::wm::CoreSegment& graphSegment;
         const armem::server::wm::CoreSegment& costmapSegment;
         const armem::server::wm::CoreSegment& humanSegment;
+        const armem::server::wm::CoreSegment& humanGroupSegment;
 
         std::unique_ptr<navigation::graph::GraphVisu> visu;
+
+    private:
+        const std::array<simox::Color, 2> PROXEMIC_ZONE_COLOR = {simox::Color::red(),
+                                                                 simox::Color::blue()};
+        int visualizationIndex;
+        void visualize(const navigation::human::Humans& humans,
+                       viz::Layer& layer,
+                       const bool visuTransparent);
+        void visualizeProxemicZones(const navigation::human::Humans& humans, viz::Layer& layer);
+        void visualize(const human::HumanGroups& humanGroups, viz::Layer& layer);
+        void
+        visualize(const algorithms::Costmap& costmap, viz::Layer& layer, const std::string& name);
     };
 
 
diff --git a/source/armarx/navigation/human/CMakeLists.txt b/source/armarx/navigation/human/CMakeLists.txt
index 836826fb77312440fdc726cc105d91e2201f0fba..360b78b6c31bc8942cc4ce5ba597e798edfc8bf6 100644
--- a/source/armarx/navigation/human/CMakeLists.txt
+++ b/source/armarx/navigation/human/CMakeLists.txt
@@ -18,6 +18,12 @@ armarx_add_library(teb_human
         types.cpp
         aron_conversions.cpp
         ProxemicZoneCreator.cpp
+        HumanGrouper.cpp
+        EuclideanDistance.cpp
+        OrientationDistance.cpp
+        CombinedDistance.cpp
+        ConvexHullGenerator.cpp
+        MovementDistance.cpp
         HumanSystemModel.cpp
         UnscentedKalmanFilter_impl.cpp
         HumanTracker.cpp
@@ -27,6 +33,14 @@ armarx_add_library(teb_human
         aron_conversions.h
         shapes.h
         ProxemicZoneCreator.h
+        HumanGrouper.h
+        DistanceFunction.h
+        EuclideanDistance.h
+        OrientationDistance.h
+        CombinedDistance.h
+        GroupShapeGenerator.h
+        ConvexHullGenerator.h
+        MovementDistance.h
         HumanSystemModel.h
         HumanTracker.h
         HumanFilter.h
@@ -100,8 +114,24 @@ armarx_add_test(human_tracker_test
     TEST_FILES
         test/human_tracker_test.cpp
     DEPENDENCIES
-        ArmarXCore
-        armarx_navigation::core
-        armarx_navigation::teb_human
-        range-v3::range-v3
+            PUBLIC
+                ArmarXCore
+                armarx_navigation::core
+                armarx_navigation::teb_human
+
+            PRIVATE
+                range-v3::range-v3
+)
+
+armarx_add_test(human_test
+    TEST_FILES
+        test/human_test.cpp
+    DEPENDENCIES
+        PUBLIC
+            ArmarXCore
+            armarx_navigation::core
+            armarx_navigation::teb_human
+
+        PRIVATE
+            range-v3::range-v3
 )
diff --git a/source/armarx/navigation/human/CombinedDistance.cpp b/source/armarx/navigation/human/CombinedDistance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..18330577f961ed5130511092d8704e6f3581a0c6
--- /dev/null
+++ b/source/armarx/navigation/human/CombinedDistance.cpp
@@ -0,0 +1,28 @@
+#include "CombinedDistance.h"
+
+namespace armarx::navigation::human
+{
+    CombinedDistance::CombinedDistance(const double maxOrientationInfluence,
+                                       const double movementInfluence) :
+        euclidean(EuclideanDistance()),
+        orientation(OrientationDistance()),
+        movement(MovementDistance()),
+        maxOrientationInfluence(maxOrientationInfluence),
+        movementInfluence(movementInfluence)
+    {
+    }
+
+    double
+    CombinedDistance::computeDistance(const Human& h1, const Human& h2) const
+    {
+        double d = euclidean.computeDistance(h1, h2);
+
+        // scales the euclidean distance by a factor in [1, maxOrientationInfluence]
+        d *= (1 + (maxOrientationInfluence - 1) * orientation.computeDistance(h1, h2));
+
+        // scales the euclidean distance by a factor in [1, inf) depending on influence
+        d *= (1 + movementInfluence * movement.computeDistance(h1, h2));
+
+        return d;
+    }
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/CombinedDistance.h b/source/armarx/navigation/human/CombinedDistance.h
new file mode 100644
index 0000000000000000000000000000000000000000..4cacc38d0971ce6499b0bcae046c12de84ad787e
--- /dev/null
+++ b/source/armarx/navigation/human/CombinedDistance.h
@@ -0,0 +1,67 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "DistanceFunction.h"
+#include "EuclideanDistance.h"
+#include "MovementDistance.h"
+#include "OrientationDistance.h"
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief The CombinedDistance class combines the standard euclidean distance multiplicatively
+     * with an orientation distance in such a way that the distance is always at least the euclidean
+     * distance but will be scaled by up to the maximum influence factor if the people face away
+     * from each other. The distance will then again be increased by the scaled difference in movement
+     * between the two people. The distance will always be at least the euclidean distance.
+     */
+    class CombinedDistance : public DistanceFunction
+    {
+    public:
+        /**
+         * @brief CombinedDistance Creates a new combined distance with the specified orientation scale factor
+         * @param maxOrientationInfluence the euclidean distance between the humans will be scaled
+         * by up to this factor if they face away from each other
+         * @param movementInfluence scales the difference in velocity of the humans before applying
+         * it to the total distance
+         */
+        CombinedDistance(double maxOrientationInfluence, double movementInfluence);
+
+        /**
+         * @brief computeDistance computes the distance value for the two given people. Should
+         * always be commutative, i.e. computeDistance(h1, h2) = computeDistance(h2, h1)
+         * @param h1 the first human
+         * @param h2 the second human
+         * @return a value specifying a distance between the two people
+         */
+        double computeDistance(const Human& h1, const Human& h2) const override;
+
+    private:
+        const EuclideanDistance euclidean;
+        const OrientationDistance orientation;
+        const MovementDistance movement;
+        const double maxOrientationInfluence;
+        const double movementInfluence;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/ConvexHullGenerator.cpp b/source/armarx/navigation/human/ConvexHullGenerator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2aff0df1f43d4aba9a1a1b76a32049c50ad371e7
--- /dev/null
+++ b/source/armarx/navigation/human/ConvexHullGenerator.cpp
@@ -0,0 +1,49 @@
+#include "ConvexHullGenerator.h"
+
+#include <boost/geometry.hpp>
+#include <boost/geometry/geometries/register/point.hpp>
+#include <boost/geometry/multi/geometries/register/multi_point.hpp>
+
+using Point = std::pair<double, double>;
+BOOST_GEOMETRY_REGISTER_POINT_2D(Point, double, boost::geometry::cs::cartesian, first, second)
+
+namespace armarx::navigation::human
+{
+
+    shapes::Polygon
+    ConvexHullGenerator::createShape(const HumanGroup& group) const
+    {
+        if (group.humans.empty())
+        {
+            throw InvalidArgumentException("Human group size has to be at least 1");
+        }
+
+        using BoostPoly = boost::geometry::model::polygon<Point>;
+
+        std::vector<Point> coordinates;
+        for (const Human& human : group.humans)
+        {
+            coordinates.emplace_back(human.pose.translation().x(), human.pose.translation().y());
+        }
+
+        BoostPoly poly;
+        BoostPoly hull;
+
+        poly.outer().assign(coordinates.begin(), coordinates.end());
+        boost::geometry::convex_hull(poly, hull);
+
+        std::vector<Eigen::Vector2f> vertices;
+
+        for (auto it = boost::begin(boost::geometry::exterior_ring(hull));
+             it != boost::end(boost::geometry::exterior_ring(hull));
+             ++it)
+        {
+            const double x = boost::geometry::get<0>(*it);
+            const double y = boost::geometry::get<1>(*it);
+
+            vertices.emplace_back(x, y);
+        }
+
+        return {.vertices = vertices};
+    }
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/ConvexHullGenerator.h b/source/armarx/navigation/human/ConvexHullGenerator.h
new file mode 100644
index 0000000000000000000000000000000000000000..d99d845dbafc308bc7acc86b5b23282374d8dfc6
--- /dev/null
+++ b/source/armarx/navigation/human/ConvexHullGenerator.h
@@ -0,0 +1,44 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "GroupShapeGenerator.h"
+#include <armarx/navigation/human/types.h>
+
+
+#pragma once
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief A GroupShapeGenerator that defines the group shape of a set of humans as the
+     * convex hull of their positions in 2D space.
+     */
+    class ConvexHullGenerator : public GroupShapeGenerator
+    {
+    public:
+        ConvexHullGenerator() = default;
+        virtual ~ConvexHullGenerator() = default;
+
+        shapes::Polygon createShape(const HumanGroup& group) const override;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/DistanceFunction.h b/source/armarx/navigation/human/DistanceFunction.h
new file mode 100644
index 0000000000000000000000000000000000000000..070d897472f6efa4d209960fdde84ae9d09eb36f
--- /dev/null
+++ b/source/armarx/navigation/human/DistanceFunction.h
@@ -0,0 +1,49 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <armarx/navigation/human/types.h>
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief A distance function is able to compute a distance between two humans.
+     * The exact semantics of this distance depends on the implementation. For example,
+     * a HumanGrouper uses distances two group humans together (the smaller the distance between
+     * two humans, the more likely it is for them to belong to the same social group).
+     */
+    class DistanceFunction
+    {
+    public:
+        DistanceFunction() = default;
+        virtual ~DistanceFunction() = default;
+
+        /**
+         * @brief computeDistance computes the (semantic or literal, depending on the implementation) distance between the given humans
+         * @param h1 the first human
+         * @param h2 the second human
+         * @return the distance between the humans (greater values correspond to humans that are further away)
+         */
+        virtual double computeDistance(const Human& h1, const Human& h2) const = 0;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/EuclideanDistance.cpp b/source/armarx/navigation/human/EuclideanDistance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..911d59a914634c00594bce7cd160af86b39b617e
--- /dev/null
+++ b/source/armarx/navigation/human/EuclideanDistance.cpp
@@ -0,0 +1,10 @@
+#include "EuclideanDistance.h"
+
+namespace armarx::navigation::human
+{
+    double
+    EuclideanDistance::computeDistance(const Human& h1, const Human& h2) const
+    {
+        return (h1.pose.translation() - h2.pose.translation()).norm();
+    }
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/EuclideanDistance.h b/source/armarx/navigation/human/EuclideanDistance.h
new file mode 100644
index 0000000000000000000000000000000000000000..c1d39ff10bfffdc3636d1a882b3ba3c59a50adbf
--- /dev/null
+++ b/source/armarx/navigation/human/EuclideanDistance.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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "DistanceFunction.h"
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief The EuclideanDistance class is an implementation of a distance function that computes
+     * the literal, euclidean distance between two human. (i.e. sqrt(dx²+dy²)).
+     */
+    class EuclideanDistance : public DistanceFunction
+    {
+    public:
+        EuclideanDistance() = default;
+        double computeDistance(const Human& h1, const Human& h2) const override;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/GroupShapeGenerator.h b/source/armarx/navigation/human/GroupShapeGenerator.h
new file mode 100644
index 0000000000000000000000000000000000000000..34f777d9fb2bbdc17a42559b37125d32a7f0889f
--- /dev/null
+++ b/source/armarx/navigation/human/GroupShapeGenerator.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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <armarx/navigation/human/types.h>
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief A strategy encapsuling a way to create a shape describing the special area occupied
+     * by a social human group
+     */
+    class GroupShapeGenerator
+    {
+    public:
+        GroupShapeGenerator() = default;
+        virtual ~GroupShapeGenerator() = default;
+
+        /**
+         * @brief createShape creates and returns the shape describing the given group
+         * @param group a social grouping of at least one human
+         * @return a polygon describing the proxemic zone occupied by the given group (not including
+         * the zones of the individual members of the group)
+         */
+        virtual shapes::Polygon createShape(const HumanGroup& group) const = 0;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/HumanGrouper.cpp b/source/armarx/navigation/human/HumanGrouper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..281a77d448668fc9bdb4422dbf2b1f09a46d256e
--- /dev/null
+++ b/source/armarx/navigation/human/HumanGrouper.cpp
@@ -0,0 +1,70 @@
+#include "HumanGrouper.h"
+
+#include <unordered_set>
+
+#include "CombinedDistance.h"
+#include "ConvexHullGenerator.h"
+
+namespace armarx::navigation::human
+{
+    HumanGrouper::HumanGrouper(const GroupingSettings& settings) :
+        distanceFunction(std::make_unique<CombinedDistance>(settings.maxOrientationInfluence,
+                                                            settings.movementInfluence)),
+        shapeGenerator(std::make_unique<ConvexHullGenerator>()),
+        settings(settings)
+    {
+    }
+
+    void
+    HumanGrouper::updateHumans(const std::vector<human::Human>& newHumans)
+    {
+        currentHumans_ = newHumans;
+    }
+
+    std::vector<human::HumanGroup>
+    HumanGrouper::getCurrentGroups()
+    {
+        const DateTime now = DateTime::Now();
+
+        std::vector<human::HumanGroup> groups = {};
+
+        // maps index of human in currentHumans to index of group in groups
+        std::unordered_map<uint64_t, uint64_t> humanGroupMap;
+
+        for (uint64_t parentIdx = 0; parentIdx < currentHumans_.size(); parentIdx++)
+        {
+            const Human parent = currentHumans_.at(parentIdx);
+
+            // if no group exists yet for this human, create one just for them
+            if (humanGroupMap.count(parentIdx) == 0)
+            {
+                const human::HumanGroup group = {{{}}, {parent}, now};
+                groups.push_back(group);
+                humanGroupMap[parentIdx] = groups.size() - 1;
+            }
+
+            for (uint64_t childIdx = 0; childIdx < currentHumans_.size(); childIdx++)
+            {
+                const Human child = currentHumans_.at(childIdx);
+
+                if ((humanGroupMap.count(childIdx) == 0 ||
+                     humanGroupMap[childIdx] != humanGroupMap[parentIdx]) &&
+                    this->distanceFunction->computeDistance(parent, child) <
+                        settings.groupingThreshold)
+                {
+                    humanGroupMap[childIdx] = humanGroupMap[parentIdx];
+                    groups.at(humanGroupMap[childIdx])
+                        .humans.push_back(currentHumans_.at(childIdx));
+                }
+            }
+        }
+
+        for (human::HumanGroup& group : groups)
+        {
+            group.shape = shapeGenerator->createShape(group);
+        }
+
+        return groups;
+    }
+
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/HumanGrouper.h b/source/armarx/navigation/human/HumanGrouper.h
new file mode 100644
index 0000000000000000000000000000000000000000..fc3fe60dc8aab190d9f3cbe6d736db7915570560
--- /dev/null
+++ b/source/armarx/navigation/human/HumanGrouper.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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "DistanceFunction.h"
+#include "GroupShapeGenerator.h"
+#include "armarx/navigation/core/basic_types.h"
+#include "armarx/navigation/human/types.h"
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief Identifies social interaction groups in a collection of detected humans.
+     *
+     * Can identify social interaction groups in a collection of humans. The currently detected
+     * humans can be set by calling updateHumans. The grouper will also return groups of size 1,
+     * and every detected human will be in exactly one group.
+     */
+    class HumanGrouper
+    {
+        using Human = armarx::navigation::human::Human;
+
+    public:
+        /**
+         * @brief The GroupingSettings struct contains settings for group generation.
+         * This includes the influence of aligned orientation and how aggressively groups
+         * are generated.
+         */
+        struct GroupingSettings
+        {
+            // people strictly closer than this distance will be grouped
+            const double groupingThreshold = 2000;
+
+            // distances between humans will be scaled by up to this factor based on their orientation
+            const double maxOrientationInfluence = 1.5;
+
+            // distances between humans will be scaled by their movement difference depending on this factor
+            const double movementInfluence = 1.5;
+        };
+
+        /**
+         * @brief Creates a new HumanGrouper
+         * @param settings different parameters that will affect group generation
+         */
+        HumanGrouper(const GroupingSettings& settings);
+
+        /**
+         * @brief Sets the currently detected humans.
+         *
+         * Sets the currently detected humans. Only these humans will be assumed to currently exist
+         * when getCurrentGroups() is called the next time.
+         * @param newHumans a vector containing the most recently detected humans
+         */
+        void updateHumans(const std::vector<Human>& newHumans);
+
+        /**
+         * @brief Recognizes groups in the current humans.
+         *
+         * Identifies and returns social groups in the humans given to this instance by the last
+         * call to updateHumans. Will also return groups of size one for single people.
+         * @return a vector containing the recognized human groups
+         */
+        std::vector<human::HumanGroup> getCurrentGroups();
+
+    private:
+        const std::unique_ptr<DistanceFunction> distanceFunction;
+        const std::unique_ptr<GroupShapeGenerator> shapeGenerator;
+        const GroupingSettings settings;
+
+        std::vector<Human> currentHumans_;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/MovementDistance.cpp b/source/armarx/navigation/human/MovementDistance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a71bda4df7a3766904251abf9d4a1b5ff6667289
--- /dev/null
+++ b/source/armarx/navigation/human/MovementDistance.cpp
@@ -0,0 +1,12 @@
+#include "MovementDistance.h"
+
+namespace armarx::navigation::human
+{
+    MovementDistance::MovementDistance() = default;
+
+    double
+    MovementDistance::computeDistance(const Human& h1, const Human& h2) const
+    {
+        return (h1.linearVelocity - h2.linearVelocity).norm();
+    }
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/MovementDistance.h b/source/armarx/navigation/human/MovementDistance.h
new file mode 100644
index 0000000000000000000000000000000000000000..57fb065fb949a8f5fc571514e51ccb00144ed6ed
--- /dev/null
+++ b/source/armarx/navigation/human/MovementDistance.h
@@ -0,0 +1,40 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "DistanceFunction.h"
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief A DistanceFunction that defines the distance between two humans as the difference
+     * in velocity (that is, the norm of the linear velocity difference vector).
+     */
+    class MovementDistance : public DistanceFunction
+    {
+    public:
+        MovementDistance();
+
+        double computeDistance(const Human& h1, const Human& h2) const override;
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/OrientationDistance.cpp b/source/armarx/navigation/human/OrientationDistance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..07f98cf2df9aeda6b63213d95b17b86f41c0d753
--- /dev/null
+++ b/source/armarx/navigation/human/OrientationDistance.cpp
@@ -0,0 +1,58 @@
+#include "OrientationDistance.h"
+
+#include <boost/math/constants/constants.hpp>
+#include <math.h>
+
+namespace armarx::navigation::human
+{
+    OrientationDistance::OrientationDistance(const double max, const double min) :
+        max(max), min(min)
+    {
+    }
+
+    double
+    OrientationDistance::computeDistance(const Human& h1, const Human& h2) const
+    {
+        // ranges from 0 to 1
+        const double factor = getOrientationFactor(h1, h2);
+
+        return min + factor * (max - min);
+    }
+
+
+    double
+    OrientationDistance::getOrientationFactor(const Human& h1, const Human& h2)
+    {
+        const double lineOrientation = getLineOrientation(h2, h1);
+        const double angleH1 = normOrientation(Eigen::Rotation2Dd(h1.pose.linear()).angle() / M_PI);
+        const double angleH2 = normOrientation(Eigen::Rotation2Dd(h2.pose.linear()).angle() / M_PI);
+
+        // assuming the angles to be in the interval [0, 2pi]
+        const double deviationFirst = std::abs(normOrientation(angleH1 - lineOrientation));
+        const double deviationSecond = std::abs(normOrientation(angleH2 - (lineOrientation + 1)));
+
+        // ranges from 0 (looking directly at each other) to 1 (back to back)
+        return (deviationFirst + deviationSecond) / 2;
+    }
+
+    double
+    OrientationDistance::getLineOrientation(const Human& h1, const Human& h2)
+    {
+        const double dx = (h1.pose.translation() - h2.pose.translation()).x();
+        const double dy = (h1.pose.translation() - h2.pose.translation()).y();
+        const double lineOrientation = atan2(dy, dx) / M_PI;
+
+        return lineOrientation;
+    }
+
+    // brings orientation to (-1, 1]
+    double
+    OrientationDistance::normOrientation(const double orientation)
+    {
+        // brings orientation to [0, 2)
+        double convertedOrientation = std::fmod(((std::fmod(orientation, 2)) + 2), 2);
+        // brings orientation to [-1, 1)
+        convertedOrientation -= static_cast<int>(convertedOrientation > 1) * 2;
+        return convertedOrientation;
+    }
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/OrientationDistance.h b/source/armarx/navigation/human/OrientationDistance.h
new file mode 100644
index 0000000000000000000000000000000000000000..3b8175e6c94e8b79cff9ec5d42c43342188a601e
--- /dev/null
+++ b/source/armarx/navigation/human/OrientationDistance.h
@@ -0,0 +1,60 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2022
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include "DistanceFunction.h"
+
+
+namespace armarx::navigation::human
+{
+    /**
+     * @brief The OrientationDistance class is a DistanceFunction that computes the distance between
+     * the two humans based upon their orientation towards or away from each other.
+     *
+     * This distance function will return
+     * - the specified minimum distance (0 by default) if the humans are exactly facing each other
+     * - the specified maximum distance (1 by default) if the humans are oriented back to back
+     * - a linear interpolation between the two for anything inbetween (for instance, two people
+     *   looking in the same direction will have a distance equal to the arithmetic mean of the
+     *   minimum and maximum distance, 0.5 by default)
+     */
+    class OrientationDistance : public DistanceFunction
+    {
+    public:
+        /**
+         * @brief OrientationDistance Creates a new OrientationDistance with the specified range
+         * @param max the distance two people will have when faced back to back
+         * @param min the distance two people will have when oriented face to face
+         */
+        OrientationDistance(double max = 1, double min = 0);
+        double computeDistance(const Human& h1, const Human& h2) const override;
+
+    private:
+        const double max;
+        const double min;
+
+        // helper methods
+        static double getOrientationFactor(const Human& h1, const Human& h2);
+        static double getLineOrientation(const Human& h2, const Human& h1);
+        static double normOrientation(double orientation);
+    };
+} // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/aron/Human.xml b/source/armarx/navigation/human/aron/Human.xml
index 57d104d858b8b346531c61b73069aba168ce374d..2f2b64b9fa6f0851e9b5bced33f3b87e816849a9 100644
--- a/source/armarx/navigation/human/aron/Human.xml
+++ b/source/armarx/navigation/human/aron/Human.xml
@@ -2,6 +2,40 @@
 <AronTypeDefinition>
     <GenerateTypes>
 
+        <Object name='armarx::navigation::human::arondto::ExponentialPenaltyModel'>
+            <ObjectChild key='minDistance'>
+                <Float />
+            </ObjectChild>
+            <ObjectChild key='epsilon'>
+                <Float />
+            </ObjectChild>
+            <ObjectChild key='exponent'>
+                <Float />
+            </ObjectChild>
+        </Object>
+
+        <Object name='armarx::navigation::human::arondto::ProxemicZone'>
+            <ObjectChild key='pose'>
+                <Pose />
+            </ObjectChild>
+            <ObjectChild key='ellipseA'>
+                <Float />
+            </ObjectChild>
+            <ObjectChild key='ellipseB'>
+                <Float />
+            </ObjectChild>
+            <ObjectChild key='penalty'>
+                <armarx::navigation::human::arondto::ExponentialPenaltyModel />
+            </ObjectChild>
+            <ObjectChild key='weight'>
+                <Float />
+            </ObjectChild>
+            <ObjectChild key='homotopicRelevance'>
+                <Bool />
+            </ObjectChild>
+        </Object>
+
+
         <Object name='armarx::navigation::human::arondto::Human'>
             <ObjectChild key='pose'>
                 <Pose />
@@ -12,6 +46,11 @@
             <ObjectChild key='detectionTime'>
                 <Time />
             </ObjectChild>
+            <ObjectChild key='proxemicZones'>
+                <List>
+                    <armarx::navigation::human::arondto::ProxemicZone />
+                </List>
+            </ObjectChild>
         </Object>
 
         <Object name='armarx::navigation::human::arondto::HumanGroup'>
diff --git a/source/armarx/navigation/human/aron_conversions.cpp b/source/armarx/navigation/human/aron_conversions.cpp
index e09a3ca376d5f75790c8adada1038e627c968161..9afef2fe91017c8d6ce3eeec9e3df1c4d2a264e8 100644
--- a/source/armarx/navigation/human/aron_conversions.cpp
+++ b/source/armarx/navigation/human/aron_conversions.cpp
@@ -14,6 +14,15 @@ namespace armarx::navigation::human
         dto.pose = conv::to3D(bo.pose).matrix();
         dto.linearVelocity = conv::to3D(bo.linearVelocity);
         dto.detectionTime = bo.detectionTime;
+        dto.proxemicZones = bo.proxemicZones |
+                            ranges::views::transform(
+                                [](const ProxemicZone& boZone) -> arondto::ProxemicZone
+                                {
+                                    arondto::ProxemicZone dtoZone;
+                                    toAron(dtoZone, boZone);
+                                    return dtoZone;
+                                }) |
+                            ranges::to_vector;
     }
 
     void
@@ -22,6 +31,15 @@ namespace armarx::navigation::human
         bo.pose = conv::to2D(core::Pose(dto.pose));
         bo.linearVelocity = conv::to2D(dto.linearVelocity);
         bo.detectionTime = dto.detectionTime;
+        bo.proxemicZones = dto.proxemicZones |
+                           ranges::views::transform(
+                               [](const arondto::ProxemicZone& dtoZone) -> ProxemicZone
+                               {
+                                   ProxemicZone boZone;
+                                   fromAron(dtoZone, boZone);
+                                   return boZone;
+                               }) |
+                           ranges::to_vector;
     }
 
 
@@ -66,4 +84,42 @@ namespace armarx::navigation::human
         bo.detectionTime = dto.detectionTime;
     }
 
+    void
+    toAron(arondto::ExponentialPenaltyModel& dto, const ExponentialPenaltyModel& bo)
+    {
+        dto.minDistance = bo.minDistance;
+        dto.epsilon = bo.epsilon;
+        dto.exponent = bo.exponent;
+    }
+
+    void
+    fromAron(const arondto::ExponentialPenaltyModel& dto, ExponentialPenaltyModel& bo)
+    {
+        bo.minDistance = dto.minDistance;
+        bo.epsilon = dto.epsilon;
+        bo.exponent = dto.exponent;
+    }
+
+    void
+    toAron(arondto::ProxemicZone& dto, const ProxemicZone& bo)
+    {
+        dto.pose = conv::to3D(bo.pose).matrix();
+        dto.ellipseA = bo.shape.a;
+        dto.ellipseB = bo.shape.b;
+        toAron(dto.penalty, bo.penalty);
+        dto.weight = bo.weight;
+        dto.homotopicRelevance = bo.homotopicRelevance;
+    }
+
+    void
+    fromAron(const arondto::ProxemicZone& dto, ProxemicZone& bo)
+    {
+        bo.pose = conv::to2D(core::Pose(dto.pose));
+        bo.shape = {.a = dto.ellipseA, .b = dto.ellipseB};
+        fromAron(dto.penalty, bo.penalty);
+        bo.weight = dto.weight;
+        bo.homotopicRelevance = dto.homotopicRelevance;
+    }
+
+
 } // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/aron_conversions.h b/source/armarx/navigation/human/aron_conversions.h
index 71159748a75ed5d44312650a530c80cc2e25e1ab..230a940333fcc086e036fbac9710a896db30631d 100644
--- a/source/armarx/navigation/human/aron_conversions.h
+++ b/source/armarx/navigation/human/aron_conversions.h
@@ -14,6 +14,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  * @author     Tobias Gröger ( tobias dot groeger at student dot kit dot edu )
+ * @author     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
  * @date       2022
  * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
  *             GNU General Public License
@@ -25,11 +26,15 @@ namespace armarx::navigation::human
 {
     struct Human;
     struct HumanGroup;
+    struct ExponentialPenaltyModel;
+    struct ProxemicZone;
 
     namespace arondto
     {
         struct Human;
         struct HumanGroup;
+        struct ExponentialPenaltyModel;
+        struct ProxemicZone;
 
     } // namespace arondto
 
@@ -43,4 +48,10 @@ namespace armarx::navigation::human
     void toAron(arondto::HumanGroup& dto, const HumanGroup& bo);
     void fromAron(const arondto::HumanGroup& dto, HumanGroup& bo);
 
+    void toAron(arondto::ExponentialPenaltyModel& dto, const ExponentialPenaltyModel& bo);
+    void fromAron(const arondto::ExponentialPenaltyModel& dto, ExponentialPenaltyModel& bo);
+
+    void toAron(arondto::ProxemicZone& dto, const ProxemicZone& bo);
+    void fromAron(const arondto::ProxemicZone& dto, ProxemicZone& bo);
+
 } // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/human/test/human_test.cpp b/source/armarx/navigation/human/test/human_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..862c7e3b28abe6bd7e5fd1a10a8c0354aede4a27
--- /dev/null
+++ b/source/armarx/navigation/human/test/human_test.cpp
@@ -0,0 +1,308 @@
+/**
+ * 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     Timo Weberruß ( timo dot weberruss at student dot kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include <math.h>
+
+#include <algorithm>
+#include <vector>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/core/logging/Logging.h>
+
+#include <SemanticObjectRelations/Shapes/Shape.h>
+#include <armarx/navigation/core/Trajectory.h>
+#include <armarx/navigation/human/EuclideanDistance.h>
+#include <armarx/navigation/human/OrientationDistance.h>
+#include <range/v3/view/zip.hpp>
+
+// test includes and other stuff
+#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::core
+#define ARMARX_BOOST_TEST
+
+#include <armarx/navigation/Test.h>
+#include <armarx/navigation/core/basic_types.h>
+#include <armarx/navigation/human/CombinedDistance.h>
+#include <armarx/navigation/human/ConvexHullGenerator.h>
+#include <armarx/navigation/human/HumanGrouper.h>
+#include <armarx/navigation/human/MovementDistance.h>
+#include <armarx/navigation/human/types.h>
+
+using armarx::navigation::core::Pose2D;
+using armarx::navigation::human::CombinedDistance;
+using armarx::navigation::human::ConvexHullGenerator;
+using armarx::navigation::human::EuclideanDistance;
+using armarx::navigation::human::Human;
+using armarx::navigation::human::HumanGroup;
+using armarx::navigation::human::HumanGrouper;
+using armarx::navigation::human::MovementDistance;
+using armarx::navigation::human::OrientationDistance;
+using armarx::navigation::human::shapes::Polygon;
+
+BOOST_AUTO_TEST_CASE(testEuclideanDistance1_standardDistance)
+{
+    const EuclideanDistance distance = EuclideanDistance();
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(3, 4);
+    pose1.linear() = Eigen::Rotation2Df(1.1).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0.2, 0.1),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(6, 8);
+    pose2.linear() = Eigen::Rotation2Df(2.3).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0.5, 0.8),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    double d = distance.computeDistance(h1, h2);
+    BOOST_CHECK_CLOSE(d, 5, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(testOrientationDistance1_faceToFace)
+{
+    const OrientationDistance distance = OrientationDistance(1, 0);
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(0, 0);
+    pose1.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0.2, 0.1),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(1, 0);
+    pose2.linear() = Eigen::Rotation2Df(M_PI).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0.5, 0.8),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    double d = distance.computeDistance(h1, h2);
+    BOOST_CHECK(d < 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(testOrientationDistance2_backToBack)
+{
+    const OrientationDistance distance = OrientationDistance(7, 0);
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(2, 2);
+    pose1.linear() = Eigen::Rotation2Df(-M_PI_2).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0.2, 0.1),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(2, 3);
+    pose2.linear() = Eigen::Rotation2Df(M_PI_2).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0.5, 0.8),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    double d = distance.computeDistance(h1, h2);
+    BOOST_CHECK_CLOSE(d, 7, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(testMovementDistance1_roughlyMovingTogether)
+{
+    const MovementDistance distance = MovementDistance();
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(-2, 3);
+    pose1.linear() = Eigen::Rotation2Df(-M_PI_2).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(1.0, 1.0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(2, 5);
+    pose2.linear() = Eigen::Rotation2Df(M_PI_2).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(1.3, 1.4),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    double d = distance.computeDistance(h1, h2);
+    BOOST_CHECK_CLOSE(d, 0.5, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(testCombinedDistance1_threeDistances)
+{
+    const CombinedDistance combined = CombinedDistance(2, 3);
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(0, 0);
+    pose1.linear() = Eigen::Rotation2Df(-M_PI_2).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(1.0, 1.0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(2, 0);
+    pose2.linear() = Eigen::Rotation2Df(M_PI_2).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(1.3, 1.4),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    double d = combined.computeDistance(h1, h2);
+    BOOST_CHECK_CLOSE(d, 2 * 1.5 * 2.5, 0.001);
+}
+
+BOOST_AUTO_TEST_CASE(testConvexHullGenerator1_triangle)
+{
+    ConvexHullGenerator generator = ConvexHullGenerator();
+
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(-2, 0);
+    pose1.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(2, 0);
+    pose2.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose3 = Pose2D::Identity();
+    pose3.translation() = Eigen::Vector2f(0, 2);
+    pose3.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h3 = {.pose = pose3,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose4 = Pose2D::Identity();
+    pose4.translation() = Eigen::Vector2f(0, 1);
+    pose4.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h4 = {.pose = pose4,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+
+    std::vector<Human> humans = {h1, h2, h3, h4};
+
+    HumanGroup group = {.shape = {{}}, .humans = humans, .detectionTime = armarx::DateTime::Now()};
+
+    Polygon hull = generator.createShape(group);
+
+    // Check that the vertices of the polygon contain all 3 positions from the outer 3 people
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose1.translation()) !=
+                hull.vertices.end());
+
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose2.translation()) !=
+                hull.vertices.end());
+
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose3.translation()) !=
+                hull.vertices.end());
+
+
+    // Check that the hull doesn't contain the inner person
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose4.translation()) ==
+                hull.vertices.end());
+}
+
+BOOST_AUTO_TEST_CASE(testConvexHullGenerator2_line)
+{
+    ConvexHullGenerator generator = ConvexHullGenerator();
+
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(-2, 0);
+    pose1.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(2, 0);
+    pose2.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    std::vector<Human> humans = {h1, h2};
+
+    HumanGroup group = {.shape = {{}}, .humans = humans, .detectionTime = armarx::DateTime::Now()};
+
+    Polygon hull = generator.createShape(group);
+
+    // Check that the vertices of the polygon contains both positions
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose1.translation()) !=
+                hull.vertices.end());
+
+    BOOST_CHECK(std::find(hull.vertices.begin(), hull.vertices.end(), pose2.translation()) !=
+                hull.vertices.end());
+}
+
+BOOST_AUTO_TEST_CASE(testHumanGrouper1_twoOfThree)
+{
+    /*          ^     ^
+     *          |     |
+     *         h1>   <h2
+     *
+     *            h3
+     *            V
+     */
+
+    Pose2D pose1 = Pose2D::Identity();
+    pose1.translation() = Eigen::Vector2f(2, 1);
+    pose1.linear() = Eigen::Rotation2Df(0).toRotationMatrix();
+    const Human h1 = {.pose = pose1,
+                      .linearVelocity = Eigen::Vector2f(0, 1),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose2 = Pose2D::Identity();
+    pose2.translation() = Eigen::Vector2f(3, 0.9);
+    pose2.linear() = Eigen::Rotation2Df(M_PI).toRotationMatrix();
+    const Human h2 = {.pose = pose2,
+                      .linearVelocity = Eigen::Vector2f(0, 1.1),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    Pose2D pose3 = Pose2D::Identity();
+    pose3.translation() = Eigen::Vector2f(2.5, 0);
+    pose3.linear() = Eigen::Rotation2Df(-M_PI_2).toRotationMatrix();
+    const Human h3 = {.pose = pose3,
+                      .linearVelocity = Eigen::Vector2f(0, 0),
+                      .detectionTime = armarx::DateTime::Now()};
+
+    HumanGrouper::GroupingSettings settings = {
+        .groupingThreshold = 2, .maxOrientationInfluence = 3, .movementInfluence = 2};
+
+    HumanGrouper grouper = HumanGrouper(settings);
+
+    std::vector<Human> humans = {h1, h2, h3};
+    grouper.updateHumans(humans);
+
+    std::vector<HumanGroup> groups = grouper.getCurrentGroups();
+
+    BOOST_CHECK_EQUAL(groups.size(), 2);
+    HumanGroup g1 = groups.at(0);
+    HumanGroup g2 = groups.at(1);
+
+    BOOST_CHECK((g1.humans.size() == 2) != (g2.humans.size() == 2));
+    HumanGroup& groupOfTwo = g1.humans.size() == 2 ? g1 : g2;
+
+
+    // Check that the group of two contains h1 and h2
+    BOOST_CHECK(std::find(groupOfTwo.humans.begin(), groupOfTwo.humans.end(), h1) !=
+                groupOfTwo.humans.end());
+    BOOST_CHECK(std::find(groupOfTwo.humans.begin(), groupOfTwo.humans.end(), h2) !=
+                groupOfTwo.humans.end());
+
+    // Check that a shape was set (the detailed convex hull generation is tested seperatly)
+    BOOST_CHECK(groupOfTwo.shape.vertices.size() > 0);
+}
diff --git a/source/armarx/navigation/human/test/human_tracker_test.cpp b/source/armarx/navigation/human/test/human_tracker_test.cpp
index 322021491589c466cccc1981fe946819c4caf812..0e45d1a8da21fb717e8177d3c6c0fe1ff869fa72 100644
--- a/source/armarx/navigation/human/test/human_tracker_test.cpp
+++ b/source/armarx/navigation/human/test/human_tracker_test.cpp
@@ -239,7 +239,7 @@ namespace armarx::navigation::human
 
         BOOST_CHECK_EQUAL(tracker.getTrackedHumans().size(), 1);
 
-        CamMm late = {.detectionTime = DateTime(Duration::MilliSeconds(1000))};
+        CamMm late = {.detectionTime = DateTime(Duration::MilliSeconds(1000)), .humanPoses = {}};
         tracker.update(late);
 
         BOOST_CHECK_EQUAL(tracker.getTrackedHumans().size(), 0);
diff --git a/source/armarx/navigation/human/types.cpp b/source/armarx/navigation/human/types.cpp
index 854b0d15569474955bde4550b77c6a168aa8238e..8bf473be7ddf059411f47b62d9373258cc688d6e 100644
--- a/source/armarx/navigation/human/types.cpp
+++ b/source/armarx/navigation/human/types.cpp
@@ -14,6 +14,15 @@ namespace armarx::navigation::human
         return estimation;
     }
 
+    bool
+    Human::operator==(const Human& other) const
+    {
+        return other.pose.matrix() == this->pose.matrix() &&
+               other.linearVelocity == this->linearVelocity &&
+               other.detectionTime == this->detectionTime;
+    }
+
+
     aron::data::DictPtr
     Human::toAron() const
     {
@@ -34,6 +43,46 @@ namespace armarx::navigation::human
         return bo;
     }
 
+    aron::data::DictPtr
+    ExponentialPenaltyModel::toAron() const
+    {
+        arondto::ExponentialPenaltyModel dto;
+        human::toAron(dto, *this);
+        return dto.toAron();
+    }
+
+    ExponentialPenaltyModel
+    ExponentialPenaltyModel::FromAron(const aron::data::DictPtr& dict)
+    {
+        ARMARX_CHECK_NOT_NULL(dict);
+        arondto::ExponentialPenaltyModel dto;
+        dto.fromAron(dict);
+
+        ExponentialPenaltyModel bo;
+        fromAron(dto, bo);
+        return bo;
+    }
+
+    aron::data::DictPtr
+    ProxemicZone::toAron() const
+    {
+        arondto::ProxemicZone dto;
+        human::toAron(dto, *this);
+        return dto.toAron();
+    }
+
+    ProxemicZone
+    ProxemicZone::FromAron(const aron::data::DictPtr& dict)
+    {
+        ARMARX_CHECK_NOT_NULL(dict);
+        arondto::ProxemicZone dto;
+        dto.fromAron(dict);
+
+        ProxemicZone bo;
+        fromAron(dto, bo);
+        return bo;
+    }
+
 
     aron::data::DictPtr
     HumanGroup::toAron() const
diff --git a/source/armarx/navigation/human/types.h b/source/armarx/navigation/human/types.h
index 0c3e08043b9fcea45aa603a0ef01d0eeba87f63d..f519375e07da4466c50a3a2839a447987639fbc8 100644
--- a/source/armarx/navigation/human/types.h
+++ b/source/armarx/navigation/human/types.h
@@ -30,14 +30,47 @@
 
 namespace armarx::navigation::human
 {
+    struct LinearPenaltyModel
+    {
+        float minDistance; // [m]
+        float epsilon; // [m]
+    };
+
+    struct ExponentialPenaltyModel
+    {
+        float minDistance; // [m]
+        float epsilon; // [m]
+        float exponent;
+
+        aron::data::DictPtr toAron() const;
+        static ExponentialPenaltyModel FromAron(const aron::data::DictPtr& dict);
+    };
+
+    struct ProxemicZone
+    {
+        core::Pose2D pose;
+        shapes::Ellipse shape;
+        ExponentialPenaltyModel penalty;
+        float weight;
+        bool homotopicRelevance = true;
+
+        aron::data::DictPtr toAron() const;
+        static ProxemicZone FromAron(const aron::data::DictPtr& dict);
+    };
+
+    using ProxemicZones = std::vector<ProxemicZone>;
+
     struct Human
     {
         core::Pose2D pose;
         Eigen::Vector2f linearVelocity;
         DateTime detectionTime;
+        ProxemicZones proxemicZones = {};
 
         core::Pose2D estimateAt(const DateTime& time) const;
 
+        bool operator==(const Human& other) const;
+
         aron::data::DictPtr toAron() const;
         static Human FromAron(const aron::data::DictPtr& dict);
     };
@@ -57,28 +90,4 @@ namespace armarx::navigation::human
 
     using HumanGroups = std::vector<HumanGroup>;
 
-    struct LinearPenaltyModel
-    {
-        float minDistance; // [m]
-        float epsilon; // [m]
-    };
-
-    struct ExponentialPenaltyModel
-    {
-        float minDistance; // [m]
-        float epsilon; // [m]
-        float exponent;
-    };
-
-    struct ProxemicZone
-    {
-        core::Pose2D pose;
-        shapes::Ellipse shape;
-        ExponentialPenaltyModel penalty;
-        float weight;
-        bool homotopicRelevance = true;
-    };
-
-    using ProxemicZones = std::vector<ProxemicZone>;
-
 } // namespace armarx::navigation::human
diff --git a/source/armarx/navigation/memory/CMakeLists.txt b/source/armarx/navigation/memory/CMakeLists.txt
index 267142f9ea3e658d9c9b1770476061f988453565..9059612ce21b6425a880412b2e526a519038ac2c 100644
--- a/source/armarx/navigation/memory/CMakeLists.txt
+++ b/source/armarx/navigation/memory/CMakeLists.txt
@@ -11,6 +11,8 @@ armarx_add_library(memory
         client/costmap/Reader.cpp
         client/human/Reader.cpp
         client/human/Writer.cpp
+        client/human_group/Reader.cpp
+        client/human_group/Writer.cpp
         # ./client/events/Reader.cpp
     HEADERS
         memory.h
@@ -24,6 +26,8 @@ armarx_add_library(memory
         client/costmap/Reader.h
         client/human/Reader.h
         client/human/Writer.h
+        client/human_group/Reader.h
+        client/human_group/Writer.h
         # ./client/events/Reader.h
     DEPENDENCIES
         ArmarXCoreInterfaces
diff --git a/source/armarx/navigation/memory/client/human/Reader.cpp b/source/armarx/navigation/memory/client/human/Reader.cpp
index f1ad2e2b96594dcf79c9b7ce5932de2fcccd320f..905eb69e53dff5ee0ea06ac0efa2bbb09e2cc980 100644
--- a/source/armarx/navigation/memory/client/human/Reader.cpp
+++ b/source/armarx/navigation/memory/client/human/Reader.cpp
@@ -41,23 +41,6 @@ namespace armarx::navigation::memory::client::human
         return qb;
     }
 
-    armarx::armem::client::query::Builder
-    Reader::buildHumanGroupsQuery(const Query& query) const
-    {
-        armarx::armem::client::query::Builder qb;
-
-        // clang-format off
-        qb
-        .coreSegments().withName(properties().coreSegmentName)
-        .providerSegments().withName(query.providerName)
-        .entities().withName("groups")
-        .snapshots().beforeOrAtTime(query.timestamp);
-        // clang-format on
-
-        return qb;
-    }
-
-
     std::string
     Reader::propertyPrefix() const
     {
@@ -107,95 +90,6 @@ namespace armarx::navigation::memory::client::human
         return humans;
     }
 
-    navigation::human::HumanGroups
-    asGroups(const armem::wm::ProviderSegment& providerSegment,
-             const DateTime& timestamp,
-             const Duration& maxAge)
-    {
-        navigation::human::HumanGroups humans;
-
-        ARMARX_CHECK(not providerSegment.empty()) << "No entities";
-        ARMARX_CHECK(providerSegment.size() == 1) << "There should be only one entity!";
-
-        providerSegment.forEachEntity(
-            [&humans, &timestamp, &maxAge](const armem::wm::Entity& entity)
-            {
-                const auto& entitySnapshot = entity.getLatestSnapshot();
-                ARMARX_CHECK(not entitySnapshot.empty()) << "No entity snapshot instances";
-
-                entitySnapshot.forEachInstance(
-                    [&](const armem::wm::EntityInstance& entityInstance)
-                    {
-                        const Duration dtToNow = timestamp - entityInstance.metadata().timeCreated;
-
-                        if (dtToNow < maxAge and dtToNow.isPositive())
-                        {
-                            const auto dto = navigation::human::arondto::HumanGroup::FromAron(
-                                entityInstance.data());
-
-                            navigation::human::HumanGroup human;
-                            fromAron(dto, human);
-                            humans.push_back(human);
-                        }
-                    });
-            });
-
-        return humans;
-    }
-
-    Reader::HumanGroupResult
-    Reader::queryHumanGroups(const Query& query) const
-    {
-        const auto qb = buildHumansQuery(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 {.groups = {},
-                    .status = HumanGroupResult::Status::Error,
-                    .errorMessage = qResult.errorMessage};
-        }
-
-        const auto coreSegment = qResult.memory.getCoreSegment(properties().coreSegmentName);
-
-        if (not coreSegment.hasProviderSegment(query.providerName))
-        {
-            ARMARX_DEBUG << "Provider segment `" << query.providerName << "` does not exist (yet).";
-            return {.groups = {}, .status = HumanGroupResult::Status::NoData};
-        }
-
-        const armem::wm::ProviderSegment& providerSegment =
-            coreSegment.getProviderSegment(query.providerName);
-
-        if (providerSegment.empty())
-        {
-            ARMARX_DEBUG << "No entities.";
-            return {.groups = {},
-                    .status = HumanGroupResult::Status::NoData,
-                    .errorMessage = "No entities"};
-        }
-
-        try
-        {
-            return HumanGroupResult{.groups =
-                                        asGroups(providerSegment, query.timestamp, query.maxAge),
-                                    .status = HumanGroupResult::Status::Success};
-        }
-        catch (...)
-        {
-            return HumanGroupResult{.groups = {},
-                                    .status = HumanGroupResult::Status::Error,
-                                    .errorMessage = GetHandledExceptionString()};
-        }
-    }
-
-
     Reader::HumanResult
     Reader::queryHumans(const Query& query) const
     {
diff --git a/source/armarx/navigation/memory/client/human/Reader.h b/source/armarx/navigation/memory/client/human/Reader.h
index 91a17afaad4cff3b4a15ad7bc11b9be0b1cb1fd2..3a47556a072f3528cc6567d5dec1ce91ee3a8ba6 100644
--- a/source/armarx/navigation/memory/client/human/Reader.h
+++ b/source/armarx/navigation/memory/client/human/Reader.h
@@ -65,31 +65,10 @@ namespace armarx::navigation::memory::client::human
             }
         };
 
-        struct HumanGroupResult
-        {
-            armarx::navigation::human::HumanGroups groups;
-
-            enum Status
-            {
-                Success,
-                NoData,
-                Error
-            } status;
-
-            std::string errorMessage = "";
-
-            operator bool() const noexcept
-            {
-                return status == Status::Success;
-            }
-        };
-
         HumanResult queryHumans(const Query& query) const;
-        HumanGroupResult queryHumanGroups(const Query& query) const;
 
     protected:
         ::armarx::armem::client::query::Builder buildHumansQuery(const Query& query) const;
-        ::armarx::armem::client::query::Builder buildHumanGroupsQuery(const Query& query) const;
 
         std::string propertyPrefix() const override;
         Properties defaultProperties() const override;
diff --git a/source/armarx/navigation/memory/client/human/Writer.cpp b/source/armarx/navigation/memory/client/human/Writer.cpp
index c38c2ac775e064890eecbf12d95c55ce017fb4a8..97767d14c5901e39f1f3cc8d309e324c0437d6e7 100644
--- a/source/armarx/navigation/memory/client/human/Writer.cpp
+++ b/source/armarx/navigation/memory/client/human/Writer.cpp
@@ -6,8 +6,8 @@
 #include <armarx/navigation/algorithms/aron/Costmap.aron.generated.h>
 #include <armarx/navigation/algorithms/aron_conversions.h>
 #include <armarx/navigation/human/aron/Human.aron.generated.h>
-#include <armarx/navigation/memory/constants.h>
 #include <armarx/navigation/human/aron_conversions.h>
+#include <armarx/navigation/memory/constants.h>
 
 
 namespace armarx::navigation::memory::client::human
@@ -42,62 +42,12 @@ namespace armarx::navigation::memory::client::human
         std::transform(humans.begin(),
                        humans.end(),
                        std::back_inserter(update.instancesData),
-                       [](const navigation::human::Human& human) -> armarx::aron::data::DictPtr {
-                            navigation::human::arondto::Human dto;
-                            toAron(dto, human);
-
-                            return dto.toAron();
-                       });
-
-
-        update.timeCreated = timestamp;
-
-        ARMARX_DEBUG << "Committing " << update << " at time " << timestamp;
-        armem::EntityUpdateResult updateResult = memoryWriter().commit(update);
-
-        ARMARX_DEBUG << updateResult;
-
-        if (not updateResult.success)
-        {
-            ARMARX_ERROR << updateResult.errorMessage;
-        }
-
-        return updateResult.success;
-    }
-
-    bool
-    Writer::store(const armarx::navigation::human::HumanGroups& groups,
-                  //   const std::string& name,
-                  const std::string& providerName,
-                  const armem::Time& timestamp)
-    {
-        std::lock_guard g{memoryWriterMutex()};
-
-        const auto result =
-            memoryWriter().addSegment(memory::constants::HumanCoreSegmentName, providerName);
-
-        if (not result.success)
-        {
-            ARMARX_ERROR << result.errorMessage;
-
-            // TODO(fabian.reister): message
-            return false;
-        }
-
-        const auto providerId = armem::MemoryID(result.segmentID);
-        const auto entityID = providerId.withEntityName("groups").withTimestamp(timestamp);
-
-        armem::EntityUpdate update;
-        update.entityID = entityID;
-
-        std::transform(groups.begin(),
-                       groups.end(),
-                       std::back_inserter(update.instancesData),
-                       [](const navigation::human::HumanGroup& group) -> armarx::aron::data::DictPtr {
-                            navigation::human::arondto::HumanGroup dto;
-                            toAron(dto, group);
+                       [](const navigation::human::Human& human) -> armarx::aron::data::DictPtr
+                       {
+                           navigation::human::arondto::Human dto;
+                           toAron(dto, human);
 
-                            return dto.toAron();
+                           return dto.toAron();
                        });
 
 
diff --git a/source/armarx/navigation/memory/client/human/Writer.h b/source/armarx/navigation/memory/client/human/Writer.h
index b224149c4aeb3c4858530f19ed051700060fc179..9afb179ae78b87bc808b6f87cf1919625278957b 100644
--- a/source/armarx/navigation/memory/client/human/Writer.h
+++ b/source/armarx/navigation/memory/client/human/Writer.h
@@ -51,12 +51,7 @@ namespace armarx::navigation::memory::client::human
         ~Writer() override;
 
         bool store(const armarx::navigation::human::Humans& humans,
-                //    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/human_group/Reader.cpp b/source/armarx/navigation/memory/client/human_group/Reader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0f6c8ad53f7ca17c1d7139384e8a805a06527f9a
--- /dev/null
+++ b/source/armarx/navigation/memory/client/human_group/Reader.cpp
@@ -0,0 +1,146 @@
+#include "Reader.h"
+
+#include <ArmarXCore/core/exceptions/LocalException.h>
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <RobotAPI/interface/armem/mns/MemoryNameSystemInterface.h>
+#include <RobotAPI/interface/armem/server/ReadingMemoryInterface.h>
+#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/wm/memory_definitions.h>
+#include <RobotAPI/libraries/armem/util/util.h>
+#include <RobotAPI/libraries/aron/core/Exception.h>
+#include <RobotAPI/libraries/aron/core/data/variant/complex/NDArray.h>
+
+#include "armarx/navigation/human/types.h"
+#include <armarx/navigation/algorithms/Costmap.h>
+#include <armarx/navigation/algorithms/aron_conversions.h>
+#include <armarx/navigation/human/aron/Human.aron.generated.h>
+#include <armarx/navigation/human/aron_conversions.h>
+#include <armarx/navigation/memory/constants.h>
+
+namespace armarx::navigation::memory::client::human_group
+{
+    Reader::~Reader() = default;
+
+    armarx::armem::client::query::Builder
+    Reader::buildHumanGroupsQuery(const Query& query) const
+    {
+        armarx::armem::client::query::Builder qb;
+
+        // clang-format off
+        qb
+        .coreSegments().withName(properties().coreSegmentName)
+        .providerSegments().withName(query.providerName)
+        .entities().withName("groups")
+        .snapshots().beforeOrAtTime(query.timestamp);
+        // clang-format on
+
+        return qb;
+    }
+
+
+    std::string
+    Reader::propertyPrefix() const
+    {
+        return "mem.nav.human_group.";
+    }
+
+    armarx::armem::client::util::SimpleReaderBase::Properties
+    Reader::defaultProperties() const
+    {
+        return {.memoryName = memory::constants::NavigationMemoryName,
+                .coreSegmentName = memory::constants::HumanGroupCoreSegmentName};
+    }
+
+    navigation::human::HumanGroups
+    asGroups(const armem::wm::ProviderSegment& providerSegment,
+             const DateTime& timestamp,
+             const Duration& maxAge)
+    {
+        navigation::human::HumanGroups humanGroups;
+
+        ARMARX_CHECK(not providerSegment.empty()) << "No entities";
+        ARMARX_CHECK(providerSegment.size() == 1) << "There should be only one entity!";
+
+        providerSegment.forEachEntity(
+            [&humanGroups, &timestamp, &maxAge](const armem::wm::Entity& entity)
+            {
+                const auto& entitySnapshot = entity.getLatestSnapshot();
+                ARMARX_CHECK(not entitySnapshot.empty()) << "No entity snapshot instances";
+
+                entitySnapshot.forEachInstance(
+                    [&](const armem::wm::EntityInstance& entityInstance)
+                    {
+                        const Duration dtToNow = timestamp - entityInstance.metadata().timeCreated;
+
+                        if (dtToNow < maxAge and dtToNow.isPositive())
+                        {
+                            const auto dto = navigation::human::arondto::HumanGroup::FromAron(
+                                entityInstance.data());
+
+                            navigation::human::HumanGroup humanGroup;
+                            fromAron(dto, humanGroup);
+                            humanGroups.push_back(humanGroup);
+                        }
+                    });
+            });
+
+        return humanGroups;
+    }
+
+    Reader::HumanGroupResult
+    Reader::queryHumanGroups(const Query& query) const
+    {
+        const auto qb = buildHumanGroupsQuery(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 {.groups = {},
+                    .status = HumanGroupResult::Status::Error,
+                    .errorMessage = qResult.errorMessage};
+        }
+
+        const auto coreSegment = qResult.memory.getCoreSegment(properties().coreSegmentName);
+
+        if (not coreSegment.hasProviderSegment(query.providerName))
+        {
+            ARMARX_DEBUG << "Provider segment `" << query.providerName << "` does not exist (yet).";
+            return {.groups = {}, .status = HumanGroupResult::Status::NoData};
+        }
+
+        const armem::wm::ProviderSegment& providerSegment =
+            coreSegment.getProviderSegment(query.providerName);
+
+        if (providerSegment.empty())
+        {
+            ARMARX_DEBUG << "No entities.";
+            return {.groups = {},
+                    .status = HumanGroupResult::Status::NoData,
+                    .errorMessage = "No entities"};
+        }
+
+        try
+        {
+            return HumanGroupResult{.groups =
+                                        asGroups(providerSegment, query.timestamp, query.maxAge),
+                                    .status = HumanGroupResult::Status::Success};
+        }
+        catch (...)
+        {
+            return HumanGroupResult{.groups = {},
+                                    .status = HumanGroupResult::Status::Error,
+                                    .errorMessage = GetHandledExceptionString()};
+        }
+    }
+
+} // namespace armarx::navigation::memory::client::human_group
diff --git a/source/armarx/navigation/memory/client/human_group/Reader.h b/source/armarx/navigation/memory/client/human_group/Reader.h
new file mode 100644
index 0000000000000000000000000000000000000000..537ac74d20f17ecf4253cef01444929b46a74348
--- /dev/null
+++ b/source/armarx/navigation/memory/client/human_group/Reader.h
@@ -0,0 +1,77 @@
+/*
+ * 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 <mutex>
+
+#include <RobotAPI/libraries/armem/client/query/Builder.h>
+#include <RobotAPI/libraries/armem/client/util/SimpleReaderBase.h>
+#include <RobotAPI/libraries/armem/core/Time.h>
+
+#include "armarx/navigation/human/types.h"
+#include <armarx/navigation/algorithms/Costmap.h>
+
+namespace armarx::navigation::memory::client::human_group
+{
+
+    class Reader : virtual public armarx::armem::client::util::SimpleReaderBase
+    {
+    public:
+        using armarx::armem::client::util::SimpleReaderBase::SimpleReaderBase;
+        ~Reader() override;
+
+        struct Query
+        {
+            std::string providerName;
+            armem::Time timestamp;
+            Duration maxAge;
+        };
+
+        struct HumanGroupResult
+        {
+            armarx::navigation::human::HumanGroups groups;
+
+            enum Status
+            {
+                Success,
+                NoData,
+                Error
+            } status;
+
+            std::string errorMessage = "";
+
+            operator bool() const noexcept
+            {
+                return status == Status::Success;
+            }
+        };
+
+        HumanGroupResult queryHumanGroups(const Query& query) const;
+
+    protected:
+        ::armarx::armem::client::query::Builder buildHumanGroupsQuery(const Query& query) const;
+
+        std::string propertyPrefix() const override;
+        Properties defaultProperties() const override;
+    };
+
+} // namespace armarx::navigation::memory::client::human_group
diff --git a/source/armarx/navigation/memory/client/human_group/Writer.cpp b/source/armarx/navigation/memory/client/human_group/Writer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2d38c45d4608672a5913f5a7cdc94329f2e34626
--- /dev/null
+++ b/source/armarx/navigation/memory/client/human_group/Writer.cpp
@@ -0,0 +1,83 @@
+#include "Writer.h"
+
+#include <iterator>
+
+#include "armarx/navigation/human/types.h"
+#include <armarx/navigation/algorithms/aron/Costmap.aron.generated.h>
+#include <armarx/navigation/algorithms/aron_conversions.h>
+#include <armarx/navigation/human/aron/Human.aron.generated.h>
+#include <armarx/navigation/human/aron_conversions.h>
+#include <armarx/navigation/memory/constants.h>
+
+
+namespace armarx::navigation::memory::client::human_group
+{
+    Writer::~Writer() = default;
+
+    bool
+    Writer::store(const armarx::navigation::human::HumanGroups& groups,
+                  //   const std::string& name,
+                  const std::string& providerName,
+                  const armem::Time& timestamp)
+    {
+        std::lock_guard g{memoryWriterMutex()};
+
+        const auto result =
+            memoryWriter().addSegment(memory::constants::HumanGroupCoreSegmentName, providerName);
+
+        if (not result.success)
+        {
+            ARMARX_ERROR << result.errorMessage;
+
+            // TODO(fabian.reister): message
+            return false;
+        }
+
+        const auto providerId = armem::MemoryID(result.segmentID);
+        const auto entityID = providerId.withEntityName("groups").withTimestamp(timestamp);
+
+        armem::EntityUpdate update;
+        update.entityID = entityID;
+
+        std::transform(groups.begin(),
+                       groups.end(),
+                       std::back_inserter(update.instancesData),
+                       [](const navigation::human::HumanGroup& group) -> armarx::aron::data::DictPtr
+                       {
+                           navigation::human::arondto::HumanGroup dto;
+                           toAron(dto, group);
+
+                           return dto.toAron();
+                       });
+
+
+        update.timeCreated = timestamp;
+
+        ARMARX_DEBUG << "Committing " << update << " at time " << timestamp;
+        armem::EntityUpdateResult updateResult = memoryWriter().commit(update);
+
+        ARMARX_DEBUG << updateResult;
+
+        if (not updateResult.success)
+        {
+            ARMARX_ERROR << updateResult.errorMessage;
+        }
+
+        return updateResult.success;
+    }
+
+    std::string
+    Writer::propertyPrefix() const
+    {
+        return "mem.nav.human_group.";
+    }
+
+    armarx::armem::client::util::SimpleWriterBase::SimpleWriterBase::Properties
+    Writer::defaultProperties() const
+    {
+        return SimpleWriterBase::Properties{.memoryName = memory::constants::NavigationMemoryName,
+                                            .coreSegmentName =
+                                                memory::constants::HumanGroupCoreSegmentName};
+    }
+
+} // namespace armarx::navigation::memory::client::human_group
diff --git a/source/armarx/navigation/memory/client/human_group/Writer.h b/source/armarx/navigation/memory/client/human_group/Writer.h
new file mode 100644
index 0000000000000000000000000000000000000000..c3de9d343a4c83c95ada602d146c339e361fa86b
--- /dev/null
+++ b/source/armarx/navigation/memory/client/human_group/Writer.h
@@ -0,0 +1,64 @@
+/*
+ * 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 <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>
+
+namespace armarx::navigation::memory::client::human_group
+{
+
+    /**
+    * @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 : virtual public armarx::armem::client::util::SimpleWriterBase
+    {
+    public:
+        using armarx::armem::client::util::SimpleWriterBase::SimpleWriterBase;
+        ~Writer() override;
+
+        bool store(const armarx::navigation::human::HumanGroups& groups,
+                   //    const std::string& name,
+                   const std::string& providerName,
+                   const armem::Time& timestamp);
+
+    protected:
+        std::string propertyPrefix() const override;
+        Properties defaultProperties() const override;
+    };
+
+
+} // namespace armarx::navigation::memory::client::human_group
diff --git a/source/armarx/navigation/memory/constants.h b/source/armarx/navigation/memory/constants.h
index f0932218de3bba512f5bbabaafba864e2e1afc37..23c01a1c112d74e1345f7e8f58bfe96ec1af190d 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 HumanGroupCoreSegmentName = "HumanGroup";
 
     inline const std::string GlobalPlannerResultCoreSegment = "Results_GlobalPlanner";
     inline const std::string LocalPlannerResultCoreSegment = "Results_LocalPlanner";