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.
- python_motion_planning/__init__.py +1 -1
- python_motion_planning/common/env/map/base_map.py +2 -8
- python_motion_planning/common/env/map/grid.py +456 -198
- python_motion_planning/common/utils/__init__.py +2 -1
- python_motion_planning/common/utils/child_tree.py +22 -0
- python_motion_planning/common/utils/geometry.py +18 -29
- python_motion_planning/common/visualizer/__init__.py +3 -1
- python_motion_planning/common/visualizer/base_visualizer.py +165 -0
- python_motion_planning/common/visualizer/{visualizer.py → visualizer_2d.py} +97 -220
- python_motion_planning/common/visualizer/visualizer_3d.py +242 -0
- python_motion_planning/controller/base_controller.py +37 -4
- python_motion_planning/controller/path_tracker/__init__.py +2 -1
- python_motion_planning/controller/path_tracker/apf.py +22 -23
- python_motion_planning/controller/path_tracker/dwa.py +14 -17
- python_motion_planning/controller/path_tracker/path_tracker.py +4 -1
- python_motion_planning/controller/path_tracker/pid.py +7 -1
- python_motion_planning/controller/path_tracker/pure_pursuit.py +7 -1
- python_motion_planning/controller/path_tracker/rpp.py +111 -0
- python_motion_planning/path_planner/__init__.py +2 -1
- python_motion_planning/path_planner/base_path_planner.py +45 -11
- python_motion_planning/path_planner/graph_search/__init__.py +4 -1
- python_motion_planning/path_planner/graph_search/a_star.py +12 -14
- python_motion_planning/path_planner/graph_search/dijkstra.py +15 -15
- python_motion_planning/path_planner/graph_search/gbfs.py +100 -0
- python_motion_planning/path_planner/graph_search/jps.py +199 -0
- python_motion_planning/path_planner/graph_search/lazy_theta_star.py +113 -0
- python_motion_planning/path_planner/graph_search/theta_star.py +17 -19
- python_motion_planning/path_planner/hybrid_search/__init__.py +1 -0
- python_motion_planning/path_planner/hybrid_search/voronoi_planner.py +204 -0
- python_motion_planning/path_planner/sample_search/__init__.py +2 -1
- python_motion_planning/path_planner/sample_search/rrt.py +73 -31
- python_motion_planning/path_planner/sample_search/rrt_connect.py +237 -0
- python_motion_planning/path_planner/sample_search/rrt_star.py +220 -150
- python_motion_planning/traj_optimizer/__init__.py +2 -0
- python_motion_planning/traj_optimizer/base_curve_generator.py +53 -0
- python_motion_planning/traj_optimizer/curve_generator/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/__init__.py +2 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/bspline.py +256 -0
- python_motion_planning/traj_optimizer/curve_generator/point_based/cubic_spline.py +115 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/__init__.py +4 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/bezier.py +121 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/dubins.py +355 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/polynomial.py +197 -0
- python_motion_planning/traj_optimizer/curve_generator/pose_based/reeds_shepp.py +606 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/METADATA +71 -29
- python_motion_planning-2.0.1.dist-info/RECORD +64 -0
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/WHEEL +1 -1
- python_motion_planning/common/env/robot/tmp.py +0 -404
- python_motion_planning/curve_generator/__init__.py +0 -9
- python_motion_planning/curve_generator/bezier_curve.py +0 -131
- python_motion_planning/curve_generator/bspline_curve.py +0 -271
- python_motion_planning/curve_generator/cubic_spline.py +0 -128
- python_motion_planning/curve_generator/curve.py +0 -64
- python_motion_planning/curve_generator/dubins_curve.py +0 -348
- python_motion_planning/curve_generator/fem_pos_smooth.py +0 -114
- python_motion_planning/curve_generator/polynomial_curve.py +0 -226
- python_motion_planning/curve_generator/reeds_shepp.py +0 -736
- python_motion_planning-2.0.dev1.dist-info/RECORD +0 -53
- {python_motion_planning-2.0.dev1.dist-info → python_motion_planning-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
2
|
+
@file: visualizer_2d.py
|
|
3
3
|
@author: Wu Maojia, Yang Haodong
|
|
4
|
-
@update: 2025.
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
99
|
-
cmap=
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 = "
|
|
165
|
-
edge_color: str = "
|
|
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
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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],
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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()
|