navigation2 icon indicating copy to clipboard operation
navigation2 copied to clipboard

Add Savitzky–Golay Path Smoother

Open SteveMacenski opened this issue 3 years ago • 1 comments

I did some quick prototypes and pretty happy with the results of applying SG filters to the problem of path smoothing. Unlike the simple smoother, this won't deviate from the path much, so no need to collision check. Its also analytical so no numerous iterations to converge.

I found the 7-point quadratic formulation to work quite well. 5 point did OK if we did so iteratively, but 7 point was actually good alone.

Task: take R&D code below to clean up and add into a new path smoother algorithm.

Questions / features:

  • Include refinement option (e.g. run recursively a few times)
  • Add in orientation vectors
  • Support for reverse segmentation
  • Does this maintain turning boundary conditions on minimum curvature? -- Seems so
  • Default include in NavFn to remove odd artifacts?
  // std::array<double, 5> filter{-3.0/35.0, 12.0/35.0, 17.0/35.0, 12.0/35.0, -3.0/35.0};
  std::array<double, 7> filter{-2.0/21.0, 3.0/21.0, 6.0/21.0, 7.0/21.0, 6.0/21.0, 3.0/21.0, -2.0/21.0};
  const unsigned int & num_sequences = plan.poses.size();

  auto applyFilter = [&](const std::vector<double> & data) -> float {
      float val = 0.0;
      for (unsigned int i = 0; i != filter.size(); i++) {
        val += filter[i] * data[i];
      }

      return val;
    };

  auto applyFilterOverX =
    [&](nav_msgs::msg::Path & path) -> void
    {
      unsigned int idx = 0;
      // TODO update for 7 pt
      // path.poses[0].pose.position.x = applyFilter({
      //   path.poses[0].pose.position.x,
      //   path.poses[0].pose.position.x,
      //   path.poses[0].pose.position.x,
      //   path.poses[1].pose.position.x,
      //   path.poses[2].pose.position.x});

      // idx++;
      // path.poses[1].pose.position.x = applyFilter({
      //   path.poses[0].pose.position.x,
      //   path.poses[1].pose.position.x,
      //   path.poses[1].pose.position.x,
      //   path.poses[2].pose.position.x,
      //   path.poses[3].pose.position.x});

      for (unsigned int idx = 3; idx != num_sequences - 3; idx++) {
        path.poses[idx].pose.position.x = applyFilter({
          path.poses[idx - 3].pose.position.x,
          path.poses[idx - 2].pose.position.x,
          path.poses[idx - 1].pose.position.x,
          path.poses[idx].pose.position.x,
          path.poses[idx + 1].pose.position.x,
          path.poses[idx + 2].pose.position.x,
          path.poses[idx + 3].pose.position.x});
      }

      // idx++;
      // path.poses[idx].pose.position.x = applyFilter({
      //     path.poses[idx - 2].pose.position.x,
      //     path.poses[idx - 1].pose.position.x,
      //     path.poses[idx].pose.position.x,
      //     path.poses[idx + 1].pose.position.x,
      //     path.poses[idx + 1].pose.position.x});

      // idx++;
      // path.poses[idx].pose.position.x = applyFilter({
      //    path.poses[idx - 2].pose.position.x,
      //     path.poses[idx - 1].pose.position.x,
      //     path.poses[idx].pose.position.x,
      //     path.poses[idx].pose.position.x,
      //     path.poses[idx].pose.position.x});
    };

  auto applyFilterOverY =
    [&](nav_msgs::msg::Path & path) -> void
    {
      // TODO update for 7 pt
      // unsigned int idx = 0;
      // path.poses[0].pose.position.y = applyFilter({
      //   path.poses[0].pose.position.y,
      //   path.poses[0].pose.position.y,
      //   path.poses[0].pose.position.y,
      //   path.poses[1].pose.position.y,
      //   path.poses[2].pose.position.y});

      // idx++;
      // path.poses[1].pose.position.y = applyFilter({
      //   path.poses[0].pose.position.y,
      //   path.poses[1].pose.position.y,
      //   path.poses[1].pose.position.y,
      //   path.poses[2].pose.position.y,
      //   path.poses[3].pose.position.y});

      for (unsigned int idx = 3; idx != num_sequences - 3; idx++) {
        path.poses[idx].pose.position.y = applyFilter({
          path.poses[idx - 3].pose.position.y,
          path.poses[idx - 2].pose.position.y,
          path.poses[idx - 1].pose.position.y,
          path.poses[idx].pose.position.y,
          path.poses[idx + 1].pose.position.y,
          path.poses[idx + 2].pose.position.y,
          path.poses[idx + 3].pose.position.y});
      }

      // idx++;
      // path.poses[idx].pose.position.y = applyFilter({
      //     path.poses[idx - 2].pose.position.y,
      //     path.poses[idx - 1].pose.position.y,
      //     path.poses[idx].pose.position.y,
      //     path.poses[idx + 1].pose.position.y,
      //     path.poses[idx + 1].pose.position.y});

      // idx++;
      // path.poses[idx].pose.position.y = applyFilter({
      //    path.poses[idx - 2].pose.position.y,
      //     path.poses[idx - 1].pose.position.y,
      //     path.poses[idx].pose.position.y,
      //     path.poses[idx].pose.position.y,
      //     path.poses[idx].pose.position.y});
    };

  applyFilterOverX(plan2);
  applyFilterOverY(plan2);

