Nav2 Navigation Stack - kilted  kilted
ROS 2 Navigation Stack
process_data.py
1 #! /usr/bin/env python3
2 # Copyright (c) 2022 Samsung R&D Institute Russia
3 # Copyright (c) 2022 Joshua Wallace
4 # Copyright (c) 2021 RoboTech Vision
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 
18 import math
19 import os
20 import pickle
21 
22 import matplotlib.pylab as plt
23 from nav2_msgs.action import ComputePathToPose, SmoothPath
24 from nav2_msgs.msg import Costmap
25 from nav_msgs.msg import Path
26 import numpy as np
27 from numpy.typing import NDArray
28 import seaborn as sns
29 from tabulate import tabulate
30 
31 
32 def getPaths(results: list[ComputePathToPose.Result | SmoothPath.Result]) -> list[Path]:
33  paths = []
34  for i in range(len(results)):
35  if (i % 2) == 0:
36  # Append non-smoothed path
37  paths.append(results[i].path)
38  else:
39  # Append smoothed paths array
40  for result in results[i]:
41  paths.append(result.path)
42  return paths
43 
44 
45 def getTimes(results: list[ComputePathToPose.Result | SmoothPath.Result]) -> list[float]:
46  times = []
47  for i in range(len(results)):
48  if (i % 2) == 0:
49  # Append non-smoothed time
50  times.append(
51  results[i].planning_time.nanosec / 1e09 + results[i].planning_time.sec
52  )
53  else:
54  # Append smoothed times array
55  for result in results[i]:
56  times.append(
57  result.smoothing_duration.nanosec / 1e09
58  + result.smoothing_duration.sec
59  )
60  return times
61 
62 
63 def getMapCoordsFromPaths(paths: list[Path], resolution: float) -> list[list[float]]:
64  coords = []
65  for path in paths:
66  x = []
67  y = []
68  for pose in path.poses:
69  x.append(pose.pose.position.x / resolution)
70  y.append(pose.pose.position.y / resolution)
71  coords.append(x)
72  coords.append(y)
73  return coords
74 
75 
76 def getPathLength(path: Path) -> float:
77  path_length = 0.0
78  x_prev = path.poses[0].pose.position.x
79  y_prev = path.poses[0].pose.position.y
80  for i in range(1, len(path.poses)):
81  x_curr = path.poses[i].pose.position.x
82  y_curr = path.poses[i].pose.position.y
83  path_length = path_length + math.sqrt(
84  (x_curr - x_prev) ** 2 + (y_curr - y_prev) ** 2
85  )
86  x_prev = x_curr
87  y_prev = y_curr
88  return path_length
89 
90 
91 # Path smoothness calculations
92 def getSmoothness(
93  pt_prev: NDArray[np.float64], pt: NDArray[np.float64],
94  pt_next: NDArray[np.float64]) -> float:
95  d1 = pt - pt_prev
96  d2 = pt_next - pt
97  delta = d2 - d1
98  return float(np.linalg.norm(delta))
99 
100 
101 def getPathSmoothnesses(paths: list[Path]) -> list[float]:
102  smoothnesses = []
103  pm0 = np.array([0.0, 0.0])
104  pm1 = np.array([0.0, 0.0])
105  pm2 = np.array([0.0, 0.0])
106  for path in paths:
107  smoothness = 0.0
108  for i in range(2, len(path.poses)):
109  pm0[0] = path.poses[i].pose.position.x
110  pm0[1] = path.poses[i].pose.position.y
111  pm1[0] = path.poses[i - 1].pose.position.x
112  pm1[1] = path.poses[i - 1].pose.position.y
113  pm2[0] = path.poses[i - 2].pose.position.x
114  pm2[1] = path.poses[i - 2].pose.position.y
115  smoothness += getSmoothness(pm2, pm1, pm0)
116  smoothnesses.append(smoothness)
117  return smoothnesses
118 
119 
120 # Curvature calculations
121 def arcCenter(pt_prev: NDArray[np.float64], pt: NDArray[np.float64],
122  pt_next: NDArray[np.float64]) -> NDArray[np.float64]:
123  cusp_thresh = -0.7
124 
125  d1 = pt - pt_prev
126  d2 = pt_next - pt
127 
128  d1_norm = d1 / np.linalg.norm(d1)
129  d2_norm = d2 / np.linalg.norm(d2)
130  cos_angle = np.dot(d1_norm, d2_norm)
131 
132  if cos_angle < cusp_thresh:
133  # cusp case
134  d2 = -d2
135  pt_next = pt + d2
136 
137  det = d1[0] * d2[1] - d1[1] * d2[0]
138  if abs(det) < 1e-4: # straight line
139  return np.array([float('inf'), float('inf')])
140 
141  # circle center is at the intersection of mirror axes of the segments:
142  # http://paulbourke.net/geometry/circlesphere/
143  # line intersection:
144  # https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Intersection%20of%20two%20lines
145  mid1 = (pt_prev + pt) / 2
146  mid2 = (pt + pt_next) / 2
147  n1 = (-d1[1], d1[0])
148  n2 = (-d2[1], d2[0])
149  det1 = (mid1[0] + n1[0]) * mid1[1] - (mid1[1] + n1[1]) * mid1[0]
150  det2 = (mid2[0] + n2[0]) * mid2[1] - (mid2[1] + n2[1]) * mid2[0]
151  center = np.array(
152  [(det1 * n2[0] - det2 * n1[0]) / det, (det1 * n2[1] - det2 * n1[1]) / det]
153  )
154  return center
155 
156 
157 def getPathCurvatures(paths: list[Path]) -> list[float]:
158  curvatures = []
159  pm0 = np.array([0.0, 0.0])
160  pm1 = np.array([0.0, 0.0])
161  pm2 = np.array([0.0, 0.0])
162  for path in paths:
163  radiuses = []
164  for i in range(2, len(path.poses)):
165  pm0[0] = path.poses[i].pose.position.x
166  pm0[1] = path.poses[i].pose.position.y
167  pm1[0] = path.poses[i - 1].pose.position.x
168  pm1[1] = path.poses[i - 1].pose.position.y
169  pm2[0] = path.poses[i - 2].pose.position.x
170  pm2[1] = path.poses[i - 2].pose.position.y
171  center = arcCenter(pm2, pm1, pm0)
172  if center[0] != float('inf'):
173  turning_rad = np.linalg.norm(pm1 - center)
174  radiuses.append(turning_rad)
175  curvatures.append(np.average(radiuses))
176  return curvatures
177 
178 
179 def plotResults(costmap: Costmap, paths: list[Path]) -> None:
180  coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
181  data = np.asarray(costmap.data)
182  data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
183  data = np.where(data <= 253, 0, data)
184 
185  plt.figure(3)
186  ax = sns.heatmap(data, cmap='Greys', cbar=False)
187  for i in range(0, len(coords), 2):
188  ax.plot(coords[i], coords[i + 1], linewidth=0.7)
189  plt.axis('off')
190  ax.set_aspect('equal', 'box')
191  plt.show()
192 
193 
194 def averagePathCost(
195  paths: list[Path], costmap: Costmap,
196  num_of_planners: int) -> list[list[float]]:
197  coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
198  data = np.asarray(costmap.data)
199  data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
200 
201  average_path_costs: list[list[float]] = []
202  for i in range(num_of_planners):
203  average_path_costs.append([])
204 
205  k = 0
206  for i in range(0, len(coords), 2):
207  costs = []
208  for j in range(len(coords[i])):
209  costs.append(data[math.floor(coords[i + 1][j])][math.floor(coords[i][j])])
210  average_path_costs[k % num_of_planners].append(sum(costs) / len(costs))
211  k += 1
212 
213  return average_path_costs
214 
215 
216 def maxPathCost(
217  paths: list[Path], costmap: Costmap,
218  num_of_planners: int) -> list[list[float]]:
219  coords = getMapCoordsFromPaths(paths, costmap.metadata.resolution)
220  data = np.asarray(costmap.data)
221  data.resize(costmap.metadata.size_y, costmap.metadata.size_x)
222 
223  max_path_costs: list[list[float]] = []
224  for i in range(num_of_planners):
225  max_path_costs.append([])
226 
227  k = 0
228  for i in range(0, len(coords), 2):
229  max_cost = 0
230  for j in range(len(coords[i])):
231  cost = data[math.floor(coords[i + 1][j])][math.floor(coords[i][j])]
232  if max_cost < cost:
233  max_cost = cost
234  max_path_costs[k % num_of_planners].append(max_cost)
235  k += 1
236 
237  return max_path_costs
238 
239 
240 def main() -> None:
241  # Read the data
242  benchmark_dir = os.getcwd()
243  print('Read data')
244  with open(os.path.join(benchmark_dir, 'results.pickle'), 'rb') as f:
245  results = pickle.load(f)
246 
247  with open(os.path.join(benchmark_dir, 'methods.pickle'), 'rb') as f:
248  smoothers = pickle.load(f)
249  planner = smoothers[0]
250  del smoothers[0]
251  methods_num = len(smoothers) + 1
252 
253  with open(os.path.join(benchmark_dir, 'costmap.pickle'), 'rb') as f:
254  costmap = pickle.load(f)
255 
256  # Paths (planner and smoothers)
257  paths = getPaths(results)
258  path_lengths_list = []
259 
260  for path in paths:
261  path_lengths_list.append(getPathLength(path))
262  path_lengths = np.asarray(path_lengths_list)
263  total_paths = len(paths)
264 
265  # [planner, smoothers] path length in a row
266  path_lengths.resize((int(total_paths / methods_num), methods_num))
267  # [planner, smoothers] path length in a column
268  path_lengths = path_lengths.transpose()
269 
270  # Times
271  times = np.asarray(getTimes(results))
272  times.resize((int(total_paths / methods_num), methods_num))
273  times = np.transpose(times)
274 
275  # Costs
276  average_path_costs = np.asarray(averagePathCost(paths, costmap, methods_num))
277  max_path_costs = np.asarray(maxPathCost(paths, costmap, methods_num))
278 
279  # Smoothness
280  smoothnesses = np.asarray(getPathSmoothnesses(paths))
281  smoothnesses.resize((int(total_paths / methods_num), methods_num))
282  smoothnesses = np.transpose(smoothnesses)
283 
284  # Curvatures
285  curvatures = np.asarray(getPathCurvatures(paths))
286  curvatures.resize((int(total_paths / methods_num), methods_num))
287  curvatures = np.transpose(curvatures)
288 
289  # Generate table
290  planner_table = [
291  [
292  'Planner',
293  'Time (s)',
294  'Path length (m)',
295  'Average cost',
296  'Max cost',
297  'Path smoothness (x100)',
298  'Average turning rad (m)',
299  ]
300  ]
301  # for path planner
302  planner_table.append(
303  [
304  planner,
305  np.average(times[0]),
306  np.average(path_lengths[0]),
307  np.average(average_path_costs[0]),
308  np.average(max_path_costs[0]),
309  np.average(smoothnesses[0]) * 100,
310  np.average(curvatures[0]),
311  ]
312  )
313  # for path smoothers
314  for i in range(1, methods_num):
315  planner_table.append(
316  [
317  smoothers[i - 1],
318  np.average(times[i]),
319  np.average(path_lengths[i]),
320  np.average(average_path_costs[i]),
321  np.average(max_path_costs[i]),
322  np.average(smoothnesses[i]) * 100,
323  np.average(curvatures[i]),
324  ]
325  )
326 
327  # Visualize results
328  print(tabulate(planner_table))
329  plotResults(costmap, paths)
330 
331  exit(0)
332 
333 
334 if __name__ == '__main__':
335  main()