python-motion-planning 2.0.dev1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. python_motion_planning/__init__.py +1 -1
  2. python_motion_planning/common/env/map/base_map.py +2 -8
  3. python_motion_planning/common/env/map/grid.py +456 -198
  4. python_motion_planning/common/utils/__init__.py +2 -1
  5. python_motion_planning/common/utils/child_tree.py +22 -0
  6. python_motion_planning/common/utils/geometry.py +18 -29
  7. python_motion_planning/common/visualizer/__init__.py +3 -1
  8. python_motion_planning/common/visualizer/base_visualizer.py +165 -0
  9. python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
  10. python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
  11. python_motion_planning/controller/base_controller.py +37 -4
  12. python_motion_planning/controller/path_tracker/__init__.py +2 -1
  13. python_motion_planning/controller/path_tracker/apf.py +22 -23
  14. python_motion_planning/controller/path_tracker/dwa.py +14 -17
  15. python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
  16. python_motion_planning/controller/path_tracker/pid.py +7 -1
  17. python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
  18. python_motion_planning/controller/path_tracker/rpp.py +111 -0
  19. python_motion_planning/path_planner/__init__.py +2 -1
  20. python_motion_planning/path_planner/base_path_planner.py +45 -11
  21. python_motion_planning/path_planner/graph_search/__init__.py +4 -1
  22. python_motion_planning/path_planner/graph_search/a_star.py +12 -14
  23. python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
  24. python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
  25. python_motion_planning/path_planner/graph_search/jps.py +199 -0
  26. python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
  27. python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
  28. python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
  29. python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
  30. python_motion_planning/path_planner/sample_search/__init__.py +2 -1
  31. python_motion_planning/path_planner/sample_search/rrt.py +73 -31
  32. python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
  33. python_motion_planning/path_planner/sample_search/rrt_star.py +220 -150
  34. python_motion_planning/traj_optimizer/__init__.py +2 -0
  35. python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
  36. python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
  37. python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
  38. python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
  39. python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
  40. python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
  41. python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
  42. python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
  43. python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
  44. python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
  45. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +71 -29
  46. python_motion_planning-2.0.1.dist-info/RECORD +64 -0
  47. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
  48. python_motion_planning/common/env/robot/tmp.py +0 -404
  49. python_motion_planning/curve_generator/__init__.py +0 -9
  50. python_motion_planning/curve_generator/bezier_curve.py +0 -131
  51. python_motion_planning/curve_generator/bspline_curve.py +0 -271
  52. python_motion_planning/curve_generator/cubic_spline.py +0 -128
  53. python_motion_planning/curve_generator/curve.py +0 -64
  54. python_motion_planning/curve_generator/dubins_curve.py +0 -348
  55. python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
  56. python_motion_planning/curve_generator/polynomial_curve.py +0 -226
  57. python_motion_planning/curve_generator/reeds_shepp.py +0 -736
  58. python_motion_planning-2.0.dev1.dist-info/RECORD +0 -53
  59. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
  60. {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,12 @@
1
1
  """
2
- @file: visualizer.py
2
+ @file: visualizer_2d.py
3
3
  @author: Wu Maojia, Yang Haodong
4
- @update: 2025.10.3
4
+ @update: 2025.12.20
5
5
  """
6
6
  from typing import Union, Dict, List, Tuple, Any
7
7
  from collections import namedtuple
8
8
  import time
9
+ import os
9
10
 
10
11
  import numpy as np
11
12
  import matplotlib
@@ -14,52 +15,60 @@ import matplotlib.colors as mcolors
14
15
  from matplotlib import animation
15
16
  import matplotlib.patheffects as path_effects
16
17
 
18
+ from python_motion_planning.common.visualizer.base_visualizer import BaseVisualizer
17
19
  from python_motion_planning.controller import BaseController
18
20
  from python_motion_planning.common.env import TYPES, ToySimulator, Grid, CircularRobot, Node
19
21
  from python_motion_planning.common.utils import Geometry
20
22
 
21
- class Visualizer:
23
+ class Visualizer2D(BaseVisualizer):
22
24
  """
23
25
  Simple visualizer for motion planning using matplotlib.
24
26
 
25
27
  Args:
26
28
  figname: Figure name (window title).
27
- figsize: Figure size (width, height).
29
+ figsize: Figure size (width, height) (matplotlib figure size, unit: inch).
30
+ cmap_dict: Color map for 2d visualization.
31
+ zorder: Zorder for 2d matplotlib visualization.
28
32
  """
29
- def __init__(self, figname: str = "", figsize: tuple = (10, 8)):
33
+ def __init__(self,
34
+ figname: str = "",
35
+ figsize: tuple = (10, 8),
36
+ cmap_dict: dict = {
37
+ TYPES.FREE: "#ffffff",
38
+ TYPES.OBSTACLE: "#000000",
39
+ TYPES.START: "#ff0000",
40
+ TYPES.GOAL: "#1155cc",
41
+ TYPES.INFLATION: "#ffccff",
42
+ TYPES.EXPAND: "#eeeeee",
43
+ TYPES.CUSTOM: "#bbbbbb",
44
+ },
45
+ zorder: dict = {
46
+ 'grid_map': 10,
47
+ 'voxels': 10,
48
+ 'esdf': 20,
49
+ 'expand_tree_edge': 30,
50
+ 'expand_tree_node': 40,
51
+ 'path_2d': 50,
52
+ 'path_3d': 700,
53
+ 'traj': 60,
54
+ 'lookahead_pose_node': 70,
55
+ 'lookahead_pose_orient': 80,
56
+ 'pred_traj': 90,
57
+ 'robot_circle': 100,
58
+ 'robot_orient': 110,
59
+ 'robot_text': 120,
60
+ 'env_info_text': 10000
61
+ }
62
+ ):
30
63
  self.fig = plt.figure(figname, figsize=figsize)
31
64
  self.ax = self.fig.add_subplot()
32
65
  self.ani = None
33
66
 
34
67
  # colors
35
- self.cmap_dict = {
36
- TYPES.FREE: "#ffffff",
37
- TYPES.OBSTACLE: "#000000",
38
- TYPES.START: "#ff0000",
39
- TYPES.GOAL: "#1155cc",
40
- TYPES.INFLATION: "#ffccff",
41
- TYPES.EXPAND: "#eeeeee",
42
- TYPES.CUSTOM: "#bbbbbb",
43
- }
68
+ self.cmap_dict = cmap_dict
44
69
  # self.norm = mcolors.BoundaryNorm(list(range(len(self.cmap_dict))), len(self.cmap_dict))
45
70
 
46
- self.zorder = {
47
- 'grid_map': 10,
48
- 'voxels': 10,
49
- 'esdf': 20,
50
- 'expand_tree_edge': 30,
51
- 'expand_tree_node': 40,
52
- 'path_2d': 50,
53
- 'path_3d': 700,
54
- 'traj': 60,
55
- 'lookahead_pose_node': 70,
56
- 'lookahead_pose_orient': 80,
57
- 'pred_traj': 90,
58
- 'robot_circle': 100,
59
- 'robot_orient': 110,
60
- 'robot_text': 120,
61
- 'env_info_text': 10000
62
- }
71
+ self.zorder = zorder
63
72
 
64
73
  self.cmap = mcolors.ListedColormap([info for info in self.cmap_dict.values()])
65
74
  self.norm = mcolors.BoundaryNorm([i for i in range(self.cmap.N + 1)], self.cmap.N)
@@ -71,15 +80,7 @@ class Visualizer:
71
80
  def __del__(self):
72
81
  self.close()
73
82
 
74
- def plot_grid_map(self, grid_map: Grid, equal: bool = False, alpha_3d: dict = {
75
- TYPES.FREE: 0.0,
76
- TYPES.OBSTACLE: 0.5,
77
- TYPES.START: 0.5,
78
- TYPES.GOAL: 0.5,
79
- TYPES.INFLATION: 0.0,
80
- TYPES.EXPAND: 0.1,
81
- TYPES.CUSTOM: 0.5,
82
- },
83
+ def plot_grid_map(self, grid_map: Grid, equal: bool = False,
83
84
  show_esdf: bool = False, alpha_esdf: float = 0.5) -> None:
84
85
  '''
85
86
  Plot grid map with static obstacles.
@@ -87,82 +88,44 @@ class Visualizer:
87
88
  Args:
88
89
  map: Grid map or its type map.
89
90
  equal: Whether to set axis equal.
90
- alpha_3d: Alpha of occupancy for 3d visualization.
91
91
  show_esdf: Whether to show esdf.
92
92
  alpha_esdf: Alpha of esdf.
93
93
  '''
94
+ if grid_map.dim != 2:
95
+ raise ValueError(f"Grid map dimension must be 2.")
96
+
94
97
  self.grid_map = grid_map
95
98
  self.dim = grid_map.dim
96
- if grid_map.dim == 2:
99
+ type_data = grid_map.type_map.data
100
+
101
+ plt.imshow(
102
+ np.transpose(type_data),
103
+ cmap=self.cmap,
104
+ norm=self.norm,
105
+ origin='lower',
106
+ interpolation='nearest',
107
+ extent=[*grid_map.bounds[0], *grid_map.bounds[1]],
108
+ zorder=self.zorder['grid_map'],
109
+ )
110
+
111
+ if show_esdf: # draw esdf hotmap
97
112
  plt.imshow(
98
- np.transpose(grid_map.type_map.array),
99
- cmap=self.cmap,
100
- norm=self.norm,
101
- origin='lower',
102
- interpolation='nearest',
113
+ np.transpose(grid_map.esdf),
114
+ cmap="jet",
115
+ origin="lower",
116
+ interpolation="nearest",
103
117
  extent=[*grid_map.bounds[0], *grid_map.bounds[1]],
104
- zorder=self.zorder['grid_map'],
105
- )
106
-
107
- if show_esdf: # draw esdf hotmap
108
- plt.imshow(
109
- np.transpose(grid_map.esdf),
110
- cmap="jet",
111
- origin="lower",
112
- interpolation="nearest",
113
- extent=[*grid_map.bounds[0], *grid_map.bounds[1]],
114
- alpha=alpha_esdf,
115
- zorder=self.zorder['esdf'],
116
- )
117
- plt.colorbar(label="ESDF distance")
118
-
119
- if equal:
120
- plt.axis("equal")
121
-
122
- elif grid_map.dim == 3:
123
- self.ax = self.fig.add_subplot(projection='3d')
124
-
125
- data = grid_map.type_map.array
126
- nx, ny, nz = data.shape
127
-
128
- filled = np.zeros_like(data, dtype=bool)
129
- colors = np.zeros(data.shape + (4,), dtype=float) # RGBA
130
-
131
- for key, color in self.cmap_dict.items():
132
- mask = (data == key)
133
- if alpha_3d[key] < 1e-6:
134
- continue
135
- filled |= mask
136
- rgba = matplotlib.colors.to_rgba(color, alpha=alpha_3d[key])
137
- colors[mask] = rgba
138
-
139
- self.ax.voxels(filled, facecolors=colors, zorder=self.zorder['voxels'])
140
-
141
- if show_esdf:
142
- # TODO
143
- raise NotImplementedError
144
-
145
- self.ax.set_xlabel("X")
146
- self.ax.set_ylabel("Y")
147
- self.ax.set_zlabel("Z")
148
-
149
- # let voxels look not stretched
150
- max_range = 0
151
- for d in range(grid_map.dim):
152
- max_range = max(max_range, grid_map.bounds[d, 1] - grid_map.bounds[d, 0])
153
- self.ax.set_xlim(grid_map.bounds[0, 0], grid_map.bounds[0, 0] + max_range)
154
- self.ax.set_ylim(grid_map.bounds[1, 0], grid_map.bounds[1, 0] + max_range)
155
- self.ax.set_zlim(grid_map.bounds[2, 0], grid_map.bounds[2, 0] + max_range)
156
-
157
- if equal:
158
- self.ax.set_box_aspect([1,1,1])
159
-
160
- else:
161
- raise NotImplementedError(f"Grid map with dim={grid_map.dim} not supported.")
118
+ alpha=alpha_esdf,
119
+ zorder=self.zorder['esdf'],
120
+ )
121
+ plt.colorbar(label="ESDF distance")
122
+
123
+ if equal:
124
+ plt.axis("equal")
162
125
 
163
126
  def plot_expand_tree(self, expand_tree: Dict[Union[Tuple[int, ...], Tuple[float, ...]], Node],
164
- node_color: str = "C5",
165
- edge_color: str = "C6",
127
+ node_color: str = "#8c564b",
128
+ edge_color: str = "#e377c2",
166
129
  node_size: float = 5,
167
130
  linewidth: float = 1.0,
168
131
  node_alpha: float = 1.0,
@@ -181,29 +144,16 @@ class Visualizer:
181
144
  connect_to_parent: Whether to draw parent-child connections.
182
145
  map_frame: whether path is in map frame or not (world frame)
183
146
  """
184
- if self.dim == 2:
185
- for coord, node in expand_tree.items():
186
- current = node.current
187
- if map_frame:
188
- current = self.grid_map.map_to_world(current)
147
+ if not isinstance(expand_tree, list): # for multiple trees
148
+ expand_tree = [expand_tree]
189
149
 
190
- self.ax.scatter(current[0], current[1],
191
- c=node_color, s=node_size, zorder=self.zorder['expand_tree_node'], alpha=node_alpha)
192
- if connect_to_parent and node.parent is not None:
193
- parent = node.parent
194
- if map_frame:
195
- parent = self.grid_map.map_to_world(parent)
196
- self.ax.plot([parent[0], current[0]],
197
- [parent[1], current[1]],
198
- color=edge_color, linewidth=linewidth, zorder=self.zorder['expand_tree_edge'], alpha=edge_alpha)
199
-
200
- elif self.dim == 3:
201
- for coord, node in expand_tree.items():
150
+ for tree in expand_tree:
151
+ for coord, node in tree.items():
202
152
  current = node.current
203
153
  if map_frame:
204
154
  current = self.grid_map.map_to_world(current)
205
155
 
206
- self.ax.scatter(current[0], current[1], current[2],
156
+ self.ax.scatter(current[0], current[1],
207
157
  c=node_color, s=node_size, zorder=self.zorder['expand_tree_node'], alpha=node_alpha)
208
158
  if connect_to_parent and node.parent is not None:
209
159
  parent = node.parent
@@ -211,13 +161,8 @@ class Visualizer:
211
161
  parent = self.grid_map.map_to_world(parent)
212
162
  self.ax.plot([parent[0], current[0]],
213
163
  [parent[1], current[1]],
214
- [parent[2], current[2]],
215
164
  color=edge_color, linewidth=linewidth, zorder=self.zorder['expand_tree_edge'], alpha=edge_alpha)
216
165
 
217
- else:
218
- raise ValueError("Dimension must be 2 or 3")
219
-
220
-
221
166
  def plot_path(self, path: List[Union[Tuple[int, ...], Tuple[float, ...]]],
222
167
  style: str = "-", color: str = "#13ae00", label: str = None,
223
168
  linewidth: float = 3, marker: str = None, map_frame: bool = True) -> None:
@@ -227,7 +172,7 @@ class Visualizer:
227
172
 
228
173
  Args:
229
174
  path: point list of path
230
- style: style of path
175
+ style: style of path
231
176
  color: color of path
232
177
  label: label of path
233
178
  linewidth: linewidth of path
@@ -242,12 +187,7 @@ class Visualizer:
242
187
 
243
188
  path = np.array(path)
244
189
 
245
- if self.dim == 2:
246
- self.ax.plot(path[:, 0], path[:, 1], style, lw=linewidth, color=color, label=label, marker=marker, zorder=self.zorder['path_2d'])
247
- elif self.dim == 3:
248
- self.ax.plot(path[:, 0], path[:, 1], path[:, 2], style, lw=linewidth, color=color, label=label, marker=marker, zorder=self.zorder['path_3d'])
249
- else:
250
- raise ValueError("Dimension not supported")
190
+ self.ax.plot(path[:, 0], path[:, 1], style, lw=linewidth, color=color, label=label, marker=marker, zorder=self.zorder['path_2d'])
251
191
 
252
192
  if label:
253
193
  self.ax.legend()
@@ -270,19 +210,13 @@ class Visualizer:
270
210
  text = self.ax.text(*robot.pos, robot.text, color=robot.text_color, ha='center', va='center',
271
211
  fontsize=fontsize, zorder=self.zorder['robot_text'])
272
212
 
273
- if robot.dim == 2:
274
- theta = robot.orient[0]
275
- dx = np.cos(theta) * robot.radius
276
- dy = np.sin(theta) * robot.radius
277
- orient_patch = self.ax.arrow(robot.pos[0], robot.pos[1], dx, dy,
278
- head_width=0.1*robot.radius, head_length=0.2*robot.radius,
279
- fc=robot.color, ec=robot.text_color, zorder=self.zorder['robot_orient'])
280
- return patch, text, orient_patch
281
- elif robot.dim == 3:
282
- # TODO: quiver for 3D vector
283
- return patch, text
284
- else:
285
- return patch, text
213
+ theta = robot.orient[0]
214
+ dx = np.cos(theta) * robot.radius
215
+ dy = np.sin(theta) * robot.radius
216
+ orient_patch = self.ax.arrow(robot.pos[0], robot.pos[1], dx, dy,
217
+ head_width=0.1*robot.radius, head_length=0.2*robot.radius,
218
+ fc=robot.color, ec=robot.text_color, zorder=self.zorder['robot_orient'])
219
+ return patch, text, orient_patch
286
220
 
287
221
  def render_toy_simulator(self, env: ToySimulator, controllers: Dict[str, BaseController],
288
222
  steps: int = 1000, interval: int = None,
@@ -313,7 +247,7 @@ class Visualizer:
313
247
  else:
314
248
  traj_color = {rid: traj_kwargs.get("color") for rid, robot in env.robots.items()}
315
249
 
316
- # 先画静态的地图和路径
250
+ # Draw static map and paths
317
251
  self.ax.clear()
318
252
  self.plot_grid_map(env.obstacle_grid, **grid_kwargs)
319
253
 
@@ -411,74 +345,6 @@ class Visualizer:
411
345
  self.fig, update, frames=steps+prepare_frames, interval=interval, blit=True, repeat=False
412
346
  )
413
347
 
414
- def get_traj_info(self, rid: int, goal_pose: np.ndarray, goal_dist_tol: float, goal_orient_tol: float) -> Dict[str, Any]:
415
- """
416
- Get trajectory information.
417
-
418
- Args:
419
- rid: Robot ID.
420
- goal_pose: Goal pose.
421
- goal_dist_tol: Distance tolerance for goal.
422
- goal_orient_tol: Orientation tolerance for goal.
423
- """
424
- traj = self.trajs[rid]
425
-
426
- info = {
427
- "traj_length": 0.0,
428
- "success": False,
429
- "dist_success": False,
430
- "oracle_success": False,
431
- "oracle_dist_success": False,
432
- "success_time": None,
433
- "dist_success_time": None,
434
- "oracle_success_time": None,
435
- "oracle_dist_success_time": None,
436
- }
437
-
438
- for i in range(len(traj["poses"])):
439
- pose = traj["poses"][i]
440
- time = traj["time"][i]
441
-
442
- pos = pose[:self.dim]
443
- orient = pose[self.dim:]
444
- goal_pos = goal_pose[:self.dim]
445
- goal_orient = goal_pose[self.dim:]
446
-
447
- if i > 0:
448
- info["traj_length"] += np.linalg.norm(pos - traj["poses"][i-1][:self.dim])
449
-
450
- if np.linalg.norm(pos - goal_pos) < goal_dist_tol:
451
- if not info["oracle_dist_success"]:
452
- info["oracle_dist_success"] = True
453
- info["oracle_dist_success_time"] = time
454
-
455
- if not info["dist_success"]:
456
- info["dist_success"] = True
457
- info["dist_success_time"] = time
458
-
459
- if np.abs(Geometry.regularize_orient(orient - goal_orient)) < goal_orient_tol:
460
- if not info["oracle_success"]:
461
- info["oracle_success"] = True
462
- info["oracle_success_time"] = time
463
-
464
- if not info["success"]:
465
- info["success"] = True
466
- info["success_time"] = time
467
-
468
- else:
469
- info["success"] = False
470
- info["success_time"] = None
471
-
472
- else:
473
- info["success"] = False
474
- info["success_time"] = None
475
- info["dist_success"] = False
476
- info["dist_success_time"] = None
477
-
478
- info["traj_length"] = float(info["traj_length"])
479
- return info
480
-
481
-
482
348
  def set_title(self, title: str) -> None:
483
349
  """
484
350
  Set title.
@@ -509,7 +375,18 @@ class Visualizer:
509
375
  Update plot.
510
376
  """
511
377
  self.fig.canvas.draw_idle()
512
-
378
+
379
+ def savefig(self, filename, *args, **kwargs):
380
+ """
381
+ Save figure.
382
+
383
+ Args:
384
+ filename: Filename to save.
385
+ *args: For 2D, see matplotlib.pyplot.savefig. For 3D, see pyvista.Plotter.screenshot.
386
+ **kwargs: For 2D, see matplotlib.pyplot.savefig. For 3D, see pyvista.Plotter.screenshot.
387
+ """
388
+ plt.savefig(fname=filename, *args, **kwargs)
389
+
513
390
  def show(self):
514
391
  """
515
392
  Show plot.
@@ -0,0 +1,242 @@
1
+ """
2
+ @file: visualizer_3d.py
3
+ @author: Wu Maojia
4
+ @update: 2025.12.20
5
+ """
6
+ from typing import Union, Dict, List, Tuple, Any
7
+ from collections import namedtuple
8
+ import time
9
+ import os
10
+
11
+ import numpy as np
12
+ import pyvista as pv
13
+
14
+ from python_motion_planning.common.visualizer.base_visualizer import BaseVisualizer
15
+ from python_motion_planning.controller import BaseController
16
+ from python_motion_planning.common.env import TYPES, ToySimulator, Grid, CircularRobot, Node
17
+ from python_motion_planning.common.utils import Geometry
18
+
19
+ class Visualizer3D(BaseVisualizer):
20
+ """
21
+ Simple 3D visualizer for motion planning using pyvista.
22
+
23
+ Args:
24
+ window_size: Window size (width, height) (pyvista window size, unit: pixel).
25
+ off_screen: `off_screen` argument for pyvista. Renders off screen when True. Useful for automated screenshots.
26
+ show_axes: Whether to show axes for pyvista.
27
+ cmap_dict: Color map for 3d voxel visualization.
28
+ """
29
+ def __init__(self,
30
+ window_size: tuple = (1200, 900),
31
+ off_screen: bool = False,
32
+ show_axes: bool = True,
33
+ cmap_dict: dict = {
34
+ TYPES.FREE: "#ffffff",
35
+ TYPES.OBSTACLE: "#000000",
36
+ TYPES.START: "#ff0000",
37
+ TYPES.GOAL: "#1155cc",
38
+ TYPES.INFLATION: "#ffccff",
39
+ TYPES.EXPAND: "#eeeeee",
40
+ TYPES.CUSTOM: "#bbbbbb",
41
+ }
42
+ ):
43
+ super().__init__()
44
+ self.pv_plotter = pv.Plotter(window_size=list(window_size), off_screen=off_screen)
45
+ if show_axes:
46
+ self.pv_plotter.show_axes()
47
+ self.pv_actors = {}
48
+
49
+ # colors
50
+ self.cmap_dict = cmap_dict
51
+
52
+ def plot_grid_map(self, grid_map: Grid, equal: bool = False, alpha_3d: dict = {
53
+ TYPES.FREE: 0.0,
54
+ TYPES.OBSTACLE: 0.1,
55
+ TYPES.START: 0.5,
56
+ TYPES.GOAL: 0.5,
57
+ TYPES.INFLATION: 0.0,
58
+ TYPES.EXPAND: 0.01,
59
+ TYPES.CUSTOM: 0.1,
60
+ }) -> None:
61
+ '''
62
+ Plot grid map with static obstacles.
63
+
64
+ Args:
65
+ map: Grid map or its type map.
66
+ equal: Whether to set axis equal.
67
+ alpha_3d: Alpha of occupancy for 3d visualization.
68
+ '''
69
+ if grid_map.dim != 3:
70
+ raise ValueError(f"Grid map dimension must be 3.")
71
+
72
+ self.grid_map = grid_map
73
+ self.dim = grid_map.dim
74
+ type_data = grid_map.type_map.data
75
+
76
+ nx, ny, nz = type_data.shape
77
+
78
+ for key, color in self.cmap_dict.items():
79
+ alpha = alpha_3d.get(key, 0.0)
80
+ if alpha < 1e-6:
81
+ continue
82
+
83
+ mask = (type_data == key)
84
+ if not np.any(mask):
85
+ continue
86
+
87
+ # voxels
88
+ points = np.argwhere(mask)
89
+
90
+ # map → world
91
+ points = np.array([
92
+ self.grid_map.map_to_world(p)
93
+ for p in points
94
+ ])
95
+
96
+ cloud = pv.PolyData(points)
97
+ glyph = cloud.glyph(
98
+ geom=pv.Cube(),
99
+ scale=False,
100
+ factor=self.grid_map.resolution
101
+ )
102
+
103
+ actor = self.pv_plotter.add_mesh(
104
+ glyph,
105
+ color=color,
106
+ opacity=alpha,
107
+ show_edges=False
108
+ )
109
+
110
+ self.pv_actors[f"voxels_{key}"] = actor
111
+
112
+ def plot_expand_tree(self, expand_tree: Dict[Union[Tuple[int, ...], Tuple[float, ...]], Node],
113
+ edge_color: str = "#e377c2",
114
+ linewidth: float = 1.0,
115
+ node_alpha: float = 1.0,
116
+ edge_alpha: float = 1.0,
117
+ map_frame: bool = True) -> None:
118
+ """
119
+ Visualize an expand tree (e.g. RRT).
120
+
121
+ Args:
122
+ expand_tree: Dict mapping coordinate tuple -> Node (world frame).
123
+ edge_color: Color of the edges (parent -> child).
124
+ linewidth: Line width of edges.
125
+ map_frame: whether path is in map frame or not (world frame)
126
+ """
127
+ if not isinstance(expand_tree, list):
128
+ expand_tree = [expand_tree]
129
+
130
+ points = []
131
+ lines = []
132
+
133
+ idx = 0
134
+ for tree in expand_tree:
135
+ for _, node in tree.items():
136
+ cur = node.current
137
+ if map_frame:
138
+ cur = self.grid_map.map_to_world(cur)
139
+
140
+ points.append(cur)
141
+
142
+ if node.parent is not None:
143
+ parent = node.parent
144
+ if map_frame:
145
+ parent = self.grid_map.map_to_world(parent)
146
+
147
+ points.append(parent)
148
+ lines.append([idx, idx + 1])
149
+ idx += 2
150
+ else:
151
+ idx += 1
152
+
153
+ if not points:
154
+ return
155
+
156
+ points = np.array(points)
157
+ poly = pv.PolyData(points)
158
+
159
+ if lines:
160
+ cells = np.hstack([[2, l[0], l[1]] for l in lines])
161
+ poly.lines = cells
162
+
163
+ self.pv_plotter.add_mesh(
164
+ poly,
165
+ color=edge_color,
166
+ line_width=linewidth
167
+ )
168
+
169
+
170
+ def plot_path(self, path: List[Union[Tuple[int, ...], Tuple[float, ...]]],
171
+ color: str = "#13ae00",
172
+ linewidth: float = 5, map_frame: bool = True) -> None:
173
+ '''
174
+ Plot path-like information.
175
+ The meaning of parameters are similar to pyvista.Plotter.add_mesh (https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.add_mesh#pyvista.Plotter.add_mesh).
176
+
177
+ Args:
178
+ path: point list of path
179
+ color: color of path
180
+ linewidth: linewidth of path
181
+ map_frame: whether path is in map frame or not (world frame)
182
+ '''
183
+ if len(path) == 0:
184
+ return
185
+
186
+ if map_frame:
187
+ path = [self.grid_map.map_to_world(point) for point in path]
188
+
189
+ path = np.array(path)
190
+
191
+ path_line = pv.lines_from_points(path)
192
+ self.pv_plotter.add_mesh(
193
+ path_line,
194
+ color=color,
195
+ line_width=linewidth
196
+ )
197
+
198
+ def set_title(self, title: str) -> None:
199
+ """
200
+ Set title.
201
+
202
+ Args:
203
+ title: Title.
204
+ """
205
+ self.pv_plotter.add_text(title, position='upper_edge', font_size=14, color='black')
206
+
207
+ def clean(self):
208
+ """
209
+ Clean plot.
210
+ """
211
+ self.pv_plotter.clear()
212
+ self.pv_actors = {}
213
+
214
+ def update(self):
215
+ """
216
+ Update plot.
217
+ """
218
+ self.pv_plotter.render()
219
+
220
+ def savefig(self, filename, *args, **kwargs):
221
+ """
222
+ Save figure.
223
+
224
+ Args:
225
+ filename: Filename to save.
226
+ *args: See pyvista.Plotter.screenshot.
227
+ **kwargs: See pyvista.Plotter.screenshot.
228
+ """
229
+ self.pv_plotter.screenshot(filename=filename, *args, **kwargs)
230
+
231
+ def show(self):
232
+ """
233
+ Show plot.
234
+ """
235
+ self.pv_plotter.reset_camera()
236
+ self.pv_plotter.show(interactive=True, auto_close=False)
237
+
238
+ def close(self):
239
+ """
240
+ Close plot.
241
+ """
242
+ self.pv_plotter.close()