$darkmode
clothoid_fit.hpp
Go to the documentation of this file.
1 /*
2  * Copyright 2023 Robert Bosch GmbH
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  * SPDX-License-Identifier: Apache-2.0
17  */
23 #pragma once
24 
25 #include <math.h> // for atan
26 #include <Eigen/Geometry> // for Isometry3d
27 #include <map> // for map
28 #include <memory> // for shared_ptr<>
29 #include <string> // for string
30 #include <vector> // for vector
31 
32 #include <cloe/component.hpp> // for Component, Json
33 #include <cloe/component/frustum.hpp> // for Frustum
34 #include <cloe/component/lane_boundary.hpp> // for LaneBoundary
35 #include <cloe/component/lane_sensor.hpp> // for LaneBoundarySensor
36 #include <cloe/conf/action.hpp> // for actions::ConfigureFactory
37 #include <cloe/handler.hpp> // for FromJson, ToJson
38 #include <cloe/registrar.hpp> // for Registrar
39 #include <cloe/sync.hpp> // for Sync
40 #include <cloe/trigger/set_action.hpp> // for actions::SetVariableActionFactory
41 
42 #include "g1_fitting.hpp" // for calc_clothoid
43 
44 namespace cloe {
45 
46 struct ClothoidFitConf : public Confable {
47  bool enabled = true;
48  bool frustum_culling = true;
49 
50  public:
51  ClothoidFitConf() = default;
52  virtual ~ClothoidFitConf() noexcept = default;
53 
54  CONFABLE_SCHEMA(ClothoidFitConf) {
55  return fable::Schema{
56  {"enable", make_schema(&enabled, "enable or disable component")},
57  {"frustum_culling", make_schema(&frustum_culling, "enable or disable frustum culling")},
58  };
59  }
60 };
61 
62 namespace component {
63 
69 double estimate_lane_boundary_length(const std::vector<Eigen::Vector3d>& points) {
70  double length = 0.0;
71  for (uint64_t i = 1; i < points.size(); ++i) {
72  length += (points[i] - points[i - 1]).norm();
73  }
74  return length;
75 }
76 
77 void cleanup_lane_boundary_points(std::vector<Eigen::Vector3d>& points) {
78  // Points shall have at least 1cm distance for robust clothoid estimation.
79  const double min_dist = 0.01;
80  std::vector<Eigen::Vector3d> pts_mod{points.front()};
81  for (uint64_t i = 1; i < points.size(); ++i) {
82  if ((points[i] - points[i - 1]).norm() > min_dist) {
83  pts_mod.push_back(points[i]);
84  }
85  }
86  points = pts_mod;
87 }
88 
89 // Keep track on which side of each frustum plane a point is located
90 const uint8_t PLANE_RIGHT_OK{0b0000'0001};
91 const uint8_t PLANE_LEFT_OK{0b0000'0010};
92 const uint8_t PLANE_LOW_OK{0b0000'0100};
93 const uint8_t PLANE_HIGH_OK{0b0000'1000};
94 const uint8_t PLANE_NEAR_OK{0b0001'0000};
95 const uint8_t PLANE_FAR_OK{0b0010'0000};
96 const uint8_t all_set =
97  PLANE_RIGHT_OK | PLANE_LEFT_OK | PLANE_LOW_OK | PLANE_HIGH_OK | PLANE_NEAR_OK | PLANE_FAR_OK;
98 
99 uint8_t get_plane_mask(const Frustum& frustum, const Eigen::Vector3d& pt) {
100  double fov_h_l = -0.5 * frustum.fov_h + frustum.offset_h;
101  double fov_h_u = 0.5 * frustum.fov_h + frustum.offset_h;
102  double fov_v_l = -0.5 * frustum.fov_v + frustum.offset_v;
103  double fov_v_u = 0.5 * frustum.fov_v + frustum.offset_v;
104 
105  if (fov_h_l < -M_PI || fov_h_u > M_PI || fov_v_l < -M_PI || fov_v_u > M_PI) {
106  // clang-format off
107  throw std::runtime_error(fmt::format(
108  "The get_plane_mask function of the clothoid fit plugin only works in the range [-M_PI, M_PI]."
109  "However, the following values were calculated and at least one condition was not met: \n"
110  "fov_h_l: {}; has to be greater equal to -M_PI, \n"
111  "fov_h_u: {}; has to be smaller equal to M_PI, \n"
112  "fov_v_l: {}; has to be greater equal to -M_PI,\n"
113  "fov_v_u: {}; has to be smaller equal to M_PI. \n",
114  fov_h_l, fov_h_u, fov_v_l, fov_v_u));
115  // clang-format on
116  }
117  double range_mult = cos(frustum.offset_h) * cos(frustum.offset_v);
118  uint8_t plane_mask{0};
119  // Horizontal field of view.
120  double x = pt.x() == 0.0 ? 1.0e-10 : pt.x();
121  double phi = atan2(pt.y(), x);
122  if (phi >= fov_h_l) {
123  // Right plane.
124  plane_mask |= PLANE_RIGHT_OK;
125  }
126  if (phi <= fov_h_u) {
127  // Left plane.
128  plane_mask |= PLANE_LEFT_OK;
129  }
130  // Vertical field of view.
131  phi = atan2(pt.z(), x);
132  if (phi >= fov_v_l) {
133  // Lower plane.
134  plane_mask |= PLANE_LOW_OK;
135  }
136  if (phi <= fov_v_u) {
137  // Upper plane.
138  plane_mask |= PLANE_HIGH_OK;
139  }
140  // Field of view distance range.
141  x = pt.x() * range_mult;
142  if (x >= frustum.clip_near) {
143  // Near plane.
144  plane_mask |= PLANE_NEAR_OK;
145  }
146  if (x <= frustum.clip_far) {
147  // Far plane.
148  plane_mask |= PLANE_FAR_OK;
149  }
150  return plane_mask;
151 }
152 
153 std::vector<Eigen::Vector3d> interpolate_to_frustum_planes(const Frustum& frustum,
154  const Eigen::Vector3d& pt1,
155  uint8_t mask1,
156  const Eigen::Vector3d& pt2,
157  uint8_t mask2) {
158  auto interpolate_point_on_plane = [](const Eigen::Vector3d& x1, const Eigen::Vector3d& x2,
159  const Eigen::Vector3d& xp,
160  const Eigen::Vector3d& nv) -> Eigen::Vector3d {
161  // Define line on which the interpolation point is located.
162  Eigen::Vector3d dir = x2 - x1; // direction of interplation line
163  double int_loc = nv.dot((xp - x1)) / nv.dot(dir);
164  return x1 + int_loc * dir;
165  };
166  // Shift interpolated points by 1 mm to the inside of the frustum.
167  double offset = 0.001;
168  std::vector<Eigen::Vector3d> pts_intrp;
169  // Use exclusive OR to check for candidates plane by plane.
170  if (((mask1 ^ mask2) & PLANE_RIGHT_OK) == PLANE_RIGHT_OK) {
171  // Interpolate to right plane.
172  double fov_h_l = -0.5 * frustum.fov_h + frustum.offset_h;
173  // Normal vector pointing towards the inside of the frustum.
174  Eigen::Vector3d nvec =
175  Eigen::Vector3d(cos(fov_h_l), sin(fov_h_l), 0).cross(Eigen::Vector3d(0, 0, -1));
176  nvec = nvec / nvec.norm();
177  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, Eigen::Vector3d(0, 0, 0), nvec);
178  pts_intrp.push_back(pt + offset * nvec);
179  }
180  if (((mask1 ^ mask2) & PLANE_LEFT_OK) == PLANE_LEFT_OK) {
181  // Interpolate to left plane.
182  double fov_h_u = 0.5 * frustum.fov_h + frustum.offset_h;
183  // Normal vector pointing towards the inside of the frustum.
184  Eigen::Vector3d nvec =
185  Eigen::Vector3d(cos(fov_h_u), sin(fov_h_u), 0).cross(Eigen::Vector3d(0, 0, 1));
186  nvec = nvec / nvec.norm();
187  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, Eigen::Vector3d(0, 0, 0), nvec);
188  pts_intrp.push_back(pt + offset * nvec);
189  }
190  if (((mask1 ^ mask2) & PLANE_LOW_OK) == PLANE_LOW_OK) {
191  // Interpolate to lower plane.
192  double fov_v_l = -0.5 * frustum.fov_v + frustum.offset_v;
193  // Normal vector pointing towards the inside of the frustum.
194  Eigen::Vector3d nvec =
195  Eigen::Vector3d(cos(fov_v_l), 0, sin(fov_v_l)).cross(Eigen::Vector3d(0, 1, 0));
196  nvec = nvec / nvec.norm();
197  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, Eigen::Vector3d(0, 0, 0), nvec);
198  pts_intrp.push_back(pt + offset * nvec);
199  }
200  if (((mask1 ^ mask2) & PLANE_HIGH_OK) == PLANE_HIGH_OK) {
201  // Interpolate to higher plane.
202  double fov_v_u = 0.5 * frustum.fov_v + frustum.offset_v;
203  // Normal vector pointing towards the inside of the frustum.
204  Eigen::Vector3d nvec =
205  Eigen::Vector3d(cos(fov_v_u), 0, sin(fov_v_u)).cross(Eigen::Vector3d(0, -1, 0));
206  nvec = nvec / nvec.norm();
207  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, Eigen::Vector3d(0, 0, 0), nvec);
208  pts_intrp.push_back(pt + offset * nvec);
209  }
210  if (((mask1 ^ mask2) & PLANE_NEAR_OK) == PLANE_NEAR_OK) {
211  // Interpolate to near plane.
212  double a = cos(frustum.offset_v);
213  Eigen::Vector3d nvec = Eigen::Vector3d(a * cos(frustum.offset_h), a * sin(frustum.offset_h),
214  sin(frustum.offset_v));
215  // Normal vector pointing towards the inside of the frustum.
216  nvec = nvec / nvec.norm();
217  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, frustum.clip_near * nvec, nvec);
218  pts_intrp.push_back(pt + offset * nvec);
219  }
220  if (((mask1 ^ mask2) & PLANE_FAR_OK) == PLANE_FAR_OK) {
221  // Interpolate to far plane.
222  double a = cos(frustum.offset_v);
223  Eigen::Vector3d nvec = Eigen::Vector3d(a * cos(frustum.offset_h), a * sin(frustum.offset_h),
224  sin(frustum.offset_v));
225  // Normal vector pointing towards the inside of the frustum.
226  nvec = -1.0 * nvec / nvec.norm();
227  Eigen::Vector3d pt = interpolate_point_on_plane(pt1, pt2, -1.0 * frustum.clip_far * nvec, nvec);
228  pts_intrp.push_back(pt + offset * nvec);
229  }
230  // Remove points that are not on frustum boundary.
231  auto it_rm = std::remove_if(
232  pts_intrp.begin(), pts_intrp.end(),
233  [&frustum](const Eigen::Vector3d& pt) { return get_plane_mask(frustum, pt) != all_set; });
234  pts_intrp.erase(it_rm, pts_intrp.end());
235  return pts_intrp;
236 }
237 
238 // Frustum culling radar approach adapted from
239 // https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/lighthouse3d_view_frustum_culling/index.html
240 void lane_boundary_point_culling(const Frustum& frustum, std::vector<Eigen::Vector3d>& points,
241  std::vector<Eigen::Vector3d>& pts_out_start,
242  std::vector<Eigen::Vector3d>& pts_out_end) {
243  std::vector<uint8_t> mask_out_start, mask_out_end;
244  // Keep only points inside the frustum.
245  std::vector<Eigen::Vector3d> pts_frustum;
246  pts_frustum.reserve(points.size());
247  bool found_pts{false};
248  for (const auto& pt : points) {
249  uint8_t plane_mask = get_plane_mask(frustum, pt);
250  // Remove point if not in frustum.
251  if (plane_mask == all_set) {
252  if (pts_out_end.size() > 0) {
253  // In case multiple segments of the polyline are inside the frustum, only keep the first visible segment.
254  break;
255  }
256  if (pts_frustum.size() == 0 && pts_out_start.size() > 0) {
257  // First point inside frustum and points outside frustum were found previously. Interpolate to frustum boundary.
258  auto pt_intrp = interpolate_to_frustum_planes(frustum, pts_out_start.back(),
259  mask_out_start.back(), pt, plane_mask);
260  if (pt_intrp.size() == 1) {
261  pts_frustum.push_back(pt_intrp.front());
262  } else {
263  throw cloe::ModelError("Interpolation of first point inside frustum should be unique.");
264  }
265  }
266  pts_frustum.push_back(pt);
267  found_pts = true;
268  } else if (!found_pts) {
269  // Point is outside field of view and no point inside frustum was found previously.
270  if (pts_out_start.size() == 0) {
271  // First point, no additional checks necessary.
272  pts_out_start.push_back(pt);
273  mask_out_start.push_back(plane_mask);
274  } else {
275  // Check special case where the lane boundary crosses the frustum, but none of the given points is located inside.
276  Eigen::Vector3d pt_prev = pts_out_start.back();
277  // If any of the frustum planes is crossed by the line between previous point and this point, interpolate to frustum boundary.
278  auto pts_intrp =
279  interpolate_to_frustum_planes(frustum, pt_prev, mask_out_start.back(), pt, plane_mask);
280  if (pts_intrp.size() > 0) {
281  // If there are multiple points, sort them by distance to the previous point.
282  std::sort(pts_intrp.begin(), pts_intrp.end(),
283  [pt_prev](const Eigen::Vector3d& pt1, const Eigen::Vector3d& pt2) {
284  return ((pt1 - pt_prev).norm() < (pt2 - pt_prev).norm());
285  });
286  // Append to points inside frustum.
287  pts_frustum.insert(pts_frustum.end(), pts_intrp.begin(), pts_intrp.end());
288  // This point is outside the frustum towards the end of the lane boundary.
289  pts_out_end.push_back(pt);
290  mask_out_end.push_back(plane_mask);
291  } else {
292  // No special case.
293  pts_out_start.push_back(pt);
294  mask_out_start.push_back(plane_mask);
295  }
296  }
297  } else {
298  // Point is outside field of view and at least one point inside was found previously.
299  if (pts_out_end.size() == 0) {
300  // Interpolate additional point to the frustum boundary.
301  auto pt_intrp =
302  interpolate_to_frustum_planes(frustum, pts_frustum.back(), all_set, pt, plane_mask);
303  if (pt_intrp.size() == 1) {
304  pts_frustum.push_back(pt_intrp.front());
305  } else {
306  throw cloe::ModelError("Interpolation of last point inside frustum should be unique.");
307  }
308  }
309  pts_out_end.push_back(pt);
310  mask_out_end.push_back(plane_mask);
311  }
312  }
313  points = pts_frustum;
314 }
315 
322 double calc_heading_angle(const Eigen::Vector3d& pt0, const Eigen::Vector3d& pt1) {
323  double dx = pt1.x() - pt0.x();
324  dx = dx == 0.0 ? 1.0e-10 : dx;
325  double slope = (pt1.y() - pt0.y()) / dx;
326  return atan(slope);
327 }
328 
329 // https://repository.uantwerpen.be/docman/irua/fb388b/3959.pdf
330 double calc_heading_angle_adv(const Eigen::Vector3d& ptm1, const Eigen::Vector3d& pt0,
331  const Eigen::Vector3d& pt1) {
332  double dx1 = pt1.x() - pt0.x();
333  dx1 = dx1 == 0.0 ? 1.0e-10 : dx1;
334  double dx0 = pt0.x() - ptm1.x();
335  dx0 = dx0 == 0.0 ? 1.0e-10 : dx0;
336  double dx = dx1 + dx0;
337  dx = dx == 0.0 ? 1.0e-10 : dx;
338  double slope = -1.0 * dx1 / (dx0 * dx) * ptm1.y() + (dx1 - dx0) / (dx0 * dx1) * pt0.y() +
339  dx0 / (dx1 * dx) * pt1.y();
340  return atan(slope);
341 }
342 
350 bool fit_clothoid(const cloe::Logger& logger, bool frustum_culling, const Frustum& frustum,
351  LaneBoundary& lb) {
352  if (estimate_lane_boundary_length(lb.points) < 0.5) {
353  // Discard tiny lane boundary snippets.
354  logger->debug("Discarding short lane boundary segment < 0.5m.");
355  return false;
356  }
357  cleanup_lane_boundary_points(lb.points);
358  // Store discarded points for advanced heading angle estimation.
359  std::vector<Eigen::Vector3d> pts_out_start, pts_out_end;
360  if (frustum_culling) {
361  lane_boundary_point_culling(frustum, lb.points, pts_out_start, pts_out_end);
362  }
363  if (lb.points.size() < 2) {
364  logger->debug("Clothoid fit requires at least two points.");
365  return false;
366  }
367  Eigen::Vector3d x0 = lb.points.at(0);
368  lb.dx_start = x0.x();
369  lb.dy_start = x0.y();
370  double hdg0;
371  if (pts_out_start.size() > 0) {
372  hdg0 = calc_heading_angle_adv(pts_out_start.back(), lb.points.at(0), lb.points.at(1));
373  } else {
374  hdg0 = calc_heading_angle(lb.points.at(0), lb.points.at(1));
375  }
376  lb.heading_start = hdg0;
377  int n = lb.points.size() - 1;
378  Eigen::Vector3d x1 = lb.points.at(n);
379  double hdg1;
380  if (pts_out_end.size() > 0) {
381  hdg1 = calc_heading_angle_adv(lb.points.at(n - 1), lb.points.at(n), pts_out_end.front());
382  } else {
383  hdg1 = calc_heading_angle(lb.points.at(n - 1), lb.points.at(n));
384  }
385  // Compute clothoid parameters curv_hor_start, curv_hor_change and dx_end.
386  g1_fit::calc_clothoid(x0.x(), x0.y(), hdg0, x1.x(), x1.y(), hdg1, lb.curv_hor_start,
387  lb.curv_hor_change, lb.dx_end);
388 
389  return true;
390 }
391 
393  public:
394  LaneBoundaryClothoidFit(const std::string& name, const ClothoidFitConf& conf,
395  std::shared_ptr<LaneBoundarySensor> sensor)
396  : LaneBoundarySensor(name), config_(conf), sensor_in_(sensor) {}
397  virtual ~LaneBoundaryClothoidFit() noexcept = default;
398 
399  void enroll(Registrar& r) override {
400  r.register_action(std::make_unique<actions::ConfigureFactory>(
401  &this->config_, "config", "configure clothoid fitting component"));
403  "activation", "switch clothoid fitting on/off", "enable", &config_.enabled);
404  }
405 
406  const LaneBoundaries& sensed_lane_boundaries() const override {
407  if (cached_) {
408  return lbs_;
409  }
410  if (config_.enabled) {
411  for (auto& kv : sensor_in_->sensed_lane_boundaries()) {
412  auto lb = kv.second;
413  if (fit_clothoid(logger(), config_.frustum_culling, this->frustum(), lb)) {
414  lbs_[kv.first] = lb;
415  }
416  }
417  } else {
418  // Copy all lane boundaries from the source sensor component.
419  lbs_ = sensor_in_->sensed_lane_boundaries();
420  }
421  cached_ = true;
422  return lbs_;
423  }
424 
425  const Frustum& frustum() const override { return sensor_in_->frustum(); }
426 
427  const Eigen::Isometry3d& mount_pose() const override { return sensor_in_->mount_pose(); }
428 
435  Duration process(const Sync& sync) override {
436  // This currently shouldn't do anything, but this class acts as a prototype
437  // for How It Should Be Done.
439  if (t < sync.time()) {
440  return t;
441  }
442 
443  // Process the underlying sensor and clear the cache.
444  t = sensor_in_->process(sync);
445  time_ = t;
446  clear_cache();
447  return t;
448  }
449 
450  void reset() override {
452  sensor_in_->reset();
453  clear_cache();
454  }
455 
456  void abort() override {
458  sensor_in_->abort();
459  }
460 
461  protected:
462  void clear_cache() {
463  lbs_.clear();
464  cached_ = false;
465  }
466 
467  private:
468  ClothoidFitConf config_;
469  std::shared_ptr<LaneBoundarySensor> sensor_in_; // provides input data
470  mutable bool cached_;
471  mutable LaneBoundaries lbs_;
472  Duration time_{0};
473 };
474 
475 } // namespace component
476 } // namespace cloe
Duration process(const Sync &sync) override
Definition: component.hpp:181
void abort() override
Definition: component.hpp:191
void reset() override
Definition: component.hpp:186
virtual Logger logger() const
Definition: entity.hpp:101
const std::string & name() const
Definition: entity.hpp:67
Definition: lane_sensor.hpp:33
Definition: lane_boundary.hpp:36
Definition: model.hpp:62
Definition: registrar.hpp:116
virtual void register_action(std::unique_ptr< ActionFactory > &&)=0
Definition: sync.hpp:34
virtual Duration time() const =0
Definition: set_action.hpp:86
Definition: clothoid_fit.hpp:392
void enroll(Registrar &r) override
Definition: clothoid_fit.hpp:399
const LaneBoundaries & sensed_lane_boundaries() const override
Definition: clothoid_fit.hpp:406
void abort() override
Definition: clothoid_fit.hpp:456
const Eigen::Isometry3d & mount_pose() const override
Definition: clothoid_fit.hpp:427
const Frustum & frustum() const override
Definition: clothoid_fit.hpp:425
void reset() override
Definition: clothoid_fit.hpp:450
Duration process(const Sync &sync) override
Definition: clothoid_fit.hpp:435
Definition: confable.hpp:98
Definition: schema.hpp:173
std::chrono::nanoseconds Duration
Definition: cloe_fwd.hpp:36
double estimate_lane_boundary_length(const std::vector< Eigen::Vector3d > &points)
Definition: clothoid_fit.hpp:69
bool fit_clothoid(const cloe::Logger &logger, bool frustum_culling, const Frustum &frustum, LaneBoundary &lb)
Definition: clothoid_fit.hpp:350
double calc_heading_angle(const Eigen::Vector3d &pt0, const Eigen::Vector3d &pt1)
Definition: clothoid_fit.hpp:322
Definition: clothoid_fit.hpp:46
Definition: frustum.hpp:37