Costmap3DWrapper.h
Go to the documentation of this file.
1#pragma once
2#include <algorithm>
3#include <cmath>
4#include <functional>
5#include <optional>
6
8
10
11#include <ceres/ceres.h>
12
14{
15
16 //
17 // A simple differentiable wrapper around Costmap3D.
18 // Produces a *continuous* obstacle cost for Ceres AutoDiff.
19 //
20 // Design choices:
21 // * bilinear interpolation in X/Y
22 // * nearest-neighbor in orientation (your grid is discrete anyway)
23 // * returns 0 if value() returns nullopt
24 // * everything templated so Ceres autodiff works
25 //
26 // Generated by GPT-5 on 2025-12-04
27
29 {
30 public:
31 using Position = Eigen::Vector2f;
33 using Index = Eigen::Array2i;
34
35 private:
36 // self-written functions
37 template <typename IndexT, typename WeightT, int size>
38 struct LinearInterpolationResult
39 {
40 std::array<IndexT, size> indices;
41 std::array<WeightT, size> weights;
42
43 template <typename T>
44 T
45 calcWeightedAccess(std::function<T(const IndexT&)> accessor) const
46 {
47 T result = T(0);
48 for (int i = 0; i < size; ++i)
49 {
50 result += weights[i] * accessor(indices[i]);
51 }
52 return result;
53 }
54 };
55
56 template <typename IndexT1, typename IndexT2, typename CommonWeightT, int size1, int size2>
57 static LinearInterpolationResult<std::tuple<IndexT1, IndexT2>, CommonWeightT, size1 * size2>
58 combineLinearInterpolationResults(
59 LinearInterpolationResult<IndexT1, CommonWeightT, size1> r1,
60 LinearInterpolationResult<IndexT2, CommonWeightT, size2> r2)
61 {
62 LinearInterpolationResult<std::tuple<IndexT1, IndexT2>, CommonWeightT, size1 * size2>
63 result;
64 for (int i = 0; i < size1; ++i)
65 {
66 for (int j = 0; j < size2; ++j)
67 {
68 result.indices[i * size2 + j] = std::make_tuple(r1.indices[i], r2.indices[j]);
69 result.weights[i * size2 + j] = r1.weights[i] * r2.weights[j];
70 }
71 }
72 return result;
73 }
74
75 public:
77 map_(map)
78 {
79 cell_size_ = map_.params().cellSize; // [mm]
80 inv_cell_size_ = 1.0f / cell_size_;
81 grid_size_ = map_.getSize();
82 map_.convertToPseudoSDF();
83 }
84
85 /// Templated call operator – fully autodiff-friendly
86 template <typename T>
87 bool
88 operator()(const T* const x_ptr,
89 const T* const y_ptr,
90 const T* const theta_ptr,
91 T* out_cost) const
92 {
93 // Idea: First get indices for accessing using provided methods
94 // Then calculate interpolation using Jet types
95
96 const auto rotation_indices = closestRotationIndexFromDegreesBilinear(*theta_ptr);
97 const auto position_indices = toIndexBilinear(Eigen::Matrix<T, 2, 1>{*x_ptr, *y_ptr});
98 const auto combined_interpolation =
99 combineLinearInterpolationResults(rotation_indices, position_indices);
100 out_cost[0] = combined_interpolation.template calcWeightedAccess<T>(
101 [this](const std::tuple<int, Eigen::Array<int, 2, 1, 0, 2, 1>>& idx) -> T
102 {
103 auto& [rot, pos] = idx;
104 Costmap3D::Index posIdx = {pos.x(), pos.y()};
105 auto v = map_.value(posIdx, rot);
106
107 if (!v.has_value())
108 {
109 // Masked or out-of-bounds cell → treat as deep obstacle so the
110 // optimizer (and hill-climber) stay away from unknown regions.
111 return T(-100.0);
112 }
113
114 T value = T(v.value());
115
116 // NOTE: We do NOT subtract an artificial safety margin here.
117 // The Costmap3D already stores the true signed distance of the full
118 // robot footprint to obstacles (with obstacleSafetyMargin baked in
119 // during construction). Adding another margin here would distort the
120 // distance field and break the collision gradient.
121 return value;
122 });
123 return true;
124 }
125
126 /// Query the costmap at a single discrete orientation (no theta interpolation).
127 /// Position is still bilinearly interpolated. Differentiable w.r.t. (x,y).
128 template <typename T>
129 bool
130 queryDiscreteOrientation(const T* const x_ptr,
131 const T* const y_ptr,
132 int rot_index,
133 T* out_cost) const
134 {
135 const auto position_indices = toIndexBilinear(Eigen::Matrix<T, 2, 1>{*x_ptr, *y_ptr});
136
137 LinearInterpolationResult<Costmap3D::RotationIndex, T, 1> rot_result;
138 rot_result.indices[0] = wrapRotationIndex(rot_index);
139 rot_result.weights[0] = T(1.0);
140
141 const auto combined_interpolation =
142 combineLinearInterpolationResults(rot_result, position_indices);
143 out_cost[0] = combined_interpolation.template calcWeightedAccess<T>(
144 [this](const std::tuple<int, Eigen::Array<int, 2, 1, 0, 2, 1>>& idx) -> T
145 {
146 auto& [rot, pos] = idx;
147 Costmap3D::Index posIdx = {pos.x(), pos.y()};
148 auto v = map_.value(posIdx, rot);
149 if (!v.has_value())
150 return T(-100.0);
151 return T(v.value());
152 });
153 return true;
154 }
155
156 private:
158
159 float cell_size_;
160 float inv_cell_size_;
162
163 template <typename T>
164 static float
165 toFloat(const T& val)
166 {
167 if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>)
168 return static_cast<float>(val);
169 else
170 return static_cast<float>(val.a); // Jet
171 }
172
173 // ----------- helper: wrap [0,360) --------------
174 template <typename T>
175 static T
176 wrapDegrees(const T& deg)
177 {
178 T d = deg;
179 d = d - T(360.0) * ceres::floor(d / T(360.0));
180 if (d > T(360.0))
181 d -= T(360.0);
182 if (d < T(0.0))
183 d += T(360.0);
184 return d;
185 }
186
187 int
188 wrapRotationIndex(const int index) const
189 {
190 int d = index;
191 if (d > int(map_.parameters.orientations - 1))
192 d -= int(map_.parameters.orientations);
193 if (d < int(0.0))
194 d += int(map_.parameters.orientations);
195 return d;
196 }
197
198 template <typename T>
199 LinearInterpolationResult<Costmap3D::RotationIndex, T, 2>
200 closestRotationIndexFromDegreesBilinear(const T& degrees_in) const
201 {
202 const T degrees = wrapDegrees(degrees_in);
203 const T deg_per_orientation = T(360.F) / T(map_.parameters.orientations);
204
205 // simply floor for lower one
206 const int lower_index = wrapRotationIndex(
207 static_cast<int>(toFloat(ceres::floor(degrees / deg_per_orientation))));
208 const int higher_index = wrapRotationIndex(lower_index + 1);
209
210 if (lower_index < 0 || lower_index > map_.parameters.orientations)
211 {
212 ARMARX_DEBUG << "lower_index out of range: " << lower_index;
213 ARMARX_DEBUG << "degrees: " << degrees;
214 ARMARX_DEBUG << VAROUT(deg_per_orientation);
215 }
216 if (lower_index < 0 || higher_index > map_.parameters.orientations)
217 {
218 ARMARX_DEBUG << "higher_index out of range: " << higher_index;
219 }
220
221 const T error_to_lower_normalized =
222 (degrees - T(lower_index) * deg_per_orientation) / deg_per_orientation;
223
224 return {.indices = {lower_index, higher_index},
225 .weights = {T(1) - error_to_lower_normalized, error_to_lower_normalized}};
226 }
227
228 template <typename T>
229 static Eigen::Transform<T, 2, 1>
230 convertTransform(const Eigen::Transform<float, 2, 1>& tf)
231 {
232 Eigen::Transform<T, 2, 1> out;
233 out.matrix() = tf.matrix().template cast<T>();
234 return out;
235 }
236
237 template <typename T>
238 LinearInterpolationResult<Costmap3D::Index, T, 4>
239 toIndexBilinear(const Eigen::Matrix<T, 2, 1>& globalPosition) const
240 {
241 // Transform world → local
242 auto tfT = convertTransform<T>(map_.global_T_Costmap3D);
243 Eigen::Matrix<T, 2, 1> localPosition = tfT.inverse() * globalPosition;
244
245 const T cell = T(map_.parameters.cellSize);
246
247 // Compute continuous cell coordinates
248 const T vX = (localPosition.x() - cell / T(2) - T(map_.sceneBounds.min.x())) / cell;
249
250 const T vY = (localPosition.y() - cell / T(2) - T(map_.sceneBounds.min.y())) / cell;
251
252 // Bilinear kernel: continuous cell coordinates must be bracketed
253 const T vX_shift = vX - T(0.01);
254 const T vY_shift = vY - T(0.01);
255
256 // Jet-safe floor/ceil
257 int iXlow = static_cast<int>(toFloat(ceres::floor(vX_shift)));
258 int iXhigh = static_cast<int>(toFloat(ceres::ceil(vX_shift)));
259 int iYlow = static_cast<int>(toFloat(ceres::floor(vY_shift)));
260 int iYhigh = static_cast<int>(toFloat(ceres::ceil(vY_shift)));
261
262 // Clamp to map bounds
263 const auto size = map_.getSize();
264 int iXlow_cl = std::clamp(iXlow, 0, size.x() - 1);
265 int iXhigh_cl = std::clamp(iXhigh, 0, size.x() - 1);
266 int iYlow_cl = std::clamp(iYlow, 0, size.y() - 1);
267 int iYhigh_cl = std::clamp(iYhigh, 0, size.y() - 1);
268
269 // Bilinear weights (fully Jet-differentiable).
270 // Note: we intentionally use the UNCLAMPED indices for weights so that
271 // w00 + w10 + w01 + w11 == 1 even when the point is outside the grid
272 // and the clamped indices collapse to the same cell. This gives
273 // correct nearest-neighbour extrapolation at the boundary.
274 const T w00 = (T(iXhigh) - vX) * (T(iYhigh) - vY); // (low, low)
275 const T w10 = (vX - T(iXlow)) * (T(iYhigh) - vY); // (high, low)
276 const T w01 = (T(iXhigh) - vX) * (vY - T(iYlow)); // (low, high)
277 const T w11 = (vX - T(iXlow)) * (vY - T(iYlow)); // (high, high)
278
279 LinearInterpolationResult<Costmap3D::Index, T, 4> result;
280
281 result.indices[0] = {iXlow_cl, iYlow_cl};
282 result.indices[1] = {iXhigh_cl, iYlow_cl};
283 result.indices[2] = {iXlow_cl, iYhigh_cl};
284 result.indices[3] = {iXhigh_cl, iYhigh_cl};
285
286 result.weights[0] = w00;
287 result.weights[1] = w10;
288 result.weights[2] = w01;
289 result.weights[3] = w11;
290
291 // Degeneracy guard: when the query point lands exactly on a grid line
292 // (iXlow == iXhigh or iYlow == iYhigh), the bilinear weights sum to 0
293 // and would incorrectly return 0 regardless of the actual cell value.
294 // Fall back to nearest-neighbour in that case.
295 T sumW = w00 + w10 + w01 + w11;
296 if (ceres::abs(sumW) < T(1e-6))
297 {
298 T bestDist = T(std::numeric_limits<double>::max());
299 int bestIdx = 0;
300 for (int k = 0; k < 4; ++k)
301 {
302 T ddx = T(result.indices[k].x()) - vX;
303 T ddy = T(result.indices[k].y()) - vY;
304 T d = ddx * ddx + ddy * ddy;
305 if (d < bestDist)
306 {
307 bestDist = d;
308 bestIdx = k;
309 }
310 }
311 for (int k = 0; k < 4; ++k)
312 result.weights[k] = T(0);
313 result.weights[bestIdx] = T(1);
314 }
315
316 return result;
317 }
318 };
319} // namespace armarx::navigation::algorithms::orientation_aware::smoothing
#define float
Definition 16_Level.h:22
uint8_t index
#define VAROUT(x)
bool operator()(const T *const x_ptr, const T *const y_ptr, const T *const theta_ptr, T *out_cost) const
Templated call operator – fully autodiff-friendly.
Costmap3DWrapper(const armarx::navigation::algorithms::orientation_aware::Costmap3D &map)
bool queryDiscreteOrientation(const T *const x_ptr, const T *const y_ptr, int rot_index, T *out_cost) const
Query the costmap at a single discrete orientation (no theta interpolation).
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
float toFloat(const std::string &input)
Converts a string to float and uses always dot as seperator.
int orientations
How many orientations of the robot each cell contains.
Definition Costmap3D.h:38