diff --git a/scenarios/ObjectPoseObserverExample/ObjectPoseObserverExample.scx b/scenarios/ObjectPoseObserverExample/ObjectPoseObserverExample.scx
index 197fe23ade0643213c3b6e9ca444098bdc5cb8ed..1aa5238bfbb09c201635d6504393e499e7e21593 100644
--- a/scenarios/ObjectPoseObserverExample/ObjectPoseObserverExample.scx
+++ b/scenarios/ObjectPoseObserverExample/ObjectPoseObserverExample.scx
@@ -5,5 +5,7 @@
 	<application name="RemoteGuiProviderApp" instance="" package="ArmarXGui" nodeName="" enabled="true" iceAutoRestart="false"/>
 	<application name="RobotStateComponent" instance="" package="RobotAPI" nodeName="" enabled="true" iceAutoRestart="false"/>
 	<application name="RobotToArVizApp" instance="" package="RobotAPI" nodeName="" enabled="true" iceAutoRestart="false"/>
+	<application name="ArVizStorage" instance="" package="RobotAPI" nodeName="" enabled="true" iceAutoRestart="false"/>
+	<application name="DebugObserver" instance="" package="ArmarXCore" nodeName="" enabled="true" iceAutoRestart="false"/>
 </scenario>
 
diff --git a/scenarios/ObjectPoseObserverExample/config/ObjectPoseObserver.cfg b/scenarios/ObjectPoseObserverExample/config/ObjectPoseObserver.cfg
index d14b730f3ab022ddb4bfc9bc0fea2038d5cd03ab..279193cc2f6feef27a705bf30dbae8451f7bfe14 100644
--- a/scenarios/ObjectPoseObserverExample/config/ObjectPoseObserver.cfg
+++ b/scenarios/ObjectPoseObserverExample/config/ObjectPoseObserver.cfg
@@ -118,6 +118,14 @@
 # ArmarX.ObjectPoseObserver.EnableProfiling = false
 
 
+# ArmarX.ObjectPoseObserver.KinematicUnitObserverName:  Name of the kinematic unit observer.
+#  Attributes:
+#  - Default:            KinematicUnitObserver
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.KinematicUnitObserverName = KinematicUnitObserver
+
+
 # ArmarX.ObjectPoseObserver.MaxHistoryRecordFrequency:  The Observer history is written with this maximum frequency. Everything faster is being skipped.
 #  Attributes:
 #  - Default:            50
@@ -175,7 +183,7 @@
 # ArmarX.ObjectPoseObserver.RemoteStateComponentName = RobotStateComponent
 
 
-# ArmarX.ObjectPoseObserver.calibration.offset:  Offset for the node to be calibrated
+# ArmarX.ObjectPoseObserver.calibration.offset:  Offset for the node to be calibrated.
 #  Attributes:
 #  - Default:            0
 #  - Case sensitivity:   yes
@@ -183,7 +191,7 @@
 # ArmarX.ObjectPoseObserver.calibration.offset = 0
 
 
-# ArmarX.ObjectPoseObserver.calibration.robotNode:  Robot node which can be calibrated
+# ArmarX.ObjectPoseObserver.calibration.robotNode:  Robot node which can be calibrated.
 #  Attributes:
 #  - Default:            Neck_2_Pitch
 #  - Case sensitivity:   yes
@@ -191,6 +199,88 @@
 # ArmarX.ObjectPoseObserver.calibration.robotNode = Neck_2_Pitch
 
 
+# ArmarX.ObjectPoseObserver.decay.delaySeconds:  Duration after latest localization before decay starts.
+#  Attributes:
+#  - Default:            5
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.decay.delaySeconds = 5
+
+
+# ArmarX.ObjectPoseObserver.decay.durationSeconds:  How long to reach minimal confidence.
+#  Attributes:
+#  - Default:            20
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.decay.durationSeconds = 20
+
+
+# ArmarX.ObjectPoseObserver.decay.enabled:  If true, object poses decay over time when not localized anymore.
+#  Attributes:
+#  - Default:            false
+#  - Case sensitivity:   yes
+#  - Required:           no
+#  - Possible values: {0, 1, false, no, true, yes}
+# ArmarX.ObjectPoseObserver.decay.enabled = false
+
+
+# ArmarX.ObjectPoseObserver.decay.maxConfidence:  Confidence when decay starts.
+#  Attributes:
+#  - Default:            1
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.decay.maxConfidence = 1
+
+
+# ArmarX.ObjectPoseObserver.decay.minConfidence:  Confidence after decay duration.
+#  Attributes:
+#  - Default:            0
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.decay.minConfidence = 0
+
+
+# ArmarX.ObjectPoseObserver.decay.removeObjectsBelowConfidence:  Remove objects whose confidence is lower than this value.
+#  Attributes:
+#  - Default:            0.100000001
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.decay.removeObjectsBelowConfidence = 0.100000001
+
+
+# ArmarX.ObjectPoseObserver.head.checkHeadVelocity:  If true, check whether the head is moving and discard updates in the meantime.
+#  Attributes:
+#  - Default:            true
+#  - Case sensitivity:   yes
+#  - Required:           no
+#  - Possible values: {0, 1, false, no, true, yes}
+# ArmarX.ObjectPoseObserver.head.checkHeadVelocity = true
+
+
+# ArmarX.ObjectPoseObserver.head.discardIntervalAfterMoveMS:  For how long new updates are ignored after moving the head.
+#  Attributes:
+#  - Default:            100
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.head.discardIntervalAfterMoveMS = 100
+
+
+# ArmarX.ObjectPoseObserver.head.maxJointVelocity:  If a head joint's velocity is higher, the head is considered moving.
+#  Attributes:
+#  - Default:            0.0500000007
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.head.maxJointVelocity = 0.0500000007
+
+
+# ArmarX.ObjectPoseObserver.tpc.pub.DebugObserver:  Name of the `DebugObserver` topic to publish data to.
+#  Attributes:
+#  - Default:            DebugObserver
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.tpc.pub.DebugObserver = DebugObserver
+
+
 # ArmarX.ObjectPoseObserver.visu.alpha:  Alpha of objects (1 = solid, 0 = transparent).
 #  Attributes:
 #  - Default:            1
@@ -199,6 +289,15 @@
 # ArmarX.ObjectPoseObserver.visu.alpha = 1
 
 
+# ArmarX.ObjectPoseObserver.visu.alphaByConfidence:  If true, use the pose confidence as alpha (if < 1.0).
+#  Attributes:
+#  - Default:            false
+#  - Case sensitivity:   yes
+#  - Required:           no
+#  - Possible values: {0, 1, false, no, true, yes}
+# ArmarX.ObjectPoseObserver.visu.alphaByConfidence = false
+
+
 # ArmarX.ObjectPoseObserver.visu.enabled:  Enable or disable visualization of objects.
 #  Attributes:
 #  - Default:            false
@@ -208,6 +307,14 @@
 ArmarX.ObjectPoseObserver.visu.enabled = true
 
 
+# ArmarX.ObjectPoseObserver.visu.frequenzyHz:  Frequency of visualization.
+#  Attributes:
+#  - Default:            25
+#  - Case sensitivity:   yes
+#  - Required:           no
+# ArmarX.ObjectPoseObserver.visu.frequenzyHz = 25
+
+
 # ArmarX.ObjectPoseObserver.visu.inGlobalFrame:  If true, show global poses. If false, show poses in robot frame.
 #  Attributes:
 #  - Default:            true
diff --git a/source/RobotAPI/components/ArViz/Client/Client.h b/source/RobotAPI/components/ArViz/Client/Client.h
index 658a1c2fd5475175b36a1412a933e5439ee6e7b0..cc8e7289a57f4302b65fae48574d04f8931d4bc5 100644
--- a/source/RobotAPI/components/ArViz/Client/Client.h
+++ b/source/RobotAPI/components/ArViz/Client/Client.h
@@ -51,13 +51,13 @@ namespace armarx::viz
 
         // ////////////////////////////////////////////////////////////////// //
         //layer
-        Layer layer(std::string const& name)
+        Layer layer(std::string const& name) const
         {
             return Layer(componentName, name);
         }
 
         template<class...Ts>
