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.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.");
239 if (
const std::optional<DetectionMode> mode = parseDetectionMode(properties.detectionMode))
241 properties.mode = *mode;
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;
251 if (
const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraForwardAxis))
253 cameraForwardLocal = *axis;
258 << properties.cameraForwardAxis <<
"'. Falling back to '+Z'.";
259 cameraForwardLocal = Eigen::Vector3f::UnitZ();
262 if (
const std::optional<Eigen::Vector3f> axis = parseAxis(properties.cameraUpAxis))
264 cameraUpLocal = *axis;
268 ARMARX_WARNING <<
"Could not parse camera up axis '" << properties.cameraUpAxis
269 <<
"'. Falling back to '-Y'.";
270 cameraUpLocal = -Eigen::Vector3f::UnitY();
273 if (std::abs(cameraForwardLocal.dot(cameraUpLocal)) > 1e-3F)
275 ARMARX_WARNING <<
"Camera forward axis '" << properties.cameraForwardAxis
276 <<
"' and up axis '" << properties.cameraUpAxis
277 <<
"' are not perpendicular. The frustum will be skewed.";
280 if (properties.frontFaceSampleGrid < 1)
282 ARMARX_WARNING <<
"p.frontFaceSampleGrid must be at least 1, but is "
283 << properties.frontFaceSampleGrid <<
". Using 1.";
284 properties.frontFaceSampleGrid = 1;
287 for (
const std::string&
id : simox::alg::split(properties.ignoredObjects,
","))
289 const std::string trimmed = simox::alg::trim_copy(
id);
290 if (not trimmed.empty())
292 ignoredObjects.insert(trimmed);
295 if (not ignoredObjects.empty())
298 << simox::alg::join(std::vector<std::string>(ignoredObjects.begin(),
299 ignoredObjects.end()),
303 for (
const std::string& dataset : simox::alg::split(properties.alwaysVisibleDatasets,
","))
305 const std::string trimmed = simox::alg::trim_copy(dataset);
306 if (not trimmed.empty())
308 alwaysVisibleDatasets.insert(trimmed);
311 for (
const std::string&
id : simox::alg::split(properties.alwaysVisibleObjects,
","))
313 const std::string trimmed = simox::alg::trim_copy(
id);
314 if (not trimmed.empty())
316 alwaysVisibleObjects.insert(trimmed);
320 for (
const std::string&
id : simox::alg::split(properties.externallyOwnedObjects,
","))
322 const std::string trimmed = simox::alg::trim_copy(
id);
323 if (not trimmed.empty())
325 externallyOwnedObjects.insert(trimmed);
328 if (not externallyOwnedObjects.empty())
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()),
337 if (not alwaysVisibleObjects.empty())
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()),
345 if (not alwaysVisibleDatasets.empty())
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()),
358 detectionTask->start();
366 detectionTask->stop();
367 detectionTask =
nullptr;
379 return FakeObjectDetector::defaultName;
385 return FakeObjectDetector::defaultName;
388 objpose::provider::RequestObjectsOutput
390 const Ice::Current& )
392 objpose::provider::RequestObjectsOutput output;
395 const std::scoped_lock lock(activeRequestsMutex);
397 for (
const auto&
id : input.objectIDs)
399 const std::string entityID =
id.dataset +
"/" +
id.className +
"/" +
id.instanceName;
401 activeRequests[entityID] =
406 output.results[id].success =
true;
409 if (not properties.requestedObjectsOnly and not input.objectIDs.empty())
412 <<
"All visible objects are reported by default. Requesting an object "
413 "has no effect unless 'p.requestedObjectsOnly' is enabled.";
419 objpose::ProviderInfo
422 objpose::ProviderInfo info;
423 info.objectType = objpose::KnownObject;
425 info.supportedObjects = {};
430 FakeObjectDetector::detectionTaskRun()
434 while (detectionTask and not detectionTask->
isStopped())
436 metronome.waitForNextTick();
442 FakeObjectDetector::detectAndReport()
444 armarx::SceneVisuData sceneData;
447 sceneData = simulatorPrx->getScene();
449 catch (
const Ice::LocalException& e)
452 <<
"Could not get the scene from the simulator: " << e.what();
457 removeExpiredRequests(now);
459 const std::vector<SceneObject> objects = collectSceneObjects(sceneData);
460 std::vector<objpose::ProvidedObjectPose> providedObjects;
462 const armarx::RobotVisuData* robotData = findRobot(sceneData);
477 const bool staticSceneDue =
478 properties.staticSceneRepublishSeconds <= 0 or not lastStaticSceneReport.isValid() or
479 (now - lastStaticSceneReport).toSecondsDouble() >=
480 properties.staticSceneRepublishSeconds;
482 for (
const SceneObject&
object : objects)
484 if (not(
object.alwaysVisible and isRequested(
object.objectID)))
489 if (externallyOwnedObjects.count(
object.objectID.str()) > 0)
494 const std::string
id =
object.objectID.str();
495 const auto it = lastReportedStaticPose.find(
id);
497 it == lastReportedStaticPose.end() or not it->second.isApprox(
object.globalPose);
499 if (staticSceneDue or moved)
501 lastReportedStaticPose[id] =
object.globalPose;
502 providedObjects.push_back(toProvidedObjectPose(
object, now));
508 lastStaticSceneReport = now;
511 if (properties.mode == DetectionMode::Always)
513 for (
const SceneObject&
object : objects)
515 if (not
object.alwaysVisible and isRequested(
object.objectID) and
516 externallyOwnedObjects.count(
object.objectID.str()) == 0)
518 providedObjects.push_back(toProvidedObjectPose(
object, now));
522 else if (robotData !=
nullptr)
524 if (
const std::optional<fod::Frustum> frustum = buildFrustum(*robotData))
526 std::vector<fod::Box> robotBoxes;
527 if (properties.mode == DetectionMode::LineOfSight and properties.occlusionByRobot)
529 robotBoxes = collectRobotBoxes(*robotData);
532 for (std::size_t i = 0; i < objects.size(); ++i)
535 if (objects[i].alwaysVisible or not isRequested(objects[i].objectID) or
536 externallyOwnedObjects.count(objects[i].objectID.str()) > 0)
541 std::vector<fod::Box> occluders;
542 if (properties.mode == DetectionMode::LineOfSight)
544 occluders.reserve(objects.size() + robotBoxes.size());
545 if (properties.occlusionByObjects)
547 for (std::size_t j = 0; j < objects.size(); ++j)
552 occluders.push_back(objects[j].box);
556 occluders.insert(occluders.end(), robotBoxes.begin(), robotBoxes.end());
559 if (isVisible(objects[i], *frustum, occluders))
561 providedObjects.push_back(toProvidedObjectPose(objects[i], now));
569 <<
"' in the simulated scene. Reporting no objects.";
573 << objects.size() <<
" objects.";
575 objpose::data::ProvidedObjectPoseSeq providedObjectsIce =
objpose::toIce(providedObjects);
579 const std::string robotName = robotData !=
nullptr ? robotData->name : properties.robotName;
580 for (objpose::data::ProvidedObjectPose& pose : providedObjectsIce)
582 pose.robotName = robotName;
589 catch (
const Ice::LocalException& e)
592 <<
"Could not report object poses to the object memory: " << e.what();
596 const armarx::RobotVisuData*
597 FakeObjectDetector::findRobot(
const armarx::SceneVisuData& sceneData)
const
599 if (sceneData.robots.empty())
603 if (properties.robotName.empty())
605 return &sceneData.robots.front();
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; });
613 return it != sceneData.robots.end() ? &(*it) :
nullptr;
616 std::vector<FakeObjectDetector::SceneObject>
617 FakeObjectDetector::collectSceneObjects(
const armarx::SceneVisuData& sceneData)
619 std::vector<SceneObject> objects;
620 objects.reserve(sceneData.objects.size());
622 for (
const armarx::ObjectVisuData& visuData : sceneData.objects)
624 if (ignoredObjects.count(visuData.name) > 0)
629 const auto poseIt = visuData.objectPoses.find(visuData.name);
630 if (poseIt == visuData.objectPoses.end())
633 <<
"' has no pose. Skipping it.";
638 object.objectID = armarx::ObjectID(visuData.name);
640 object.localOOBB = getLocalOOBB(
object.objectID);
641 object.alwaysVisible = alwaysVisibleDatasets.count(
object.objectID.dataset()) > 0 or
642 alwaysVisibleObjects.count(
object.objectID.str()) > 0;
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);
651 objects.push_back(std::move(
object));
657 std::optional<simox::OrientedBoxf>
658 FakeObjectDetector::getLocalOOBB(
const armarx::ObjectID& objectID)
663 if (
const auto it = oobbCache.find(classID); it != oobbCache.end())
668 std::optional<simox::OrientedBoxf> oobb;
669 if (
const std::optional<ObjectInfo> info = objectFinder.findObject(objectID))
671 oobb = info->loadOOBB();
674 if (not oobb and warnedMissingOOBB.insert(classID).second)
676 ARMARX_WARNING <<
"No bounding box found for object class '" << classID
677 <<
"'. Using a cube of " << properties.fallbackOobbSize
681 oobbCache[classID] = oobb;
685 std::optional<fod::Frustum>
686 FakeObjectDetector::buildFrustum(
const armarx::RobotVisuData& robot)
const
688 const auto it = robot.robotNodePoses.find(properties.cameraFrame);
689 if (it == robot.robotNodePoses.end())
691 if (not warnedMissingCameraNode)
693 std::vector<std::string> nodeNames;
694 nodeNames.reserve(robot.robotNodePoses.size());
695 for (
const auto& [name, _] : robot.robotNodePoses)
697 nodeNames.push_back(name);
700 << properties.cameraFrame <<
"' to use as camera. "
701 <<
"Available nodes are: " << nodeNames;
702 const_cast<FakeObjectDetector*
>(
this)->warnedMissingCameraNode =
true;
707 fod::Frustum frustum;
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;
719 FakeObjectDetector::loadRobotModel(
const armarx::RobotVisuData& robotData)
721 if (robotModel and robotModelName == robotData.name)
725 if (robotModelLoadFailed)
730 if (robotData.robotFile.empty())
733 <<
"' has no model file. It cannot occlude objects.";
734 robotModelLoadFailed =
true;
738 if (not robotData.project.empty())
740 const CMakePackageFinder finder(robotData.project);
741 if (finder.packageFound())
747 ARMARX_WARNING <<
"ArmarX package '" << robotData.project <<
"' was not found.";
751 std::string filename = robotData.robotFile;
758 VirtualRobot::RobotIO::loadRobot(filename, VirtualRobot::RobotIO::eCollisionModel);
760 catch (
const std::exception& e)
762 ARMARX_WARNING <<
"Could not load the robot model from '" << filename
763 <<
"': " << e.what() <<
". The robot will not occlude objects.";
768 robotModelLoadFailed =
true;
772 robotModelName = robotData.name;
773 ARMARX_INFO <<
"Loaded robot model '" << robotData.name <<
"' from '" << filename
774 <<
"' for self-occlusion checks.";
778 std::vector<fod::Box>
779 FakeObjectDetector::collectRobotBoxes(
const armarx::RobotVisuData& robotData)
787 std::map<std::string, float> jointValues;
788 for (
const auto& [name, value] : robotData.jointValues)
790 if (robot->hasRobotNode(name))
792 jointValues[name] =
value;
795 robot->setJointValues(jointValues);
801 std::vector<fod::Box> boxes;
802 for (
const VirtualRobot::RobotNodePtr& node : robot->getRobotNodes())
804 const VirtualRobot::CollisionModelPtr collisionModel = node->getCollisionModel();
805 if (not collisionModel)
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)
822 Eigen::Matrix4f localCenter = Eigen::Matrix4f::Identity();
823 localCenter.topRightCorner<3, 1>() = (
min +
max) / 2;
826 box.centerPose = collisionModel->getGlobalPose() * localCenter;
827 box.halfExtents = extents / 2;
828 boxes.push_back(box);
835 FakeObjectDetector::isVisible(
const SceneObject&
object,
837 const std::vector<fod::Box>& occluders)
const
839 if (not fod::isInFieldOfView(frustum,
object.box, properties.requireFullyInFov))
844 if (properties.mode != DetectionMode::LineOfSight)
849 const float fraction = fod::visibleFraction(
850 frustum,
object.box, occluders, properties.frontFaceSampleGrid, properties.rayEpsilon);
852 return fraction >= properties.minVisibleFraction;
856 FakeObjectDetector::toProvidedObjectPose(
const SceneObject&
object,
const DateTime& time)
const
858 objpose::ProvidedObjectPose pose;
861 pose.objectType = objpose::ObjectType::KnownObject;
864 pose.isStatic =
object.alwaysVisible;
866 pose.objectID =
object.objectID;
867 pose.objectPose =
object.globalPose;
870 pose.localOOBB =
object.localOOBB;
871 pose.confidence = properties.confidence;
873 pose.timestamp = time;
879 FakeObjectDetector::removeExpiredRequests(
const DateTime& time)
881 const std::scoped_lock lock(activeRequestsMutex);
883 std::experimental::erase_if(activeRequests,
884 [&time](
const auto& request)
886 const DateTime& until = request.second;
887 return (not until.isInvalid()) and time > until;
892 FakeObjectDetector::isRequested(
const armarx::ObjectID& objectID)
const
894 if (not properties.requestedObjectsOnly)
899 const std::scoped_lock lock(activeRequestsMutex);
900 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.