HandoverCostmapBuilder.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 * @author ( )
17 * @date 2026
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
23
24#include <algorithm>
25#include <cmath>
26#include <optional>
27
28#include <Eigen/Geometry>
29
32
39
41
43{
44
45
47 const Parameters& handoverParameters) :
48 costmapParameters_(costmapParameters), handoverParameters_(handoverParameters)
49 {
50 ARMARX_CHECK_POSITIVE(handoverParameters_.handoverDistanceMin);
51 ARMARX_CHECK_POSITIVE(handoverParameters_.handoverDistanceMax);
52 ARMARX_CHECK_GREATER(handoverParameters_.handoverDistanceMax,
53 handoverParameters_.handoverDistanceMin);
54
55 ARMARX_CHECK_LESS_EQUAL(handoverParameters_.handoverAngleMin,
56 handoverParameters_.handoverAngleMax);
57
58 ARMARX_CHECK_LESS_EQUAL(handoverParameters_.handoverAngleMin,
59 handoverParameters_.handoverAnglePreference);
60 ARMARX_CHECK_LESS_EQUAL(handoverParameters_.handoverAnglePreference,
61 handoverParameters_.handoverAngleMax);
62
63 ARMARX_CHECK_GREATER_EQUAL(handoverParameters_.robotPathCostWeight, 0.F);
64 ARMARX_CHECK_POSITIVE(handoverParameters_.robotPathMaxCostMM);
65 }
66
67 std::optional<Costmap>
69 {
70 const std::optional<core::Pose2D> global_T_human_opt =
71 sceneInfo.humanPose2DOverride.has_value()
72 ? sceneInfo.humanPose2DOverride
74
75 if (not global_T_human_opt.has_value())
76 {
77 // cannot compute human pose
78 return std::nullopt;
79 }
80
81 // obtain scene information from human position
82 const auto sceneBounds = [this, global_T_human = global_T_human_opt.value()]
83 {
84 // scene bounds
85 const Eigen::Vector2f human_T_left{-handoverParameters_.humanSceneBoundOffsetSideLeft,
86 0};
87 const Eigen::Vector2f human_T_right{handoverParameters_.humanSceneBoundOffsetSideRight,
88 0};
89 const Eigen::Vector2f human_T_front{0, handoverParameters_.humanSceneBoundOffsetFront};
90
91 const Eigen::Vector2f human_T_back{0, -handoverParameters_.humanSceneBoundOffsetBehind};
92
93 SceneBounds initialSceneBounds;
94
95 const auto sceneBounds = computeSceneBounds({global_T_human * human_T_left,
96 global_T_human * human_T_right,
97 global_T_human * human_T_front,
98 global_T_human * human_T_back},
99 initialSceneBounds,
100 200);
101
102 return sceneBounds;
103 }();
104
105 // helper function to project a value to [-1, 1].
106 // Values outside the min/max range are clamped.
107 // min maps to -1.0, max maps to 1.0 while preference maps to 0.0
108 const auto projectToInterval =
109 [](const float value, const float min, const float max, const float preference) -> float
110 {
112 if (value <= min)
113 {
114 return -1.0f;
115 }
116 if (value >= max)
117 {
118 return 1.0f;
119 }
120 if (value == preference)
121 {
122 return 0.0f;
123 }
124 if (value < preference)
125 {
126 return -((preference - value) / (preference - min));
127 }
128 // value > preference
129 return (value - preference) / (max - preference);
130 };
131
132 const auto costFn = [this, &projectToInterval](float distanceToObstacle,
133 float distanceToHuman,
134 float angleToHuman) -> std::optional<float>
135 {
136 // check bounds
137 if (distanceToHuman < handoverParameters_.handoverDistanceMin ||
138 distanceToHuman > handoverParameters_.handoverDistanceMax)
139 {
140 return std::nullopt;
141 }
142
143 if (angleToHuman < handoverParameters_.handoverAngleMin ||
144 angleToHuman > handoverParameters_.handoverAngleMax)
145 {
146 return std::nullopt;
147 }
148
149
150 // distance cost (scaled to [-1, 1])
151 const float xDistance =
152 projectToInterval(distanceToHuman,
153 handoverParameters_.handoverDistanceMin,
154 handoverParameters_.handoverDistanceMax,
155 handoverParameters_.handoverDistancePreference);
156 const float xAngle = projectToInterval(angleToHuman,
157 handoverParameters_.handoverAngleMin,
158 handoverParameters_.handoverAngleMax,
159 handoverParameters_.handoverAnglePreference);
160
161 // compute factors that scale cost based on distance and angle
162 // both factors are in [0, +inf); 0 means optimal, +inf means worst
163 const double fDistance = std::abs(std::tan(xDistance * M_PI_2));
164 const double fAngle = std::abs(std::tan(xAngle * M_PI_2));
165
166 // in order to avoid multiplication by zero, we add an offset of 1. This avoids
167 // zero-cost areas that cannot be distinguished.
168
169 // base cost is the product of both factors
170 const double f = (fDistance + 1) * (fAngle + 1);
171
172 // slight additive penalty for candidates too close to an obstacle, instead
173 // of scaling the whole cost by the (raw) distance to the nearest obstacle
174 const double obstaclePenalty =
175 (distanceToObstacle < handoverParameters_.obstacleProximityThreshold)
176 ? handoverParameters_.obstacleProximityPenalty
177 : 0.0;
178
179 return static_cast<float>(f + obstaclePenalty);
180 };
181
182
183 // create costmap
184 auto grid = CostmapBuilder::createUniformGrid(sceneBounds, costmapParameters_);
185
186 Costmap costmap(grid, costmapParameters_, sceneBounds);
187 // the loop below writes through `getMutableMask()`, which requires the mask
188 // to already hold a value (otherwise it is a disengaged std::optional). Every
189 // cell visited by the loop overwrites its mask entry unconditionally, so the
190 // mask's initial content here is irrelevant, only its presence and size matter.
192
193 costmap.forEachCell(
194 [&costmap, &sceneInfo, global_T_human = global_T_human_opt.value(), &costFn](
195 const Costmap::Index& idx)
196 {
197 // obtain global position of the cell
198 const auto global_P_cell = costmap.toPositionGlobal(idx);
199
200 // initialize based on distance map
201
202 const auto vertex = sceneInfo.distanceMap.toVertexOrInvalid(global_P_cell);
203
204 // initialize as invalid
205 {
206 costmap.getMutableGrid()(idx.x(), idx.y()) = 0.0f;
207 costmap.getMutableMask()->operator()(idx.x(), idx.y()) = false;
208 }
209
210 if (!vertex.has_value())
211 {
212 // outside distance map or invalid
213 return;
214 }
215
216 // Use isValid()+direct grid access instead of value(), which logs an
217 // ARMARX_IMPORTANT message on every masked-out cell. Since the human-
218 // centered local scene bounds routinely extend beyond the valid region
219 // of the (larger, static) distance map, that path is hit very frequently
220 // here and would otherwise spam the log.
221 if (!sceneInfo.distanceMap.isValid(vertex->index))
222 {
223 // masked out / invalid distance
224 return;
225 }
226
227 // Now we know that the cell is valid. Hence, we can compute its cost.
228 {
229
230 const float distanceToObstacle =
231 sceneInfo.distanceMap.getGrid()(vertex->index.x(), vertex->index.y());
232 const Eigen::Vector2f human_P_cell = global_T_human.inverse() * global_P_cell;
233
234 const float distanceToHuman = human_P_cell.norm();
235
236 const float angleToHuman = std::atan2(human_P_cell.y(), human_P_cell.x());
237
238
239 const auto costOpt = costFn(distanceToObstacle, distanceToHuman, angleToHuman);
240
241 if (not costOpt.has_value())
242 {
243 // invalid cost
244 return;
245 }
246
247 costmap.getMutableGrid()(idx.x(), idx.y()) = costOpt.value();
248 costmap.getMutableMask()->operator()(idx.x(), idx.y()) = true;
249 }
250 });
251
252 // If a robot position is provided, run SPFA from the robot on the static
253 // distance map. The resulting path cost is added to every reachable
254 // candidate, which favors placements that are closer to the robot and
255 // reachable without passing through narrow spaces (SPFA's edge cost
256 // penalizes proximity to obstacles). Unreachable candidates are masked out.
257 if (sceneInfo.robotPosition.has_value())
258 {
259 const auto robotVertexOpt =
260 sceneInfo.distanceMap.toVertexOrInvalid(sceneInfo.robotPosition.value());
261 if (not robotVertexOpt.has_value())
262 {
263 ARMARX_WARNING << "Robot position is outside the distance map; ignoring robot "
264 "path cost and reachability.";
265 }
266 else
267 {
268 spfa::ShortestPathFasterAlgorithm spfa(sceneInfo.distanceMap,
269 handoverParameters_.robotPathParams);
270 const auto spfaResult = spfa.spfa(sceneInfo.robotPosition.value());
271
272 costmap.forEachCell(
273 [&](const Costmap::Index& idx)
274 {
275 if (not costmap.isValid(idx))
276 {
277 return;
278 }
279
280 const Eigen::Vector2f globalP = costmap.toPositionGlobal(idx);
281 const auto vertexOpt = sceneInfo.distanceMap.toVertexOrInvalid(globalP);
282 if (not vertexOpt.has_value())
283 {
284 costmap.getMutableMask()->operator()(idx.x(), idx.y()) = false;
285 return;
286 }
287
288 const Eigen::Vector2i dmIdx = vertexOpt->index;
289 if (not sceneInfo.distanceMap.isValid(dmIdx) or
290 not spfaResult.reachable(dmIdx.x(), dmIdx.y()))
291 {
292 costmap.getMutableMask()->operator()(idx.x(), idx.y()) = false;
293 return;
294 }
295
296 const float pathCostMM = spfaResult.distances(dmIdx.x(), dmIdx.y());
297 const float normalizedCost =
298 std::min(pathCostMM / handoverParameters_.robotPathMaxCostMM, 1.F);
299
300 costmap.getMutableGrid()(idx.x(), idx.y()) +=
301 handoverParameters_.robotPathCostWeight * normalizedCost;
302 });
303 }
304 }
305
306 return costmap;
307 }
308
309 std::optional<core::Pose2D>
311 {
312 const auto costmapOpt = create(sceneInfo);
313
314 if (not costmapOpt.has_value())
315 {
316 return std::nullopt;
317 }
318
319 const auto& costmap = costmapOpt.value();
320 const auto optimum = costmap.optimum();
321
322 if (optimum.index.isConstant(-1))
323 {
324 // no valid optimum found
325 return std::nullopt;
326 }
327
328 // compute orientation towards human
329 const auto global_P_human = armem::human::computeMeanPosition(sceneInfo.humanPose);
330
331 if (not global_P_human.has_value())
332 {
333 // cannot compute human pose
334 return std::nullopt;
335 }
336
337 const auto global_P_handover = optimum.position;
338
339 const Eigen::Vector2f direction =
340 (global_P_human->head<2>() - global_P_handover).normalized();
341
342 const float yaw = std::atan2(direction.y(), direction.x());
343
344 return core::Pose2D{Eigen::Translation2f{global_P_handover.x(), global_P_handover.y()} *
345 Eigen::Rotation2Df{yaw}};
346 }
347} // namespace armarx::navigation::algorithms::costmap
if(!yyvaluep)
Definition Grammar.cpp:645
static Eigen::MatrixXf createUniformGrid(const SceneBounds &sceneBounds, const Costmap::Parameters &parameters)
bool isValid(const Index &index) const noexcept
checks whether the cell is masked out
Definition Costmap.cpp:82
std::optional< core::Pose2D > getOptimalHandoverPose(const SceneInformation &sceneInfo)
std::optional< Costmap > create(const SceneInformation &sceneInfo)
HandoverCostmapBuilder(const Costmap::Parameters &costmapParameters, const Parameters &handoverParameters)
#define ARMARX_CHECK_GREATER(lhs, rhs)
This macro evaluates whether lhs is greater (>) than rhs and if it turns out to be false it will thro...
#define ARMARX_CHECK_POSITIVE(number)
This macro evaluates whether number is positive (> 0) and if it turns out to be false it will throw a...
#define ARMARX_CHECK_LESS_EQUAL(lhs, rhs)
This macro evaluates whether lhs is less or equal (<=) rhs and if it turns out to be false it will th...
#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_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
std::optional< Eigen::Vector3f > computeMeanPosition(const HumanPose &humanPose, KeyPointCoordinateSystem coordSystem)
Definition util.cpp:31
std::optional< Eigen::Isometry2f > calculatePose(const HumanPose &humanPose)
Definition util.cpp:273
SceneBounds computeSceneBounds(const VirtualRobot::SceneObjectSetPtr &obstacles, const std::vector< VirtualRobot::RobotPtr > &articulatedObjects, const SceneBounds &init, const std::vector< Room > &rooms, const float margin, const bool restrictToRooms)
Definition util.cpp:62
Eigen::Isometry2f Pose2D
Definition basic_types.h:34
std::vector< T > max(const std::vector< T > &v1, const std::vector< T > &v2)
std::vector< T > min(const std::vector< T > &v1, const std::vector< T > &v2)
std::optional< armarx::navigation::core::Pose2D > humanPose2DOverride
Optional override for the human's 2-D pose.
armarx::armem::human::HumanPose humanPose
information about the human for handover