Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
savitzky_golay_smoother.cpp
1 // Copyright (c) 2022, Samsung Research America
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License. Reserved.
14 
15 #include <vector>
16 #include <memory>
17 #include "nav2_smoother/savitzky_golay_smoother.hpp"
18 #include "nav2_core/smoother_exceptions.hpp"
19 
20 namespace nav2_smoother
21 {
22 using namespace nav2_util::geometry_utils; // NOLINT
23 using namespace std::chrono; // NOLINT
25 
26 void SavitzkyGolaySmoother::configure(
27  const nav2::LifecycleNode::WeakPtr & parent,
28  std::string name, nav2::TransformBuffer::SharedPtr/*tf*/,
29  std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>/*costmap_sub*/,
30  std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>/*footprint_sub*/)
31 {
32  auto node = parent.lock();
33  logger_ = node->get_logger();
34 
35  do_refinement_ = node->declare_or_get_parameter(
36  name + ".do_refinement", true);
37  refinement_num_ = node->declare_or_get_parameter(
38  name + ".refinement_num", 2);
39  enforce_path_inversion_ = node->declare_or_get_parameter(
40  name + ".enforce_path_inversion", true);
41  window_size_ = node->declare_or_get_parameter(
42  name + ".window_size", 7);
43  poly_order_ = node->declare_or_get_parameter(
44  name + ".poly_order", 3);
45 
46  if (window_size_ % 2 == 0 || window_size_ <= 2) {
48  "Savitzky-Golay Smoother requires an odd window size of 3 or greater");
49  }
50  half_window_size_ = (window_size_ - 1) / 2;
51  calculateCoefficients();
52 }
53 
54 // For more details on calculating Savitzky–Golay filter coefficients,
55 // see: https://www.colmryan.org/posts/savitsky_golay/
57 {
58  // We construct the Vandermonde matrix here
59  Eigen::VectorXd v = Eigen::VectorXd::LinSpaced(
60  window_size_, -half_window_size_,
61  half_window_size_);
62  Eigen::MatrixXd x = Eigen::MatrixXd::Ones(window_size_, poly_order_ + 1);
63  for (int i = 1; i <= poly_order_; i++) {
64  x.col(i) = (x.col(i - 1).array() * v.array()).matrix();
65  }
66  // Compute the pseudoinverse of X by solving the least-squares problem X * C = I.
67  // HouseholderQR factors X into an orthogonal matrix and an upper-triangular matrix,
68  // then solves for the coefficient matrix C without explicitly inverting X.
69  Eigen::MatrixXd coeff_mat =
70  x.householderQr().solve(Eigen::MatrixXd::Identity(window_size_, window_size_));
71 
72  // Extract the smoothing coefficients
73  sg_coeffs_ = coeff_mat.row(0).transpose();
74 }
75 
77  nav_msgs::msg::Path & path,
78  const rclcpp::Duration & max_time)
79 {
80  steady_clock::time_point start = steady_clock::now();
81  double time_remaining = max_time.seconds();
82 
83  bool success = true, reversing_segment;
84  nav_msgs::msg::Path curr_path_segment;
85  curr_path_segment.header = path.header;
86 
87  std::vector<PathSegment> path_segments{
88  PathSegment{0u, static_cast<unsigned int>(path.poses.size() - 1)}};
89  if (enforce_path_inversion_) {
90  path_segments = nav2_util::findDirectionalPathSegments(path);
91  }
92 
93  // Minimum point size to smooth is SG filter size + start + end
94  unsigned int minimum_points = window_size_ + 2;
95  for (unsigned int i = 0; i != path_segments.size(); i++) {
96  if (path_segments[i].end - path_segments[i].start > minimum_points) {
97  // Populate path segment
98  curr_path_segment.poses.clear();
99  std::copy(
100  path.poses.begin() + path_segments[i].start,
101  path.poses.begin() + path_segments[i].end + 1,
102  std::back_inserter(curr_path_segment.poses));
103 
104  // Make sure we're still able to smooth with time remaining
105  steady_clock::time_point now = steady_clock::now();
106  time_remaining = max_time.seconds() - duration_cast<duration<double>>(now - start).count();
107 
108  if (time_remaining <= 0.0) {
109  RCLCPP_WARN(
110  logger_,
111  "Smoothing time exceeded allowed duration of %0.2f.", max_time.seconds());
112  throw nav2_core::SmootherTimedOut("Smoothing time exceed allowed duration");
113  }
114 
115  // Smooth path segment
116  success = success && smoothImpl(curr_path_segment, reversing_segment);
117 
118  // Assemble the path changes to the main path
119  std::copy(
120  curr_path_segment.poses.begin(),
121  curr_path_segment.poses.end(),
122  path.poses.begin() + path_segments[i].start);
123  }
124  }
125 
126  return success;
127 }
128 
130  nav_msgs::msg::Path & path,
131  bool & reversing_segment)
132 {
133  const unsigned int & path_size = path.poses.size();
134 
135  // Convert PoseStamped to Eigen
136  auto toEigenVec = [](const geometry_msgs::msg::PoseStamped & pose) -> Eigen::Vector2d {
137  return {pose.pose.position.x, pose.pose.position.y};
138  };
139 
140  auto applyFilterOverAxes =
141  [&](std::vector<geometry_msgs::msg::PoseStamped> & plan_pts,
142  const std::vector<Eigen::Vector2d> & init_plan_pts) -> void
143  {
144  // First point is fixed
145  for (unsigned int idx = 1; idx != path_size - 1; idx++) {
146  Eigen::Vector2d accum(0.0, 0.0);
147 
148  for (int j = -half_window_size_; j <= half_window_size_; j++) {
149  int path_idx = std::clamp<int>(idx + j, 0, path_size - 1);
150  accum += sg_coeffs_(j + half_window_size_) * init_plan_pts[path_idx];
151  }
152  plan_pts[idx].pose.position.x = accum.x();
153  plan_pts[idx].pose.position.y = accum.y();
154  }
155  };
156 
157  std::vector<Eigen::Vector2d> initial_path_poses(path.poses.size());
158  std::transform(
159  path.poses.begin(), path.poses.end(),
160  initial_path_poses.begin(), toEigenVec);
161  applyFilterOverAxes(path.poses, initial_path_poses);
162 
163  // Let's do additional refinement, it shouldn't take more than a couple milliseconds
164  if (do_refinement_) {
165  for (int i = 0; i < refinement_num_; i++) {
166  std::vector<Eigen::Vector2d> reined_initial_path_poses(path.poses.size());
167  std::transform(
168  path.poses.begin(), path.poses.end(),
169  reined_initial_path_poses.begin(), toEigenVec);
170  applyFilterOverAxes(path.poses, reined_initial_path_poses);
171  }
172  }
173 
174  nav2_util::updateApproximatePathOrientations(path, reversing_segment);
175  return true;
176 }
177 
178 } // namespace nav2_smoother
179 
180 #include "pluginlib/class_list_macros.hpp"
181 #include "nav2_ros_common/tf2_factories.hpp"
smoother interface that acts as a virtual base class for all smoother plugins
Definition: smoother.hpp:37
A path smoother implementation using Savitzky Golay filters.
void calculateCoefficients()
Method to calculate SavitzkyGolay Coefficients.
bool smoothImpl(nav_msgs::msg::Path &path, bool &reversing_segment)
Smoother method - does the smoothing on a segment.
bool smooth(nav_msgs::msg::Path &path, const rclcpp::Duration &max_time) override
Method to smooth given path.
A segment of a path in start/end indices.