-        Layer layerContaining(std::string const& name, Ts&& ...elems)
+        Layer layerContaining(std::string const& name, Ts&& ...elems) const
         {
             auto l = layer(name);
             l.add(std::forward<Ts>(elems)...);
diff --git a/source/RobotAPI/components/ObjectPoseObserver/CMakeLists.txt b/source/RobotAPI/components/ObjectPoseObserver/CMakeLists.txt
index fd526dd81804ccfa68fa288d576831b58e1e5bce..74da78adf1af125b3ccab6669b1b47a2a9d0d9e8 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/CMakeLists.txt
+++ b/source/RobotAPI/components/ObjectPoseObserver/CMakeLists.txt
@@ -12,8 +12,11 @@ set(COMPONENT_LIBS
 
 set(SOURCES
     ObjectPoseObserver.cpp
-    detail/ObjectPoseDecay.cpp
+
+    detail/Data.cpp
+    detail/Decay.cpp
     detail/RobotHeadMovement.cpp
+    detail/Visu.cpp
 
     plugins/ObjectPoseProviderPlugin.cpp
     plugins/ObjectPoseClientPlugin.cpp
@@ -21,8 +24,11 @@ set(SOURCES
 )
 set(HEADERS
     ObjectPoseObserver.h
-    detail/ObjectPoseDecay.h
+
+    detail/Data.h
+    detail/Decay.h
     detail/RobotHeadMovement.h
+    detail/Visu.h
 
     plugins/ObjectPoseProviderPlugin.h
     plugins/ObjectPoseClientPlugin.h
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
index 2bc0726ed9a169e73fc91ceba560a4c09313ad2b..62a56ebf248e9601ea4e281f84a0d043bef68a61 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
@@ -22,21 +22,18 @@
 
 #include "ObjectPoseObserver.h"
 
-#include <SimoxUtility/algorithm/get_map_keys_values.h>
-#include <SimoxUtility/math/pose/pose.h>
-#include <SimoxUtility/meta/EnumNames.hpp>
-#include <SimoxUtility/shapes/OrientedBox.h>
-
-#include <VirtualRobot/Robot.h>
-#include <VirtualRobot/RobotConfig.h>
+#include <RobotAPI/libraries/core/Pose.h>
+#include <RobotAPI/libraries/core/FramedPose.h>
+#include <RobotAPI/libraries/ArmarXObjects/ice_conversions.h>
 
-#include <ArmarXCore/core/exceptions/local/UnexpectedEnumValueException.h>
 #include <ArmarXCore/core/time/CycleUtil.h>
 #include <ArmarXCore/observers/variant/Variant.h>
 
-#include <RobotAPI/libraries/core/Pose.h>
-#include <RobotAPI/libraries/core/FramedPose.h>
-#include <RobotAPI/libraries/ArmarXObjects/ice_conversions.h>
+#include <VirtualRobot/Robot.h>
+#include <VirtualRobot/RobotConfig.h>
+
+#include <SimoxUtility/algorithm/get_map_keys_values.h>
+#include <SimoxUtility/meta/EnumNames.hpp>
 
 
 namespace armarx
@@ -55,10 +52,10 @@ namespace armarx
         defs->defineOptionalProperty<std::string>("KinematicUnitObserverName", "KinematicUnitObserver", "Name of the kinematic unit observer.");
         defs->topic(debugObserver);
 
-        visu.defineProperties(defs, "visu.");
         calibration.defineProperties(defs, "calibration.");
+        data.defineProperties(defs);
         robotHead.defineProperties(defs, "head.");
-        decay.defineProperties(defs, "decay.");
+        visu.defineProperties(defs, "visu.");
 
         return defs;
     }
@@ -71,7 +68,9 @@ namespace armarx
     void ObjectPoseObserver::onInitObserver()
     {
         data.setTag(getName());
+        data.decay.setTag(getName());
         robotHead.setTag(getName());
+        visu.setTag(getName());
 
         usingTopicFromProperty("ObjectPoseTopicName");
     }
@@ -84,19 +83,13 @@ namespace armarx
         {
             data.robot = RobotState::addRobot("robot", VirtualRobot::RobotIO::RobotDescription::eStructure);
         }
+        data.robotStateComponent = getRobotStateComponent();
 
         getProxyFromProperty(robotHead.kinematicUnitObserver, "KinematicUnitObserverName", false, "", false);
         robotHead.debugObserver = debugObserver;
         robotHead.fetchDatafields();
 
-        if (!decay.updateTask)
-        {
-            decay.updateTask = new SimpleRunningTask<>([this]()
-            {
-                this->decayUpdateRun();
-            });
-            decay.updateTask->start();
-        }
+        visu.arviz = arviz;
         if (!visu.updateTask)
         {
             visu.updateTask = new SimpleRunningTask<>([this]()
@@ -128,7 +121,44 @@ namespace armarx
     void ObjectPoseObserver::reportObjectPoses(
         const std::string& providerName, const objpose::data::ProvidedObjectPoseSeq& providedPoses, const Ice::Current&)
     {
-        ARMARX_VERBOSE << "Received object poses from '" << providerName << "'.";
+        ARMARX_VERBOSE << "Received object " << providedPoses.size() << " poses from provider '" << providerName << "'.";
+        updateObjectPoses(providerName, providedPoses);
+    }
+
+
+    void ObjectPoseObserver::updateProviderInfo(const std::string& providerName, const objpose::ProviderInfo& info)
+    {
+        if (!info.proxy)
+        {
+            ARMARX_WARNING << "Received availability signal by provider '" << providerName << "' "
+                           << "with invalid provider proxy.\nIgnoring provider '" << providerName << "'.";
+            return;
+        }
+        {
+            std::scoped_lock lock(dataMutex);
+            std::stringstream ss;
+            for (const auto& id : info.supportedObjects)
+            {
+                ss << "- " << id << "\n";
+            }
+            ARMARX_VERBOSE << "Provider '" << providerName << "' available.\n"
+                           << "Supported objects: \n" << ss.str();
+            data.providers[providerName] = info;
+        }
+
+        if (!existsChannel(providerName))
+        {
+            offerChannel(providerName, "Channel of provider '" + providerName + "'.");
+        }
+        offerOrUpdateDataField(providerName, "objectType", objpose::ObjectTypeEnumNames.to_name(info.objectType),
+                               "The object type (known or unknown)");
+        offerOrUpdateDataField(providerName, "numSupportedObjects", int(info.supportedObjects.size()),
+                               "Number of requestable objects.");
+    }
+
+
+    void ObjectPoseObserver::updateObjectPoses(const std::string& providerName, const objpose::data::ProvidedObjectPoseSeq& providedPoses)
+    {
         TIMING_START(ReportObjectPoses);
 
         std::optional<IceUtil::Time> discardUpdatesUntil;
@@ -156,20 +186,20 @@ namespace armarx
         }
         if (debugObserver)
         {
-            StringVariantBaseMap map =
-            {
-                { "Discarding All Updates", new Variant(discardAll ? 1.f : 0.f) },
-            };
+            StringVariantBaseMap map;
+            map["Discarding All Updates"] = new Variant(discardAll ? 1.f : 0.f);
             if (discardAll)
             {
                 map["Proportion Updated Poses"] = new Variant(0.f);
             }
             debugObserver->setDebugChannel(getName(), map);
         }
+
         if (discardAll)
         {
             return;
         }
+
         {
             std::scoped_lock lock(dataMutex);
             RobotState::synchronizeLocalClone(data.robot);
@@ -269,58 +299,35 @@ namespace armarx
     }
 
 
-    void ObjectPoseObserver::updateProviderInfo(const std::string& providerName, const objpose::ProviderInfo& info)
+    void ObjectPoseObserver::handleProviderUpdate(const std::string& providerName)
     {
-        if (!info.proxy)
-        {
-            ARMARX_WARNING << "Received availability signal by provider '" << providerName << "' "
-                           << "with invalid provider proxy.\nIgnoring provider '" << providerName << "'.";
-            return;
-        }
+        // Initialized to 0 on first access.
+        if (data.providers.count(providerName) == 0)
         {
-            std::scoped_lock lock(dataMutex);
-            std::stringstream ss;
-            for (const auto& id : info.supportedObjects)
-            {
-                ss << "- " << id << "\n";
-            }
-            ARMARX_VERBOSE << "Provider '" << providerName << "' available.\n"
-                           << "Supported objects: \n" << ss.str();
-            data.providers[providerName] = info;
-
-            if (data.updateCounters.count(providerName) == 0)
-            {
-                data.updateCounters[providerName] = 0;
-            }
+            data.providers[providerName] = objpose::ProviderInfo();
         }
 
         if (!existsChannel(providerName))
         {
             offerChannel(providerName, "Channel of provider '" + providerName + "'.");
         }
-        offerOrUpdateDataField(providerName, "objectType", objpose::ObjectTypeEnumNames.to_name(info.objectType),
-                               "The object type (known or unknown)");
-        offerOrUpdateDataField(providerName, "numSupportedObjects", int(info.supportedObjects.size()),
-                               "Number of requestable objects.");
+        offerOrUpdateDataField(providerName, "objectCount", Variant(int(data.objectPoses.at(providerName).size())), "Number of provided object poses.");
     }
 
 
     objpose::data::ObjectPoseSeq ObjectPoseObserver::getObjectPoses(const Ice::Current&)
     {
+        TIMING_START(GetObjectPoses);
+
         TIMING_START(GetObjectPosesLock);
-        std::scoped_lock lock(dataMutex, decayMutex);
+        std::scoped_lock lock(dataMutex);
         TIMING_END_STREAM(GetObjectPosesLock, ARMARX_VERBOSE);
 
-        TIMING_START(GetObjectPoses);
-
-        bool synchronized = false;
-        objpose::data::ObjectPoseSeq result;
-        for (auto& [name, poses] : data.objectPoses)
-        {
-            toIceWithAttachments(poses, data.robot, result, synchronized, decay);
-        }
+        const IceUtil::Time now = TimeUtil::GetTime();
+        const objpose::data::ObjectPoseSeq result = objpose::toIce(data.getObjectPoses(now));
 
         TIMING_END_STREAM(GetObjectPoses, ARMARX_VERBOSE);
+
         if (debugObserver)
         {
             debugObserver->setDebugChannel(getName(),
@@ -335,11 +342,26 @@ namespace armarx
 
     objpose::data::ObjectPoseSeq ObjectPoseObserver::getObjectPosesByProvider(const std::string& providerName, const Ice::Current&)
     {
-        std::scoped_lock lock(dataMutex, decayMutex);
+        TIMING_START(GetObjectPoses);
+
+        TIMING_START(GetObjectPosesLock);
+        std::scoped_lock lock(dataMutex);
+        TIMING_END_STREAM(GetObjectPosesLock, ARMARX_VERBOSE);
+
+        const IceUtil::Time now = TimeUtil::GetTime();
+        const objpose::data::ObjectPoseSeq result = objpose::toIce(data.getObjectPosesByProvider(providerName, now));
+
+        TIMING_END_STREAM(GetObjectPoses, ARMARX_VERBOSE);
+
+        if (debugObserver)
+        {
+            debugObserver->setDebugChannel(getName(),
+            {
+                { "getObjectPosesByProvider() [ms]", new Variant(GetObjectPoses.toMilliSecondsDouble()) },
+                { "getObjectPosesByProvider() lock [ms]", new Variant(GetObjectPosesLock.toMilliSecondsDouble()) }
+            });
+        }
 
-        bool synchronized = false;
-        objpose::data::ObjectPoseSeq result;
-        toIceWithAttachments(data.objectPoses.at(providerName), data.robot, result, synchronized, decay);
         return result;
     }
 
@@ -434,193 +456,34 @@ namespace armarx
     objpose::ProviderInfo ObjectPoseObserver::getProviderInfo(const std::string& providerName, const Ice::Current&)
     {
         std::scoped_lock lock(dataMutex);
-        try
-        {
-            return data.providers.at(providerName);
-        }
-        catch (const std::out_of_range&)
-        {
-            std::stringstream ss;
-            ss << "No provider with name '" << providerName << "' available.\n";
-            ss << "Available are:\n";
-            for (const auto& [name, _] : data.providers)
-            {
-                ss << "- '" << name << "'\n";
-            }
-            throw std::out_of_range(ss.str());
-        }
+        return data.getProviderInfo(providerName);
     }
 
     bool ObjectPoseObserver::hasProvider(const std::string& providerName, const Ice::Current&)
     {
+        std::scoped_lock lock(dataMutex);
         return data.providers.count(providerName) > 0;
     }
 
 
-
     objpose::AttachObjectToRobotNodeOutput ObjectPoseObserver::attachObjectToRobotNode(
         const objpose::AttachObjectToRobotNodeInput& input, const Ice::Current&)
     {
         std::scoped_lock lock(dataMutex);
-
-        objpose::AttachObjectToRobotNodeOutput output;
-        output.success = false;  // We are not successful until proven otherwise.
-
-        ObjectID objectID = armarx::fromIce(input.objectID);
-
-        if (input.agentName != "" && input.agentName != data.robot->getName())
-        {
-            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown agent '" << input.agentName << "'."
-                           << "\n(You can leave the agent name empty if there is only one agent.)\n"
-                           << "\nKnown agents: " << std::vector<std::string> {data.robot->getName()};
-            return output;
-        }
-        VirtualRobot::RobotPtr agent = data.robot;
-
-        if (!agent->hasRobotNode(input.frameName))
-        {
-            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown node '" << input.frameName
-                           << "' of agent '" << agent->getName() << "'.";
-            return output;
-        }
-        std::string frameName = input.frameName;
-
-
-        // Find object pose provider name can be empty.
-        objpose::ObjectPose* currentObjectPose = data.findObjectPose(objectID, input.providerName);
-        if (!currentObjectPose)
-        {
-            ARMARX_WARNING << "Tried to attach object " << objectID << " to node '" << frameName
-                           << "' of agent '" << agent->getName() << "', but object is currently not provided.";
-            return output;
-        }
-
-        objpose::ObjectAttachmentInfo info;
-        info.agentName = agent->getName();
-        info.frameName = frameName;
-
-        if (input.poseInFrame)
-        {
-            info.poseInFrame = PosePtr::dynamicCast(input.poseInFrame)->toEigen();
-        }
-        else
-        {
-            RobotState::synchronizeLocalClone(data.robot);
-
-            armarx::FramedPose framed(currentObjectPose->objectPoseGlobal, armarx::GlobalFrame, agent->getName());
-            if (frameName == armarx::GlobalFrame)
-            {
-                info.poseInFrame = framed.toGlobalEigen(data.robot);
-            }
-            else
-            {
-                framed.changeFrame(data.robot, info.frameName);
-                info.poseInFrame = framed.toEigen();
-            }
-        }
-        data.attachments[std::make_pair(currentObjectPose->providerName, objectID)] = info;
-
-        ARMARX_INFO << "Attached object " << objectID << " by provider '" << currentObjectPose->providerName << "' "
-                    << "to node '" << info.frameName << "' of agent '" << info.agentName << "'.\n"
-                    << "Object pose in frame: \n" << info.poseInFrame;
-
-        output.success = true;
-        output.attachment = new objpose::data::ObjectAttachmentInfo();
-        output.attachment->frameName = info.frameName;
-        output.attachment->agentName = info.agentName;
-        output.attachment->poseInFrame = new Pose(info.poseInFrame);
-
-        return output;
+        return data.attachObjectToRobotNode(input);
     }
 
     objpose::DetachObjectFromRobotNodeOutput ObjectPoseObserver::detachObjectFromRobotNode(
         const objpose::DetachObjectFromRobotNodeInput& input, const Ice::Current&)
     {
-        ObjectID objectID = armarx::fromIce(input.objectID);
-        std::string providerName = input.providerName;
-
-        std::optional<objpose::ObjectAttachmentInfo> attachment;
-        {
-            std::scoped_lock lock(dataMutex);
-
-            // Remove from latest pose (if it was cached).
-            objpose::ObjectPose* objectPose = data.findObjectPose(objectID, input.providerName);
-            if (objectPose)
-            {
-                objectPose->attachment = std::nullopt;
-            }
-
-            if (providerName.empty() && objectPose)
-            {
-                providerName = objectPose->providerName;
-            }
-            // Remove from attachment map.
-            if (input.providerName.size() > 0)
-            {
-                auto it = data.attachments.find(std::make_pair(input.providerName, objectID));
-                if (it != data.attachments.end())
-                {
-                    attachment = it->second;
-                    data.attachments.erase(it);
-                }
-            }
-            else
-            {
-                // Search for entry with matching object ID.
-                for (auto it = data.attachments.begin(); it != data.attachments.end(); ++it)
-                {
-                    const ObjectID& id = it->first.second;
-                    if (id == objectID)
-                    {
-                        attachment = it->second;
-                        data.attachments.erase(it);
-                        break;
-                    }
-                }
-            }
-        }
-
-        objpose::DetachObjectFromRobotNodeOutput output;
-        output.wasAttached = bool(attachment);
-        if (attachment)
-        {
-            ARMARX_INFO << "Detached object " << objectID << " by provider '" << providerName << "' from robot node '"
-                        << attachment->frameName << "' of agent '" << attachment->agentName << "'.";
-        }
-        else
-        {
-            ARMARX_INFO << "Tried to detach object " << objectID << " by provider '" << providerName << "' "
-                        << "from robot node, but it was not attached.";
-        }
-
-        return output;
+        std::scoped_lock lock(dataMutex);
+        return data.detachObjectFromRobotNode(input);
     }
 
     objpose::DetachAllObjectsFromRobotNodesOutput ObjectPoseObserver::detachAllObjectsFromRobotNodes(const Ice::Current&)
     {
-        objpose::DetachAllObjectsFromRobotNodesOutput output;
-        output.numDetached = 0;
-
-        {
-            std::scoped_lock lock(dataMutex);
-
-            // Clear attachment map.
-            size_t num = data.attachments.size();
-            data.attachments.clear();
-            output.numDetached = int(num);
-
-            // Remove from poses (if it was cached).
-            for (auto& [prov, poses] : data.objectPoses)
-            {
-                for (auto& pose : poses)
-                {
-                    pose.attachment = std::nullopt;
-                }
-            }
-        }
-        ARMARX_INFO << "Detached all objects (" << output.numDetached << ") from robot nodes.";
-
-        return output;
+        std::scoped_lock lock(dataMutex);
+        return data.detachAllObjectsFromRobotNodes();
     }
 
 
@@ -643,49 +506,7 @@ namespace armarx
     ObjectPoseObserver::signalHeadMovement(const objpose::SignalHeadMovementInput& input, const Ice::Current&)
     {
         std::scoped_lock lock(robotHeadMutex);
-        objpose::SignalHeadMovementOutput output;
-        switch (input.action)
-        {
-            case objpose::HeadMovementAction::HeadMovementAction_Starting:
-                robotHead.movementStarts(input.discardUpdatesIntervalMilliSeconds < 0
-                                         ? robotHead.discardIntervalAfterMoveMS
-                                         : input.discardUpdatesIntervalMilliSeconds);
-                break;
-            case objpose::HeadMovementAction::HeadMovementAction_Stopping:
-                robotHead.movementStops(input.discardUpdatesIntervalMilliSeconds);
-                break;
-            default:
-                ARMARX_UNEXPECTED_ENUM_VALUE(objpose::HeadMovementaction, input.action);
-                break;
-        }
-        output.discardUpdatesUntilMilliSeconds = robotHead.discardUpdatesUntil.toMilliSeconds();
-        return output;
-    }
-
-
-    void ObjectPoseObserver::handleProviderUpdate(const std::string& providerName)
-    {
-        // Initialized to 0 on first access.
-        data.updateCounters[providerName]++;
-        if (data.providers.count(providerName) == 0)
-        {
-            data.providers[providerName] = objpose::ProviderInfo();
-        }
-
-        if (!existsChannel(providerName))
-        {
-            offerChannel(providerName, "Channel of provider '" + providerName + "'.");
-        }
-        offerOrUpdateDataField(providerName, "updateCounter", Variant(data.updateCounters.at(providerName)), "Counter incremented for each update.");
-        offerOrUpdateDataField(providerName, "objectCount", Variant(int(data.objectPoses.at(providerName).size())), "Number of provided object poses.");
-
-        if (visu.enabled)
-        {
-            // Trigger a visu update (we are holding dataMutex).
-            std::vector<viz::Layer> layers;
-            visualizeProvider(providerName, data.objectPoses.at(providerName), data.robot, layers, decay);
-            arviz.commit(layers);
-        }
+        return robotHead.signalHeadMovement(input);
     }
 
 
@@ -697,318 +518,58 @@ namespace armarx
             {
                 std::scoped_lock lock(visuMutex);
 
-                TIMING_START(Visu);
-                if (visu.enabled)
-                {
-                    std::scoped_lock lock(dataMutex, decayMutex);
-                    visualizeCommit(data.objectPoses, data.robot, decay);
-                }
                 if (visu.enabled)
                 {
-                    TIMING_END_STREAM(Visu, ARMARX_VERBOSE);
-                    if (debugObserver)
+                    TIMING_START(Visu);
+
+                    std::map<std::string, objpose::ObjectPoseSeq> objectPoses;
+                    ObjectFinder objectFinder;
+                    float minConfidence = -1;
                     {
-                        debugObserver->setDebugChannel(getName(),
+                        std::scoped_lock lock(dataMutex);
+
+                        const IceUtil::Time now = TimeUtil::GetTime();
+                        data.updateObjectPoses(now);
+                        objectPoses = data.objectPoses;
+                        objectFinder = data.objectFinder;
+                        if (data.decay.enabled)
                         {
-                            { "Visualize [ms]", new Variant(Visu.toMilliSecondsDouble()) },
-                        });
+                            minConfidence = data.decay.removeObjectsBelowConfidence;
+                        }
                     }
-                }
-            }
-            cycle.waitForCycleDuration();
-        }
-    }
-
-
-    void ObjectPoseObserver::visualizeCommit(
-        std::map<std::string, objpose::ObjectPoseSeq>& objectPoses, VirtualRobot::RobotPtr robot,
-        const objpose::ObjectPoseDecay& decay)
-    {
-        std::vector<viz::Layer> layers;
-        for (auto& [name, poses] : objectPoses)
-        {
-            visualizeProvider(name, poses, robot, layers, decay);
-        }
-        arviz.commit(layers);
-    }
-
-    void ObjectPoseObserver::visualizeProvider(
-        const std::string& providerName, objpose::ObjectPoseSeq& objectPoses,
-        VirtualRobot::RobotPtr robot, std::vector<viz::Layer>& layers,
-        const objpose::ObjectPoseDecay& decay)
-    {
-        viz::Layer& layer = layers.emplace_back(arviz.layer(providerName));
-        for (objpose::ObjectPose& objectPose : objectPoses)
-        {
-            bool show = not(decay.enabled and objectPose.confidence < decay.removeObjectsBelowConfidence);
-            if (show)
-            {
-                visualizeObjectPose(layer, objectPose, robot);
-            }
-        }
-    }
-
-    void ObjectPoseObserver::visualizeObjectPose(viz::Layer& layer, objpose::ObjectPose& objectPose, VirtualRobot::RobotPtr robot)
-    {
-        bool synchronized = false;
-
-        const armarx::ObjectID id = objectPose.objectID;
-        std::string key = id.str();
-
-        Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
-
-        // TODO: Get this out of visu function.
-        if (data.isAttachedCached(objectPose))
-        {
-            if (!synchronized)
-            {
-                RobotState::synchronizeLocalClone(robot);
-                synchronized = true;
-            }
-            objpose::ObjectPose updated = objectPose.getAttached(robot);
-            pose = visu.inGlobalFrame ? updated.objectPoseGlobal : updated.objectPoseRobot;
-        }
-        else
-        {
-            pose = visu.inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
-        }
-
-        {
-            viz::Object object = viz::Object(key).pose(pose);
-            if (std::optional<ObjectInfo> objectInfo = data.objectFinder.findObject(id))
-            {
-                object.file(objectInfo->package(), objectInfo->simoxXML().relativePath);
-            }
-            else
-            {
-                object.fileByObjectFinder(id);
-            }
-            if (visu.alphaByConfidence && objectPose.confidence < 1.0f)
-            {
-                object.overrideColor(simox::Color::white().with_alpha(objectPose.confidence));
-            }
-            else if (visu.alpha < 1)
-            {
-                object.overrideColor(simox::Color::white().with_alpha(visu.alpha));
-            }
-            layer.add(object);
-        }
+                    const std::vector<viz::Layer> layers = visu.visualizeCommit(objectPoses, minConfidence, objectFinder);
+                    arviz.commit(layers);
 
-        if (visu.oobbs && objectPose.localOOBB)
-        {
-            const simox::OrientedBoxf& oobb = *objectPose.localOOBB;
-            layer.add(viz::Box(key + " OOBB").set(oobb.transformed(pose))
-                      .color(simox::Color::lime(255, 64)));
-        }
-        if (visu.objectFrames)
-        {
-            layer.add(viz::Pose(key + " Pose").pose(pose).scale(visu.objectFramesScale));
-        }
-    }
-
-    objpose::ObjectPoseProviderPrx ObjectPoseObserver::getProviderProxy(const std::string& providerName)
-    {
-        return getProxy<objpose::ObjectPoseProviderPrx>(providerName, false, "", false);
-    }
-
-
-    void ObjectPoseObserver::toIceWithAttachments(
-        objpose::ObjectPoseSeq& objectPoses, VirtualRobot::RobotPtr agent,
-        objpose::data::ObjectPoseSeq& result, bool& synchronized,
-        const objpose::ObjectPoseDecay& decay)
-    {
-        for (objpose::ObjectPose& pose : objectPoses)
-        {
-            if (decay.enabled && pose.confidence < decay.removeObjectsBelowConfidence)
-            {
-                // Skip.
-                continue;
-            }
-            if (data.isAttachedCached(pose))
-            {
-                if (!synchronized)  // Synchronize only once.
-                {
-                    RobotState::synchronizeLocalClone(agent);
-                    synchronized = true;
-                }
-                result.push_back(pose.getAttached(agent).toIce());
-            }
-            else
-            {
-                result.push_back(pose.toIce());
-            }
-        }
-    }
-
-    void ObjectPoseObserver::decayUpdateRun()
-    {
-        CycleUtil cycle(static_cast<int>(1000 / decay.updateFrequencyHz));
-        while (decay.updateTask && !decay.updateTask->isStopped())
-        {
-            {
-                std::scoped_lock lock(decayMutex);
+                    TIMING_END_STREAM(Visu, ARMARX_VERBOSE);
 
-                TIMING_START(Decay);
-                if (decay.enabled)
-                {
-                    std::scoped_lock lock(dataMutex);
-                    IceUtil::Time now = TimeUtil::GetTime();
-                    for (auto& [providerName, objectPoses] : data.objectPoses)
-                    {
-                        decay.updateConfidences(objectPoses, now);
-                    }
-                }
-                if (decay.enabled)
-                {
-                    TIMING_END_STREAM(Decay, ARMARX_VERBOSE);
                     if (debugObserver)
                     {
                         debugObserver->setDebugChannel(getName(),
                         {
-                            { "Decay Update [ms]", new Variant(Decay.toMilliSecondsDouble()) },
+                            { "Visualize [ms]", new Variant(Visu.toMilliSecondsDouble()) },
                         });
                     }
                 }
             }
-
             cycle.waitForCycleDuration();
         }
     }
 
-    objpose::ObjectPose* ObjectPoseObserver::Data::findObjectPose(const ObjectID& objectID, const std::string& providerName)
-    {
-        objpose::ObjectPose* pose = nullptr;
-        if (!providerName.empty())
-        {
-            pose = findObjectPoseByID(objectPoses.at(providerName), objectID);
-        }
-        else
-        {
-            for (auto& [_, poses] : objectPoses)
-            {
-                pose = findObjectPoseByID(poses, objectID);
-                if (pose)
-                {
-                    break;
-                }
-            }
-        }
-        return pose;
-    }
-
-
-    std::optional<simox::OrientedBoxf> ObjectPoseObserver::Data::getObjectOOBB(const ObjectID& id)
-    {
-        return oobbCache.get(id, [this](const ObjectID & id) -> std::optional<simox::OrientedBoxf>
-        {
-            // Try to get OOBB from repository.
-            if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
-            {
-                try
-                {
-                    return objectInfo->loadOOBB();
-                }
-                catch (const std::ios_base::failure& e)
-                {
-                    // Give up - no OOBB information.
-                    ARMARX_WARNING << "Could not get OOBB of object " << id << ".\n- " << e.what();
-                    return std::nullopt;
-                }
-            }
-            else
-            {
-                return std::nullopt;
-            }
-        });
-    }
 
-    bool ObjectPoseObserver::Data::isAttachedCached(objpose::ObjectPose& objectPose) const
+    objpose::ObjectPoseProviderPrx ObjectPoseObserver::getProviderProxy(const std::string& providerName)
     {
-        if (!objectPose.attachment)
-        {
-            auto it = attachments.find(std::make_pair(objectPose.providerName, objectPose.objectID));
-            if (it != attachments.end())
-            {
-                objectPose.attachment = it->second;
-            }
-        }
-        return bool(objectPose.attachment);
+        return getProxy<objpose::ObjectPoseProviderPrx>(providerName, false, "", false);
     }
 
-
     void ObjectPoseObserver::createRemoteGuiTab()
     {
         using namespace armarx::RemoteGui::Client;
 
-        GroupBox visuGroup;
-        {
-            tab.visu.enabled.setValue(visu.enabled);
-            tab.visu.inGlobalFrame.setValue(visu.inGlobalFrame);
-            tab.visu.alpha.setRange(0, 1.0);
-            tab.visu.alpha.setValue(visu.alpha);
-            tab.visu.alphaByConfidence.setValue(visu.alphaByConfidence);
-            tab.visu.oobbs.setValue(visu.oobbs);
-            tab.visu.objectFrames.setValue(visu.objectFrames);
-            {
-                float max = 10000;
-                tab.visu.objectFramesScale.setRange(0, max);
-                tab.visu.objectFramesScale.setDecimals(2);
-                tab.visu.objectFramesScale.setSteps(int(10 * max));
-                tab.visu.objectFramesScale.setValue(visu.objectFramesScale);
-            }
-
-            GridLayout grid;
-            int row = 0;
-            grid.add(Label("Enabled"), {row, 0}).add(tab.visu.enabled, {row, 1});
-            row++;
-            grid.add(Label("Global Frame"), {row, 0}).add(tab.visu.inGlobalFrame, {row, 1});
-            row++;
-            grid.add(Label("Alpha"), {row, 0}).add(tab.visu.alpha, {row, 1}, {1, 3});
-            row++;
-            grid.add(Label("Alpha By Confidence"), {row, 0}).add(tab.visu.alphaByConfidence, {row, 1});
-            row++;
-            grid.add(Label("OOBB"), {row, 0}).add(tab.visu.oobbs, {row, 1});
-            row++;
-            grid.add(Label("Object Frames"), {row, 0}).add(tab.visu.objectFrames, {row, 1});
-            grid.add(Label("Scale:"), {row, 2}).add(tab.visu.objectFramesScale, {row, 3});
-            row++;
-
-            visuGroup.setLabel("Visualization");
-            visuGroup.addChild(grid);
-        }
-
-        GroupBox robotHeadGroup;
-        {
-            tab.robotHead.checkHeadVelocity.setValue(robotHead.checkHeadVelocity);
-            {
-                float max = 10.0;
-                tab.robotHead.maxJointVelocity.setRange(0, max);
-                tab.robotHead.maxJointVelocity.setDecimals(3);
-                tab.robotHead.maxJointVelocity.setSteps(int(100 * max));  // = 0.01 steps
-                tab.robotHead.maxJointVelocity.setValue(robotHead.maxJointVelocity);
-            }
-            {
-                int max = 10 * 1000;
-                tab.robotHead.discardIntervalAfterMoveMS.setRange(0, max);
-                tab.robotHead.discardIntervalAfterMoveMS.setValue(robotHead.discardIntervalAfterMoveMS);
-            }
-
-            GridLayout grid;
-            int row = 0;
-            grid.add(Label("Check Head Motion"), {row, 0}).add(tab.robotHead.checkHeadVelocity, {row, 1});
-            row++;
-            grid.add(Label("Max Joint Velocity"), {row, 0}).add(tab.robotHead.maxJointVelocity, {row, 1});
-            row++;
-            grid.add(Label("Discard Interval after Move [ms]"), {row, 0}).add(tab.robotHead.discardIntervalAfterMoveMS, {row, 1});
-            row++;
-
-            robotHeadGroup.setLabel("Robot Head Motion");
-            robotHeadGroup.addChild(grid);
-        }
-
-        tab.decay.setup(this->decay);
+        tab.visu.setup(this->visu);
+        tab.decay.setup(this->data.decay);
+        tab.robotHead.setup(this->robotHead);
 
-        VBoxLayout root = {visuGroup, tab.decay.group, robotHeadGroup, VSpacer()};
+        VBoxLayout root = {tab.visu.group, tab.decay.group, tab.robotHead.group, VSpacer()};
         RemoteGui_createTab(getName(), root, &tab);
     }
 
@@ -1017,45 +578,23 @@ namespace armarx
         // Non-atomic variables need to be guarded by a mutex if accessed by multiple threads
         {
             std::scoped_lock lock(visuMutex);
-
-            visu.enabled = tab.visu.enabled.getValue();
-            visu.inGlobalFrame = tab.visu.inGlobalFrame.getValue();
-            visu.alpha = tab.visu.alpha.getValue();
-            visu.oobbs = tab.visu.oobbs.getValue();
-            visu.objectFrames = tab.visu.objectFrames.getValue();
-            visu.objectFramesScale = tab.visu.objectFramesScale.getValue();
+            tab.visu.update(this->visu);
         }
         {
             std::scoped_lock lock(robotHeadMutex);
-
-            robotHead.checkHeadVelocity = tab.robotHead.checkHeadVelocity.getValue();
-            robotHead.maxJointVelocity = tab.robotHead.maxJointVelocity.getValue();
-            robotHead.discardIntervalAfterMoveMS = tab.robotHead.discardIntervalAfterMoveMS.getValue();
+            tab.robotHead.update(this->robotHead);
         }
         {
-            std::scoped_lock lock(decayMutex);
-            tab.decay.update(this->decay);
+            std::scoped_lock lock(dataMutex);
+            tab.decay.update(this->data.decay);
         }
     }
 
-    void ObjectPoseObserver::Visu::defineProperties(PropertyDefinitionsPtr defs, const std::string& prefix)
-    {
-        defs->optional(enabled, prefix + "enabled", "Enable or disable visualization of objects.");
-        defs->optional(frequencyHz, prefix + "frequenzyHz", "Frequency of visualization.");
-        defs->optional(inGlobalFrame, prefix + "inGlobalFrame", "If true, show global poses. If false, show poses in robot frame.");
-        defs->optional(alpha, prefix + "alpha", "Alpha of objects (1 = solid, 0 = transparent).");
-        defs->optional(alphaByConfidence, prefix + "alphaByConfidence", "If true, use the pose confidence as alpha (if < 1.0).");
-        defs->optional(oobbs, prefix + "oobbs", "Enable showing oriented bounding boxes.");
-        defs->optional(objectFrames, prefix + "objectFrames", "Enable showing object frames.");
-        defs->optional(objectFramesScale, prefix + "objectFramesScale", "Scaling of object frames.");
-    }
-
     void ObjectPoseObserver::Calibration::defineProperties(PropertyDefinitionsPtr defs, const std::string& prefix)
     {
         defs->optional(robotNode, prefix + "robotNode", "Robot node which can be calibrated.");
         defs->optional(offset, prefix + "offset", "Offset for the node to be calibrated.");
     }
 
-
 }
 
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
index 22fa6496f7f4a9db0cdede26939175be00657e62..8fb3369ceaed1f4880037c741bd74f8e2eb5625e 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
@@ -24,23 +24,17 @@
 
 #include <mutex>
 
-#include <SimoxUtility/caching/CacheMap.h>
-
-#include <ArmarXCore/core/logging/Logging.h>
-#include <ArmarXCore/core/services/tasks/TaskUtil.h>
 #include <ArmarXCore/observers/Observer.h>
-#include <ArmarXCore/observers/variant/DatafieldRef.h>
 
 #include <ArmarXGui/libraries/ArmarXGuiComponentPlugins/LightweightRemoteGuiComponentPlugin.h>
 
 #include <RobotAPI/interface/objectpose/ObjectPoseObserver.h>
-#include <RobotAPI/interface/observers/KinematicUnitObserverInterface.h>
 #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
 #include <RobotAPI/libraries/RobotAPIComponentPlugins/RobotStateComponentPlugin.h>
-#include <RobotAPI/libraries/ArmarXObjects/ObjectFinder.h>
-#include <RobotAPI/libraries/ArmarXObjects/ObjectPose.h>
 
-#include <RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.h>
+#include <RobotAPI/components/ObjectPoseObserver/detail/Data.h>
+#include <RobotAPI/components/ObjectPoseObserver/detail/Decay.h>
+#include <RobotAPI/components/ObjectPoseObserver/detail/Visu.h>
 #include <RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.h>
 
 #define ICE_CURRENT_ARG const Ice::Current& = Ice::emptyCurrent
@@ -79,6 +73,8 @@ namespace armarx
         , virtual public armarx::LightweightRemoteGuiComponentPluginUser
         , virtual public armarx::ArVizComponentPluginUser
     {
+        class Data;
+
     public:
         using RobotState = armarx::RobotStateComponentPluginUser;
 
@@ -144,88 +140,33 @@ namespace armarx
     private:
 
         void updateProviderInfo(const std::string& providerName, const objpose::ProviderInfo& info);
+
+        void updateObjectPoses(const std::string& providerName, const objpose::data::ProvidedObjectPoseSeq& providedPoses);
         void handleProviderUpdate(const std::string& providerName);
 
         objpose::ObjectPoseProviderPrx getProviderProxy(const std::string& providerName);
 
-        void toIceWithAttachments(objpose::ObjectPoseSeq& objectPoses, VirtualRobot::RobotPtr agent,
-                                  objpose::data::ObjectPoseSeq& result, bool& synchronized,
-                                  const objpose::ObjectPoseDecay& decay);
-
-        void decayUpdateRun();
 
 
         // Visualization
 
         void visualizeRun();
 
-        void visualizeCommit(std::map<std::string, objpose::ObjectPoseSeq>& objectPoses, VirtualRobot::RobotPtr robot,
-                             const objpose::ObjectPoseDecay& decay);
-        void visualizeProvider(const std::string& providerName, objpose::ObjectPoseSeq& objectPoses,
-                               VirtualRobot::RobotPtr robot, std::vector<viz::Layer>& layers,
-                               const objpose::ObjectPoseDecay& decay);
-        void visualizeObjectPose(viz::Layer& layer, objpose::ObjectPose& objectPose, VirtualRobot::RobotPtr robot);
-
 
     private:
 
         DebugObserverInterfacePrx debugObserver;
 
-        class Data : public armarx::Logging
-        {
-        public:
-            VirtualRobot::RobotPtr robot;
-
-            objpose::ProviderInfoMap providers;
-
-            std::map<std::string, objpose::ObjectPoseSeq> objectPoses;
-            std::map<std::string, int> updateCounters;
-
-            std::map<std::pair<std::string, ObjectID>, objpose::ObjectAttachmentInfo> attachments;
-
-
-            ObjectFinder objectFinder;
-            /// Caches results of attempts to retrieve the OOBB from ArmarXObjects.
-            simox::caching::CacheMap<ObjectID, std::optional<simox::OrientedBoxf>> oobbCache;
-
-
-        public:
-            objpose::ObjectPose* findObjectPose(const ObjectID& objectID, const std::string& providerName = "");
-            std::optional<simox::OrientedBoxf> getObjectOOBB(const ObjectID& id);
-
-            bool isAttachedCached(objpose::ObjectPose& objectPose) const;
-        };
-        Data data;
+        objpose::observer::Data data;
         std::mutex dataMutex;
 
-
-        objpose::ObjectPoseDecay decay;
-        std::mutex decayMutex;
-
-
-        objpose::RobotHeadMovement robotHead;
+        objpose::observer::RobotHeadMovement robotHead;
         std::mutex robotHeadMutex;
 
-
-        struct Visu
-        {
-            bool enabled = false;
-            float frequencyHz = 10;
-
-            bool inGlobalFrame = true;
-            float alpha = 1.0;
-            bool alphaByConfidence = false;
-            bool oobbs = false;
-            bool objectFrames = false;
-            float objectFramesScale = 1.0;
-
-            SimpleRunningTask<>::pointer_type updateTask;
-
-            void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix = "visu.");
-        };
-        Visu visu;
+        objpose::observer::Visu visu;
         std::mutex visuMutex;
 
+
         struct Calibration
         {
             std::string robotNode = "Neck_2_Pitch";
@@ -238,28 +179,9 @@ namespace armarx
 
         struct RemoteGuiTab : RemoteGui::Client::Tab
         {
-            struct Visu
-            {
-                RemoteGui::Client::CheckBox enabled;
-
-                RemoteGui::Client::CheckBox inGlobalFrame;
-                RemoteGui::Client::FloatSlider alpha;
-                RemoteGui::Client::CheckBox alphaByConfidence;
-                RemoteGui::Client::CheckBox oobbs;
-                RemoteGui::Client::CheckBox objectFrames;
-                RemoteGui::Client::FloatSpinBox objectFramesScale;
-            };
-            Visu visu;
-
-            struct RobotHead
-            {
-                RemoteGui::Client::CheckBox checkHeadVelocity;
-                RemoteGui::Client::FloatSpinBox maxJointVelocity;
-                RemoteGui::Client::IntSpinBox discardIntervalAfterMoveMS;
-            };
-            RobotHead robotHead;
-
-            objpose::ObjectPoseDecay::RemoteGui decay;
+            objpose::observer::Visu::RemoteGui visu;
+            objpose::observer::Decay::RemoteGui decay;
+            objpose::observer::RobotHeadMovement::RemoteGui robotHead;
         };
         RemoteGuiTab tab;
 
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be89f745d995ff82431d4b2fb285181a607730dc
--- /dev/null
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp
@@ -0,0 +1,359 @@
+#include "Data.h"
+
+#include <RobotAPI/libraries/core/Pose.h>
+#include <RobotAPI/libraries/core/FramedPose.h>
+#include <RobotAPI/libraries/core/remoterobot/RemoteRobot.h>
+#include <RobotAPI/libraries/ArmarXObjects/ObjectFinder.h>
+#include <RobotAPI/libraries/ArmarXObjects/ice_conversions.h>
+
+#include <ArmarXCore/core/time/TimeUtil.h>
+
+#include <sstream>
+
+
+namespace armarx::objpose::observer
+{
+
+    void Data::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
+    {
+        decay.defineProperties(defs, prefix + "decay.");
+    }
+
+    ObjectPoseSeq Data::getObjectPoses(IceUtil::Time now)
+    {
+        bool synchronized = false;
+        ObjectPoseSeq result;
+
+        for (auto& [providerName, objectPoses] : objectPoses)
+        {
+            // Update data.
+            updateObjectPoses(objectPoses, now, robot, synchronized);
+
+            // Collect results.
+            for (const ObjectPose& objectPose : objectPoses)
+            {
+                if (!(decay.enabled && objectPose.confidence < decay.removeObjectsBelowConfidence))
+                {
+                    result.push_back(objectPose);
+                }
+            }
+        }
+        return result;
+    }
+
+
+    ObjectPoseSeq Data::getObjectPosesByProvider(
+        const std::string& providerName,
+        IceUtil::Time now)
+    {
+        bool synchronized = false;
+
+        // Update data.
+        ObjectPoseSeq& objectPoses = this->objectPoses.at(providerName);
+        updateObjectPoses(objectPoses, now, robot, synchronized);
+
+        // Collect results.
+        ObjectPoseSeq result;
+        for (const ObjectPose& objectPose : objectPoses)
+        {
+            if (!(decay.enabled && objectPose.confidence < decay.removeObjectsBelowConfidence))
+            {
+                result.push_back(objectPose);
+            }
+        }
+        return result;
+    }
+
+
+    void Data::updateObjectPoses(IceUtil::Time now)
+    {
+        bool synchronized = false;
+        for (auto& [providerName, objectPoses] : objectPoses)
+        {
+            updateObjectPoses(objectPoses, now, robot, synchronized);
+        }
+    }
+
+
+    void Data::updateObjectPoses(
+        ObjectPoseSeq& objectPoses,
+        IceUtil::Time now,
+        VirtualRobot::RobotPtr agent,
+        bool& agentSynchronized) const
+    {
+        for (ObjectPose& pose : objectPoses)
+        {
+            updateObjectPose(pose, now, agent, agentSynchronized);
+        }
+    }
+
+
+    void Data::updateObjectPose(
+        ObjectPose& objectPose,
+        IceUtil::Time now,
+        VirtualRobot::RobotPtr agent,
+        bool& agentSynchronized) const
+    {
+        updateAttachement(objectPose, agent, agentSynchronized);
+
+        if (decay.enabled)
+        {
+            decay.updateConfidence(objectPose, now);
+        }
+    }
+
+
+    void Data::updateAttachement(
+        ObjectPose& objectPose, VirtualRobot::RobotPtr agent, bool& synchronized) const
+    {
+        if (!objectPose.attachment)
+        {
+            // Fetch attachment info from internal data structure.
+            auto it = attachments.find(std::make_pair(objectPose.providerName, objectPose.objectID));
+            if (it != attachments.end())
+            {
+                // Store it in the objectPose.
+                objectPose.attachment = it->second;
+            }
+            else
+            {
+                // No attachment, nothing to do.
+                return;
+            }
+        }
+        ARMARX_CHECK(objectPose.attachment);
+
+        if (!synchronized)  // Synchronize only once.
+        {
+            RemoteRobot::synchronizeLocalClone(agent, robotStateComponent);
+            synchronized = true;
+        }
+        objectPose.updateAttached(agent);
+    }
+
+
+    ObjectPose* Data::findObjectPose(const ObjectID& objectID, const std::string& providerName)
+    {
+        ObjectPose* pose = nullptr;
+        if (!providerName.empty())
+        {
+            pose = findObjectPoseByID(objectPoses.at(providerName), objectID);
+        }
+        else
+        {
+            for (auto& [_, poses] : objectPoses)
+            {
+                pose = findObjectPoseByID(poses, objectID);
+                if (pose)
+                {
+                    break;
+                }
+            }
+        }
+        return pose;
+    }
+
+
+    std::optional<simox::OrientedBoxf> Data::getObjectOOBB(const ObjectID& id)
+    {
+        return oobbCache.get(id, [this](const ObjectID & id) -> std::optional<simox::OrientedBoxf>
+        {
+            // Try to get OOBB from repository.
+            if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
+            {
+                try
+                {
+                    return objectInfo->loadOOBB();
+                }
+                catch (const std::ios_base::failure& e)
+                {
+                    // Give up - no OOBB information.
+                    ARMARX_WARNING << "Could not get OOBB of object " << id << ".\n- " << e.what();
+                    return std::nullopt;
+                }
+            }
+            else
+            {
+                return std::nullopt;
+            }
+        });
+    }
+
+    ProviderInfo Data::getProviderInfo(const std::string& providerName)
+    {
+        try
+        {
+            return providers.at(providerName);
+        }
+        catch (const std::out_of_range&)
+        {
+            std::stringstream ss;
+            ss << "No provider with name '" << providerName << "' available.\n";
+            ss << "Available are:\n";
+            for (const auto& [name, _] : providers)
+            {
+                ss << "- '" << name << "'\n";
+            }
+            throw std::out_of_range(ss.str());
+        }
+    }
+
+
+
+    AttachObjectToRobotNodeOutput
+    Data::attachObjectToRobotNode(const AttachObjectToRobotNodeInput& input)
+    {
+        AttachObjectToRobotNodeOutput output;
+        output.success = false;  // We are not successful until proven otherwise.
+
+        ObjectID objectID = armarx::fromIce(input.objectID);
+
+        if (input.agentName != "" && input.agentName != this->robot->getName())
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown agent '" << input.agentName << "'."
+                           << "\n(You can leave the agent name empty if there is only one agent.)\n"
+                           << "\nKnown agents: " << std::vector<std::string> {this->robot->getName()};
+            return output;
+        }
+        VirtualRobot::RobotPtr agent = this->robot;
+
+        if (!agent->hasRobotNode(input.frameName))
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown node '" << input.frameName
+                           << "' of agent '" << agent->getName() << "'.";
+            return output;
+        }
+        std::string frameName = input.frameName;
+
+
+        // Find object pose provider name can be empty.
+        ObjectPose* currentObjectPose = this->findObjectPose(objectID, input.providerName);
+        if (!currentObjectPose)
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to node '" << frameName
+                           << "' of agent '" << agent->getName() << "', but object is currently not provided.";
+            return output;
+        }
+
+        ObjectAttachmentInfo info;
+        info.agentName = agent->getName();
+        info.frameName = frameName;
+
+        if (input.poseInFrame)
+        {
+            info.poseInFrame = PosePtr::dynamicCast(input.poseInFrame)->toEigen();
+        }
+        else
+        {
+            RemoteRobot::synchronizeLocalClone(agent, robotStateComponent);
+
+            armarx::FramedPose framed(currentObjectPose->objectPoseGlobal, armarx::GlobalFrame, agent->getName());
+            if (frameName == armarx::GlobalFrame)
+            {
+                info.poseInFrame = framed.toGlobalEigen(this->robot);
+            }
+            else
+            {
+                framed.changeFrame(this->robot, info.frameName);
+                info.poseInFrame = framed.toEigen();
+            }
+        }
+        this->attachments[std::make_pair(currentObjectPose->providerName, objectID)] = info;
+
+        ARMARX_INFO << "Attached object " << objectID << " by provider '" << currentObjectPose->providerName << "' "
+                    << "to node '" << info.frameName << "' of agent '" << info.agentName << "'.\n"
+                    << "Object pose in frame: \n" << info.poseInFrame;
+
+        output.success = true;
+        output.attachment = new data::ObjectAttachmentInfo();
+        output.attachment->frameName = info.frameName;
+        output.attachment->agentName = info.agentName;
+        output.attachment->poseInFrame = new Pose(info.poseInFrame);
+
+        return output;
+    }
+
+    DetachObjectFromRobotNodeOutput Data::detachObjectFromRobotNode(const DetachObjectFromRobotNodeInput& input)
+    {
+        ObjectID objectID = armarx::fromIce(input.objectID);
+        std::string providerName = input.providerName;
+
+        std::optional<ObjectAttachmentInfo> attachment;
+        {
+            // Remove from latest pose (if it was cached).
+            ObjectPose* objectPose = this->findObjectPose(objectID, input.providerName);
+            if (objectPose)
+            {
+                objectPose->attachment = std::nullopt;
+            }
+
+            if (providerName.empty() && objectPose)
+            {
+                providerName = objectPose->providerName;
+            }
+            // Remove from attachment map.
+            if (input.providerName.size() > 0)
+            {
+                auto it = this->attachments.find(std::make_pair(input.providerName, objectID));
+                if (it != this->attachments.end())
+                {
+                    attachment = it->second;
+                    this->attachments.erase(it);
+                }
+            }
+            else
+            {
+                // Search for entry with matching object ID.
+                for (auto it = this->attachments.begin(); it != this->attachments.end(); ++it)
+                {
+                    const ObjectID& id = it->first.second;
+                    if (id == objectID)
+                    {
+                        attachment = it->second;
+                        this->attachments.erase(it);
+                        break;
+                    }
+                }
+            }
+        }
+
+        DetachObjectFromRobotNodeOutput output;
+        output.wasAttached = bool(attachment);
+        if (attachment)
+        {
+            ARMARX_INFO << "Detached object " << objectID << " by provider '" << providerName << "' from robot node '"
+                        << attachment->frameName << "' of agent '" << attachment->agentName << "'.";
+        }
+        else
+        {
+            ARMARX_INFO << "Tried to detach object " << objectID << " by provider '" << providerName << "' "
+                        << "from robot node, but it was not attached.";
+        }
+
+        return output;
+    }
+
+    DetachAllObjectsFromRobotNodesOutput Data::detachAllObjectsFromRobotNodes()
+    {
+        DetachAllObjectsFromRobotNodesOutput output;
+        output.numDetached = int(this->attachments.size());
+
+        // Clear attachment map.
+        this->attachments.clear();
+
+        // Remove from poses (if it was cached).
+        for (auto& [prov, poses] : this->objectPoses)
+        {
+            for (auto& pose : poses)
+            {
+                pose.attachment = std::nullopt;
+            }
+        }
+
+        ARMARX_INFO << "Detached all objects (" << output.numDetached << ") from robot nodes.";
+
+        return output;
+    }
+
+
+}
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h
new file mode 100644
index 0000000000000000000000000000000000000000..e825a95340f71e4357392433ae16f8c3fec1ee30
--- /dev/null
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include <map>
+#include <string>
+#include <optional>
+
+#include <SimoxUtility/caching/CacheMap.h>
+#include <SimoxUtility/shapes/OrientedBox.h>
+
+#include <ArmarXCore/core/logging/Logging.h>
+
+#include <RobotAPI/interface/core/RobotState.h>
+#include <RobotAPI/interface/objectpose/ObjectPoseObserver.h>
+
+#include <RobotAPI/libraries/ArmarXObjects/ObjectID.h>
+#include <RobotAPI/libraries/ArmarXObjects/ObjectPose.h>
+#include <RobotAPI/libraries/ArmarXObjects/ObjectFinder.h>
+
+#include "Decay.h"
+
+
+namespace armarx::objpose::observer
+{
+
+    /**
+     * @brief Models decay of object localizations by decreasing the confidence
+     * the longer the object was not localized.
+     */
+    class Data : public armarx::Logging
+    {
+    public:
+
+        void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix = "");
+
+        ObjectPoseSeq getObjectPoses(IceUtil::Time now);
+        ObjectPoseSeq getObjectPosesByProvider(const std::string& providerName, IceUtil::Time now);
+
+        ObjectPose* findObjectPose(const ObjectID& objectID, const std::string& providerName = "");
+        std::optional<simox::OrientedBoxf> getObjectOOBB(const ObjectID& id);
+
+        ProviderInfo getProviderInfo(const std::string& providerName);
+
+
+        AttachObjectToRobotNodeOutput attachObjectToRobotNode(const AttachObjectToRobotNodeInput& input);
+        DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(const DetachObjectFromRobotNodeInput& input);
+        DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes();
+
+
+        void updateObjectPoses(IceUtil::Time now);
+        void updateObjectPoses(
+            ObjectPoseSeq& objectPoses,
+            IceUtil::Time now,
+            VirtualRobot::RobotPtr agent,
+            bool& agentSynchronized
+        ) const;
+        void updateObjectPose(
+            ObjectPose& objectPose,
+            IceUtil::Time now,
+            VirtualRobot::RobotPtr agent,
+            bool& agentSynchronized
+        ) const;
+
+        /**
+         * @brief If the object is attached to a robot node, update it according to the current robot state.
+         *
+         * If there is no attachement info in `objectPose` itself, the internal data
+         * structure `attachments` is queried. If an attachment is found there,
+         * it is written into the given `objectPose` (thus, it is "cached" in the
+         * info `objectPose`).
+         *
+         * @param synchronized Indicates whether the agent is already synchronized to the current time.
+         */
+        void updateAttachement(ObjectPose& objectPose, VirtualRobot::RobotPtr agent,
+                               bool& synchronized) const;
+
+
+    public:
+
+        RobotStateComponentInterfacePrx robotStateComponent;
+        VirtualRobot::RobotPtr robot;
+
+        ProviderInfoMap providers;
+
+        std::map<std::string, ObjectPoseSeq> objectPoses;
+
+        std::map<std::pair<std::string, ObjectID>, ObjectAttachmentInfo> attachments;
+
+
+        ObjectFinder objectFinder;
+        /// Caches results of attempts to retrieve the OOBB from ArmarXObjects.
+        simox::caching::CacheMap<ObjectID, std::optional<simox::OrientedBoxf>> oobbCache;
+
+        /// Decay model.
+        Decay decay;
+
+    };
+
+}
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/Decay.cpp
similarity index 86%
rename from source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.cpp
rename to source/RobotAPI/components/ObjectPoseObserver/detail/Decay.cpp
index 0df6db13c53aa295a8117c3845307375e95402c9..522a5cc13d83acf95a4efe1c3289c3aec2912487 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Decay.cpp
@@ -1,14 +1,14 @@
-#include "ObjectPoseDecay.h"
+#include "Decay.h"
 
 #include <SimoxUtility/math/scale_value.h>
 
 #include <ArmarXCore/core/time/TimeUtil.h>
 
 
