Costmap3D.cpp
Go to the documentation of this file.
1#include "Costmap3D.h"
2
3#include <algorithm>
4#include <cmath>
5#include <cstddef>
6#include <limits>
7#include <optional>
8#include <utility>
9#include <vector>
10
11#include <Eigen/Core>
12#include <Eigen/Geometry>
13
14#include <opencv2/imgproc.hpp>
15
16#include <range/v3/view/zip.hpp>
17
21
24
26{
27
28
30 const Parameters& parameters,
31 const SceneBounds& sceneBounds,
32 const std::optional<Mask>& mask,
33 const core::Pose2D& origin) :
34 grid(grid),
35 mask(mask),
36 sceneBounds(sceneBounds),
37 parameters(parameters),
38 global_T_Costmap3D(origin)
39 {
40 validateSizes();
41 }
42
43 void
44 Costmap3D::validateSizes() const
45 {
46 // Check that the costmap for all orientations has the same size
47 if (grid.size() > 0)
48 {
49 const auto size = getSize();
50 auto rows = size.x();
51 auto columns = size.y();
52 // In case there is a mask, also check if that has the same size
53 if (mask.has_value())
54 {
55 rows = mask->rows();
56 columns = mask->cols();
57 }
58
59 for (const auto& g : grid)
60 {
61 ARMARX_CHECK_EQUAL(g.rows(), rows);
62 ARMARX_CHECK_EQUAL(g.cols(), columns);
63 }
64 }
65 }
66
69 {
71
72 Eigen::Vector2f posLocal;
73 posLocal.x() =
74 sceneBounds.min.x() + index.x() * parameters.cellSize + parameters.cellSize / 2;
75 posLocal.y() =
76 sceneBounds.min.y() + index.y() * parameters.cellSize + parameters.cellSize / 2;
77
78 return posLocal;
79 }
80
83 {
84 const Costmap3D::Position Costmap3D_P_pos = toPositionLocal(index);
85 return global_T_Costmap3D * Costmap3D_P_pos;
86 }
87
88 bool
90 {
91 if (not isWithinRange(index))
92 {
93 return false;
94 }
95
96 if (mask.has_value())
97 {
98 if (not mask.value()(index.x(), index.y()))
99 {
100 ARMARX_DEBUG << "Index masked out (mask)";
101 }
102 return mask.value()(index.x(), index.y());
103 }
104
105 return true;
106 }
107
108 bool
110 {
111 if (index.x() < 0 || index.y() < 0)
112 {
113 ARMARX_VERBOSE << "Index out of bounds (< 0)";
114 return false;
115 }
116
117 if (index.x() >= getSize().x() || index.y() >= getSize().y())
118 {
119 ARMARX_VERBOSE << "Index out of bounds (>= grid size)";
120 return false;
121 }
122 return true;
123 }
124
125 bool
127 {
128 if (mask.has_value())
129 {
130 if (not mask.value()(index.x(), index.y()))
131 {
132 ARMARX_DEBUG << "Index masked out (mask)";
133 }
134 return not mask.value()(index.x(), index.y());
135 }
136 return false;
137 }
138
140 Costmap3D::toVertex(const Eigen::Vector2f& globalPosition) const
141 {
142 const auto localPosition = global_T_Costmap3D.inverse() * globalPosition;
143
144 const float vX = (localPosition.x() - parameters.cellSize / 2 - sceneBounds.min.x()) /
145 parameters.cellSize;
146 const float vY = (localPosition.y() - parameters.cellSize / 2 - sceneBounds.min.y()) /
147 parameters.cellSize;
148
149 const int iX = std::round(vX - 0.01);
150 const int iY = std::round(vY - 0.01);
151
152 const auto size = getSize();
153 const int iXSan = std::clamp<int>(iX, 0, size.x() - 1);
154 const int iYSan = std::clamp<int>(iY, 0, size.y() - 1);
155
156 // FIXME accept one cell off
157
158 // ARMARX_CHECK_GREATER(iX, 0);
159 // ARMARX_CHECK_GREATER(iY, 0);
160
161 // ARMARX_CHECK_LESS_EQUAL(iX, grid.rows() - 1);
162 // ARMARX_CHECK_LESS_EQUAL(iY, grid.cols() - 1);
163
164 return Vertex{.index = Index{iXSan, iYSan}, .position = globalPosition};
165 }
166
169 {
170 const float degrees = (360.F / parameters.orientations) * index;
171 return {.index = index, .degrees = degrees};
172 }
173
174 float
175 rotationFrom0To360(float degrees)
176 {
177 float deg = std::fmod(degrees, 360.F);
178 if (deg < 0.F)
179 {
180 deg += 360.F;
181 }
182 return deg;
183 }
184
185 Costmap3D::Rotation
187 {
188 degrees = rotationFrom0To360(degrees);
189 //const float tmp = (degrees - (180.F / parameters.orientations)) / 360.F; // add half of discretization
190 const float tmp = (degrees + (180.F / parameters.orientations)) /
191 (360.F / parameters.orientations); // normalize to [0, orientations)
192 int index = tmp; // round up
193 index = std::min(index, parameters.orientations - 1);
194 return {.index = index, .degrees = degrees};
195 }
196
197 std::pair<Costmap3D::Rotation, Costmap3D::Rotation>
199 {
200 const float deg = rotationFrom0To360(degrees);
201 const float step = 360.F / parameters.orientations;
202
203 int lowIndex = static_cast<int>(deg / step);
204 lowIndex = std::min(lowIndex, parameters.orientations - 1);
205 const int highIndex = (lowIndex + 1) % parameters.orientations;
206
207 return {rotationFromIndex(lowIndex), rotationFromIndex(highIndex)};
208 }
209
210 const SceneBounds&
212 {
213 return sceneBounds;
214 }
215
218 {
220
221 if (mask.has_value())
222 {
223 ARMARX_CHECK_GREATER(mask->array().sum(), 0)
224 << "At least one element has to be valid. Here, all elements are masked out!";
225 }
226
227 // index of the min element
228 Grid::value_type::Index row = 0;
229 Grid::value_type::Index col = 0;
230 const auto size = getSize();
231
232 // value of the min element
233 float minVal = std::numeric_limits<float>::max();
234
235 for (int r = 0; r < size.x(); r++)
236 {
237 for (int c = 0; c < size.y(); c++)
238 {
239 if (mask.has_value())
240 {
241 if (not mask.value()(r, c)) // skip invalid cells
242 {
243 continue;
244 }
245
246 if (rotation.index == ALL_ORIENTATIONS.index)
247 {
248 for (const auto& g : grid)
249 {
250 // min value across all orientations
251 const float currentVal = g(r, c);
252 if (currentVal < minVal)
253 {
254 minVal = currentVal;
255 row = r;
256 col = c;
257 }
258 }
259 }
260 else
261 {
262 const auto& g = grid[rotation.index];
263 // min value across all orientations
264 const float currentVal = g(r, c);
265 if (currentVal < minVal)
266 {
267 minVal = currentVal;
268 row = r;
269 col = c;
270 }
271 }
272 }
273 }
274 }
275
276 return {.value = minVal, .index = {row, col}, .position = toPositionGlobal({row, col})};
277 }
278
280 Costmap3D::params() const noexcept
281 {
282 return parameters;
283 }
284
285 const Costmap3D::Grid&
287 {
288 return grid;
289 }
290
291 bool
292 Costmap3D::isInCollision(const Position& p, std::optional<Rotation> rotation) const
293 {
294 const auto v = toVertex(p);
295 if (rotation.has_value())
296 {
297 return grid.at(rotation->index)(v.index.x(), v.index.y()) <= 0.F;
298 }
299
300 // No rotation given
301 // So check all rotations for collision
302 return std::any_of(grid.begin(),
303 grid.end(),
304 [&v](const auto& grid2d)
305 { return grid2d(v.index.x(), v.index.y()) <= 0.F; });
306 }
307
308 bool
310 const RotationDegrees orientationDeg,
311 const float clearance) const
312 {
313 const auto v = toVertex(p);
314 if (not isValid(v.index))
315 {
316 return false;
317 }
318
319 const auto [low, high] = bracketingRotationsFromDegrees(orientationDeg);
320 return grid.at(low.index)(v.index.x(), v.index.y()) > clearance and
321 grid.at(high.index)(v.index.x(), v.index.y()) > clearance;
322 }
323
324 std::optional<Costmap3D::Vertex>
326 const RotationDegrees orientationDeg,
327 const float maxDistance,
328 const float minClearance) const
329 {
331
332 const auto [low, high] = bracketingRotationsFromDegrees(orientationDeg);
333
334 // Overlap the two bracketing orientation slices: a cell is only free if the robot
335 // fits at both discrete orientations, i.e. also at the continuous orientation in
336 // between. Shifting by minClearance turns the 2D search's `> 0` test into
337 // `> minClearance`; clamping at 0 is required because Costmap::isInCollision
338 // treats exactly 0 (not negative values) as collision.
339 const Eigen::MatrixXf combined =
340 (grid.at(low.index).cwiseMin(grid.at(high.index)).array() - minClearance)
341 .cwiseMax(0.F)
342 .matrix();
343
344 const Costmap combinedCostmap{combined,
346 .binaryGrid = false,
347 .cellSize = parameters.cellSize,
348 .robotRadius = 0.F,
349 .sceneBoundsMargin = parameters.sceneBoundsMargin,
350 },
351 sceneBounds,
352 mask,
353 global_T_Costmap3D};
354
355 const auto vertex = combinedCostmap.findClosestCollisionFreeVertex(position, maxDistance);
356 if (not vertex.has_value())
357 {
358 return std::nullopt;
359 }
360
361 return Vertex{.index = vertex->index, .position = vertex->position};
362 }
363
364 const std::optional<Costmap3D::Mask>&
365 Costmap3D::getMask() const noexcept
366 {
367 return mask;
368 }
369
370 std::optional<Costmap3D::Mask>&
372 {
373 return mask;
374 }
375
376 float
378 {
379 return grid.at(rot_index)(index.x(), index.y());
380 }
381
382 std::optional<float>
384 {
385 if (not isValid(index))
386 {
387 ARMARX_IMPORTANT << "Requested index " << index << " but it is masked out!";
388 return std::nullopt;
389 }
390 if (rot_index < 0 || rot_index > this->grid.size() - 1)
391 {
392 ARMARX_IMPORTANT << "Requested rotation index " << rot_index
393 << " but it is not within allowed range (0 .. "
394 << this->grid.size() - 1 << ")";
395 return std::nullopt;
396 }
397
398 return grid.at(rot_index)(index.x(), index.y());
399 }
400
401 std::optional<float>
402 Costmap3D::value(const Position& position,
403 const Costmap3D::RotationDegrees rotation_degrees) const
404 {
405 ARMARX_DEBUG << "value ...";
406
407 const auto v = toVertex(position);
408 const auto r = closestRotationFromDegrees(rotation_degrees);
409 return value(v.index, r.index);
410 }
411
412 void
414 {
416
417 const auto [min, max] = getValidBoundingBox();
418
419 // calculate new scene bounds based on bounding box
420 const auto newSceneBounds = SceneBounds{
421 .min = sceneBounds.min + parameters.cellSize * Eigen::Vector2f{min.cast<float>()},
422 .max = sceneBounds.min + parameters.cellSize * Eigen::Vector2f{max.cast<float>()}};
423
424 ARMARX_VERBOSE << "Cutting grid to valid bounding box: " << min << " and " << max;
425
426 // update grids
427 for (auto& grid2d : grid)
428 {
429 const auto newGrid =
430 grid2d.block(min.x(), min.y(), max.x() - min.x() + 1, max.y() - min.y() + 1);
431 grid2d = newGrid;
432 }
433
434
435 if (mask.has_value())
436 {
437 ARMARX_VERBOSE << "Cutting mask to valid bounding box: " << min << " and " << max;
438
439 const auto newMask =
440 mask->block(min.x(), min.y(), max.x() - min.x() + 1, max.y() - min.y() + 1);
441 mask = newMask;
442 }
443
444 // update scene bounds
445 sceneBounds.min = newSceneBounds.min;
446 sceneBounds.max = newSceneBounds.max;
447
448 ARMARX_VERBOSE << "New scene bounds: " << sceneBounds.min << " and " << sceneBounds.max;
449 }
450
451 std::size_t
453 {
454 if (not mask.has_value())
455 {
456 const auto size = getSize();
457 return size.y() * size.x();
458 }
459
460 return mask->array().cast<int>().sum();
461 }
462
465 {
467
468 if (isValid(index))
469 {
470 return index;
471 }
472
473 const auto x = index.x();
474 const auto y = index.y();
475
476 // Check all cells in a radius around the given index
477 for (int r = 1; r < 10; r++)
478 {
479 for (int i = -r; i <= r; i++)
480 {
481 for (int j = -r; j <= r; j++)
482 {
483 const auto idx = Costmap3D::Index{x + i, y + j};
484 if (isValid(idx))
485 {
486 return idx;
487 }
488 }
489 }
490 }
491
492 ARMARX_ERROR << "No valid index found in the vicinity of " << index;
493 return index;
494 }
495
496 std::pair<Costmap3D::Index, Costmap3D::Index>
498 {
499 if (not mask.has_value())
500 {
501 return {Index{0, 0}, getSize() - Index{1, 1}};
502 }
503
504 ARMARX_VERBOSE << "Fraction of valid elements: "
505 << mask->cast<float>().sum() / mask->size();
506
508
509 const Index size = getSize();
510 Index min = getSize();
511 Index max = Index{0, 0};
512
513 for (int x = 0; x < size.x(); x++)
514 {
515 for (int y = 0; y < size.y(); y++)
516 {
517 if (mask.value()(x, y))
518 {
519 min.x() = std::min(min.x(), x);
520 min.y() = std::min(min.y(), y);
521 max.x() = std::max(max.x(), x);
522 max.y() = std::max(max.y(), y);
523 }
524 }
525 }
526
527 return {min, max};
528 }
529
532 {
533 return {grid[0].rows(), grid[0].cols()};
534 }
535
536 Costmap
537 Costmap3D::costmapForOrientation(RotationIndex rotationIndex, bool initializeMask) const
538 {
539 Costmap c{grid[rotationIndex],
541 .binaryGrid = false,
542 .cellSize = parameters.cellSize,
543 .robotRadius = 0.F,
544 .sceneBoundsMargin = parameters.sceneBoundsMargin,
545 },
546 sceneBounds};
547 // c.getMutableGrid().array() += 2000.0;
548 if (initializeMask)
549 {
550 c.getMutableMask() = c.getGrid().array() > 0.0F;
551 }
552 return c;
553 }
554
557 const Grid& grid,
558 const std::optional<Mask>& mask)
559 {
560 return {grid, other.params(), other.getLocalSceneBounds(), mask, other.origin()};
561 }
562
563 void
565 {
566 for (int rotIdx = 0; rotIdx < parameters.orientations; rotIdx++)
567 {
568 ARMARX_DEBUG << "Converting to Euclidean SDF for rotation index " << rotIdx;
570 }
571
572 // remove mask
573 mask.reset();
574 }
575
576 void
578 {
580 if (not mask.has_value())
581 {
582 ARMARX_DEBUG << "Costmap3D::convertToPseudoSDF: No mask present, nothing to do.";
583 return;
584 }
585
586 const Eigen::MatrixXf& grid2d = grid[rotationIndex];
587 const int nx = getSize().x();
588 const int ny = getSize().y();
589
590 // Build binary mask for cv::distanceTransform.
591 // OpenCV Mat uses (row=y, col=x); Eigen uses (row=x, col=y).
592 // Free cells (positive distance) = 255; collision/masked = 0.
593 cv::Mat mask8U(ny, nx, CV_8U);
594 for (int x = 0; x < nx; ++x)
595 {
596 for (int y = 0; y < ny; ++y)
597 {
598 bool isFree = mask.value()(x, y) && grid2d(x, y) > 0.0f;
599 mask8U.at<uchar>(y, x) = isFree ? 255 : 0;
600 }
601 }
602
603 // Compute exact Euclidean distance (in pixels) from every zero pixel
604 // to the nearest non-zero pixel.
605 cv::Mat distImage;
606 cv::distanceTransform(mask8U, distImage, cv::DIST_L2, cv::DIST_MASK_PRECISE, CV_32F);
607
608 const float cellSize = parameters.cellSize;
609
610 // Overwrite collision cells with negative Euclidean distance (in mm).
611 // Free cells keep their original positive obstacle distances.
612 for (int x = 0; x < nx; ++x)
613 {
614 for (int y = 0; y < ny; ++y)
615 {
616 if (grid2d(x, y) <= 0.0f)
617 {
618 float distPixels = distImage.at<float>(y, x);
619 grid[rotationIndex](x, y) = -distPixels * cellSize;
620 }
621 }
622 }
623
624 ARMARX_DEBUG << "Converted orientation " << rotationIndex
625 << " to true Euclidean SDF. Range: [" << grid[rotationIndex].minCoeff() << ", "
626 << grid[rotationIndex].maxCoeff() << "]";
627 }
628} // namespace armarx::navigation::algorithms::orientation_aware
uint8_t index
constexpr T c
std::optional< Vertex > findClosestCollisionFreeVertex(const Position &position, float maxDistance) const
Find the closest collision-free position to a given position.
Definition Costmap.cpp:860
std::pair< Rotation, Rotation > bracketingRotationsFromDegrees(RotationDegrees degrees) const
The two orientation slices bracketing the given continuous rotation.
Costmap costmapForOrientation(RotationIndex rotationIndex, bool initializeMask=false) const
bool isWithinRange(const Index &index) const noexcept
const SceneBounds & getLocalSceneBounds() const noexcept
static Costmap3D WithSameDimsAs(const Costmap3D &other, const Grid &grid, const std::optional< Mask > &mask=std::nullopt)
Create a Costmap3D with the same dimensions and parameters as the given one.
std::optional< Vertex > findClosestCollisionFreeVertex(const Position &position, RotationDegrees orientationDeg, float maxDistance, float minClearance=0.F) const
Find the closest position where the robot fits at the given continuous orientation with more than min...
Costmap3D(const Grid &grid, const Parameters &parameters, const SceneBounds &sceneBounds, const std::optional< Mask > &mask=std::nullopt, const core::Pose2D &origin=core::Pose2D::Identity())
Definition Costmap3D.cpp:29
bool isInCollision(const Position &p, std::optional< Rotation > rotation=std::nullopt) const
const std::optional< Mask > & getMask() const noexcept
bool isMaskedOut(const Index &index) const noexcept
Rotation rotationFromIndex(RotationIndex index) const
Rotation closestRotationFromDegrees(RotationDegrees degrees) const
std::optional< Costmap3D::Mask > & getMutableMask() noexcept
std::optional< float > value(const Index &index, const RotationIndex rot_index) const
Vertex toVertex(const Position &globalPosition) const
bool isValid(const Index &index) const noexcept
checks whether the cell is masked out
Definition Costmap3D.cpp:89
Optimum optimum(Rotation rotation=ALL_ORIENTATIONS) const
bool isFreeWithClearance(const Position &p, RotationDegrees orientationDeg, float clearance=0.F) const
Whether the robot fits at the given position and continuous orientation with more than the given obst...
float value_ignore_mask(const Index &index, const RotationIndex rot_index) const
#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_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:190
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:196
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:187
Eigen::Isometry2f Pose2D
Definition basic_types.h:34
This file offers overloads of toIce() and fromIce() functions for STL container types.
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)
#define ARMARX_TRACE
Definition trace.h:77