FakeObjectDetector.cpp
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package ArmarXSimulation::components::FakeObjectDetector
17 * @author Timo Birr ( timo dot birr at kit dot edu )
18 * @date 2026
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
23#include "FakeObjectDetector.h"
24
25#include <algorithm>
26#include <experimental/map>
27
28#include <SimoxUtility/algorithm/string.h>
29#include <SimoxUtility/math/convert/deg_to_rad.h>
30#include <VirtualRobot/BoundingBox.h>
31#include <VirtualRobot/CollisionDetection/CollisionModel.h>
32#include <VirtualRobot/Robot.h>
33#include <VirtualRobot/RobotNodeSet.h>
34#include <VirtualRobot/XML/RobotIO.h>
35
40
43
44namespace armarx
45{
46 // `armarx::Box` already exists, so the geometry types stay explicitly qualified.
47 namespace fod = armarx::fake_object_detector;
48
49 namespace
50 {
52
53 /// Parse an axis specification such as "+Z" or "-y".
54 std::optional<Eigen::Vector3f>
55 parseAxis(const std::string& text)
56 {
57 const std::string trimmed = simox::alg::trim_copy(text);
58 if (trimmed.size() != 2)
59 {
60 return std::nullopt;
61 }
62
63 float sign = 0;
64 switch (trimmed.front())
65 {
66 case '+':
67 sign = 1.0F;
68 break;
69 case '-':
70 sign = -1.0F;
71 break;
72 default:
73 return std::nullopt;
74 }
75
76 switch (std::toupper(static_cast<unsigned char>(trimmed.back())))
77 {
78 case 'X':
79 return sign * Eigen::Vector3f::UnitX();
80 case 'Y':
81 return sign * Eigen::Vector3f::UnitY();
82 case 'Z':
83 return sign * Eigen::Vector3f::UnitZ();
84 default:
85 return std::nullopt;
86 }
87 }
88
89 std::optional<DetectionMode>
90 parseDetectionMode(const std::string& text)
91 {
92 const std::string lower = simox::alg::to_lower(simox::alg::trim_copy(text));
93 if (lower == "always")
94 {
95 return DetectionMode::Always;
96 }
97 if (lower == "fieldofview")
98 {
99 return DetectionMode::FieldOfView;
100 }
101 if (lower == "lineofsight")
102 {
103 return DetectionMode::LineOfSight;
104 }
105 return std::nullopt;
106 }
107
108 /// Turn an object-local OOBB plus the object's global pose into a global box.
110 toGlobalBox(const simox::OrientedBoxf& localOOBB, const Eigen::Matrix4f& globalPose)
111 {
112 fod::Box box;
113 box.centerPose = globalPose * localOOBB.transformation_centered();
114 box.halfExtents = localOOBB.dimensions() / 2;
115 return box;
116 }
117 } // namespace
118
119 const std::string FakeObjectDetector::defaultName = "FakeObjectDetector";
120
123 {
126
127 def->component(simulatorPrx, "Simulator");
128
129 def->optional(properties.updateFrequency,
130 "UpdateFrequency",
131 "Frequency at which objects are detected and reported [Hz].");
132
133 def->optional(properties.detectionMode,
134 "p.detectionMode",
135 "How strictly an object must be perceivable before it is reported: "
136 "'Always' (report everything), "
137 "'FieldOfView' (report once the bounding box enters the camera frustum), "
138 "'LineOfSight' (additionally require a clear line of sight to the front "
139 "facing side of the bounding box).");
140
141 def->optional(properties.robotName,
142 "p.robotName",
143 "Name of the robot to take the camera pose from. "
144 "If empty, the first robot in the simulated scene is used.");
145 def->optional(properties.cameraFrame,
146 "p.cameraFrame",
147 "Robot node used as the camera. Use a rendering ('...Sim') node so that "
148 "the viewing axis matches the simulated cameras.");
149
150 def->optional(properties.horizontalFovDeg,
151 "p.horizontalFovDeg",
152 "Horizontal opening angle of the camera frustum [deg].");
153 def->optional(properties.verticalFovDeg,
154 "p.verticalFovDeg",
155 "Vertical opening angle of the camera frustum [deg].");
156 def->optional(properties.minDistance,
157 "p.minDistance",
158 "Minimal distance along the viewing axis at which objects are detected "
159 "[mm].");
160 def->optional(properties.maxDistance,
161 "p.maxDistance",
162 "Maximal distance along the viewing axis at which objects are detected "
163 "[mm].");
164 def->optional(properties.cameraForwardAxis,
165 "p.cameraForwardAxis",
166 "Viewing axis of the camera node, e.g. '+Z' or '-X'.");
167 def->optional(
168 properties.cameraUpAxis, "p.cameraUpAxis", "Up axis of the camera node, e.g. '-Y'.");
169 def->optional(properties.requireFullyInFov,
170 "p.requireFullyInFov",
171 "If true, the whole bounding box must be inside the frustum. "
172 "Otherwise it is enough that a part of it is.");
173
174 def->optional(properties.frontFaceSampleGrid,
175 "p.frontFaceSampleGrid",
176 "Line of sight is tested against an N x N grid of points on the front "
177 "facing side of the bounding box. This is N. 1 tests the face centre "
178 "only.");
179 def->optional(properties.minVisibleFraction,
180 "p.minVisibleFraction",
181 "Fraction of the front face sample points that must have a clear line of "
182 "sight for the object to be detected.");
183 def->optional(properties.occlusionByObjects,
184 "p.occlusionByObjects",
185 "If true, other objects in the scene can occlude an object.");
186 def->optional(properties.occlusionByRobot,
187 "p.occlusionByRobot",
188 "If true, the robot's own body can occlude an object.");
189 def->optional(properties.rayEpsilon,
190 "p.rayEpsilon",
191 "Numerical tolerance for the line of sight rays [mm].");
192
193 def->optional(properties.fallbackOobbSize,
194 "p.fallbackOobbSize",
195 "Edge length of the cube used as bounding box for objects that have no "
196 "bounding box on disk [mm].");
197 def->optional(
198 properties.confidence, "p.confidence", "Confidence reported for a detected object.");
199 def->optional(properties.ignoredObjects,
200 "p.ignoredObjects",
201 "Comma separated object IDs (e.g. 'Kitchen/green-cup/0') that are never "
202 "reported.");
203 def->optional(properties.alwaysVisibleDatasets,
204 "p.alwaysVisibleDatasets",
205 "Comma separated object datasets (e.g. 'Interior') that make up the static "
206 "scene. Their objects are reported without any visibility check and marked "
207 "as static, so they never decay out of the object memory. All other "
208 "objects are gated by 'p.detectionMode'.");
209 def->optional(properties.alwaysVisibleObjects,
210 "p.alwaysVisibleObjects",
211 "Comma separated object IDs (e.g. 'Kitchen/mobile-dishwasher/0') that are "
212 "part of the static scene even though their dataset is not. Same effect as "
213 "'p.alwaysVisibleDatasets', per object: needed where one dataset holds both "
214 "room furniture and the small objects the robot manipulates, because only "
215 "the furniture should be known without looking at it.");
216 def->optional(properties.externallyOwnedObjects,
217 "p.externallyOwnedObjects",
218 "Comma separated object IDs that another component is the authority on, "
219 "e.g. an articulated object whose joint state a skill provider writes. "
220 "They are never reported, so this detector cannot overwrite that state "
221 "-- but unlike 'p.ignoredObjects' they are still used as occluders, "
222 "because they are physically there.");
223 def->optional(properties.staticSceneRepublishSeconds,
224 "p.staticSceneRepublishSeconds",
225 "How often the always-visible static scene is reported even though it "
226 "has not moved [s]. Between those, a static object is reported only "
227 "when its pose changes. 0 reports it every cycle.");
228 def->optional(properties.requestedObjectsOnly,
229 "p.requestedObjectsOnly",
230 "If true, only report visible objects that were requested via "
231 "`requestObjects()`. If false, all visible objects are reported.");
232
233 return def;
234 }
235
236 void
238 {
239 if (const std::optional<DetectionMode> mode = parseDetectionMode(properties.detectionMode))
240 {
241 properties.mode = *mode;
242 }
243 else
244 {
245 ARMARX_WARNING << "Unknown detection mode '" << properties.detectionMode
246 << "'. Expected one of 'Always', 'FieldOfView', 'LineOfSight'. "
247 << "Falling back to 'FieldOfView'.";
248 properties.mode = DetectionMode::FieldOfView;
249 }
250
251 if (const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraForwardAxis))
252 {
253 cameraForwardLocal = *axis;
254 }
255 else
256 {
257 ARMARX_WARNING << "Could not parse camera forward axis '"
258 << properties.cameraForwardAxis << "'. Falling back to '+Z'.";
259 cameraForwardLocal = Eigen::Vector3f::UnitZ();
260 }
261
262 if (const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraUpAxis))
263 {
264 cameraUpLocal = *axis;
265 }
266 else
267 {
268 ARMARX_WARNING << "Could not parse camera up axis '" << properties.cameraUpAxis
269 << "'. Falling back to '-Y'.";
270 cameraUpLocal = -Eigen::Vector3f::UnitY();
271 }
272
273 if (std::abs(cameraForwardLocal.dot(cameraUpLocal)) > 1e-3F)
274 {
275 ARMARX_WARNING << "Camera forward axis '" << properties.cameraForwardAxis
276 << "' and up axis '" << properties.cameraUpAxis
277 << "' are not perpendicular. The frustum will be skewed.";
278 }
279
280 if (properties.frontFaceSampleGrid < 1)
281 {
282 ARMARX_WARNING << "p.frontFaceSampleGrid must be at least 1, but is "
283 << properties.frontFaceSampleGrid << ". Using 1.";
284 properties.frontFaceSampleGrid = 1;
285 }
286
287 for (const std::string& id : simox::alg::split(properties.ignoredObjects, ","))
288 {
289 const std::string trimmed = simox::alg::trim_copy(id);
290 if (not trimmed.empty())
291 {
292 ignoredObjects.insert(trimmed);
293 }
294 }
295 if (not ignoredObjects.empty())
296 {
297 ARMARX_INFO << "Ignoring objects: "
298 << simox::alg::join(std::vector<std::string>(ignoredObjects.begin(),
299 ignoredObjects.end()),
300 ", ");
301 }
302
303 for (const std::string& dataset : simox::alg::split(properties.alwaysVisibleDatasets, ","))
304 {
305 const std::string trimmed = simox::alg::trim_copy(dataset);
306 if (not trimmed.empty())
307 {
308 alwaysVisibleDatasets.insert(trimmed);
309 }
310 }
311 for (const std::string& id : simox::alg::split(properties.alwaysVisibleObjects, ","))
312 {
313 const std::string trimmed = simox::alg::trim_copy(id);
314 if (not trimmed.empty())
315 {
316 alwaysVisibleObjects.insert(trimmed);
317 }
318 }
319
320 for (const std::string& id : simox::alg::split(properties.externallyOwnedObjects, ","))
321 {
322 const std::string trimmed = simox::alg::trim_copy(id);
323 if (not trimmed.empty())
324 {
325 externallyOwnedObjects.insert(trimmed);
326 }
327 }
328 if (not externallyOwnedObjects.empty())
329 {
330 ARMARX_INFO << "Not reporting these objects (another component owns their state, "
331 << "they are still used as occluders): "
332 << simox::alg::join(std::vector<std::string>(
333 externallyOwnedObjects.begin(),
334 externallyOwnedObjects.end()),
335 ", ");
336 }
337 if (not alwaysVisibleObjects.empty())
338 {
339 ARMARX_INFO << "Reporting these objects as part of the always visible static scene: "
340 << simox::alg::join(std::vector<std::string>(alwaysVisibleObjects.begin(),
341 alwaysVisibleObjects.end()),
342 ", ");
343 }
344
345 if (not alwaysVisibleDatasets.empty())
346 {
347 ARMARX_INFO << "Reporting these datasets as the always visible static scene: "
348 << simox::alg::join(std::vector<std::string>(alwaysVisibleDatasets.begin(),
349 alwaysVisibleDatasets.end()),
350 ", ");
351 }
352 }
353
354 void
356 {
357 detectionTask = new SimpleRunningTask<>([this]() { this->detectionTaskRun(); });
358 detectionTask->start();
359 }
360
361 void
363 {
364 if (detectionTask)
365 {
366 detectionTask->stop();
367 detectionTask = nullptr;
368 }
369 }
370
371 void
375
376 std::string
378 {
379 return FakeObjectDetector::defaultName;
380 }
381
382 std::string
384 {
385 return FakeObjectDetector::defaultName;
386 }
387
388 objpose::provider::RequestObjectsOutput
389 FakeObjectDetector::requestObjects(const objpose::provider::RequestObjectsInput& input,
390 const Ice::Current& /*unused*/)
391 {
392 objpose::provider::RequestObjectsOutput output;
393
394 const DateTime now = DateTime::Now();
395 const std::scoped_lock lock(activeRequestsMutex);
396
397 for (const auto& id : input.objectIDs)
398 {
399 const std::string entityID = id.dataset + "/" + id.className + "/" + id.instanceName;
400
401 activeRequests[entityID] =
402 now + armarx::core::time::Duration::MilliSeconds(input.relativeTimeoutMS);
403
404 // Whether the object is actually reported depends on visibility, which is decided
405 // per cycle - so the request itself always succeeds.
406 output.results[id].success = true;
407 }
408
409 if (not properties.requestedObjectsOnly and not input.objectIDs.empty())
410 {
412 << "All visible objects are reported by default. Requesting an object "
413 "has no effect unless 'p.requestedObjectsOnly' is enabled.";
414 }
415
416 return output;
417 }
418
419 objpose::ProviderInfo
420 FakeObjectDetector::getProviderInfo(const Ice::Current& /*unused*/)
421 {
422 objpose::ProviderInfo info;
423 info.objectType = objpose::KnownObject;
425 info.supportedObjects = {};
426 return info;
427 }
428
429 void
430 FakeObjectDetector::detectionTaskRun()
431 {
432 Metronome metronome(Frequency::Hertz(properties.updateFrequency));
433
434 while (detectionTask and not detectionTask->isStopped())
435 {
436 metronome.waitForNextTick();
437 detectAndReport();
438 }
439 }
440
441 void
442 FakeObjectDetector::detectAndReport()
443 {
444 armarx::SceneVisuData sceneData;
445 try
446 {
447 sceneData = simulatorPrx->getScene();
448 }
449 catch (const Ice::LocalException& e)
450 {
452 << "Could not get the scene from the simulator: " << e.what();
453 return;
454 }
455
456 const DateTime now = DateTime::Now();
457 removeExpiredRequests(now);
458
459 const std::vector<SceneObject> objects = collectSceneObjects(sceneData);
460 std::vector<objpose::ProvidedObjectPose> providedObjects;
461
462 const armarx::RobotVisuData* robotData = findRobot(sceneData);
463
464 // The static scene is known to the robot at all times, in every mode, and even if the
465 // camera cannot be resolved. It is still used as an occluder for the gated objects.
466 //
467 // It is NOT reported every cycle, though. A static pose does not decay, so one report
468 // is enough until it moves -- and reporting ~25 unchanging objects at the update
469 // frequency has two costs that are easy to miss:
470 // * distance_to_obstacle_costmap_provider subscribes per provider and rebuilds the
471 // whole costmap on every commit that touches a "relevant" dataset (which is every
472 // dataset except the manipulable ones). A rebuild takes ~670 ms, so a 10 Hz
473 // report keeps it permanently rebuilding and permanently behind.
474 // * every commit is a snapshot in the object memory's LTM export.
475 // The pose is still compared each cycle, so a static object that IS moved (e.g. by a
476 // failure-induction script) is reported immediately.
477 const bool staticSceneDue =
478 properties.staticSceneRepublishSeconds <= 0 or not lastStaticSceneReport.isValid() or
479 (now - lastStaticSceneReport).toSecondsDouble() >=
480 properties.staticSceneRepublishSeconds;
481
482 for (const SceneObject& object : objects)
483 {
484 if (not(object.alwaysVisible and isRequested(object.objectID)))
485 {
486 continue;
487 }
488
489 if (externallyOwnedObjects.count(object.objectID.str()) > 0)
490 {
491 continue;
492 }
493
494 const std::string id = object.objectID.str();
495 const auto it = lastReportedStaticPose.find(id);
496 const bool moved =
497 it == lastReportedStaticPose.end() or not it->second.isApprox(object.globalPose);
498
499 if (staticSceneDue or moved)
500 {
501 lastReportedStaticPose[id] = object.globalPose;
502 providedObjects.push_back(toProvidedObjectPose(object, now));
503 }
504 }
505
506 if (staticSceneDue)
507 {
508 lastStaticSceneReport = now;
509 }
510
511 if (properties.mode == DetectionMode::Always)
512 {
513 for (const SceneObject& object : objects)
514 {
515 if (not object.alwaysVisible and isRequested(object.objectID) and
516 externallyOwnedObjects.count(object.objectID.str()) == 0)
517 {
518 providedObjects.push_back(toProvidedObjectPose(object, now));
519 }
520 }
521 }
522 else if (robotData != nullptr)
523 {
524 if (const std::optional<fod::Frustum> frustum = buildFrustum(*robotData))
525 {
526 std::vector<fod::Box> robotBoxes;
527 if (properties.mode == DetectionMode::LineOfSight and properties.occlusionByRobot)
528 {
529 robotBoxes = collectRobotBoxes(*robotData);
530 }
531
532 for (std::size_t i = 0; i < objects.size(); ++i)
533 {
534 // Already reported above as part of the static scene.
535 if (objects[i].alwaysVisible or not isRequested(objects[i].objectID) or
536 externallyOwnedObjects.count(objects[i].objectID.str()) > 0)
537 {
538 continue;
539 }
540
541 std::vector<fod::Box> occluders;
542 if (properties.mode == DetectionMode::LineOfSight)
543 {
544 occluders.reserve(objects.size() + robotBoxes.size());
545 if (properties.occlusionByObjects)
546 {
547 for (std::size_t j = 0; j < objects.size(); ++j)
548 {
549 // The target must never occlude itself.
550 if (j != i)
551 {
552 occluders.push_back(objects[j].box);
553 }
554 }
555 }
556 occluders.insert(occluders.end(), robotBoxes.begin(), robotBoxes.end());
557 }
558
559 if (isVisible(objects[i], *frustum, occluders))
560 {
561 providedObjects.push_back(toProvidedObjectPose(objects[i], now));
562 }
563 }
564 }
565 }
566 else
567 {
568 ARMARX_WARNING << deactivateSpam(10) << "No robot '" << properties.robotName
569 << "' in the simulated scene. Reporting no objects.";
570 }
571
572 ARMARX_DEBUG << deactivateSpam(10) << "Detected " << providedObjects.size() << " of "
573 << objects.size() << " objects.";
574
575 objpose::data::ProvidedObjectPoseSeq providedObjectsIce = objpose::toIce(providedObjects);
576
577 // `robotName` only exists on the Ice struct. The object memory needs it to resolve the
578 // robot and fill in `objectPoseRobot`; without it, it logs "Failed to retrieve robot".
579 const std::string robotName = robotData != nullptr ? robotData->name : properties.robotName;
580 for (objpose::data::ProvidedObjectPose& pose : providedObjectsIce)
581 {
582 pose.robotName = robotName;
583 }
584
585 try
586 {
587 objectPoseTopic->reportObjectPoses(getName(), providedObjectsIce);
588 }
589 catch (const Ice::LocalException& e)
590 {
592 << "Could not report object poses to the object memory: " << e.what();
593 }
594 }
595
596 const armarx::RobotVisuData*
597 FakeObjectDetector::findRobot(const armarx::SceneVisuData& sceneData) const
598 {
599 if (sceneData.robots.empty())
600 {
601 return nullptr;
602 }
603 if (properties.robotName.empty())
604 {
605 return &sceneData.robots.front();
606 }
607
608 const auto it = std::find_if(sceneData.robots.begin(),
609 sceneData.robots.end(),
610 [this](const armarx::RobotVisuData& robot)
611 { return robot.name == properties.robotName; });
612
613 return it != sceneData.robots.end() ? &(*it) : nullptr;
614 }
615
616 std::vector<FakeObjectDetector::SceneObject>
617 FakeObjectDetector::collectSceneObjects(const armarx::SceneVisuData& sceneData)
618 {
619 std::vector<SceneObject> objects;
620 objects.reserve(sceneData.objects.size());
621
622 for (const armarx::ObjectVisuData& visuData : sceneData.objects)
623 {
624 if (ignoredObjects.count(visuData.name) > 0)
625 {
626 continue;
627 }
628
629 const auto poseIt = visuData.objectPoses.find(visuData.name);
630 if (poseIt == visuData.objectPoses.end())
631 {
632 ARMARX_WARNING << deactivateSpam(10) << "Simulated object '" << visuData.name
633 << "' has no pose. Skipping it.";
634 continue;
635 }
636
637 SceneObject object;
638 object.objectID = armarx::ObjectID(visuData.name);
639 object.globalPose = armarx::fromIce(poseIt->second);
640 object.localOOBB = getLocalOOBB(object.objectID);
641 object.alwaysVisible = alwaysVisibleDatasets.count(object.objectID.dataset()) > 0 or
642 alwaysVisibleObjects.count(object.objectID.str()) > 0;
643
644 const Eigen::Matrix4f identity = Eigen::Matrix4f::Identity();
645 const Eigen::Vector3f fallbackExtents =
646 Eigen::Vector3f::Constant(properties.fallbackOobbSize);
647 const simox::OrientedBoxf oobb =
648 object.localOOBB.value_or(simox::OrientedBoxf(identity, fallbackExtents));
649 object.box = toGlobalBox(oobb, object.globalPose);
650
651 objects.push_back(std::move(object));
652 }
653
654 return objects;
655 }
656
657 std::optional<simox::OrientedBoxf>
658 FakeObjectDetector::getLocalOOBB(const armarx::ObjectID& objectID)
659 {
660 // The OOBB is a property of the object class, so cache it per class.
661 const std::string classID = objectID.getClassID().str();
662
663 if (const auto it = oobbCache.find(classID); it != oobbCache.end())
664 {
665 return it->second;
666 }
667
668 std::optional<simox::OrientedBoxf> oobb;
669 if (const std::optional<ObjectInfo> info = objectFinder.findObject(objectID))
670 {
671 oobb = info->loadOOBB();
672 }
673
674 if (not oobb and warnedMissingOOBB.insert(classID).second)
675 {
676 ARMARX_WARNING << "No bounding box found for object class '" << classID
677 << "'. Using a cube of " << properties.fallbackOobbSize
678 << " mm instead.";
679 }
680
681 oobbCache[classID] = oobb;
682 return oobb;
683 }
684
685 std::optional<fod::Frustum>
686 FakeObjectDetector::buildFrustum(const armarx::RobotVisuData& robot) const
687 {
688 const auto it = robot.robotNodePoses.find(properties.cameraFrame);
689 if (it == robot.robotNodePoses.end())
690 {
691 if (not warnedMissingCameraNode)
692 {
693 std::vector<std::string> nodeNames;
694 nodeNames.reserve(robot.robotNodePoses.size());
695 for (const auto& [name, _] : robot.robotNodePoses)
696 {
697 nodeNames.push_back(name);
698 }
699 ARMARX_WARNING << "Robot '" << robot.name << "' has no node '"
700 << properties.cameraFrame << "' to use as camera. "
701 << "Available nodes are: " << nodeNames;
702 const_cast<FakeObjectDetector*>(this)->warnedMissingCameraNode = true;
703 }
704 return std::nullopt;
705 }
706
707 fod::Frustum frustum;
708 frustum.cameraPose = armarx::fromIce(it->second);
709 frustum.horizontalFov = simox::math::deg_to_rad(properties.horizontalFovDeg);
710 frustum.verticalFov = simox::math::deg_to_rad(properties.verticalFovDeg);
711 frustum.minDistance = properties.minDistance;
712 frustum.maxDistance = properties.maxDistance;
713 frustum.forwardLocal = cameraForwardLocal;
714 frustum.upLocal = cameraUpLocal;
715 return frustum;
716 }
717
719 FakeObjectDetector::loadRobotModel(const armarx::RobotVisuData& robotData)
720 {
721 if (robotModel and robotModelName == robotData.name)
722 {
723 return robotModel;
724 }
725 if (robotModelLoadFailed)
726 {
727 return nullptr;
728 }
729
730 if (robotData.robotFile.empty())
731 {
732 ARMARX_WARNING << "Robot '" << robotData.name
733 << "' has no model file. It cannot occlude objects.";
734 robotModelLoadFailed = true;
735 return nullptr;
736 }
737
738 if (not robotData.project.empty())
739 {
740 const CMakePackageFinder finder(robotData.project);
741 if (finder.packageFound())
742 {
743 ArmarXDataPath::addDataPaths(finder.getDataDir());
744 }
745 else
746 {
747 ARMARX_WARNING << "ArmarX package '" << robotData.project << "' was not found.";
748 }
749 }
750
751 std::string filename = robotData.robotFile;
752 ArmarXDataPath::getAbsolutePath(filename, filename);
753
754 try
755 {
756 // Collision models are required - the default (eStructure) has no geometry at all.
757 robotModel =
758 VirtualRobot::RobotIO::loadRobot(filename, VirtualRobot::RobotIO::eCollisionModel);
759 }
760 catch (const std::exception& e)
761 {
762 ARMARX_WARNING << "Could not load the robot model from '" << filename
763 << "': " << e.what() << ". The robot will not occlude objects.";
764 }
765
766 if (not robotModel)
767 {
768 robotModelLoadFailed = true;
769 return nullptr;
770 }
771
772 robotModelName = robotData.name;
773 ARMARX_INFO << "Loaded robot model '" << robotData.name << "' from '" << filename
774 << "' for self-occlusion checks.";
775 return robotModel;
776 }
777
778 std::vector<fod::Box>
779 FakeObjectDetector::collectRobotBoxes(const armarx::RobotVisuData& robotData)
780 {
781 const VirtualRobot::RobotPtr robot = loadRobotModel(robotData);
782 if (not robot)
783 {
784 return {};
785 }
786
787 std::map<std::string, float> jointValues;
788 for (const auto& [name, value] : robotData.jointValues)
789 {
790 if (robot->hasRobotNode(name))
791 {
792 jointValues[name] = value;
793 }
794 }
795 robot->setJointValues(jointValues);
796 if (robotData.pose)
797 {
798 robot->setGlobalPose(armarx::fromIce(robotData.pose));
799 }
800
801 std::vector<fod::Box> boxes;
802 for (const VirtualRobot::RobotNodePtr& node : robot->getRobotNodes())
803 {
804 const VirtualRobot::CollisionModelPtr collisionModel = node->getCollisionModel();
805 if (not collisionModel)
806 {
807 continue;
808 }
809
810 // Take the bounding box in the link's own frame and orient it with the link, rather
811 // than using getBoundingBox(true). A global axis aligned box around a link lying
812 // diagonally is far larger than the link and would occlude much of the scene.
813 const VirtualRobot::BoundingBox boundingBox = collisionModel->getBoundingBox(false);
814 const Eigen::Vector3f min = boundingBox.getMin();
815 const Eigen::Vector3f max = boundingBox.getMax();
816 const Eigen::Vector3f extents = max - min;
817 if (extents.minCoeff() <= 0)
818 {
819 continue;
820 }
821
822 Eigen::Matrix4f localCenter = Eigen::Matrix4f::Identity();
823 localCenter.topRightCorner<3, 1>() = (min + max) / 2;
824
825 fod::Box box;
826 box.centerPose = collisionModel->getGlobalPose() * localCenter;
827 box.halfExtents = extents / 2;
828 boxes.push_back(box);
829 }
830
831 return boxes;
832 }
833
834 bool
835 FakeObjectDetector::isVisible(const SceneObject& object,
836 const fod::Frustum& frustum,
837 const std::vector<fod::Box>& occluders) const
838 {
839 if (not fod::isInFieldOfView(frustum, object.box, properties.requireFullyInFov))
840 {
841 return false;
842 }
843
844 if (properties.mode != DetectionMode::LineOfSight)
845 {
846 return true;
847 }
848
849 const float fraction = fod::visibleFraction(
850 frustum, object.box, occluders, properties.frontFaceSampleGrid, properties.rayEpsilon);
851
852 return fraction >= properties.minVisibleFraction;
853 }
854
856 FakeObjectDetector::toProvidedObjectPose(const SceneObject& object, const DateTime& time) const
857 {
858 objpose::ProvidedObjectPose pose;
859
860 pose.providerName = getName();
861 pose.objectType = objpose::ObjectType::KnownObject;
862 // Static poses never decay. That is what keeps the static scene permanently in memory,
863 // and exactly why a detected object must not be marked static.
864 pose.isStatic = object.alwaysVisible;
865
866 pose.objectID = object.objectID;
867 pose.objectPose = object.globalPose;
868 pose.objectPoseFrame = armarx::GlobalFrame;
869
870 pose.localOOBB = object.localOOBB;
871 pose.confidence = properties.confidence;
872 // Must advance every cycle, otherwise the memory discards the update.
873 pose.timestamp = time;
874
875 return pose;
876 }
877
878 void
879 FakeObjectDetector::removeExpiredRequests(const DateTime& time)
880 {
881 const std::scoped_lock lock(activeRequestsMutex);
882
883 std::experimental::erase_if(activeRequests,
884 [&time](const auto& request)
885 {
886 const DateTime& until = request.second;
887 return (not until.isInvalid()) and time > until;
888 });
889 }
890
891 bool
892 FakeObjectDetector::isRequested(const armarx::ObjectID& objectID) const
893 {
894 if (not properties.requestedObjectsOnly)
895 {
896 return true;
897 }
898
899 const std::scoped_lock lock(activeRequestsMutex);
900 return activeRequests.count(objectID.str()) > 0;
901 }
902
905
906} // namespace armarx
#define ARMARX_REGISTER_COMPONENT_EXECUTABLE(ComponentT, applicationName)
Definition Decoupled.h:29
static bool getAbsolutePath(const std::string &relativeFilename, std::string &storeAbsoluteFilename, const std::vector< std::string > &additionalSearchPaths={}, bool verbose=true)
static void addDataPaths(const std::string &dataPathList)
Default component property definition container.
Definition Component.h:70
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:88
static DateTime Now()
Definition DateTime.cpp:51
A fake object detector for simulation.
objpose::ProviderInfo getProviderInfo(const Ice::Current &) override
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
static std::string GetDefaultName()
Get the component's default name.
objpose::provider::RequestObjectsOutput requestObjects(const objpose::provider::RequestObjectsInput &input, const Ice::Current &) override
std::string getDefaultName() const override
static Frequency Hertz(std::int64_t hertz)
Definition Frequency.cpp:20
SpamFilterDataPtr deactivateSpam(float deactivationDurationSec=10.0f, const std::string &identifier="", bool deactivate=true) const
disables the logging for the current line for the given amount of seconds.
Definition Logging.cpp:99
std::string getName() const
Retrieve name of object.
Ice::ObjectPrx getProxy(long timeoutMs=0, bool waitForScheduler=true) const
Returns the proxy of this object (optionally it waits for the proxy)
ObjectID getClassID() const
Return just the class ID without an intance name.
Definition ObjectID.cpp:71
std::string str() const
Return "dataset/className" or "dataset/className/instanceName".
Definition ObjectID.cpp:60
objpose::ObjectPoseStorageInterfacePrx objectPoseTopic
bool isStopped()
Retrieve whether stop() has been called.
Represents a point in time.
Definition DateTime.h:25
static Duration MilliSeconds(std::int64_t milliSeconds)
Constructs a duration in milliseconds.
Definition Duration.cpp:48
Simple rate limiter for use in loops to maintain a certain frequency given a clock.
Definition Metronome.h:57
An object pose provided by an ObjectPoseProvider.
std::string providerName
Name of the providing component.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#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
std::string const GlobalFrame
Variable of the global coordinate system.
Definition FramedPose.h:65
std::shared_ptr< class Robot > RobotPtr
Definition Bus.h:19
Geometric primitives for the fake object detector.
DetectionMode
How strictly an object must be perceivable before it is reported.
objpose::AABB toIce(const simox::AxisAlignedBoundingBox &aabb)
This file offers overloads of toIce() and fromIce() functions for STL container types.
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
std::vector< T > max(const std::vector< T > &v1, const std::vector< T > &v2)
T sign(T t)
Definition algorithm.h:214
void fromIce(const std::map< IceKeyT, IceValueT > &iceMap, boost::container::flat_map< CppKeyT, CppValueT > &cppMap)
std::vector< T > min(const std::vector< T > &v1, const std::vector< T > &v2)
SimpleRunningTask(Ts...) -> SimpleRunningTask< std::function< void(void)> >
std::shared_ptr< Value > value()
Definition cxxopts.hpp:855
An oriented bounding box in the global frame.
Eigen::Matrix4f centerPose
Rotation = box axes, translation = box centre.