SteveMacenski avatar Oct 13 '22 22:10 SteveMacenski

This branch as the work: https://github.com/ros-planning/navigation2/tree/sgf_smoother

Still needs unit tests and general testing that it works as expected, but mostly ready to go

Remaining:

  • [x] Tests
  • [x] Testing with actual planning algorithms (Navfn, Smac's 2D-A*/hybridA*)
  • [ ] Configuration Documentation / migration guide / announcement of new algorithm

Unfortunately with ROSCon/IROS travel shortly, I won't be able to complete this in October :cry: So if someone else is motivated and interested, I'd appreciate the help!

I don't expect the smoother to differ greatly from the original path. Its great to smooth out grid-search based jaggedness like from raw Smac 2D-A* as well as NavFn to remove small odd artifacts from grad descent (NavFn) that exist at the end of the path. Since it doesn't impact the path very much, its a nice low-impact smoother that doesn't require collision checking and low parameter (really no tuning required). Unlike the simple smoother which will deviate quite a bit, this stays on the main "path function" and removes "errors" by fitting the polynomial. Fits a nice niche for especially grid techniques or small jagged patterns -- lessor so to remove oscillation -- but then also doesn't meaningfully impact feasibility.

I'm not sure how much utility this algorithm will really have, but it fills a niche that we don't have yet in the smoother algorithms:

  • Simple Smoother -- Restricted movement of the path based on keeping near original points but maximizing smoothness as a naive first-order approach that works well and relatively fast. Short cuts corners / turns though.
  • Constrained Smoother -- freely move the path based on an optimization based objective function minimization of the key elements of path quality (smoothness, curvature, cost, etc). Very slow.
  • Savitsky-Golay Smoother -- Does not move the path at all and tries to model the path "function" to remove jitter / errors, does not deviate from the planners original intention. Very fast and analytical with a long history. Though doesn't remove anything other than path qualities that can be modeled as "error / noise". Good for planners with jitter due to discrete data (NavFn) or jagged search patterns (Moore / Van Nuemann neighborhood search) but doesn't deviate -- only tries to find the "true" function from the data.

SteveMacenski avatar Oct 13 '22 23:10 SteveMacenski

Duplicating the Slack, just for the case: The SGF smoother was estimated with SmacHybrid-A* and default NavFn path planner. On both, it produces pretty aligned smoothed path, except of its goal. At the end of path, SGF smoother every time makes incorrect movements, as shown at the attached pictures below (for SmacHybrid-A* planner): Screenshot from 2022-10-28 11-39-33 Screenshot from 2022-10-28 11-40-45 Screenshot from 2022-10-28 11-42-06

Similar situation for NavFn planner, but unlike SHA* planner, the goal to be reached in this case: Screenshot from 2022-10-28 11-49-22 Screenshot from 2022-10-28 11-49-32 From the testing sight, it seems that final direction is being to be correct, while the smoothed poses coordinates near the goal are walking around.

AlexeyMerzlyakov avatar Oct 28 '22 09:10 AlexeyMerzlyakov

Pushed branch resolves!

SteveMacenski avatar Oct 31 '22 14:10 SteveMacenski

I hope tomorrow to get on unit tests & doc updates. I'd like this pushed before week's end so I can focus on the EOY tasks (Nav Algo paper + MPPI)

SteveMacenski avatar Nov 02 '22 03:11 SteveMacenski

I've verified the SG path smoother with NavFn, ThetaStar and Smac-HybridA* (DUBIN). Now all cases are working pretty well, smoothing the local factured paths produced by path planners. Here are the examples for NavFn (high resolution collage, best to click) (red - raw path, blue - smoothed): NavFn ... and for ThetaStar: theta_star

For SmacHybridA*, the following metrics (collected by smoother_benchmarking, and compared with SS and CS) were obtained:

--------------------  --------------------  ------------------  ------------------  --------  ----------------------  -----------------------
Planner               Time (s)              Path length (m)     Average cost        Max cost  Path smoothness (x100)  Average turning rad (m)
SmacHybrid            0.11719178941200001   11.504739520948565  12.258683968318367  129.462   1.586654909354627       0.7057524803168388
simple_smoother       0.001119348878        11.002066883616866  18.05353044518565   127.16    0.4624616549599383      3.102313187507408
constrained_smoother  0.022246623292000004  11.493715590115078  6.881435517035139   103.975   1.314291295118698       2.5888961854885775
sg_smoother           5.5814876e-05         11.467456502405593  12.181844171244819  127.762   1.2115300728553242      2.2909156972353713
--------------------  --------------------  ------------------  ------------------  --------  ----------------------  -----------------------

AlexeyMerzlyakov avatar Nov 02 '22 08:11 AlexeyMerzlyakov

Sweet - you already knew what I was thinking too. Needed metrics for it to add to the paper :wink:

Thanks for doing the real-world tests. I plan to write the unit tests tomorrow which should button up this topic then beyond documentation!

SteveMacenski avatar Nov 02 '22 09:11 SteveMacenski

Merging imminent

SteveMacenski avatar Nov 03 '22 02:11 SteveMacenski