-namespace armarx::objpose
+namespace armarx::objpose::observer
 {
 
-    void ObjectPoseDecay::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
+    void Decay::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
     {
         defs->optional(enabled, prefix + "enabled",
                        "If true, object poses decay over time when not localized anymore.");
@@ -24,15 +24,15 @@ namespace armarx::objpose
                        "Remove objects whose confidence is lower than this value.");
     }
 
-    void ObjectPoseDecay::updateConfidence(ObjectPose& pose, IceUtil::Time now)
+    void Decay::updateConfidence(ObjectPose& pose, IceUtil::Time now) const
     {
         float confidence = calculateConfidence(pose.timestamp, now);
         pose.confidence = confidence;
     }
 
-    void ObjectPoseDecay::updateConfidences(ObjectPoseSeq& objectPoses, IceUtil::Time now)
+    void Decay::updateConfidences(ObjectPoseSeq& objectPoses, IceUtil::Time now) const
     {
-        for (objpose::ObjectPose& pose : objectPoses)
+        for (ObjectPose& pose : objectPoses)
         {
             if (pose.attachment)
             {
@@ -45,7 +45,7 @@ namespace armarx::objpose
         }
     }
 
-    float ObjectPoseDecay::calculateConfidence(IceUtil::Time localization, IceUtil::Time now)
+    float Decay::calculateConfidence(IceUtil::Time localization, IceUtil::Time now) const
     {
         float duration = (now - localization).toSeconds();
         if (duration < delaySeconds)
@@ -67,7 +67,7 @@ namespace armarx::objpose
 
 
 
-    void ObjectPoseDecay::RemoteGui::setup(const ObjectPoseDecay& decay)
+    void Decay::RemoteGui::setup(const Decay& decay)
     {
         using namespace armarx::RemoteGui::Client;
 
@@ -121,7 +121,7 @@ namespace armarx::objpose
         group.addChild(grid);
     }
 
-    void ObjectPoseDecay::RemoteGui::update(ObjectPoseDecay& decay)
+    void Decay::RemoteGui::update(Decay& decay)
     {
         decay.enabled = enabled.getValue();
         decay.delaySeconds = delaySeconds.getValue();
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.h b/source/RobotAPI/components/ObjectPoseObserver/detail/Decay.h
similarity index 81%
rename from source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.h
rename to source/RobotAPI/components/ObjectPoseObserver/detail/Decay.h
index 22a4940da95f94edbe5abc35dd4f14efc36d20a3..ec64221f3e202395ff4f0f8c83d3bb737b6571be 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/ObjectPoseDecay.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Decay.h
@@ -10,25 +10,25 @@
 #include <RobotAPI/libraries/ArmarXObjects/ObjectPose.h>
 
 
-namespace armarx::objpose
+namespace armarx::objpose::observer
 {
 
     /**
      * @brief Models decay of object localizations by decreasing the confidence
      * the longer the object was not localized.
      */
-    class ObjectPoseDecay : public armarx::Logging
+    class Decay : public armarx::Logging
     {
     public:
 
         void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix = "decay.");
 
-        void updateConfidence(ObjectPose& pose, IceUtil::Time now);
-        void updateConfidences(ObjectPoseSeq& objectPoses, IceUtil::Time now);
+        void updateConfidence(ObjectPose& pose, IceUtil::Time now) const;
+        void updateConfidences(ObjectPoseSeq& objectPoses, IceUtil::Time now) const;
 
     private:
 
-        float calculateConfidence(IceUtil::Time localization, IceUtil::Time now);
+        float calculateConfidence(IceUtil::Time localization, IceUtil::Time now) const;
 
 
     public:
@@ -45,10 +45,6 @@ namespace armarx::objpose
 
         float removeObjectsBelowConfidence = 0.1f;
 
-        /// Frequency of updating the current decay.
-        float updateFrequencyHz = 10.0;
-        SimpleRunningTask<>::pointer_type updateTask;
-
 
         struct RemoteGui
         {
@@ -63,8 +59,8 @@ namespace armarx::objpose
 
             armarx::RemoteGui::Client::FloatSlider removeObjectsBelowConfidence;
 
-            void setup(const ObjectPoseDecay& decay);
-            void update(ObjectPoseDecay& decay);
+            void setup(const Decay& decay);
+            void update(Decay& decay);
         };
 
     };
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.cpp
index b9ac721ca1ade44a8667b28667520093169d0770..a5e78b5942704a7e53a3aae34d10c32b07baaa73 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.cpp
@@ -1,10 +1,11 @@
 #include "RobotHeadMovement.h"
 
-#include <ArmarXCore/core/time/TimeUtil.h>
 #include <ArmarXCore/core/application/properties/PropertyDefinitionContainer.h>
+#include <ArmarXCore/core/exceptions/local/UnexpectedEnumValueException.h>
+#include <ArmarXCore/core/time/TimeUtil.h>
 
 
-namespace armarx::objpose
+namespace armarx::objpose::observer
 {
 
     void RobotHeadMovement::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
@@ -97,4 +98,65 @@ namespace armarx::objpose
         }
     }
 
+    objpose::SignalHeadMovementOutput RobotHeadMovement::signalHeadMovement(const objpose::SignalHeadMovementInput& input)
+    {
+        objpose::SignalHeadMovementOutput output;
+        switch (input.action)
+        {
+            case objpose::HeadMovementAction::HeadMovementAction_Starting:
+                this->movementStarts(input.discardUpdatesIntervalMilliSeconds < 0
+                                     ? this->discardIntervalAfterMoveMS
+                                     : input.discardUpdatesIntervalMilliSeconds);
+                break;
+            case objpose::HeadMovementAction::HeadMovementAction_Stopping:
+                this->movementStops(input.discardUpdatesIntervalMilliSeconds);
+                break;
+            default:
+                ARMARX_UNEXPECTED_ENUM_VALUE(objpose::HeadMovementAction, input.action);
+                break;
+        }
+        output.discardUpdatesUntilMilliSeconds = this->discardUpdatesUntil.toMilliSeconds();
+        return output;
+    }
+
+
+
+    void RobotHeadMovement::RemoteGui::setup(const RobotHeadMovement& rhm)
+    {
+        using namespace armarx::RemoteGui::Client;
+
+        checkHeadVelocity.setValue(rhm.checkHeadVelocity);
+        {
+            float max = 10.0;
+            maxJointVelocity.setRange(0, max);
+            maxJointVelocity.setDecimals(3);
+            maxJointVelocity.setSteps(int(100 * max));  // = 0.01 steps
+            maxJointVelocity.setValue(rhm.maxJointVelocity);
+        }
+        {
+            int max = 10 * 1000;
+            discardIntervalAfterMoveMS.setRange(0, max);
+            discardIntervalAfterMoveMS.setValue(rhm.discardIntervalAfterMoveMS);
+        }
+
+        GridLayout grid;
+        int row = 0;
+        grid.add(Label("Check Head Motion"), {row, 0}).add(checkHeadVelocity, {row, 1});
+        row++;
+        grid.add(Label("Max Joint Velocity"), {row, 0}).add(maxJointVelocity, {row, 1});
+        row++;
+        grid.add(Label("Discard Interval after Move [ms]"), {row, 0}).add(discardIntervalAfterMoveMS, {row, 1});
+        row++;
+
+        group.setLabel("Robot Head Movement");
+        group.addChild(grid);
+    }
+
+    void RobotHeadMovement::RemoteGui::update(RobotHeadMovement& rhm)
+    {
+        rhm.checkHeadVelocity = checkHeadVelocity.getValue();
+        rhm.maxJointVelocity = maxJointVelocity.getValue();
+        rhm.discardIntervalAfterMoveMS = discardIntervalAfterMoveMS.getValue();
+    }
+
 }
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.h b/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.h
index fd06a1b2d83831642b4abcfe6fc16784c105c554..fdd6078d9fcefeb619339c15f3e93029e90c3049 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/RobotHeadMovement.h
@@ -9,7 +9,10 @@
 #include <ArmarXCore/core/logging/Logging.h>
 #include <ArmarXCore/observers/variant/DatafieldRef.h>
 
+#include <ArmarXGui/libraries/RemoteGui/Client/Widgets.h>
+
 #include <RobotAPI/interface/observers/KinematicUnitObserverInterface.h>
+#include <RobotAPI/interface/objectpose/ObjectPoseObserver.h>
 
 
 namespace armarx
@@ -18,7 +21,7 @@ namespace armarx
     using PropertyDefinitionsPtr = IceUtil::Handle<PropertyDefinitionContainer>;
 }
 
