LaserBasedProximity.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstddef>
6#include <cstdlib>
7#include <functional>
8#include <limits>
9#include <optional>
10#include <string>
11#include <utility>
12#include <vector>
13
14#include <boost/geometry.hpp>
15#include <boost/geometry/algorithms/append.hpp>
16#include <boost/geometry/geometries/multi_point.hpp>
17
18#include <Eigen/Core>
19#include <Eigen/Geometry>
20
21#include <range/v3/algorithm/all_of.hpp>
22#include <range/v3/algorithm/min.hpp>
23#include <range/v3/all.hpp>
24#include <range/v3/numeric/accumulate.hpp>
25#include <range/v3/range/conversion.hpp>
26#include <range/v3/view/drop.hpp>
27#include <range/v3/view/enumerate.hpp>
28#include <range/v3/view/filter.hpp>
29#include <range/v3/view/reverse.hpp>
30#include <range/v3/view/transform.hpp>
31#include <range/v3/view/zip_with.hpp>
32
33#include <SimoxUtility/color/Color.h>
34#include <SimoxUtility/color/cmaps/colormaps.h>
35#include <SimoxUtility/shapes/OrientedBox.h>
36#include <VirtualRobot/Nodes/RobotNode.h>
37#include <VirtualRobot/Robot.h>
38
42
49
57#include <armarx/navigation/safety_guard/aron/LaserBasedProximityParams.aron.generated.h>
61
62namespace rv = ::ranges::views;
63
65{
66
67 namespace
68 {
69
70 inline core::TwistLimits
71 mergeSafetyLimits(const std::vector<std::optional<core::TwistLimits>>& resultsAll)
72 {
73 const auto isValidFn =
74 [](const std::optional<core::TwistLimits>& result) noexcept -> bool
75 { return result.has_value(); };
76
77 const std::vector<core::TwistLimits> validResults =
78 resultsAll | rv::filter(isValidFn) |
79 rv::transform(
80 [](const std::optional<core::TwistLimits>& result) noexcept -> core::TwistLimits
81 { return result.value(); }) |
82 ranges::to_vector;
83
84
85 if (validResults.empty())
86 {
88 }
89
90 const std::vector<float> linearLimits =
91 validResults |
92 rv::transform([](const core::TwistLimits& result) noexcept -> float
93 { return result.linear; }) |
94 ranges::to_vector;
95
96 const std::vector<float> angularLimits =
97 validResults |
98 rv::transform([](const core::TwistLimits& result) noexcept -> float
99 { return result.angular; }) |
100 ranges::to_vector;
101
102 const core::TwistLimits combinedResult{.linear = ranges::min(linearLimits),
103 .angular = ranges::min(angularLimits)};
104
105 return combinedResult;
106 }
107
108 simox::color::Color
109 filterReasonColor(const FilterReason reason)
110 {
111 switch (reason)
112 {
114 return simox::Color::white();
116 return simox::Color::gray();
118 return simox::Color::magenta();
120 return simox::Color::cyan();
122 return simox::Color::yellow();
124 return simox::Color::blue();
126 return simox::Color::green();
128 return simox::Color::orange();
129 }
130
131 return simox::Color::black();
132 }
133
134 std::string
135 filterReasonName(const FilterReason reason)
136 {
137 switch (reason)
138 {
140 return "None";
142 return "NoPoints";
144 return "IgnoredRegion";
146 return "AttachedObject";
148 return "NearFieldArtifact";
150 return "TooFarAway";
152 return "MovingAway";
154 return "Tangential";
155 }
156
157 return "Unknown";
158 }
159
160 } // namespace
161
167
170 {
171 arondto::LaserBasedProximityParams dto;
172
174
175 return dto.toAron();
176 }
177
180 {
181 arondto::LaserBasedProximityParams dto;
182 dto.fromAron(dict);
183
185 fromAron(dto, bo);
186
187 return bo;
188 }
189
191 const core::GeneralConfig& generalConfig,
192 const core::Scene& scene,
193 const Context& ctx) :
194 SafetyGuard(scene, ctx),
195 params(params),
196 generalConfig(generalConfig),
197 humanColorMap_(simox::color::cmaps::BuPu().reversed()),
198 laserColorMap_(simox::color::cmaps::OrRd().reversed())
199 {
200 }
201
203 LaserBasedProximity::computeSafetyLimits(const Eigen::Vector2f& global_V_movement)
204 {
205 ARMARX_CHECK(scene.dynamicScene.has_value());
206
207 auto layer = viz.layer("safety_guard");
208
209 // Debug only: hide these layers in ArViz when they are not needed.
210 auto clusterLayer = viz.layer("safety_guard_clusters");
211 auto filteredLayer = viz.layer("safety_guard_filtered");
212
213 const std::optional<core::TwistLimits> resultHumans = safetyLimitsHumans(layer);
214 const std::optional<core::TwistLimits> resultLaserScanners =
215 safetyLimitsLaserScanners(layer, clusterLayer, filteredLayer, global_V_movement);
216
217 // Commit always. This ensures that objects eventually will be cleared.
218 viz.commit(std::vector<viz::Layer>{layer, clusterLayer, filteredLayer});
219
220 const core::TwistLimits combinedResult =
221 mergeSafetyLimits({resultHumans, resultLaserScanners});
222 ARMARX_VERBOSE << "Safety limits: " << VAROUT(combinedResult.linear)
223 << VAROUT(combinedResult.angular);
224
225 return SafetyGuardResult{.twistLimits = combinedResult};
226 }
227
228 std::optional<core::TwistLimits>
229 LaserBasedProximity::safetyLimitsLaserScanners(viz::Layer& layer,
230 viz::Layer& clusterLayer,
231 viz::Layer& filteredLayer,
232 const Eigen::Vector2f& global_V_movement) const
233 {
235 {
236 return std::nullopt;
237 }
238
239 if (scene.dynamicScene->laserScannerFeatures.empty())
240 {
241 ARMARX_INFO << deactivateSpam(5) << "No laser scanner features for SafetyGuard";
242
243 return std::nullopt;
244 }
245
247 auto& debugObserver = *context.debugObserver;
248
249 debugObserver.setDebugObserverDatafield("numLaserScannerFeatures",
250 scene.dynamicScene->laserScannerFeatures.size());
251 debugObserver.setDebugObserverDatafield(
252 "numFirstLaserScannerFeatures",
253 scene.dynamicScene->laserScannerFeatures.front().features.size());
254
255 // Diagnostics: the size of the smallest cluster of the *current* frame, before any
256 // filtering. Note that `closestClusterSize` (see velocityLimitsDirectionDependent) refers
257 // to the velocity-constraining obstacle and is computed over the accumulated history of
258 // several frames, so the two are not comparable.
259 {
260 std::size_t smallestClusterSize = std::numeric_limits<std::size_t>::max();
261
262 // Only `points` is evaluated here; a feature that carries a convex hull but no points
263 // is invisible to this safety guard while still being drawn by the memory visu.
264 std::size_t numFeaturesWithoutPoints = 0;
265
266 for (const auto& features : scene.dynamicScene->laserScannerFeatures)
267 {
268 for (const auto& feature : features.features)
269 {
270 smallestClusterSize = std::min(smallestClusterSize, feature.points.size());
271
272 if (feature.points.empty())
273 {
274 numFeaturesWithoutPoints++;
275 }
276 }
277 }
278
279 if (smallestClusterSize != std::numeric_limits<std::size_t>::max())
280 {
281 debugObserver.setDebugObserverDatafield("smallestClusterSize", smallestClusterSize);
282 }
283
284 debugObserver.setDebugObserverDatafield("numFeaturesWithoutPoints",
285 numFeaturesWithoutPoints);
286 }
287
288 ARMARX_VERBOSE << VAROUT(scene.dynamicScene->laserScannerFeatures.size());
289 ARMARX_VERBOSE << VAROUT(scene.dynamicScene->laserScannerFeatures.front().features.size());
290
291 const core::Pose global_T_robot{scene.robot->getGlobalPose()};
292
293 ARMARX_VERBOSE << VAROUT(global_T_robot.translation());
294
295 const core::Pose robot_T_global{global_T_robot.inverse()};
296
297 // compute distance based on convex hull of robot
298 util::geometry::polygon_type robotConvexHull;
299 {
300 boost::geometry::model::multi_point<util::geometry::point_type> robotNodes;
301 for (const auto& node : scene.robot->getRobotNodes())
302 {
303 Eigen::Vector2f pos2d = conv::to2D(core::Pose(node->getGlobalPose())).translation();
304 boost::geometry::append(robotNodes,
305 util::geometry::point_type(pos2d.x(), pos2d.y()));
306 }
307 boost::geometry::convex_hull(robotNodes, robotConvexHull);
308 }
309
310 // Compute the minimal distance to the robot for a set of features (point clusters)
311 const auto minDistanceFn =
312 [this, &robotConvexHull, &robot_T_global](const memory::LaserScannerFeatures& features)
313 -> std::vector<DistanceAndClosestPoint>
314 {
315 const core::Pose& global_T_sensor = features.frameGlobalPose;
316
317 // Compute the minimal distance to the robot for a single feature (point cluster)
318 const auto minDistanceFnInner =
319 [this, &robotConvexHull, &global_T_sensor, &robot_T_global](
320 const memory::LaserScannerFeature& feature) -> DistanceAndClosestPoint
321 {
322 // transform points to 3D global frame
323 const std::vector<Eigen::Vector3f> points3dGlobal =
324 feature.points |
325 rv::transform([&global_T_sensor](const Eigen::Vector2f& pt) -> Eigen::Vector3f
326 { return global_T_sensor * conv::to3D(pt); }) |
327 ranges::to_vector;
328
329 return calculateMinDistance(robotConvexHull, robot_T_global, points3dGlobal);
330 };
331
332 // Compute the minimal distance to the robot for all features (point clusters)
333 ARMARX_VERBOSE << VAROUT(features.features.size());
334 const std::vector<DistanceAndClosestPoint> distances =
335 features.features | rv::transform(minDistanceFnInner) | ranges::to_vector;
336
337 return distances;
338 };
339
340 laserScannerFeatureHistory_.push_back(scene.dynamicScene->laserScannerFeatures);
341
342 const std::vector<memory::LaserScannerFeatures> accumulatedLaserScannerFeatures =
343 laserScannerFeatureHistory_ | ranges::views::join |
344 ranges::to<std::vector<memory::LaserScannerFeatures>>;
345
346 // not const: velocityLimitsDirectionDependent() annotates the filter reasons
347 std::vector<DistanceAndClosestPoint> minDistanceToObstacles =
348 accumulatedLaserScannerFeatures | rv::transform(minDistanceFn) | ranges::views::join |
349 ranges::to_vector;
350
351 const auto result = [&]() -> std::optional<InternalVelocityLimitResult>
352 {
353 switch (params.laserScannerProximityField.mode)
354 {
356 return std::nullopt;
358 return velocityLimitsDirectionDependent(
359 global_V_movement, minDistanceToObstacles, global_T_robot);
361 return velocityLimitsDirectionIndependent(minDistanceToObstacles);
362 }
363
364 ARMARX_ERROR << "Unknown ProximityFieldParams::Mode: "
365 << static_cast<int>(params.laserScannerProximityField.mode);
366 return std::nullopt;
367 }();
368
369 layer.clear();
370
371 // visualize robot and obstacle-independent information (always)
372 {
373 // visualize convex hull of robot
374 simox::color::Color color = simox::Color::blue();
375 viz::Polygon vizPoly("robot_convex_hull");
376 for (const auto& p : rv::reverse(robotConvexHull.outer()))
377 {
378 Eigen::Vector2f pt(p.x(), p.y());
379 vizPoly.addPoint(conv::to3D(pt));
380 }
381 layer.add(vizPoly.color(color).lineColor(color));
382
383 // visualize movement direction
384 layer.add(viz::Line("robot_movement_Dir")
385 .fromTo(global_T_robot.translation(),
386 global_T_robot.translation() +
387 conv::to3D(global_V_movement).normalized() * 1000)
388 .lineWidth(10.F)
389 .color(simox::Color::blue()));
390
391 // visualize ignored regions
392 for (const auto& [i, region] : ranges::views::enumerate(params.ignoredRegions))
393 {
394 viz::Polygon vizRegion("ignored_region_" + std::to_string(i));
395 vizRegion.addPoint(Eigen::Vector3f(region.min().x(), region.min().y(), 15));
396 vizRegion.addPoint(Eigen::Vector3f(region.max().x(), region.min().y(), 15));
397 vizRegion.addPoint(Eigen::Vector3f(region.max().x(), region.max().y(), 15));
398 vizRegion.addPoint(Eigen::Vector3f(region.min().x(), region.max().y(), 15));
399 layer.add(vizRegion.color(simox::Color::magenta()));
400 }
401
402 // visualize attached objects
403 for (const auto& [i, obj] :
404 ranges::views::enumerate(scene.dynamicScene->attachedObjects))
405 {
406 const auto& oobb = obj->oobbGlobal();
407 ARMARX_CHECK(oobb.has_value());
408
409 simox::OrientedBoxf inflatedOobb{
410 oobb->transformation_centered(),
411 oobb->dimensions() + Eigen::Vector3f::Ones() * params.attachedObjectsInflation};
412
413 viz::Box vizObj("attached_object_" + std::to_string(i));
414 vizObj.set(inflatedOobb);
415 layer.add(vizObj.color(simox::Color::magenta()));
416 }
417 }
418
419 // Debug only: visualize every feature of the *current* frame and, on a separate layer,
420 // why it was filtered out. Note that the velocity limits are computed over the
421 // accumulated feature history, so these counts are lower than the `numObstacles*`
422 // datafields. In `DirectionIndependent` mode only the reasons determined by
423 // calculateMinDistance() are available.
424 {
425 const std::size_t numCurrentFrameFeatures = ranges::accumulate(
426 scene.dynamicScene->laserScannerFeatures |
427 rv::transform([](const memory::LaserScannerFeatures& features) noexcept -> std::size_t
428 { return features.features.size(); }),
429 std::size_t{0});
430
431 ARMARX_CHECK_GREATER_EQUAL(minDistanceToObstacles.size(), numCurrentFrameFeatures);
432
433 // the current frame was appended last, see laserScannerFeatureHistory_
434 const auto currentFrameFeatures =
435 minDistanceToObstacles |
436 rv::drop(minDistanceToObstacles.size() - numCurrentFrameFeatures);
437
438 const float sphereRadius = 50.F;
439 const float textScale = 3.F;
440 const Eigen::Vector3f sphereOffset{0, 0, 50};
441 const Eigen::Vector3f sizeTextOffset{0, 0, 200};
442 const Eigen::Vector3f reasonTextOffset{0, 0, 350};
443
444 for (const auto& [i, obstacle] : ranges::views::enumerate(currentFrameFeatures))
445 {
446 if (obstacle.filterReason == FilterReason::NoPoints)
447 {
448 // without points there is no position to draw
449 continue;
450 }
451
452 const std::string suffix = std::to_string(i);
453 const Eigen::Vector3f position = conv::to3D(obstacle.centroid);
454
455 clusterLayer.add(viz::Sphere("cluster_" + suffix)
456 .position(position + sphereOffset)
457 .radius(sphereRadius)
458 .color(simox::Color::white()));
459 clusterLayer.add(viz::Text("cluster_size_" + suffix)
460 .position(position + sizeTextOffset)
461 .text(std::to_string(obstacle.clusterSize))
462 .scale(textScale)
463 .color(simox::Color::white()));
464
465 if (obstacle.filterReason == FilterReason::None)
466 {
467 continue;
468 }
469
470 const simox::color::Color color = filterReasonColor(obstacle.filterReason);
471
472 filteredLayer.add(viz::Sphere("filtered_" + suffix)
473 .position(position + sphereOffset)
474 .radius(sphereRadius)
475 .color(color));
476 filteredLayer.add(viz::Text("filtered_reason_" + suffix)
477 .position(position + reasonTextOffset)
478 .text(filterReasonName(obstacle.filterReason))
479 .scale(textScale)
480 .color(color));
481 }
482 }
483
484 if (not result.has_value())
485 {
486 return std::nullopt;
487 }
488
489 // visualize the obstacle that leads to the calculated safety limits and the corresponding velocity limits
490 if (result->closestPoint.has_value())
491 {
492 // increase transparency with increased distance, but never make fully transparent
493 const auto color = laserColorMap_.at(
494 std::abs(result->twistLimits.linear), 0, generalConfig.maxVel.linear);
495
496 layer.add(
497 viz::Line("nearest_obstacle")
498 .fromTo(global_T_robot.translation(), conv::to3D(result->closestPoint.value()))
499 .lineWidth(50.F)
500 .color(color));
501 }
502
503 return result->twistLimits;
504 }
505
506 std::optional<core::TwistLimits>
507 LaserBasedProximity::safetyLimitsHumans(viz::Layer& layer) const
508 {
509 if (not params.enableHumans)
510 {
511 return std::nullopt;
512 }
513
514 const core::Pose global_T_robot{scene.robot->getGlobalPose()};
515
516 // Compute the minimal distance to the robot for a human
517 // TODO(utetg): is minimum distance = distance to robot center or robot edge
518 const auto minimalSegmentDistanceToRobot =
519 [&global_T_robot](const human::Human& human) -> float
520 {
521 const Eigen::Isometry3f global_T_human = conv::to3D(human.pose);
522 const Eigen::Isometry3f robot_T_human = global_T_robot.inverse() * global_T_human;
523
524 // TODO extension: if also human keypoints are available, use them to compute the minimal distance
525 return robot_T_human.translation().norm();
526 };
527
528 if (scene.dynamicScene->humans.empty())
529 {
530 ARMARX_VERBOSE << "No humans";
531 return std::nullopt;
532 }
533
534 const auto distanceRobotToHumans = scene.dynamicScene->humans |
535 rv::transform(minimalSegmentDistanceToRobot) |
536 ranges::to_vector;
537
538 const std::optional<float> minDistanceToHumans = scene.dynamicScene->humans.empty()
539 ? std::optional<float>{std::nullopt}
540 : ranges::min(distanceRobotToHumans);
541
542 // handle if no humans are detected
543 if (not minDistanceToHumans.has_value())
544 {
546 }
547
548 const auto result =
549 evaluateProximityField(params.humanProximityField, minDistanceToHumans.value());
550
551 {
552 // increase transparency with increased distance, but never make fully transparent
553 const auto color = humanColorMap_(result.second).with_alpha(1 - result.second + 0.1);
554
555 layer.add(viz::Cylinder("distance_humans")
556 .position(global_T_robot.translation() + Eigen::Vector3f{0, 0, 10})
557 .direction(Eigen::Vector3f::UnitZ())
558 .radius(minDistanceToHumans.value())
559 .height(10)
560 .color(color));
561 }
562
563 return result.first;
564 }
565
566 LaserBasedProximity::DistanceAndClosestPoint
567 LaserBasedProximity::calculateMinDistance(
568 const util::geometry::polygon_type& convexHull,
569 const core::Pose& robot_T_global,
570 const std::vector<Eigen::Vector3f>& globalPoints) const
571 {
572 // Compute the minimal distance to the robot for a list of points
573
574 // transform points to 2D global frame
575 const std::vector<Eigen::Vector2f> points2dGlobal =
576 globalPoints |
577 rv::transform([](const Eigen::Vector3f& pt) -> Eigen::Vector2f
578 { return conv::to2D(pt); }) |
579 ranges::to_vector;
580
581 // a feature that is ignored must never constrain the velocity
582 const auto ignoredResult = [&globalPoints](const Eigen::Vector2f& centroid,
583 const FilterReason filterReason)
584 -> DistanceAndClosestPoint
585 {
586 return {.distance = std::numeric_limits<float>::max(),
587 .closestPoint = Eigen::Vector2f::Zero(),
588 .clusterSize = globalPoints.size(),
589 .centroid = centroid,
590 .filterReason = filterReason};
591 };
592
593 if (globalPoints.empty())
594 {
595 ARMARX_VERBOSE << "Discarding feature: it has no points";
596 return ignoredResult(Eigen::Vector2f::Zero(), FilterReason::NoPoints);
597 }
598
599 const Eigen::Vector2f centroid =
600 ranges::accumulate(points2dGlobal,
601 Eigen::Vector2f{Eigen::Vector2f::Zero()},
602 [](const Eigen::Vector2f& lhs, const Eigen::Vector2f& rhs)
603 -> Eigen::Vector2f { return lhs + rhs; }) /
604 static_cast<float>(points2dGlobal.size());
605
606 if (const FilterReason ignoreReason =
607 featureIgnoreReason(globalPoints, points2dGlobal, centroid);
608 ignoreReason != FilterReason::None)
609 {
610 // featureIgnoreReason() logs the reason
611 return ignoredResult(centroid, ignoreReason);
612 }
613
614 // calculate distance to robot center
615 const std::vector<float> pointsDistanceRobotCenter =
616 globalPoints |
617 rv::transform([&robot_T_global](const Eigen::Vector3f& pt) -> float
618 { return conv::to2D(robot_T_global * pt).norm(); }) |
619 ranges::to_vector;
620
621 // Small features *close to the robot* are most likely measuring artifacts of the sensors.
622 // This filter is deliberately restricted to the near field: further away, a cluster with
623 // few points is a real (small or distant) obstacle and must not be discarded.
624 if (params.enableLaserScannerFiltering and
625 globalPoints.size() <= static_cast<std::size_t>(params.laserScannerFilteringThreshold))
626 {
627 const auto isCloseToRobot = [this](const float distanceToCenter) noexcept -> bool
628 {
629 return std::max(distanceToCenter - params.robotRadius, 0.F) <
630 params.laserScannerMaxFilteringDistance;
631 };
632
633 // distance of the point closest to the robot surface, for logging
634 const float minSurfaceDistance =
635 std::max(ranges::min(pointsDistanceRobotCenter) - params.robotRadius, 0.F);
636
637 if (ranges::all_of(pointsDistanceRobotCenter, isCloseToRobot))
638 {
639 // the feature is close to the robot
640 // -> it should be filtered out
641 ARMARX_VERBOSE << "Ignoring small feature with " << globalPoints.size()
642 << " points (<= laserScannerFilteringThreshold "
643 << params.laserScannerFilteringThreshold
644 << "): all points are within laserScannerMaxFilteringDistance "
645 << params.laserScannerMaxFilteringDistance
646 << " mm of the robot surface (closest point at "
647 << minSurfaceDistance
648 << " mm). Distances to robot center: " << pointsDistanceRobotCenter;
649 return ignoredResult(centroid, FilterReason::NearFieldArtifact);
650 }
651
652 ARMARX_VERBOSE << "Keeping small feature with " << globalPoints.size()
653 << " points: not all points are within "
654 << params.laserScannerMaxFilteringDistance
655 << " mm of the robot surface (closest point at " << minSurfaceDistance
656 << " mm)";
657 }
658
659 const auto distancesAndClosestPoint =
660 rv::zip_with(
661 [this, &convexHull, &globalPoints, &centroid](
662 const Eigen::Vector2f& pt, float distanceToCenter) -> DistanceAndClosestPoint
663 {
664 const auto distanceToRobotConvexHull =
665 static_cast<float>(boost::geometry::distance(
666 util::geometry::point_type(pt.x(), pt.y()), convexHull));
667
668 const float distanceUsingRobotRadius =
669 std::max(distanceToCenter - params.robotRadius, 0.F);
670
671 return {.distance =
672 std::min(distanceToRobotConvexHull, distanceUsingRobotRadius),
673 .closestPoint = pt,
674 .clusterSize = globalPoints.size(),
675 .centroid = centroid,
676 .filterReason = FilterReason::None};
677 },
678 points2dGlobal,
679 pointsDistanceRobotCenter) |
680 ranges::to_vector;
681
682 return ranges::min(
683 distancesAndClosestPoint, std::less{}, &DistanceAndClosestPoint::distance);
684 }
685
686 std::pair<core::TwistLimits, float>
687 LaserBasedProximity::evaluateProximityField(const ProximityFieldParams& proximityField,
688 float minDistance) const
689 {
690 if (minDistance < proximityField.safetyDistance)
691 {
692 return {core::TwistLimits::ZeroLimits(), 0.F};
693 }
694
695 if (minDistance > proximityField.influenceDistance)
696 {
697 return {core::TwistLimits::NoLimits(), 1.F};
698 }
699
700
701 const float proximityRange =
702 proximityField.influenceDistance - proximityField.safetyDistance;
703 const float clippedDistance =
704 std::min(minDistance, proximityField.influenceDistance) - proximityField.safetyDistance;
705
706 const float fractionalDistance = std::clamp(clippedDistance / proximityRange, 0.F, 1.F);
707
708 if (not proximityField.reduceVelocity)
709 {
710 return {core::TwistLimits::NoLimits(), fractionalDistance};
711 }
712
713 const float d_s = std::pow(1 - fractionalDistance, proximityField.k);
714
715 const auto permissibleVelocity = [&proximityField](const float d_s,
716 const float v_max) -> float
717 { return v_max / (1 + proximityField.lambda * d_s); };
718
719 const core::TwistLimits result{
720 .linear = permissibleVelocity(d_s, generalConfig.maxVel.linear),
721 .angular = permissibleVelocity(d_s, generalConfig.maxVel.angular)};
722
723 return {result, fractionalDistance};
724 }
725
726 template <class T, class Func>
727 static bool
728 allPointsInside(const std::vector<T>& points, Func isInsideFunc)
729 {
730 for (const auto& p : points)
731 {
732 if (not isInsideFunc(p))
733 {
734 // this point is not inside
735 return false;
736 }
737 }
738 return true;
739 }
740
742 LaserBasedProximity::featureIgnoreReason(
743 const std::vector<Eigen::Vector3f>& featurePoints3DGlobal,
744 const std::vector<Eigen::Vector2f>& featurePoints2DGlobal,
745 const Eigen::Vector2f& centroid) const
746 {
747 // Note: features are *not* ignored based on their number of points alone. Small clusters
748 // are real obstacles (e.g. table legs, poles). Filtering of small clusters that are likely
749 // to be sensor artifacts happens in calculateMinDistance() and is restricted to the near
750 // field of the robot.
751
752 const auto featureInfo = [&]() -> std::string
753 {
754 return "feature with " + std::to_string(featurePoints3DGlobal.size()) +
755 " points around global (" + std::to_string(centroid.x()) + ", " +
756 std::to_string(centroid.y()) + ")";
757 };
758
759 // first check against ignored regions
760 for (const auto& [i, region] : ranges::views::enumerate(params.ignoredRegions))
761 {
762 ARMARX_CHECK(not region.isEmpty());
763
764 if (allPointsInside(featurePoints2DGlobal,
765 [&region](const Eigen::Vector2f& p) { return region.contains(p); }))
766 {
767 // the feature lies fully inside this region -> it is ignored
768 ARMARX_VERBOSE << "Ignoring " << featureInfo() << ": lies fully inside ignored "
769 << "region " << i << " [" << region.min().transpose() << "; "
770 << region.max().transpose() << "]";
772 }
773 }
774
775 // then check against attached objects
776 if (not params.ignoreAttachedObjects)
777 {
779 << "Not checking features against attached objects "
780 "(params.ignoreAttachedObjects is false)";
781 return FilterReason::None;
782 }
783
784 for (const auto& [i, o] : ranges::views::enumerate(scene.dynamicScene->attachedObjects))
785 {
786 const auto& oobbGlobal = o->oobbGlobal();
787 ARMARX_CHECK(oobbGlobal.has_value());
788
789 simox::OrientedBoxf inflatedOobb{oobbGlobal->transformation_centered(),
790 oobbGlobal->dimensions() +
791 Eigen::Vector3f::Ones() *
792 params.attachedObjectsInflation};
793
794 if (allPointsInside(featurePoints3DGlobal,
795 [&inflatedOobb](const Eigen::Vector3f& p)
796 { return inflatedOobb.contains(p); }))
797 {
798 // the feature lies fully inside the objects oobb -> it is ignored
799 ARMARX_VERBOSE << "Ignoring " << featureInfo() << ": lies fully inside the "
800 << "(inflated by " << params.attachedObjectsInflation
801 << " mm) OOBB of attached object " << i;
803 }
804 }
805
806 ARMARX_VERBOSE << "Keeping " << featureInfo() << ": neither inside one of the "
807 << params.ignoredRegions.size() << " ignored regions nor inside one of the "
808 << scene.dynamicScene->attachedObjects.size() << " attached objects";
809
810 return FilterReason::None;
811 }
812
813 LaserBasedProximity::InternalVelocityLimitResult
814 LaserBasedProximity::velocityLimitsDirectionDependent(
815 const Eigen::Vector2f& global_V_movement,
816 std::vector<DistanceAndClosestPoint>& minDistanceToObstacles,
817 const Eigen::Isometry3f& global_T_robot) const
818 {
819
820 float maxPermissibleRelativeVelocityLinear = generalConfig.maxVel.linear;
821
822 std::optional<Eigen::Vector2f> minPoint = std::nullopt;
823 std::optional<float> minDistance = std::nullopt;
824
825 const Eigen::Isometry3f robot_T_global = global_T_robot.inverse();
826
827 ARMARX_CHECK_NOT_NULL(context.debugObserver);
828 auto& debugObserver = *context.debugObserver;
829
830 debugObserver.setDebugObserverDatafield("numObstacles", minDistanceToObstacles.size());
831
832 std::size_t closestClusterSize = 0;
833 // ARMARX_VERBOSE << VAROUT(minDistanceToObstacles.size());
834
835 // is there at least one obstacle that forces us to stop? if so, only such obstacles are
836 // reported as the constraining one.
837 bool mustStop = false;
838
839 // a zero movement command has no direction. normalized() would yield NaN, which would
840 // silently disable all comparisons below.
841 const Eigen::Vector2f global_V_movementDirection =
842 global_V_movement.norm() > 1e-6F ? global_V_movement.normalized().eval()
843 : Eigen::Vector2f::Zero();
844
845 // why obstacles did not constrain the velocity (reported to the debug observer below)
846 std::size_t numFiltered = 0; // ignored features, see calculateMinDistance()
847 std::size_t numTooFarAway = 0; // outside the influence distance
848 std::size_t numMovingAway = 0; // robot is moving away from them
849 std::size_t numTangential = 0; // robot passes them exactly tangentially
850 std::size_t numConstraining = 0;
851
852 for (auto& obstacle : minDistanceToObstacles)
853 {
854 const float distance = obstacle.distance;
855 const Eigen::Vector2f& global_P_obstacle_pt = obstacle.closestPoint;
856 const std::size_t clusterSize = obstacle.clusterSize;
857
858 const float ds = params.laserScannerProximityField.safetyDistance;
859 const float di = params.laserScannerProximityField.influenceDistance;
860 const float d = distance;
861
862 // too far away
863 if (d >= di)
864 {
865 // features that were ignored got a distance of float max, see calculateMinDistance()
866 if (d >= std::numeric_limits<float>::max())
867 {
868 // keep the reason recorded by calculateMinDistance()
869 numFiltered++;
870 }
871 else
872 {
873 numTooFarAway++;
874 obstacle.filterReason = FilterReason::TooFarAway;
875 ARMARX_VERBOSE << "Obstacle (cluster size " << clusterSize << ") at distance "
876 << d << " mm is outside the influence distance " << di << " mm";
877 }
878 continue;
879 }
880
881 float velocity_damper = generalConfig.maxVel.linear * (d - ds) / (di - ds);
882
883 ARMARX_VERBOSE << VAROUT(velocity_damper);
884
885 const Eigen::Vector2f minPointGlobal = global_P_obstacle_pt;
886
887 ARMARX_VERBOSE << VAROUT(minPointGlobal);
888 ARMARX_VERBOSE << VAROUT(robot_T_global.translation().head<2>());
889
890 const Eigen::Vector2f directionRobotToObstacle =
891 (minPointGlobal - global_T_robot.translation().head<2>()).normalized();
892
893 ARMARX_VERBOSE << VAROUT(directionRobotToObstacle);
894
895 // Cases:
896 // 1) > 0: approaching object
897 // 2) < 0: moving away from object
898 const float relativeVelocityScaling =
899 directionRobotToObstacle.dot(global_V_movementDirection);
900
902 ARMARX_VERBOSE << VAROUT(relativeVelocityScaling);
903
904 // moving away from obstacle?
905 if (relativeVelocityScaling < 0)
906 {
907 numMovingAway++;
908 obstacle.filterReason = FilterReason::MovingAway;
909 ARMARX_VERBOSE << "Obstacle (cluster size " << clusterSize << ") at distance " << d
910 << " mm does not constrain the velocity: the robot is moving away "
911 "from it ("
912 << VAROUT(relativeVelocityScaling) << ")";
913 continue;
914 }
915
916 if (d <= ds) // we must stop, if an obstacle is too close
917 {
918 numConstraining++;
919 maxPermissibleRelativeVelocityLinear = 0;
920
921 ARMARX_VERBOSE << "Obstacle (cluster size " << clusterSize << ") at distance " << d
922 << " mm is within the safety distance " << ds
923 << " mm -> the robot must stop";
924
925 // report the closest of all obstacles that force us to stop, not the last one
926 if (not mustStop or d < minDistance.value())
927 {
928 mustStop = true;
929 minPoint = minPointGlobal;
930 minDistance = d;
931 closestClusterSize = clusterSize;
932 }
933 continue;
934 }
935
936 // if not exactly tangential
937 if (std::abs(relativeVelocityScaling) > 1e-4)
938 {
939 numConstraining++;
940
941 const float thisMaxPermissibileRelVelLinear =
942 velocity_damper / relativeVelocityScaling;
943 ARMARX_VERBOSE << VAROUT(thisMaxPermissibileRelVelLinear);
944
945 if (thisMaxPermissibileRelVelLinear < maxPermissibleRelativeVelocityLinear)
946 {
947 ARMARX_VERBOSE << "New most constraining obstacle (cluster size " << clusterSize
948 << ") at distance " << d << " mm: limiting the linear velocity "
949 << "to " << thisMaxPermissibileRelVelLinear << " mm/s";
950
951 maxPermissibleRelativeVelocityLinear = thisMaxPermissibileRelVelLinear;
952 minPoint = minPointGlobal;
953 minDistance = d;
954 closestClusterSize = clusterSize;
955 }
956 }
957 else
958 {
959 numTangential++;
960 obstacle.filterReason = FilterReason::Tangential;
961 ARMARX_VERBOSE << "Obstacle (cluster size " << clusterSize << ") at distance " << d
962 << " mm does not constrain the velocity: the robot passes it "
963 "tangentially ("
964 << VAROUT(relativeVelocityScaling) << ")";
965 }
966 }
967
968 ARMARX_VERBOSE << "Of " << minDistanceToObstacles.size() << " obstacles (accumulated over "
969 << "the feature history): " << numFiltered << " filtered out, "
970 << numTooFarAway << " too far away, " << numMovingAway << " moving away, "
971 << numTangential << " tangential, " << numConstraining << " constraining";
972
973 debugObserver.setDebugObserverDatafield("maxPermissibleRelativeVelocityLinear",
974 maxPermissibleRelativeVelocityLinear);
975 debugObserver.setDebugObserverDatafield("numObstaclesFiltered", numFiltered);
976 debugObserver.setDebugObserverDatafield("numObstaclesTooFarAway", numTooFarAway);
977 debugObserver.setDebugObserverDatafield("numObstaclesMovingAway", numMovingAway);
978 debugObserver.setDebugObserverDatafield("numObstaclesTangential", numTangential);
979 debugObserver.setDebugObserverDatafield("numObstaclesConstraining", numConstraining);
980
981 if (minDistance.has_value())
982 {
983 debugObserver.setDebugObserverDatafield("minDistance", minDistance.value());
984 debugObserver.setDebugObserverDatafield("closestClusterSize", closestClusterSize);
985 }
986
987 core::TwistLimits result{.linear = std::max<float>(0, maxPermissibleRelativeVelocityLinear),
988 .angular = generalConfig.maxVel.angular};
989 return {.twistLimits = result, .minDistance = minDistance, .closestPoint = minPoint};
990 }
991
992 LaserBasedProximity::InternalVelocityLimitResult
993 LaserBasedProximity::velocityLimitsDirectionIndependent(
994 const std::vector<DistanceAndClosestPoint>& minDistanceToObstacles) const
995 {
996 const DistanceAndClosestPoint minDistance =
997 ranges::min(minDistanceToObstacles, std::less{}, &DistanceAndClosestPoint::distance);
998
999 const auto result =
1000 evaluateProximityField(params.laserScannerProximityField, minDistance.distance);
1001
1002 return {.twistLimits = result.first,
1003 .minDistance = minDistance.distance,
1004 .closestPoint = minDistance.closestPoint};
1005 }
1006} // namespace armarx::navigation::safety_guard
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
#define VAROUT(x)
SafetyGuardResult computeSafetyLimits(const Eigen::Vector2f &global_V_movement) override
LaserBasedProximity(const Params &params, const core::GeneralConfig &generalConfig, const core::Scene &scene, const Context &ctx)
SafetyGuard(const core::Scene &scene, const Context &ctx)
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_GREATER_EQUAL(lhs, rhs)
This macro evaluates whether lhs is greater or equal (>=) rhs and if it turns out to be false it will...
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::shared_ptr< Dict > DictPtr
Definition Dict.h:42
std::vector< Eigen::Vector2f > to2D(const std::vector< Eigen::Vector3f > &v)
Definition eigen.cpp:29
std::vector< Eigen::Vector3f > to3D(const std::vector< Eigen::Vector2f > &v)
Definition eigen.cpp:14
Eigen::Isometry3f Pose
Definition basic_types.h:31
This file is part of ArmarX.
Definition fwd.h:55
void fromAron(const arondto::ProximityFieldParams &dto, ProximityFieldParams &bo)
void toAron(arondto::ProximityFieldParams &dto, const ProximityFieldParams &bo)
FilterReason
Why a laser scanner feature does not constrain the velocity.
@ TooFarAway
The feature is outside the influence distance.
@ MovingAway
The robot is moving away from the feature.
@ None
The feature constrains, or is a candidate to constrain, the velocity.
@ NearFieldArtifact
Small feature close to the robot, most likely a measuring artifact of the sensor.
@ AttachedObject
The feature lies fully inside the inflated OOBB of an attached object.
@ IgnoredRegion
The feature lies fully inside one of the ignored regions.
@ Tangential
The robot passes the feature exactly tangentially.
boost::geometry::model::d2::point_xy< float > point_type
Definition geometry.h:35
boost::geometry::model::polygon< point_type > polygon_type
Definition geometry.h:36
This file is part of ArmarX.
Definition Impl.cpp:41
double distance(const Point &a, const Point &b)
Definition point.hpp:95
std::optional< core::DynamicScene > dynamicScene
Definition types.h:71
static TwistLimits ZeroLimits()
Definition types.h:99
static TwistLimits NoLimits()
Definition types.h:92
static LaserBasedProximityParams FromAron(const aron::data::DictPtr &dict)
std::experimental::observer_ptr< DebugObserverComponentPluginUser > debugObserver
Definition SafetyGuard.h:81
void add(ElementT const &element)
Definition Layer.h:31