26#include <experimental/map>
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>
47 namespace fod = armarx::fake_object_detector;
54 std::optional<Eigen::Vector3f>
55 parseAxis(
const std::string& text)
57 const std::string trimmed = simox::alg::trim_copy(text);
58 if (trimmed.size() != 2)
64 switch (trimmed.front())
76 switch (std::toupper(
static_cast<unsigned char>(trimmed.back())))
79 return sign * Eigen::Vector3f::UnitX();
81 return sign * Eigen::Vector3f::UnitY();
83 return sign * Eigen::Vector3f::UnitZ();
89 std::optional<DetectionMode>
90 parseDetectionMode(
const std::string& text)
92 const std::string lower = simox::alg::to_lower(simox::alg::trim_copy(text));
93 if (lower ==
"always")
95 return DetectionMode::Always;
97 if (lower ==
"fieldofview")
99 return DetectionMode::FieldOfView;
101 if (lower ==
"lineofsight")
103 return DetectionMode::LineOfSight;
110 toGlobalBox(
const simox::OrientedBoxf& localOOBB,
const Eigen::Matrix4f& globalPose)
113 box.
centerPose = globalPose * localOOBB.transformation_centered();
114 box.halfExtents = localOOBB.dimensions() / 2;
119 const std::string FakeObjectDetector::defaultName =
"FakeObjectDetector";
127 def->component(simulatorPrx,
"Simulator");
129 def->optional(properties.updateFrequency,
131 "Frequency at which objects are detected and reported [Hz].");
133 def->optional(properties.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).");
141 def->optional(properties.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,
147 "Robot node used as the camera. Use a rendering ('...Sim') node so that "
148 "the viewing axis matches the simulated cameras.");
150 def->optional(properties.horizontalFovDeg,
151 "p.horizontalFovDeg",
152 "Horizontal opening angle of the camera frustum [deg].");
153 def->optional(properties.verticalFovDeg,
155 "Vertical opening angle of the camera frustum [deg].");
156 def->optional(properties.minDistance,
158 "Minimal distance along the viewing axis at which objects are detected "
160 def->optional(properties.maxDistance,
162 "Maximal distance along the viewing axis at which objects are detected "
164 def->optional(properties.cameraForwardAxis,
165 "p.cameraForwardAxis",
166 "Viewing axis of the camera node, e.g. '+Z' or '-X'.");
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.");
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 "
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,
191 "Numerical tolerance for the line of sight rays [mm].");
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].");
198 properties.confidence,
"p.confidence",
"Confidence reported for a detected object.");
199 def->optional(properties.ignoredObjects,
201 "Comma separated object IDs (e.g. 'Kitchen/green-cup/0') that are never "
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.requestedObjectsOnly,
210 "p.requestedObjectsOnly",
211 "If true, only report visible objects that were requested via "
212 "`requestObjects()`. If false, all visible objects are reported.");
220 if (
const std::optional<DetectionMode> mode = parseDetectionMode(properties.detectionMode))
222 properties.mode = *mode;
226 ARMARX_WARNING <<
"Unknown detection mode '" << properties.detectionMode
227 <<
"'. Expected one of 'Always', 'FieldOfView', 'LineOfSight'. "
228 <<
"Falling back to 'FieldOfView'.";
229 properties.mode = DetectionMode::FieldOfView;
232 if (
const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraForwardAxis))
234 cameraForwardLocal = *axis;
239 << properties.cameraForwardAxis <<
"'. Falling back to '+Z'.";
240 cameraForwardLocal = Eigen::Vector3f::UnitZ();
243 if (
const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraUpAxis))
245 cameraUpLocal = *axis;
249 ARMARX_WARNING <<
"Could not parse camera up axis '" << properties.cameraUpAxis
250 <<
"'. Falling back to '-Y'.";
251 cameraUpLocal = -Eigen::Vector3f::UnitY();
254 if (std::abs(cameraForwardLocal.dot(cameraUpLocal)) > 1e-3F)
256 ARMARX_WARNING <<
"Camera forward axis '" << properties.cameraForwardAxis
257 <<
"' and up axis '" << properties.cameraUpAxis
258 <<
"' are not perpendicular. The frustum will be skewed.";
261 if (properties.frontFaceSampleGrid < 1)
263 ARMARX_WARNING <<
"p.frontFaceSampleGrid must be at least 1, but is "
264 << properties.frontFaceSampleGrid <<
". Using 1.";
265 properties.frontFaceSampleGrid = 1;
268 for (
const std::string&
id : simox::alg::split(properties.ignoredObjects,
","))
270 const std::string trimmed = simox::alg::trim_copy(
id);
271 if (not trimmed.empty())
273 ignoredObjects.insert(trimmed);
276 if (not ignoredObjects.empty())
279 << simox::alg::join(std::vector<std::string>(ignoredObjects.begin(),
280 ignoredObjects.end()),
284 for (
const std::string& dataset : simox::alg::split(properties.alwaysVisibleDatasets,
","))
286 const std::string trimmed = simox::alg::trim_copy(dataset);
287 if (not trimmed.empty())
289 alwaysVisibleDatasets.insert(trimmed);
292 if (not alwaysVisibleDatasets.empty())
294 ARMARX_INFO <<
"Reporting these datasets as the always visible static scene: "
295 << simox::alg::join(std::vector<std::string>(alwaysVisibleDatasets.begin(),
296 alwaysVisibleDatasets.end()),
305 detectionTask->start();
313 detectionTask->stop();
314 detectionTask =
nullptr;
326 return FakeObjectDetector::defaultName;
332 return FakeObjectDetector::defaultName;
335 objpose::provider::RequestObjectsOutput
337 const Ice::Current& )
339 objpose::provider::RequestObjectsOutput output;
342 const std::scoped_lock lock(activeRequestsMutex);
344 for (
const auto&
id : input.objectIDs)
346 const std::string entityID =
id.dataset +
"/" +
id.className +
"/" +
id.instanceName;
348 activeRequests[entityID] =
353 output.results[id].success =
true;
356 if (not properties.requestedObjectsOnly and not input.objectIDs.empty())
359 <<
"All visible objects are reported by default. Requesting an object "
360 "has no effect unless 'p.requestedObjectsOnly' is enabled.";
366 objpose::ProviderInfo
369 objpose::ProviderInfo info;
370 info.objectType = objpose::KnownObject;
372 info.supportedObjects = {};
377 FakeObjectDetector::detectionTaskRun()
381 while (detectionTask and not detectionTask->
isStopped())
383 metronome.waitForNextTick();
389 FakeObjectDetector::detectAndReport()
391 armarx::SceneVisuData sceneData;
394 sceneData = simulatorPrx->getScene();
396 catch (
const Ice::LocalException& e)
399 <<
"Could not get the scene from the simulator: " << e.what();
404 removeExpiredRequests(now);
406 const std::vector<SceneObject> objects = collectSceneObjects(sceneData);
407 std::vector<objpose::ProvidedObjectPose> providedObjects;
409 const armarx::RobotVisuData* robotData = findRobot(sceneData);
413 for (
const SceneObject&
object : objects)
415 if (
object.alwaysVisible and isRequested(
object.objectID))
417 providedObjects.push_back(toProvidedObjectPose(
object, now));
421 if (properties.mode == DetectionMode::Always)
423 for (
const SceneObject&
object : objects)
425 if (not
object.alwaysVisible and isRequested(
object.objectID))
427 providedObjects.push_back(toProvidedObjectPose(
object, now));
431 else if (robotData !=
nullptr)
433 if (
const std::optional<fod::Frustum> frustum = buildFrustum(*robotData))
435 std::vector<fod::Box> robotBoxes;
436 if (properties.mode == DetectionMode::LineOfSight and properties.occlusionByRobot)
438 robotBoxes = collectRobotBoxes(*robotData);
441 for (std::size_t i = 0; i < objects.size(); ++i)
444 if (objects[i].alwaysVisible or not isRequested(objects[i].objectID))
449 std::vector<fod::Box> occluders;
450 if (properties.mode == DetectionMode::LineOfSight)
452 occluders.reserve(objects.size() + robotBoxes.size());
453 if (properties.occlusionByObjects)
455 for (std::size_t j = 0; j < objects.size(); ++j)
460 occluders.push_back(objects[j].box);
464 occluders.insert(occluders.end(), robotBoxes.begin(), robotBoxes.end());
467 if (isVisible(objects[i], *frustum, occluders))
469 providedObjects.push_back(toProvidedObjectPose(objects[i], now));
477 <<
"' in the simulated scene. Reporting no objects.";
481 << objects.size() <<
" objects.";
483 objpose::data::ProvidedObjectPoseSeq providedObjectsIce =
objpose::toIce(providedObjects);
487 const std::string robotName = robotData !=
nullptr ? robotData->name : properties.robotName;
488 for (objpose::data::ProvidedObjectPose& pose : providedObjectsIce)
490 pose.robotName = robotName;
497 catch (
const Ice::LocalException& e)
500 <<
"Could not report object poses to the object memory: " << e.what();
504 const armarx::RobotVisuData*
505 FakeObjectDetector::findRobot(
const armarx::SceneVisuData& sceneData)
const
507 if (sceneData.robots.empty())
511 if (properties.robotName.empty())
513 return &sceneData.robots.front();
516 const auto it = std::find_if(sceneData.robots.begin(),
517 sceneData.robots.end(),
518 [
this](
const armarx::RobotVisuData& robot)
519 { return robot.name == properties.robotName; });
521 return it != sceneData.robots.end() ? &(*it) :
nullptr;
524 std::vector<FakeObjectDetector::SceneObject>
525 FakeObjectDetector::collectSceneObjects(
const armarx::SceneVisuData& sceneData)
527 std::vector<SceneObject> objects;
528 objects.reserve(sceneData.objects.size());
530 for (
const armarx::ObjectVisuData& visuData : sceneData.objects)
532 if (ignoredObjects.count(visuData.name) > 0)
537 const auto poseIt = visuData.objectPoses.find(visuData.name);
538 if (poseIt == visuData.objectPoses.end())
541 <<
"' has no pose. Skipping it.";
546 object.objectID = armarx::ObjectID(visuData.name);
548 object.localOOBB = getLocalOOBB(
object.objectID);
549 object.alwaysVisible = alwaysVisibleDatasets.count(
object.objectID.dataset()) > 0;
551 const Eigen::Matrix4f identity = Eigen::Matrix4f::Identity();
552 const Eigen::Vector3f fallbackExtents =
553 Eigen::Vector3f::Constant(properties.fallbackOobbSize);
554 const simox::OrientedBoxf oobb =
555 object.localOOBB.value_or(simox::OrientedBoxf(identity, fallbackExtents));
556 object.box = toGlobalBox(oobb,
object.globalPose);
558 objects.push_back(std::move(
object));
564 std::optional<simox::OrientedBoxf>
565 FakeObjectDetector::getLocalOOBB(
const armarx::ObjectID& objectID)
570 if (
const auto it = oobbCache.find(classID); it != oobbCache.end())
575 std::optional<simox::OrientedBoxf> oobb;
576 if (
const std::optional<ObjectInfo> info = objectFinder.findObject(objectID))
578 oobb = info->loadOOBB();
581 if (not oobb and warnedMissingOOBB.insert(classID).second)
583 ARMARX_WARNING <<
"No bounding box found for object class '" << classID
584 <<
"'. Using a cube of " << properties.fallbackOobbSize
588 oobbCache[classID] = oobb;
592 std::optional<fod::Frustum>
593 FakeObjectDetector::buildFrustum(
const armarx::RobotVisuData& robot)
const
595 const auto it = robot.robotNodePoses.find(properties.cameraFrame);
596 if (it == robot.robotNodePoses.end())
598 if (not warnedMissingCameraNode)
600 std::vector<std::string> nodeNames;
601 nodeNames.reserve(robot.robotNodePoses.size());
602 for (
const auto& [name, _] : robot.robotNodePoses)
604 nodeNames.push_back(name);
607 << properties.cameraFrame <<
"' to use as camera. "
608 <<
"Available nodes are: " << nodeNames;
609 const_cast<FakeObjectDetector*
>(
this)->warnedMissingCameraNode =
true;
614 fod::Frustum frustum;
616 frustum.horizontalFov = simox::math::deg_to_rad(properties.horizontalFovDeg);
617 frustum.verticalFov = simox::math::deg_to_rad(properties.verticalFovDeg);
618 frustum.minDistance = properties.minDistance;
619 frustum.maxDistance = properties.maxDistance;
620 frustum.forwardLocal = cameraForwardLocal;
621 frustum.upLocal = cameraUpLocal;
626 FakeObjectDetector::loadRobotModel(
const armarx::RobotVisuData& robotData)
628 if (robotModel and robotModelName == robotData.name)
632 if (robotModelLoadFailed)
637 if (robotData.robotFile.empty())
640 <<
"' has no model file. It cannot occlude objects.";
641 robotModelLoadFailed =
true;
645 if (not robotData.project.empty())
647 const CMakePackageFinder finder(robotData.project);
648 if (finder.packageFound())
654 ARMARX_WARNING <<
"ArmarX package '" << robotData.project <<
"' was not found.";
658 std::string filename = robotData.robotFile;
665 VirtualRobot::RobotIO::loadRobot(filename, VirtualRobot::RobotIO::eCollisionModel);
667 catch (
const std::exception& e)
669 ARMARX_WARNING <<
"Could not load the robot model from '" << filename
670 <<
"': " << e.what() <<
". The robot will not occlude objects.";
675 robotModelLoadFailed =
true;
679 robotModelName = robotData.name;
680 ARMARX_INFO <<
"Loaded robot model '" << robotData.name <<
"' from '" << filename
681 <<
"' for self-occlusion checks.";
685 std::vector<fod::Box>
686 FakeObjectDetector::collectRobotBoxes(
const armarx::RobotVisuData& robotData)
694 std::map<std::string, float> jointValues;
695 for (
const auto& [name, value] : robotData.jointValues)
697 if (robot->hasRobotNode(name))
699 jointValues[name] =
value;
702 robot->setJointValues(jointValues);
708 std::vector<fod::Box> boxes;
709 for (
const VirtualRobot::RobotNodePtr& node : robot->getRobotNodes())
711 const VirtualRobot::CollisionModelPtr collisionModel = node->getCollisionModel();
712 if (not collisionModel)
720 const VirtualRobot::BoundingBox boundingBox = collisionModel->getBoundingBox(
false);
721 const Eigen::Vector3f
min = boundingBox.getMin();
722 const Eigen::Vector3f
max = boundingBox.getMax();
723 const Eigen::Vector3f extents =
max -
min;
724 if (extents.minCoeff() <= 0)
729 Eigen::Matrix4f localCenter = Eigen::Matrix4f::Identity();
730 localCenter.topRightCorner<3, 1>() = (
min +
max) / 2;
733 box.centerPose = collisionModel->getGlobalPose() * localCenter;
734 box.halfExtents = extents / 2;
735 boxes.push_back(box);
742 FakeObjectDetector::isVisible(
const SceneObject&
object,
744 const std::vector<fod::Box>& occluders)
const
746 if (not fod::isInFieldOfView(frustum,
object.box, properties.requireFullyInFov))
751 if (properties.mode != DetectionMode::LineOfSight)
756 const float fraction = fod::visibleFraction(
757 frustum,
object.box, occluders, properties.frontFaceSampleGrid, properties.rayEpsilon);
759 return fraction >= properties.minVisibleFraction;
763 FakeObjectDetector::toProvidedObjectPose(
const SceneObject&
object,
const DateTime& time)
const
765 objpose::ProvidedObjectPose pose;
768 pose.objectType = objpose::ObjectType::KnownObject;
771 pose.isStatic =
object.alwaysVisible;
773 pose.objectID =
object.objectID;
774 pose.objectPose =
object.globalPose;
777 pose.localOOBB =
object.localOOBB;
778 pose.confidence = properties.confidence;
780 pose.timestamp = time;
786 FakeObjectDetector::removeExpiredRequests(
const DateTime& time)
788 const std::scoped_lock lock(activeRequestsMutex);
790 std::experimental::erase_if(activeRequests,
791 [&time](
const auto& request)
793 const DateTime& until = request.second;
794 return (not until.isInvalid()) and time > until;
799 FakeObjectDetector::isRequested(
const armarx::ObjectID& objectID)
const
801 if (not properties.requestedObjectsOnly)
806 const std::scoped_lock lock(activeRequestsMutex);
807 return activeRequests.count(objectID.
str()) > 0;
#define ARMARX_REGISTER_COMPONENT_EXECUTABLE(ComponentT, applicationName)
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.
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
A fake object detector for simulation.
void onInitComponent() override
objpose::ProviderInfo getProviderInfo(const Ice::Current &) override
void onDisconnectComponent() override
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
void onConnectComponent() override
static std::string GetDefaultName()
Get the component's default name.
void onExitComponent() override
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)
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.
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.
std::string str() const
Return "dataset/className" or "dataset/className/instanceName".
objpose::ObjectPoseStorageInterfacePrx objectPoseTopic
bool isStopped()
Retrieve whether stop() has been called.
Represents a point in time.
static Duration MilliSeconds(std::int64_t milliSeconds)
Constructs a duration in milliseconds.
Simple rate limiter for use in loops to maintain a certain frequency given a clock.
An object pose provided by an ObjectPoseProvider.
std::string providerName
Name of the providing component.
#define ARMARX_INFO
The normal logging level.
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
std::string const GlobalFrame
Variable of the global coordinate system.
std::shared_ptr< class Robot > RobotPtr
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)
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()
An oriented bounding box in the global frame.
Eigen::Matrix4f centerPose
Rotation = box axes, translation = box centre.
A symmetric view frustum.