-namespace armarx::objpose
+namespace armarx::objpose::observer
 {
     class RobotHeadMovement : public armarx::Logging
     {
@@ -34,6 +37,8 @@ namespace armarx::objpose
         void movementStops(long discardIntervalMs);
         void movementStops(IceUtil::Time discardInterval);
 
+        objpose::SignalHeadMovementOutput signalHeadMovement(const objpose::SignalHeadMovementInput& input);
+
 
     public:
 
@@ -50,9 +55,19 @@ namespace armarx::objpose
 
         DebugObserverInterfacePrx debugObserver;
 
-    };
 
+        struct RemoteGui
+        {
+            armarx::RemoteGui::Client::GroupBox group;
 
+            armarx::RemoteGui::Client::CheckBox checkHeadVelocity;
+            armarx::RemoteGui::Client::FloatSpinBox maxJointVelocity;
+            armarx::RemoteGui::Client::IntSpinBox discardIntervalAfterMoveMS;
 
+            void setup(const RobotHeadMovement& rhm);
+            void update(RobotHeadMovement& rhm);
+        };
+
+    };
 
 }
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..467dba24f947b38c02b47044986cf3086775b6a1
--- /dev/null
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp
@@ -0,0 +1,156 @@
+#include "Visu.h"
+
+#include <ArmarXCore/core/time/TimeUtil.h>
+
+#include <RobotAPI/libraries/ArmarXObjects/ObjectFinder.h>
+
+
+namespace armarx::objpose::observer
+{
+
+    void Visu::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
+    {
+        defs->optional(enabled, prefix + "enabled",
+                       "Enable or disable visualization of objects.");
+        defs->optional(frequencyHz, prefix + "frequenzyHz",
+                       "Frequency of visualization.");
+        defs->optional(inGlobalFrame, prefix + "inGlobalFrame",
+                       "If true, show global poses. If false, show poses in robot frame.");
+        defs->optional(alpha, prefix + "alpha",
+                       "Alpha of objects (1 = solid, 0 = transparent).");
+        defs->optional(alphaByConfidence, prefix + "alphaByConfidence",
+                       "If true, use the pose confidence as alpha (if < 1.0).");
+        defs->optional(oobbs, prefix + "oobbs",
+                       "Enable showing oriented bounding boxes.");
+        defs->optional(objectFrames, prefix + "objectFrames",
+                       "Enable showing object frames.");
+        defs->optional(objectFramesScale, prefix + "objectFramesScale",
+                       "Scaling of object frames.");
+    }
+
+
+    std::vector<viz::Layer> Visu::visualizeCommit(
+        const std::map<std::string, ObjectPoseSeq>& objectPoses,
+        float minConfidence,
+        const ObjectFinder& objectFinder) const
+    {
+        std::vector<viz::Layer> layers;
+        for (const auto& [name, poses] : objectPoses)
+        {
+            layers.push_back(visualizeProvider(name, poses, minConfidence, objectFinder));
+        }
+        return layers;
+    }
+
+    viz::Layer Visu::visualizeProvider(
+        const std::string& providerName,
+        const ObjectPoseSeq& objectPoses,
+        float minConfidence,
+        const ObjectFinder& objectFinder) const
+    {
+        viz::Layer layer = arviz.layer(providerName);
+        for (const ObjectPose& objectPose : objectPoses)
+        {
+            const bool show = not(objectPose.confidence < minConfidence);
+            if (show)
+            {
+                visualizeObjectPose(layer, objectPose, objectFinder);
+            }
+        }
+        return layer;
+    }
+
+    void Visu::visualizeObjectPose(
+        viz::Layer& layer,
+        const ObjectPose& objectPose,
+        const ObjectFinder& objectFinder) const
+    {
+        const armarx::ObjectID id = objectPose.objectID;
+        const std::string key = id.str();
+
+        Eigen::Matrix4f pose = inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
+        {
+            viz::Object object = viz::Object(key).pose(pose);
+            if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
+            {
+                object.file(objectInfo->package(), objectInfo->simoxXML().relativePath);
+            }
+            else
+            {
+                object.fileByObjectFinder(id);
+            }
+            if (alphaByConfidence && objectPose.confidence < 1.0f)
+            {
+                object.overrideColor(simox::Color::white().with_alpha(objectPose.confidence));
+            }
+            else if (alpha < 1)
+            {
+                object.overrideColor(simox::Color::white().with_alpha(alpha));
+            }
+            layer.add(object);
+        }
+
+        if (oobbs && objectPose.localOOBB)
+        {
+            const simox::OrientedBoxf& oobb = *objectPose.localOOBB;
+            layer.add(viz::Box(key + " OOBB").set(oobb.transformed(pose))
+                      .color(simox::Color::lime(255, 64)));
+        }
+        if (objectFrames)
+        {
+            layer.add(viz::Pose(key + " Pose").pose(pose).scale(objectFramesScale));
+        }
+    }
+
+
+    void Visu::RemoteGui::setup(const Visu& visu)
+    {
+        using namespace armarx::RemoteGui::Client;
+
+        enabled.setValue(visu.enabled);
+        inGlobalFrame.setValue(visu.inGlobalFrame);
+        alpha.setRange(0, 1.0);
+        alpha.setValue(visu.alpha);
+        alphaByConfidence.setValue(visu.alphaByConfidence);
+        oobbs.setValue(visu.oobbs);
+        objectFrames.setValue(visu.objectFrames);
+        {
+            float max = 10000;
+            objectFramesScale.setRange(0, max);
+            objectFramesScale.setDecimals(2);
+            objectFramesScale.setSteps(int(10 * max));
+            objectFramesScale.setValue(visu.objectFramesScale);
+        }
+
+        GridLayout grid;
+        int row = 0;
+        grid.add(Label("Enabled"), {row, 0}).add(enabled, {row, 1});
+        row++;
+        grid.add(Label("Global Frame"), {row, 0}).add(inGlobalFrame, {row, 1});
+        row++;
+        grid.add(Label("Alpha"), {row, 0}).add(alpha, {row, 1}, {1, 3});
+        row++;
+        grid.add(Label("Alpha By Confidence"), {row, 0}).add(alphaByConfidence, {row, 1});
+        row++;
+        grid.add(Label("OOBB"), {row, 0}).add(oobbs, {row, 1});
+        row++;
+        grid.add(Label("Object Frames"), {row, 0}).add(objectFrames, {row, 1});
+        grid.add(Label("Scale:"), {row, 2}).add(objectFramesScale, {row, 3});
+        row++;
+
+        group.setLabel("Visualization");
+        group.addChild(grid);
+    }
+
+    void Visu::RemoteGui::update(Visu& visu)
+    {
+        visu.enabled = enabled.getValue();
+        visu.inGlobalFrame = inGlobalFrame.getValue();
+        visu.alpha = alpha.getValue();
+        visu.alphaByConfidence = alphaByConfidence.getValue();
+        visu.oobbs = oobbs.getValue();
+        visu.objectFrames = objectFrames.getValue();
+        visu.objectFramesScale = objectFramesScale.getValue();
+    }
+
+}
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.h b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.h
new file mode 100644
index 0000000000000000000000000000000000000000..9677f6198adcd9f52918061925876860640ba999
--- /dev/null
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.h
@@ -0,0 +1,86 @@
+#pragma once
+
+
+#include <ArmarXCore/core/logging/Logging.h>
+#include <ArmarXCore/core/services/tasks/TaskUtil.h>
+
+#include <ArmarXGui/libraries/RemoteGui/Client/Widgets.h>
+
+#include <RobotAPI/components/ArViz/Client/Client.h>
+#include <RobotAPI/libraries/ArmarXObjects/ObjectPose.h>
+
+
+namespace armarx
+{
+    class ObjectFinder;
+}
+namespace armarx::objpose::observer
+{
+
+    /**
+     * @brief Models decay of object localizations by decreasing the confidence
+     * the longer the object was not localized.
+     */
+    class Visu : public armarx::Logging
+    {
+    public:
+
+        void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix = "visu.");
+
+        std::vector<viz::Layer> visualizeCommit(
+            const std::map<std::string, ObjectPoseSeq>& objectPoses,
+            float minConfidence,
+            const ObjectFinder& objectFinder
+        ) const;
+
+        viz::Layer visualizeProvider(
+            const std::string& providerName,
+            const ObjectPoseSeq& objectPoses,
+            float minConfidence,
+            const ObjectFinder& objectFinder
+        ) const;
+
+        void visualizeObjectPose(
+            viz::Layer& layer,
+            const ObjectPose& objectPose,
+            const ObjectFinder& objectFinder
+        ) const;
+
+
+    public:
+
+        viz::Client arviz;
+
+        bool enabled = false;
+        float frequencyHz = 25;
+
+        bool inGlobalFrame = true;
+        float alpha = 1.0;
+        bool alphaByConfidence = false;
+        bool oobbs = false;
+        bool objectFrames = false;
+        float objectFramesScale = 1.0;
+
+        SimpleRunningTask<>::pointer_type updateTask;
+
+
+        struct RemoteGui
+        {
+            armarx::RemoteGui::Client::GroupBox group;
+
+            armarx::RemoteGui::Client::CheckBox enabled;
+
+            armarx::RemoteGui::Client::CheckBox inGlobalFrame;
+            armarx::RemoteGui::Client::FloatSlider alpha;
+            armarx::RemoteGui::Client::CheckBox alphaByConfidence;
+            armarx::RemoteGui::Client::CheckBox oobbs;
+            armarx::RemoteGui::Client::CheckBox objectFrames;
+            armarx::RemoteGui::Client::FloatSpinBox objectFramesScale;
+
+            void setup(const Visu& visu);
+            void update(Visu& visu);
+        };
+
+    };
+
+}
diff --git a/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.cpp b/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.cpp
index 02b04bf6f3961074891472deed1ade625850b467..53da6a10a507a5a63c5205f22d8a6643bfb867c0 100644
--- a/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.cpp
+++ b/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.cpp
@@ -119,30 +119,35 @@ namespace armarx::objpose
         return std::nullopt;
     }
 
