UpdateConsumer.cpp
Go to the documentation of this file.
1#include "UpdateConsumer.h"
2
3#include <exception>
4#include <iomanip>
5#include <mutex>
6#include <optional>
7#include <sstream>
8#include <string>
9
10#include <Eigen/Geometry>
11
16
21#include <RobotAPI/libraries/armem/client/query/Builder.h> // IWYU pragma: keep
28
31#include <VisionX/libraries/armem_human/aron/FaceRecognition.aron.generated.h>
32#include <VisionX/libraries/armem_human/aron/HumanPose.aron.generated.h>
33#include <VisionX/libraries/armem_human/aron/Person.aron.generated.h>
34#include <VisionX/libraries/armem_human/aron/PersonInstance.aron.generated.h>
41
43{
44
46 const Properties& properties) :
47 faceRecognitionReader(mns.useReader(armarx::human::FaceRecognitionCoreSegmentID)),
48 personInstanceWriter(mns.useWriter(armarx::human::PersonInstanceCoreSegmentID)),
49 personInstanceReader(mns.useReader(armarx::human::PersonInstanceCoreSegmentID)),
50 poseReader(mns.useReader(armarx::human::PoseCoreSegmentID)),
51 properties_(properties)
52 {
53 personInstanceReaderV2.connect(mns);
54
55 ARMARX_IMPORTANT << "At startup:";
56 {
58 query.providerName = "";
61 query.resolveFaceDetection = false;
62 query.resolveHumanPose = true;
63 query.resolveProfile = true;
64
65 const auto result = personInstanceReaderV2.queryResolved(query);
66
67 ARMARX_DEBUG << "Humans with tracking ids:";
68 for (const auto& personInstance : result.personInstances)
69 {
70 std::string name = "~unknown~";
71 if (personInstance.profile.has_value())
72 {
73 name = personInstance.profile->id.firstName + " " +
74 personInstance.profile->id.lastName;
75 }
76
77 std::string trackingId = "~none~";
78 if (personInstance.humanPose.has_value() and
79 personInstance.humanPose->humanTrackingId.has_value())
80 {
81 trackingId = personInstance.humanPose->humanTrackingId.value();
82 }
83
84 ARMARX_DEBUG << trackingId << ": " << name;
85 }
86 }
87 }
88
89 //TODO: add some form of history to allow for better understanding of memory changed of the personInstances
90 //TODO: add visualization of
91
92 /**
93 * Process a face recognition update:
94 * 1. Find existing PersonInstance by profile ID
95 * 2. If found and has valid pose: check if pose is still plausible (nearby)
96 * - If implausible: clear pose link and try to find new matching pose
97 * 3. If found but no pose: try to find matching pose by proximity
98 * 4. If not found: create new PersonInstance and try to find matching pose
99 * 5. Ensure tracking ID uniqueness: remove pose links from other PersonInstances
100 * that were using the same tracking ID
101 */
102 void
104 const armarx::armem::human::FaceRecognition& faceRecognition,
105 const armarx::armem::MemoryID& faceRecognitionID)
106 {
107 std::lock_guard g{consumeMtx}; // Ensure thread-safe processing
108
109 ARMARX_DEBUG << "Consuming face recognition update for "
110 << (faceRecognition.profileID.has_value()
111 ? faceRecognition.profileID->entityName
112 : "unknown");
113
115 [this](const armarx::armem::Duration& duration)
116 { logFaceRecognitionUpdateDuration(duration); });
117
118 // Query all existing PersonInstances to check if this face already has one
120 personInstanceReader.getLatestSnapshotsIn(armarx::human::PersonInstanceCoreSegmentID);
121
122 // Face recognition without profile ID cannot be matched to a PersonInstance
123 if (not faceRecognition.profileID.has_value())
124 {
126 << "Face recognition " + faceRecognitionID.entityName +
127 " does not provide valid profileID, finding matching "
128 "personInstance is not possible.";
129 return;
130 }
131
132 const armarx::armem::MemoryID& profileID = faceRecognition.profileID.value();
133
134 ARMARX_DEBUG << "Trying to match face recognition with profile ID: "
135 << profileID.entityName;
136
137 if (queryResult.success)
138 {
139 // ========== Face-only mode: body tracking disabled ==========
140 // Update (or create) the PersonInstance purely from this face detection.
141 // No matching against body tracking data is performed and the poseID is
142 // left unset, so the global pose is taken from the face position only.
143 if (not properties_.enableBodyTracking)
144 {
145 if (profileID.entityName.empty())
146 {
147 ARMARX_WARNING << deactivateSpam() << "Face Recognition has no valid profile";
148 return;
149 }
150
151 std::optional<PersonInstanceWithID> matched =
152 findMatchingPersonInstance(queryResult, profileID);
153
154 armarx::human::arondto::PersonInstance personInstance;
155 if (matched.has_value())
156 {
157 personInstance = matched->personInstance;
158 }
159 else
160 {
161 toAron(personInstance.profileID, profileID);
162 }
163
164 // Link the latest face detection and ensure no pose link remains set.
165 toAron(personInstance.faceRecognitionID, faceRecognitionID);
166 toAron(personInstance.poseID, armarx::armem::MemoryID()); // leave unset
167
168 // Global pose from face position only (orientation unknown).
169 Eigen::Isometry3f globalPose = Eigen::Isometry3f::Identity();
170 globalPose.translation() = faceRecognition.position3DGlobal;
171 personInstance.pose = globalPose.matrix();
172
175 .withProviderSegmentName(PersonInstanceUpdater::provider_name)
176 .withEntityName(profileID.entityName);
177 update.referencedTime = armarx::armem::Time::Now();
178 update.instancesData = {personInstance.toAron()};
179 personInstanceWriter.commit(update);
180 return;
181 }
182
183 //TODO: add early return in case face recognition id is already used by a personInstance
184
185 // ========== STEP 1: Search for existing PersonInstance with matching profile ID ==========
186 std::optional<PersonInstanceWithID> personInstanceMatched =
187 findMatchingPersonInstance(queryResult, profileID);
188
189 // ========== STEP 2: If PersonInstance found, validate and update pose link ==========
190 if (personInstanceMatched.has_value())
191 {
192 ARMARX_DEBUG << "Found existing PersonInstance, updating it.";
193 ARMARX_DEBUG << "matched PersonInstance entity name: "
194 << personInstanceMatched->memoryId.entityName;
195
196 // First, update the face recognition data (use toAron to convert types)
197 toAron(personInstanceMatched->personInstance.faceRecognitionID, faceRecognitionID);
198
199 // Extract the current pose ID from the matched PersonInstance
201 fromAron(personInstanceMatched->personInstance.poseID, poseID);
202
203 ARMARX_DEBUG << "Current linked pose ID: "
204 << (isMemoryIdFullySpecified(poseID) ? poseID.entityName : "~none~");
205
206 // Retrieve the actual pose and face data from memory
207 const std::optional<::armarx::armem::human::HumanPose> humanPose =
208 humanPoseFromMemId(poseID);
209 const std::optional<armarx::armem::human::FaceRecognition> faceRecognition =
210 faceRecognitionFromMemId(faceRecognitionID);
211
212 // We just received this face recognition, so it must exist
213 ARMARX_CHECK(faceRecognition.has_value());
214
215 // Check if the current pose link is still valid (plausible)
216 bool poseIdWasCleared = false;
217 if (humanPose.has_value())
218 {
219 // If pose exists, check if face and head positions are close enough
220 if (not checkHumanPoseForPlausability(humanPose.value(),
221 faceRecognition.value()))
222 {
224 << "The face recognition and the human pose are not "
225 "consistent. Dropping the human pose track.";
226 clearPoseIdFromPersonInstance(personInstanceMatched.value());
227 poseIdWasCleared = true;
228 }
229 }
230 else if (isMemoryIdFullySpecified(poseID))
231 {
232 // Pose ID was set but pose data no longer exists (outdated/deleted)
234 << "The human pose is likely outdated and meaningless. We remove "
235 "the link to it.";
236 clearPoseIdFromPersonInstance(personInstanceMatched.value());
237 poseIdWasCleared = true;
238 }
239
240 ARMARX_DEBUG << "Human Pose exists and memory ID is fully specified";
241
242 // Only refresh globalPose from the face when no valid pose provides a
243 // richer (orientation-bearing) pose; otherwise keep the pose-writer's data.
244 if (poseIdWasCleared or not humanPose.has_value())
245 {
246 Eigen::Isometry3f globalPose = Eigen::Isometry3f::Identity();
247 globalPose.translation() = faceRecognition.value().position3DGlobal;
248 personInstanceMatched->personInstance.pose = globalPose.matrix();
249 }
250
251 // If we still have a valid pose link, we're done
252 if (not poseIdWasCleared and isMemoryIdFullySpecified(poseID))
253 {
254 ARMARX_DEBUG << "A valid pose id for the person instance exists";
255 ARMARX_DEBUG << "Commiting PersonInstance for ";
256 ARMARX_DEBUG << VAROUT(profileID);
257 // Ensure no other person has this tracking ID and commit
258 // Commit the new PersonInstance to memory
260 update.entityID =
262 .withProviderSegmentName(PersonInstanceUpdater::provider_name)
263 .withEntityName(faceRecognition.value().profileID.value().entityName);
264 update.referencedTime = armarx::armem::Time::Now();
265 update.instancesData = {personInstanceMatched.value().personInstance.toAron()};
266 personInstanceWriter.commit(update);
267 return;
268 }
269
270 // No valid pose link - try to find a matching pose by spatial proximity
272 << "The person instance does not have an assigned pose id. Will try to "
273 "match existing ones.";
274
275 {
276 ARMARX_CHECK(faceRecognition.has_value());
277 // Find the closest pose to this face position
278 const std::optional<HumanPoseWithID> closestPoseID =
279 getClosestPoseID(faceRecognition->position3DGlobal);
280
281 if (closestPoseID.has_value())
282 {
283 // Found a nearby pose - link it to this PersonInstance
285 << "Found matching human pose with tracking id "
286 << QUOTED(closestPoseID->humanPose.humanTrackingId.value_or(""))
287 << " for "
288 << personInstanceMatched->personInstance.profileID.entityName;
289 setPoseIdForPersonInstance(personInstanceMatched.value(),
290 closestPoseID->memoryId);
291
292 armarx::armem::MemoryID profileId;
293 fromAron(personInstanceMatched->personInstance.profileID, profileId);
294
295 // Ensure no other person has this tracking ID
296 ensureTrackingIdUniqueness(closestPoseID->humanPose.humanTrackingId.value(),
297 profileId);
298 }
299 else
300 {
302 << "No human pose could be found close to the given face position";
303
304 // Still commit the updated face recognition link and the refreshed
305 // (face-based) global pose; otherwise this update would be lost.
307 update.entityID = personInstanceMatched->memoryId.getEntityID();
308 update.referencedTime = armarx::armem::Time::Now();
309 update.instancesData = {personInstanceMatched->personInstance.toAron()};
310 personInstanceWriter.commit(update);
311 }
312 }
313 }
314
315
316 // ========== STEP 3: No existing PersonInstance found - create new one ==========
317 else
318 {
319 armarx::human::arondto::PersonInstance personInstance;
320
321 // Set the face recognition and profile IDs
322 toAron(personInstance.faceRecognitionID, faceRecognitionID);
323 toAron(personInstance.profileID, profileID);
324
325 // Try to find a pose nearby that we can link to this new PersonInstance
326 const std::optional<HumanPoseWithID> closestPoseID =
327 getClosestPoseID(faceRecognition.position3DGlobal);
328 if (closestPoseID.has_value())
329 {
330 std::string name = "";
331 if (faceRecognition.profileID.has_value())
332 {
333 name = faceRecognition.profileID->entityName;
334 }
335
336 ARMARX_INFO << deactivateSpam(10) << "Found matching human pose (tracking id "
337 << QUOTED(closestPoseID->humanPose.humanTrackingId.value_or(""))
338 << ") for face recognition of " << QUOTED(name)
339 << " due to spatial proximity.";
340 toAron(personInstance.poseID, closestPoseID->memoryId);
341
342 // Ensure tracking ID uniqueness: no other person should have this tracking ID
343 ensureTrackingIdUniqueness(closestPoseID->humanPose.humanTrackingId.value(),
344 profileID);
345 }
346
347 if (faceRecognition.profileID.value().entityName.empty())
348 {
349 ARMARX_WARNING << "Face Recognition has no valid profile";
350 return;
351 }
352
353 // Set the global position based on face position (orientation unknown)
354 ARMARX_VERBOSE << "update pose based on latest face recognition result ("
355 << faceRecognition.position3DGlobal.transpose() << ")";
356 Eigen::Isometry3f globalPose = Eigen::Isometry3f::Identity();
357 // orientation is not available from face recognition
358 globalPose.translation() = faceRecognition.position3DGlobal;
359 personInstance.pose = globalPose.matrix();
360
361 // Commit the new PersonInstance to memory
364 .withProviderSegmentName(PersonInstanceUpdater::provider_name)
365 .withEntityName(faceRecognition.profileID.value().entityName);
366 update.referencedTime = armarx::armem::Time::Now();
367 update.instancesData = {personInstance.toAron()};
368 personInstanceWriter.commit(update);
369 }
370 }
371 }
372
373 /**
374 * Process a pose update:
375 * 1. Search for PersonInstance with matching tracking ID
376 * 2. If not found, try to match by spatial proximity to a recognized face
377 * 3. If matched: Update PersonInstance with new pose ID
378 * 4. If not matched: Create new PersonInstance based on pose alone
379 *
380 * Note: Whenever a pose can be plausibly matched to a recognized face, it will be.
381 * Matched instances get their poseId's updated. If no match is available, a new one is created.
382 */
383 void
385 const armarx::armem::MemoryID& poseID)
386 {
387 std::lock_guard g{consumeMtx}; // Ensure thread-safe processing
388
389 // Body tracking disabled: ignore all pose updates. PersonInstances are maintained
390 // solely from face detections (see consumeFaceRecognitionUpdate).
391 if (not properties_.enableBodyTracking)
392 {
394 << "Body tracking is disabled (enableBodyTracking=false); ignoring pose "
395 "update.";
396 return;
397 }
398
400 [](const armarx::armem::Duration& duration)
401 { ARMARX_DEBUG << "consumePoseUpdate took " << duration.toMilliSeconds() << " ms."; });
402
403 ARMARX_INFO << deactivateSpam(10) << "Consuming pose update with tracking ID: "
404 << (humanPose.humanTrackingId.has_value() ? humanPose.humanTrackingId.value()
405 : "none");
406
407 // Query all existing PersonInstances
409 personInstanceReader.getLatestSnapshotsIn(armarx::human::PersonInstanceCoreSegmentID);
410
411 // ========== Search for matching PersonInstance ==========
412 // TODO: what to do if multiple instances match?
413
414 // Matching strategies (in priority order):
415 // 1. Tracking ID: pose has same humanTrackingId as PersonInstance's current pose
416 // (overrides any spatial match by setting closestDistance to a sentinel)
417 // 2. Spatial proximity: pose head position is close to a face recognition
418 ARMARX_DEBUG << "Iterating over existing instances";
419 if (queryResult.success)
420 {
421 std::optional<::armarx::armem::human::PersonInstance> matchingInstance = std::nullopt;
422 std::optional<::armarx::armem::MemoryID> matchingInstanceId = std::nullopt;
423 float closestDistance = std::numeric_limits<float>::max();
424
427 armarx::human::arondto::PersonInstance>& instance)
428 {
429 ARMARX_DEBUG << "In iteration: " << instance.id().entityName;
430
431 const auto& personInstance = instance.data();
432
433 armarx::armem::MemoryID currentPoseID;
434 fromAron(personInstance.poseID, currentPoseID);
435 auto pose = humanPoseFromMemId(currentPoseID);
436
437 // ===== PRIORITY 2: Match by spatial proximity (face-head distance) =====
438 // Find the CLOSEST PersonInstance, not just the first plausible one
439 armarx::armem::MemoryID currentRecognitionId;
440 fromAron(personInstance.faceRecognitionID, currentRecognitionId);
441 auto recognition = faceRecognitionFromMemId(currentRecognitionId);
442 if (recognition.has_value())
443 {
444 // Check if this pose's head is close to the face recognition position
445 auto headPos = getHeadPos(humanPose);
446 if (headPos.has_value())
447 {
448 float distance =
449 getDistance(recognition->position3DGlobal, headPos.value());
450 ARMARX_VERBOSE << "Distance from pose to " << instance.id().entityName
451 << ": " << distance << "mm";
452
453 // Keep track of the closest match within threshold
454 if (distance < closestDistance &&
455 distance <= properties_.maxFaceHeadDistance)
456 {
458 fromAron(personInstance, bo);
459 matchingInstance = bo;
460 matchingInstanceId = instance.id();
461 closestDistance = distance;
462 ARMARX_DEBUG << "Instance " << instance.id().entityName
463 << " is closer (distance: " << distance << "mm)";
464 }
465 }
466 }
467
468 // ===== PRIORITY 1: Match by tracking ID =====
469 // This takes precedence if we find an exact tracking ID match
470 if (pose.has_value())
471 {
472 ARMARX_INFO << deactivateSpam(10) << "Person instance "
473 << instance.id().entityName << " with tracking id "
474 << QUOTED(pose->humanTrackingId.value_or(""));
475
476 if (pose->humanTrackingId.has_value() and
477 humanPose.humanTrackingId.has_value() and
478 pose->humanTrackingId.value() == humanPose.humanTrackingId.value())
479 {
480 // Exact tracking ID match - this overrides spatial proximity
481 ARMARX_DEBUG << "Found exact tracking ID match for "
482 << instance.id().entityName;
484 fromAron(personInstance, bo);
485 matchingInstance = bo;
486 matchingInstanceId = instance.id();
487 closestDistance = -1.0f; // Sentinel value to indicate tracking ID match
488 }
489 }
490 });
491
492 // ========== Handle the matching result ==========
493 // Either a matching instance was found (update it with new pose),
494 // OR no match was found (create new PersonInstance)
495 if (matchingInstance.has_value())
496 {
497 ARMARX_DEBUG << "Found matching instance, handling it";
498
499 // Ensure tracking ID uniqueness before updating
500 if (humanPose.humanTrackingId.has_value())
501 {
502 armarx::armem::MemoryID profileId = matchingInstance->profileID;
503 ensureTrackingIdUniqueness(humanPose.humanTrackingId.value(), profileId);
504 }
505
506 // Update the PersonInstance with the new pose ID and position
507 updateInstanceWithNewPoseId(
508 matchingInstanceId.value(), matchingInstance.value(), poseID, humanPose);
509 }
510 else
511 {
512 // No matching PersonInstance - create a new one based on this pose
513 ARMARX_DEBUG << "No matching instance found, creating new...";
514 createNewInstanceBasedOnPose(poseID, humanPose);
515 }
516 }
517 else
518 {
519 ARMARX_WARNING << deactivateSpam() << "Fetching instance memory failed.";
520 }
521 }
522
523 void
524 UpdateConsumer::consumeProfileUpdate(const armarx::human::arondto::Person& /*profile*/,
525 const armarx::armem::MemoryID& /*profileID*/)
526 {
527 //TODO: implement profile update logic
528 std::lock_guard g{consumeMtx};
529 }
530
531 void
532 UpdateConsumer::removePoseFromPersonInstance(const armarx::armem::MemoryID& personInstanceId,
534 {
535 data.poseID = {};
536 armarx::human::arondto::PersonInstance dto;
537 toAron(dto, data);
538 armarx::armem::EntityUpdate update{.entityID = personInstanceId.getEntityID(),
539 .instancesData = {dto.toAron()},
540 .referencedTime = armarx::armem::Time::Now()};
542 << "Removing pose from PersonInstance: " << personInstanceId.entityName;
543 personInstanceWriter.commit(update);
544 }
545
546 /**
547 * Create a new PersonInstance based on a pose.
548 * Tries to find a nearby face recognition to link with, otherwise creates
549 * a PersonInstance with only pose information (no identity).
550 */
551 void
552 UpdateConsumer::createNewInstanceBasedOnPose(const armarx::armem::MemoryID& poseId,
553 const armarx::armem::human::HumanPose& pose)
554 {
555 ARMARX_INFO << deactivateSpam(10) << "Processing tracking id "
556 << QUOTED(pose.humanTrackingId.value_or("")) << ".";
557
558 // Extract head position from the pose
559 std::optional<armarx::FramedPosition> headPos = getHeadPos(pose);
560 if (headPos.has_value())
561 {
562 // Try to find a nearby face recognition to link with
563 std::optional<armarx::armem::MemoryID> faceRecognitionID =
564 getClosestFaceID(headPos.value());
565 if (faceRecognitionID.has_value())
566 {
567 armarx::human::arondto::PersonInstance personInstance;
568 toAron(personInstance.faceRecognitionID, faceRecognitionID.value());
569
570 // determine profile entity name, to generate personinstance entity name
571
572 auto faceRecognition = faceRecognitionFromMemId(faceRecognitionID.value());
573 if (not faceRecognition.has_value())
574 {
575 ARMARX_WARNING << deactivateSpam() << "Could not get face recognition from id";
576 return;
577 }
578
579 const auto profileId = faceRecognition->profileID;
580
581 // TODO: remove the following if you want to have unknown persons in the memory:
582 // if (not profileId.has_value())
583 // {
584 // ARMARX_INFO << deactivateSpam(10)
585 // << "While creating a new instance based on pose: closest face "
586 // "recognition has no attached profile. Giving up.";
587 // return;
588 // }
589
590 std::string entityName = "";
591 if (profileId.has_value())
592 {
593 entityName = profileId->entityName;
594
595 // set profile id of instance
596 toAron(personInstance.profileID, profileId.value());
597 }
598 else
599 {
600 // we have an unknown person
601
602 static unsigned int i = 0;
603 std::stringstream s;
604 s << "unknown-" << std::setw(4) << std::setfill('0') << i++;
605 entityName = s.str();
606 }
607
608
609 {
610 const auto headKp =
613
614 // update pose based on latest human pose result
615 ARMARX_VERBOSE << "update pose based on latest human pose result";
616 Eigen::Isometry3f globalPose = Eigen::Isometry3f::Identity();
617
618 const auto headIt = pose.keypoints.find(headKp);
619 if (headIt == pose.keypoints.end())
620 {
621 ARMARX_WARNING << deactivateSpam() << "Pose does not contain head keypoint "
622 << QUOTED(headKp) << "; keeping identity global pose.";
623 }
624 else
625 {
626 if (headIt->second.orientationGlobal)
627 {
628 ARMARX_CHECK_EQUAL(headIt->second.orientationGlobal->getFrame(),
630 globalPose.linear() = headIt->second.orientationGlobal->toEigen();
631 }
632
633 if (headIt->second.positionGlobal)
634 {
635 ARMARX_CHECK_EQUAL(headIt->second.positionGlobal->getFrame(),
637 globalPose.translation() = headIt->second.positionGlobal->toEigen();
638 }
639 }
640
641 personInstance.pose = globalPose.matrix();
642 }
643
644 //TODO: get profile id
645 //toAron(personInstance.profileID, profileID);
646 toAron(personInstance.poseID, poseId);
647
648 // Ensure tracking ID uniqueness before creating the new PersonInstance
649 if (pose.humanTrackingId.has_value() and profileId.has_value())
650 {
651 ensureTrackingIdUniqueness(pose.humanTrackingId.value(), profileId.value());
652 }
653
654 armarx::armem::EntityUpdate update;
657 .withEntityName(entityName);
658 update.referencedTime = armarx::armem::Time::Now();
659 update.instancesData = {personInstance.toAron()};
660 personInstanceWriter.commit(update);
661 }
662 else
663 {
664 ARMARX_WARNING << deactivateSpam() << "Could not get closest face";
665 }
666 }
667 }
668
669 void
670 UpdateConsumer::updateInstanceWithNewPoseId(const armarx::armem::MemoryID& instanceId,
671 armarx::armem::human::PersonInstance oldInstance,
672 const armarx::armem::MemoryID& newPoseId,
673 const ::armarx::armem::human::HumanPose& pose)
674 {
675
676 // update pose based on latest human pose result
677 {
680
681 oldInstance.globalPose.setIdentity();
682
683 const auto headIt = pose.keypoints.find(headKp);
684 if (headIt == pose.keypoints.end())
685 {
686 ARMARX_WARNING << deactivateSpam() << "Pose does not contain head keypoint "
687 << QUOTED(headKp) << "; keeping identity global pose.";
688 }
689 else
690 {
691 if (headIt->second.orientationGlobal)
692 {
693 ARMARX_CHECK_EQUAL(headIt->second.orientationGlobal->getFrame(),
695 oldInstance.globalPose.linear() = headIt->second.orientationGlobal->toEigen();
696 }
697
698 if (headIt->second.positionGlobal)
699 {
700 ARMARX_CHECK_EQUAL(headIt->second.positionGlobal->getFrame(),
702 oldInstance.globalPose.translation() = headIt->second.positionGlobal->toEigen();
703 }
704 }
705
706 ARMARX_DEBUG << "update pose based on latest human pose result: "
707 << oldInstance.globalPose.translation().transpose();
708 }
709
710
711 ARMARX_DEBUG << "Start update with new pose id";
712 oldInstance.poseID = newPoseId;
713 armarx::human::arondto::PersonInstance dto;
714 toAron(dto, oldInstance);
715 armarx::armem::EntityUpdate update{
716 .entityID = instanceId.getEntityID(),
717 .instancesData = {dto.toAron()},
718 .referencedTime = armarx::armem::Time::Now(),
719 .confidence = 1.0F,
720 };
721
722 ARMARX_DEBUG << "Committing new person instance with updated pose for entity: "
723 << instanceId.entityName;
724 personInstanceWriter.commit(update);
725 }
726
727 bool
728 UpdateConsumer::checkHumanPoseForPlausability(
729 const ::armarx::armem::human::HumanPose& pose,
730 const ::armarx::armem::human::FaceRecognition& face)
731 {
732 // Here, we check whether the "anonymous" human pose and the detected face are consistent.
733 // If a new face detection is not consistent with the human pose, we don't accept it.
734
735 ARMARX_DEBUG << "checking for plausability";
736 auto facePosFromPose = getHeadPos(pose);
737 if (not facePosFromPose.has_value())
738 {
739 ARMARX_WARNING << "Conversion failed";
740 return false;
741 }
742 auto facePosFromFaceRecognition = face.position3DGlobal;
743 float dist = getDistance(facePosFromFaceRecognition, facePosFromPose.value());
744 ARMARX_DEBUG << "distance is: " << dist;
745 return dist <= properties_.maxFaceHeadDistance;
746 }
747
748 bool
749 UpdateConsumer::checkForPlausability(const ::armarx::armem::human::HumanPose& pose,
750 const ::armarx::armem::human::FaceRecognition& face)
751 {
752 ARMARX_DEBUG << "checking for plausability";
753 auto facePosFromPose = getHeadPos(pose);
754 if (not facePosFromPose.has_value())
755 {
756 ARMARX_WARNING << "Conversion failed";
757 return false;
758 }
759 auto facePosFromFaceRecognition = face.position3DGlobal;
760 float dist = getDistance(facePosFromFaceRecognition, facePosFromPose.value());
761 ARMARX_DEBUG << "distance is: " << dist;
762 return dist <= properties_.maxFaceHeadDistance;
763 }
764
765 /**
766 * Find the closest pose to a given face position.
767 *
768 * Iterates through all available poses and finds the one whose head position
769 * is closest to the given face position. Only considers poses within the
770 * maxFaceHeadDistance threshold.
771 *
772 * @param facePos The 3D position of the detected face
773 * @return The closest pose with its ID, or std::nullopt if none within threshold
774 */
775 std::optional<UpdateConsumer::HumanPoseWithID>
776 UpdateConsumer::getClosestPoseID(const Eigen::Vector3f& facePos)
777 {
778 // Query all available poses
779 armarx::armem::client::QueryResult queryResult =
780 poseReader.getLatestSnapshotsIn(armarx::human::PoseCoreSegmentID);
781
782 std::optional<UpdateConsumer::HumanPoseWithID> closestPoseID;
783 float closestDistance =
784 properties_.maxFaceHeadDistance; // Only consider poses within threshold
785
786 if (queryResult.success)
787 {
788 // Iterate through all poses to find the closest one
790 [&facePos, &closestPoseID, &closestDistance](
792 instance)
793 {
794 armarx::armem::human::HumanPose pose;
795 fromAron(instance.data(), pose);
796
797 // Extract head position from pose keypoints
798 std::optional<armarx::FramedPosition> headPos_opt = getHeadPos(pose);
799 if (headPos_opt.has_value())
800 {
801 const armarx::FramedPosition& headPos = headPos_opt.value();
802 float distance = getDistance(facePos, headPos);
803
804 ARMARX_VERBOSE << pose.humanTrackingId.value_or("") << ": "
805 << VAROUT(distance);
806
807 // Keep track of the closest pose
808 if (distance < closestDistance)
809 {
810 closestPoseID =
811 HumanPoseWithID{.humanPose = pose, .memoryId = instance.id()};
812 closestDistance = distance;
813 }
814 }
815 });
816 }
817 return closestPoseID;
818 }
819
820 std::optional<armarx::armem::MemoryID>
821 UpdateConsumer::getClosestFaceID(const armarx::FramedPosition& headPos)
822 {
823 armarx::armem::client::QueryResult queryResult =
824 faceRecognitionReader.getLatestSnapshotsIn(armarx::human::FaceRecognitionCoreSegmentID);
825
826 std::optional<armarx::armem::MemoryID> closestFaceID = std::nullopt;
827 float closestDistance = properties_.maxFaceHeadDistance;
828
829 if (queryResult.success)
830 {
832 [&headPos, &closestFaceID, &closestDistance](
834 armarx::human::arondto::FaceRecognition>& instance)
835 {
836 armarx::armem::human::FaceRecognition faceRecognition;
837 fromAron(instance.data(), faceRecognition);
838
839 const float distance = getDistance(faceRecognition.position3DGlobal, headPos);
841
842 ARMARX_VERBOSE << "Face: " << faceRecognition.position3DGlobal.transpose();
843 ARMARX_VERBOSE << "Head: " << headPos.toEigen().transpose();
844
845 if (distance < closestDistance)
846 {
847 ARMARX_VERBOSE << "Found matching instance: " << instance.id();
848 closestFaceID = instance.id();
849 closestDistance = distance;
850 }
851 });
852 }
853 return closestFaceID;
854 }
855
856 void
857 UpdateConsumer::addPoseToInstance(const armarx::armem::MemoryID& memId,
858 const armarx::armem::MemoryID& poseId,
859 armarx::armem::human::PersonInstance currentInstance)
860 {
861 currentInstance.poseID = poseId;
862 armarx::human::arondto::PersonInstance dto;
863 toAron(dto, currentInstance);
864 armarx::armem::EntityUpdate update{
865 .entityID = memId,
866 .instancesData = {dto.toAron()},
867 .referencedTime = armarx::armem::Time::Now(),
868 };
869
870 ARMARX_DEBUG << "Adding pose to instance";
871 personInstanceWriter.commit(update);
872 }
873
874 std::optional<armarx::armem::human::FaceRecognition>
875 UpdateConsumer::faceRecognitionFromMemId(const armarx::armem::MemoryID& memId)
876 {
877 // All faces of a snapshot are stored as separate instances under a single
878 // shared entity ("human_poses"); an individual person is identified by the
879 // instance (timestamp + instanceIndex), not by the entity name. The query layer
880 // only filters down to the snapshot (timestamp) granularity - it returns *all*
881 // persons of that snapshot - so we must select the exact instanceIndex ourselves,
882 // otherwise we'd pick an arbitrary (the last-iterated) person.
883 if (not isMemoryIdFullySpecified(memId))
884 {
885 return std::nullopt;
886 }
887
888 ARMARX_DEBUG << "Searching for face recognition";
889 auto result = faceRecognitionReader.queryMemoryIDs({memId});
890 if (not result.success)
891 {
892 ARMARX_WARNING << deactivateSpam() << "Could not query recognition.";
893 return std::nullopt;
894 }
895 std::optional<armarx::armem::human::FaceRecognition> resFace = std::nullopt;
896 result.memory.forEachInstanceWithDataAs(
897 [&resFace, &memId](const armarx::armem::wm::EntityInstanceBase<
898 armarx::human::arondto::FaceRecognition>& instance)
899 {
900 // Select only the person referenced by memId.instanceIndex.
901 if (instance.id().instanceIndex != memId.instanceIndex)
902 {
903 return;
904 }
905 armarx::armem::human::FaceRecognition res;
906 armarx::armem::human::fromAron(instance.data(), res);
907 resFace = res;
908 ARMARX_DEBUG << "Found recognition instance";
909 });
910 if (not resFace.has_value())
911 {
912 ARMARX_WARNING << deactivateSpam() << "Recognition result empty";
913 }
914 return resFace;
915 }
916
917 std::optional<armarx::armem::human::HumanPose>
918 UpdateConsumer::humanPoseFromMemId(const armarx::armem::MemoryID& memId)
919 {
920 // All poses of a snapshot are stored as separate instances under a single
921 // shared entity ("human_poses"); an individual person is identified by the
922 // instance (timestamp + instanceIndex), not by the entity name. The query layer
923 // only filters down to the snapshot (timestamp) granularity - it returns *all*
924 // persons of that snapshot - so we must select the exact instanceIndex ourselves,
925 // otherwise we'd pick an arbitrary (the last-iterated) person.
926 if (not isMemoryIdFullySpecified(memId))
927 {
928 return std::nullopt;
929 }
930
931 auto result = poseReader.queryMemoryIDs({memId});
932 if (not result.success)
933 {
934 ARMARX_WARNING << deactivateSpam() << "Could not query poses.";
935 return std::nullopt;
936 }
937 std::optional<armarx::armem::human::HumanPose> resPose;
938 result.memory.forEachInstanceWithDataAs(
939 [&resPose,
941 instance)
942 {
943 // Select only the person referenced by memId.instanceIndex.
944 if (instance.id().instanceIndex != memId.instanceIndex)
945 {
946 return;
947 }
948 armarx::armem::human::HumanPose res;
949 armarx::armem::human::fromAron(instance.data(), res);
950 resPose = res;
951 ARMARX_DEBUG << "Found profile instance";
952 });
953 if (not resPose.has_value())
954 {
955 ARMARX_VERBOSE << deactivateSpam() << "Pose result empty";
956 }
957 return resPose;
958 }
959
960 std::optional<armarx::FramedPosition>
961 UpdateConsumer::getHeadPos(armarx::armem::human::HumanPose pose)
962 {
963
965 {
966 //TODO: implement pose type
967 }
969 {
972 if (pose.keypoints.find(headKp) == pose.keypoints.end())
973 {
974 ARMARX_WARNING << "Pose does not contain head keypoint " << QUOTED(headKp);
975 return std::nullopt;
976 }
977
978 return pose.keypoints.at(headKp).positionGlobal;
979 }
981 {
982 //TODO: implement pose type
983 }
984 else
985 {
986 ARMARX_WARNING << "Unknown pose model ID '" << pose.poseModelId;
987 }
988
989 return std::nullopt;
990 }
991
992 float
993 UpdateConsumer::getDistance(const Eigen::Vector3f& facePos,
994 const armarx::FramedPosition& headPos)
995 {
997
998 const Eigen::Vector3f headPosition = headPos.toEigen();
999
1000 return (headPosition - facePos).norm();
1001 }
1002
1003 void
1004 UpdateConsumer::ensureTrackingIdUniqueness(const std::string& trackingId,
1005 const armarx::armem::MemoryID& exceptProfileID)
1006 {
1007 // Query all PersonInstances
1008 armarx::armem::client::QueryResult queryResult =
1009 personInstanceReader.getLatestSnapshotsIn(armarx::human::PersonInstanceCoreSegmentID);
1010
1011 if (not queryResult.success)
1012 {
1013 return;
1014 }
1015
1016 // Check each PersonInstance
1018 [this, &trackingId, &exceptProfileID](
1020 instance)
1021 {
1022 armarx::armem::MemoryID thisProfileID;
1023 fromAron(instance.data().profileID, thisProfileID);
1024
1025 // Skip the PersonInstance that should keep this tracking ID
1026 if (thisProfileID == exceptProfileID)
1027 {
1028 return;
1029 }
1030
1031 // Check if this PersonInstance has a pose with the tracking ID
1032 armarx::human::arondto::PersonInstance personInstance = instance.data();
1033 armarx::armem::MemoryID humanPoseId;
1034 fromAron(personInstance.poseID, humanPoseId);
1035 const auto pose = humanPoseFromMemId(humanPoseId);
1036
1037 if (pose.has_value() && pose->humanTrackingId.has_value() &&
1038 pose->humanTrackingId.value() == trackingId)
1039 {
1040 // Clear the pose link from this PersonInstance (only if not already empty)
1041 armarx::armem::MemoryID currentPoseID;
1042 fromAron(personInstance.poseID, currentPoseID);
1043
1044 // Only commit if the pose was actually set (avoid redundant commits)
1045 if (currentPoseID.hasMemoryName() || currentPoseID.hasCoreSegmentName())
1046 {
1047 ARMARX_INFO << deactivateSpam(10) << "Clearing tracking ID "
1048 << QUOTED(trackingId) << " from PersonInstance "
1049 << QUOTED(personInstance.profileID.entityName)
1050 << " because it was reassigned.";
1051
1052 toAron(personInstance.poseID, armarx::armem::MemoryID()); // clear
1053
1054 armarx::armem::EntityUpdate update;
1055 update.entityID = instance.id();
1056 update.referencedTime = armarx::armem::Time::Now();
1057 update.instancesData = {personInstance.toAron()};
1058 personInstanceWriter.commit(update);
1059 }
1060 }
1061 });
1062 }
1063
1064 void
1065 UpdateConsumer::logFaceRecognitionUpdateDuration(const armarx::armem::Duration& duration)
1066 {
1067 ARMARX_DEBUG << "consumeFaceRecognitionUpdate took " << duration.toMilliSeconds() << " ms.";
1068
1069 // This method is invoked from a ScopedStopWatch destructor callback. Destructors are
1070 // noexcept, so any exception escaping here would call std::terminate. Since this is only
1071 // diagnostic logging, swallow errors instead of taking the whole component down.
1072 try
1073 {
1074 // Resolve human poses and check for human tracking id
1075 armarx::armem::human::client::PersonInstanceReader::QueryResolved query;
1076 query.providerName = "";
1077 query.timestamp = armarx::Clock::Now();
1079 query.resolveFaceDetection = false;
1080 query.resolveHumanPose = true;
1081 query.resolveProfile = true;
1082
1083 ARMARX_VERBOSE << "Query";
1084 const auto result = personInstanceReaderV2.queryResolved(query);
1085
1086 ARMARX_DEBUG << "Humans with tracking ids:";
1087 for (const auto& personInstance : result.personInstances)
1088 {
1089 std::string name = "~unknown~";
1090 if (personInstance.profile.has_value())
1091 {
1092 name = personInstance.profile->id.firstName + " " +
1093 personInstance.profile->id.lastName;
1094 }
1095
1096 std::string trackingId = "~none~";
1097 if (personInstance.humanPose.has_value() and
1098 personInstance.humanPose->humanTrackingId.has_value())
1099 {
1100 trackingId = personInstance.humanPose->humanTrackingId.value();
1101 }
1102
1103 ARMARX_DEBUG << trackingId << ": " << name;
1104 }
1105 }
1106 catch (const std::exception& e)
1107 {
1109 << "Failed to log face recognition update summary: " << e.what();
1110 }
1111 }
1112
1113 std::optional<UpdateConsumer::PersonInstanceWithID>
1114 UpdateConsumer::findMatchingPersonInstance(
1115 const armarx::armem::client::QueryResult& queryResult,
1116 const armarx::armem::MemoryID& profileID)
1117 {
1118 std::optional<PersonInstanceWithID> result;
1119
1121 [this, &profileID, &result](
1123 instance)
1124 {
1125 armarx::human::arondto::PersonInstance personInstance = instance.data();
1126 // Match by profile ID (the person's identity)
1127 if (profileID.entityName == personInstance.profileID.entityName)
1128 {
1129 // Found matching PersonInstance - store it for later processing
1130 // (we'll update face data and validate/update pose links below)
1131 ARMARX_DEBUG << "Found matching personInstance "
1132 << QUOTED(instance.id().entityName) << ".";
1133
1134 armarx::armem::human::PersonInstance bo;
1135 fromAron(personInstance, bo);
1136 result = PersonInstanceWithID{.personInstance = personInstance,
1137 .memoryId = instance.id()};
1138 }
1139 });
1140
1141 return result;
1142 }
1143
1144 void
1145 UpdateConsumer::clearPoseIdFromPersonInstance(const PersonInstanceWithID& personInstanceMatched)
1146 {
1147 ARMARX_INFO << deactivateSpam(10) << "Clearing pose ID of person instance "
1148 << personInstanceMatched.personInstance.profileID.entityName;
1149 auto personInstanceRevised = personInstanceMatched.personInstance;
1150 toAron(personInstanceRevised.poseID, armarx::armem::MemoryID()); // clear
1151
1152 armarx::armem::EntityUpdate update;
1153 update.entityID = personInstanceMatched.memoryId;
1154 update.referencedTime = armarx::armem::Time::Now();
1155 update.instancesData = {personInstanceRevised.toAron()};
1156 personInstanceWriter.commit(update);
1157 }
1158
1159 void
1160 UpdateConsumer::setPoseIdForPersonInstance(const PersonInstanceWithID& personInstanceMatched,
1161 const armarx::armem::MemoryID& poseId)
1162 {
1163 ARMARX_INFO << deactivateSpam(10) << "Setting pose ID of person instance "
1164 << QUOTED(personInstanceMatched.personInstance.profileID.entityName) << " to "
1165 << QUOTED(poseId);
1166 auto personInstanceRevised = personInstanceMatched.personInstance;
1167 toAron(personInstanceRevised.poseID, poseId);
1168
1169 armarx::armem::EntityUpdate update;
1170 update.entityID = personInstanceMatched.memoryId;
1171 update.referencedTime = armarx::armem::Time::Now();
1172 update.instancesData = {personInstanceRevised.toAron()};
1173 personInstanceWriter.commit(update);
1174 }
1175
1176 bool
1177 UpdateConsumer::isMemoryIdFullySpecified(const armarx::armem::MemoryID& memoryId)
1178 {
1179 return memoryId.hasMemoryName() and memoryId.hasCoreSegmentName() and
1180 memoryId.hasProviderSegmentName() and memoryId.hasEntityName() and
1181 memoryId.hasTimestamp() and memoryId.hasInstanceIndex();
1182 }
1183
1184} // namespace VisionX::components::person_instance_updater
uint8_t data[1]
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
#define VAROUT(x)
#define QUOTED(x)
void consumePoseUpdate(const armarx::armem::human::HumanPose &humanPose, const armarx::armem::MemoryID &poseID)
Process a new human pose update.
UpdateConsumer(armarx::armem::client::MemoryNameSystem &mns, const Properties &properties)
void consumeFaceRecognitionUpdate(const armarx::armem::human::FaceRecognition &faceRecognition, const armarx::armem::MemoryID &faceRecognitionID)
Process a new face recognition result.
void consumeProfileUpdate(const armarx::human::arondto::Person &profile, const armarx::armem::MemoryID &profileID)
Process a profile update (not yet implemented).
static DateTime Now()
Current time on the virtual clock.
Definition Clock.cpp:93
static Duration Seconds(std::int64_t seconds)
Constructs a duration in seconds.
Definition Duration.cpp:72
virtual Eigen::Vector3f toEigen() const
Definition Pose.cpp:134
MemoryID withProviderSegmentName(const std::string &name) const
Definition MemoryID.cpp:417
bool hasProviderSegmentName() const
Definition MemoryID.h:115
bool hasEntityName() const
Definition MemoryID.h:121
bool hasInstanceIndex() const
Definition MemoryID.h:139
bool hasMemoryName() const
Definition MemoryID.h:103
MemoryID withEntityName(const std::string &name) const
Definition MemoryID.cpp:425
bool hasCoreSegmentName() const
Definition MemoryID.h:109
std::string entityName
Definition MemoryID.h:53
bool hasTimestamp() const
Definition MemoryID.h:127
MemoryID getEntityID() const
Definition MemoryID.cpp:310
The memory name system (MNS) client.
CommitResult commit(const Commit &commit) const
Writes a Commit to the memory.
Definition Writer.cpp:68
static DateTime Now()
Definition DateTime.cpp:51
std::int64_t toMilliSeconds() const
Returns the amount of milliseconds.
Definition Duration.cpp:60
Measures the time this stop watch was inside the current scope.
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:188
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::string const GlobalFrame
Variable of the global coordinate system.
Definition FramedPose.h:65
void fromAron(const armarx::human::arondto::HumanPose &dto, HumanPose &bo)
bool update(mongocxx::collection &coll, const nlohmann::json &query, const nlohmann::json &update)
Definition mongodb.cpp:68
base::EntityInstanceBase< AronDtoT, EntityInstanceMetadata > EntityInstanceBase
Entity instance with a concrete ARON DTO type as data.
armarx::core::time::Duration Duration
double s(double t, double s0, double v0, double a0, double j)
Definition CtrlUtil.h:33
const simox::meta::EnumNames< Joints > JointNames
Names of the joints as defined in the body model.
const armem::MemoryID FaceRecognitionCoreSegmentID
const armem::MemoryID PersonInstanceCoreSegmentID
const armem::MemoryID PoseCoreSegmentID
This file offers overloads of toIce() and fromIce() functions for STL container types.
void toAron(arondto::PackagePath &dto, const PackageFileLocation &bo)
void fromAron(const arondto::PackagePath &dto, PackageFileLocation &bo)
double distance(const Point &a, const Point &b)
Definition point.hpp:95
An update of an entity for a specific point in time.
Definition Commit.h:26
bool forEachInstanceWithDataAs(EntityInstanceBaseAronDtoFunctionT &&func) const
Call func on each instance with its data converted to Aron DTO class.
Result of a QueryInput.
Definition Query.h:51
wm::Memory memory
The slice of the memory that matched the query.
Definition Query.h:58
std::optional< armarx::armem::MemoryID > profileID
Definition types.h:80
std::optional< std::string > humanTrackingId
Definition types.h:47
Eigen::Isometry3f globalPose
Definition types.h:90
armarx::armem::MemoryID poseID
Definition types.h:87