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 
23 using namespace smoother_utils; // NOLINT
24 using namespace nav2_util::geometry_utils; // NOLINT
25 using namespace std::chrono; // NOLINT
26 using nav2::declare_parameter_if_not_declared;
27 
28 void SavitzkyGolaySmoother::configure(
29  const nav2::LifecycleNode::WeakPtr & parent,
30  std::string name, std::shared_ptr<tf2_ros::Buffer>/*tf*/,
31  std::shared_ptr<nav2_costmap_2d::CostmapSubscriber>/*costmap_sub*/,
32  std::shared_ptr<nav2_costmap_2d::FootprintSubscriber>/*footprint_sub*/)
33 {
34  auto node = parent.lock();
35  logger_ = node->get_logger();
36 
37  declare_parameter_if_not_declared(
38  node, name + ".do_refinement", rclcpp::ParameterValue(true));
39  declare_parameter_if_not_declared(
40  node, name + ".refinement_num", rclcpp::ParameterValue(2));
41  declare_parameter_if_not_declared(
42  node, name + ".enforce_path_inversion", rclcpp::ParameterValue(true));
43  declare_parameter_if_not_declared(
44  node, name + ".window_size", rclcpp::ParameterValue(7));
45  declare_parameter_if_not_declared(
46  node, name + ".poly_order", rclcpp::ParameterValue(3));
47  node->get_parameter(name + ".do_refinement", do_refinement_);
48  node->get_parameter(name + ".refinement_num", refinement_num_);
49  node->get_parameter(name + ".enforce_path_inversion", enforce_path_inversion_);
50  node->get_parameter(name + ".window_size", window_size_);
51  node->get_parameter(name + ".poly_order", poly_order_);
52  if (window_size_ % 2 == 0 || window_size_ <= 2) {
54  "Savitzky-Golay Smoother requires an odd window size of 3 or greater");
55  }
56  half_window_size_ = (window_size_ - 1) / 2;
57  calculateCoefficients();
58 }
59 
60 // For more details on calculating Savitzky–Golay filter coefficients,
61 // see: https://www.colmryan.org/posts/savitsky_golay/
63 {
64  // We construct the Vandermonde matrix here
65  Eigen::VectorXd v = Eigen::VectorXd::LinSpaced(window_size_, -half_window_size_,
66  half_window_size_);
67  Eigen::MatrixXd x = Eigen::MatrixXd::Ones(window_size_, poly_order_ + 1);
68  for(int i = 1; i <= poly_order_; i++) {
69  x.col(i) = (x.col(i - 1).array() * v.array()).matrix();
70  }
71  // Compute the pseudoinverse of X, (X^T * X)^-1 * X^T
72  Eigen::MatrixXd coeff_mat = (x.transpose() * x).inverse() * x.transpose();
73 
74  // Extract the smoothing coefficients
75  sg_coeffs_ = coeff_mat.row(0).transpose();
76 }
77 
79  nav_msgs::msg::Path & path,
80  const rclcpp::Duration & max_time)
81 {
82  steady_clock::time_point start = steady_clock::now();
83  double time_remaining = max_time.seconds();
84 
85  bool success = true, reversing_segment;
86  nav_msgs::msg::Path curr_path_segment;
87  curr_path_segment.header = path.header;
88 
89  std::vector<PathSegment> path_segments{
90  PathSegment{0u, static_cast<unsigned int>(path.poses.size() - 1)}};
91  if (enforce_path_inversion_) {
92  path_segments = findDirectionalPathSegments(path);
93  }
94 
95  // Minimum point size to smooth is SG filter size + start + end
96  unsigned int minimum_points = window_size_ + 2;
97  for (unsigned int i = 0; i != path_segments.size(); i++) {
98  if (path_segments[i].end - path_segments[i].start > minimum_points) {
99  // Populate path segment
100  curr_path_segment.poses.clear();
101  std::copy(
102  path.poses.begin() + path_segments[i].start,
103  path.poses.begin() + path_segments[i].end + 1,
104  std::back_inserter(curr_path_segment.poses));
105 
106  // Make sure we're still able to smooth with time remaining
107  steady_clock::time_point now = steady_clock::now();
108  time_remaining = max_time.seconds() - duration_cast<duration<double>>(now - start).count();
109 
110  if (time_remaining <= 0.0) {
111  RCLCPP_WARN(
112  logger_,
113  "Smoothing time exceeded allowed duration of %0.2f.", max_time.seconds());
114  throw nav2_core::SmootherTimedOut("Smoothing time exceed allowed duration");
115  }
116 
117  // Smooth path segment
118  success = success && smoothImpl(curr_path_segment, reversing_segment);
119 
120  // Assemble the path changes to the main path
121  std::copy(
122  curr_path_segment.poses.begin(),
123  curr_path_segment.poses.end(),
124  path.poses.begin() + path_segments[i].start);
125  }
126  }
127 
128  return success;
129 }
130 
132  nav_msgs::msg::Path & path,
133  bool & reversing_segment)
134 {
135  const unsigned int & path_size = path.poses.size();
136 
137  // Convert PoseStamped to Eigen
138  auto toEigenVec = [](const geometry_msgs::msg::PoseStamped & pose) -> Eigen::Vector2d {
139  return {pose.pose.position.x, pose.pose.position.y};
140  };
141 
142  auto applyFilterOverAxes =
143  [&](std::vector<geometry_msgs::msg::PoseStamped> & plan_pts,
144  const std::vector<Eigen::Vector2d> & init_plan_pts) -> void
145  {
146  // First point is fixed
147  for (unsigned int idx = 1; idx != path_size - 1; idx++) {
148  Eigen::Vector2d accum(0.0, 0.0);
149 
150  for(int j = -half_window_size_; j <= half_window_size_; j++) {
151  int path_idx = std::clamp<int>(idx + j, 0, path_size - 1);
152  accum += sg_coeffs_(j + half_window_size_) * init_plan_pts[path_idx];
153  }
154  plan_pts[idx].pose.position.x = accum.x();
155  plan_pts[idx].pose.position.y = accum.y();
156  }
157  };
158 
159  std::vector<Eigen::Vector2d> initial_path_poses(path.poses.size());
160  std::transform(path.poses.begin(), path.poses.end(),
161  initial_path_poses.begin(), toEigenVec);
162  applyFilterOverAxes(path.poses, initial_path_poses);
163 
164  // Let's do additional refinement, it shouldn't take more than a couple milliseconds
165  if (do_refinement_) {
166  for (int i = 0; i < refinement_num_; i++) {
167  std::vector<Eigen::Vector2d> reined_initial_path_poses(path.poses.size());
168  std::transform(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  updateApproximatePathOrientations(path, reversing_segment);
175  return true;
176 }
177 
178 } // namespace nav2_smoother
179 
180 #include "pluginlib/class_list_macros.hpp"
smoother interface that acts as a virtual base class for all smoother plugins
Definition: smoother.hpp:38
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.