-    objpose::ObjectPose ObjectPose::getAttached(VirtualRobot::RobotPtr agent)
+    objpose::ObjectPose ObjectPose::getAttached(VirtualRobot::RobotPtr agent) const
     {
         ARMARX_CHECK(attachment);
 
         objpose::ObjectPose updated = *this;
-        const objpose::ObjectAttachmentInfo& attachment = *updated.attachment;
+        updated.updateAttached(agent);
+        return updated;
+    }
+
+    void ObjectPose::updateAttached(VirtualRobot::RobotPtr agent)
+    {
+        ARMARX_CHECK(attachment);
 
-        ARMARX_CHECK_EQUAL(attachment.agentName, agent->getName());
+        ARMARX_CHECK_EQUAL(attachment->agentName, agent->getName());
 
-        VirtualRobot::RobotNodePtr robotNode = agent->getRobotNode(attachment.frameName);
+        VirtualRobot::RobotNodePtr robotNode = agent->getRobotNode(attachment->frameName);
         ARMARX_CHECK_NOT_NULL(robotNode);
 
         // Overwrite object pose
-        updated.objectPoseRobot = robotNode->getPoseInRootFrame() * attachment.poseInFrame;
-        updated.objectPoseGlobal = agent->getGlobalPose() * updated.objectPoseRobot;
+        objectPoseRobot = robotNode->getPoseInRootFrame() * attachment->poseInFrame;
+        objectPoseGlobal = agent->getGlobalPose() * objectPoseRobot;
 
-        updated.robotPose = agent->getGlobalPose();
-        updated.robotConfig = agent->getConfig()->getRobotNodeJointValueMap();
-
-        return updated;
+        // Overwrite robot config.
+        robotPose = agent->getGlobalPose();
+        robotConfig = agent->getConfig()->getRobotNodeJointValueMap();
     }
-
 }
 
+
 namespace armarx
 {
 
diff --git a/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.h b/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.h
index f7fec6b7776d1638d6dbdbd9c043872c372c803b..36379f97d0d3d047058dbb9a9d57e9517a0caa71 100644
--- a/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.h
+++ b/source/RobotAPI/libraries/ArmarXObjects/ObjectPose.h
@@ -74,8 +74,17 @@ namespace armarx::objpose
         /// Get the OOBB in the global frame (according to `objectPoseGlobal`).
         std::optional<simox::OrientedBoxf> oobbGlobal() const;
 
-        /// Get `*this` with applied attachment.
-        objpose::ObjectPose getAttached(VirtualRobot::RobotPtr agent);
+        /**
+         * @brief Get a copy of `*this` with updated attachment.
+         * @see `updateAttached()`
+         */
+        objpose::ObjectPose getAttached(VirtualRobot::RobotPtr agent) const;
+        /**
+         * @brief Update an the object pose and robot state of an attached
+         * object pose according to the given agent state (in-place).
+         * @param agent The agent/robot.
+         */
+        void updateAttached(VirtualRobot::RobotPtr agent);
     };
     using ObjectPoseSeq = std::vector<ObjectPose>;