Skip to content

Commit 6ea3060

Browse files
committed
Cleaned up Kinodynamic RRT* implementation
1 parent 5d21442 commit 6ea3060

6 files changed

Lines changed: 763 additions & 633 deletions

File tree

GEMstack/onboard/planning/collision.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import numpy as np
21
from scipy.ndimage import distance_transform_edt
32

43
def build_collision_lookup(grid, safety_margin=2, vehicle_width=1.0):

GEMstack/onboard/planning/kinodynamic_rrt_star.py

Lines changed: 719 additions & 469 deletions
Large diffs are not rendered by default.
Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
import numpy as np
22
import cv2
3-
import yaml
43

5-
def load_pgm_to_occupancy_grid(pgm_path, yaml_path):
4+
def load_pgm_to_occupancy_grid(pgm_path):
65
"""
76
Load a PGM file and convert it to an occupancy grid.
87
98
Args:
109
pgm_path: Path to the PGM file
11-
yaml_path: Path to the YAML metadata file
1210
1311
Returns:
14-
Tuple of (occupancy_grid, metadata)
12+
2D Array occupancy_grid
1513
"""
16-
# # Load YAML metadata
17-
with open(yaml_path, 'r') as f:
18-
metadata = yaml.safe_load(f)
19-
2014
# Load PGM file
2115
image = cv2.imread(pgm_path, cv2.IMREAD_GRAYSCALE)
2216
if image is None:
@@ -25,13 +19,8 @@ def load_pgm_to_occupancy_grid(pgm_path, yaml_path):
2519
# Use a threshold to create binary occupancy grid
2620
thresh = 128
2721
occupancy_grid = (image >= thresh).astype(np.uint8)
28-
29-
# In ROS maps, occupied pixels are typically black (0) and free pixels are white (255)
30-
# If negate is True, we need to invert the grid
31-
# if metadata.get('negate', 0):
32-
# occupancy_grid = 1 - occupancy_grid
3322

34-
return occupancy_grid, metadata
23+
return occupancy_grid
3524

3625
def downsample_occupancy_grid(grid, factor):
3726
"""
@@ -47,44 +36,4 @@ def downsample_occupancy_grid(grid, factor):
4736
h, w = grid.shape
4837
nh, nw = h // factor, w // factor
4938
g = grid[:nh*factor, :nw*factor]
50-
return g.reshape(nh, factor, nw, factor).max(axis=(1,3))
51-
52-
def world_to_grid(x, y, theta, metadata):
53-
"""
54-
Convert world coordinates to grid coordinates.
55-
56-
Args:
57-
x, y, theta: World coordinates
58-
metadata: Map metadata
59-
60-
Returns:
61-
Tuple of (grid_x, grid_y, grid_theta)
62-
"""
63-
origin_x, origin_y, _ = metadata['origin']
64-
resolution = metadata['resolution']
65-
66-
grid_x = int((x - origin_x) / resolution)
67-
grid_y = int((y - origin_y) / resolution)
68-
grid_theta = theta # Heading remains the same
69-
70-
return grid_x, grid_y, grid_theta
71-
72-
def grid_to_world(grid_x, grid_y, grid_theta, metadata):
73-
"""
74-
Convert grid coordinates to world coordinates.
75-
76-
Args:
77-
grid_x, grid_y, grid_theta: Grid coordinates
78-
metadata: Map metadata
79-
80-
Returns:
81-
Tuple of (x, y, theta) in world coordinates
82-
"""
83-
origin_x, origin_y, _ = metadata['origin']
84-
resolution = metadata['resolution']
85-
86-
x = origin_x + grid_x * resolution
87-
y = origin_y + grid_y * resolution
88-
theta = grid_theta # Heading remains the same
89-
90-
return x, y, theta
39+
return g.reshape(nh, factor, nw, factor).max(axis=(1,3))

GEMstack/onboard/planning/planner.py

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,20 @@
11
import os
22
import argparse
33
import time
4-
import math
5-
import matplotlib.pyplot as plt
6-
import numpy as np
74
import matplotlib
8-
# from collision import build_collision_lookup
9-
# from kinodynamic_rrt_star import OptimizedKinodynamicRRT
10-
# from map_utils import load_pgm_to_occupancy_grid
11-
# from visualization import visualize_path, animate_path
5+
126
from .collision import build_collision_lookup
13-
from .map_utils import world_to_grid, grid_to_world
7+
from .map_utils import load_pgm_to_occupancy_grid
148
from .visualization import visualize_path, animate_path
159
from .kinodynamic_rrt_star import OptimizedKinodynamicRRT
16-
def optimized_kinodynamic_rrt_planning(start_world, goal_world, occupancy_grid, metadata = None,
17-
safety_margin=10, vehicle_width=5, step_size=1.0,
18-
turning_radius=3.0, max_iterations=100000, bidirectional=True):
10+
11+
def optimized_kinodynamic_rrt_planning(start_world, goal_world, occupancy_grid, safety_margin=10,
12+
vehicle_width=20, vehicle_length=45.0, step_size=1.0, max_iterations=100000):
1913
"""
20-
parser.add_argument("--test", action="store_true", help="run unit tests and exit")
21-
parser.add_argument("--vis", default=True, action="store_true", help="show visualizations for tests or planning result")
22-
parser.add_argument("--animate", "-a", action="store_true", help="animate planned path")
23-
parser.add_argument("--max-iter", type=int, default=20000, help="maximum RRT iterations")
24-
parser.add_argument("--pad", type=int, default=100, help="crop padding (cells)")
25-
parser.add_argument("--safety", type=int, default=2, help="safety margin (cells)")
26-
parser.add_argument("--map-pgm", type=str, default="occupancy_grid_after_>0.pgm", help="path to PGM map file")
27-
parser.add_argument("--map-yaml", type=str, default="rrt_occupancy_map.yaml", help="path to YAML metadata file")
28-
parser.add_argument("--step-size", type=float, default=1.0, help="step size for path sampling")
29-
parser.add_argument("--turning-radius", type=float, default=1.0, help="minimum turning radius")
30-
parser.add_argument("--vehicle-width", type=float, default=1.5, help="vehicle width in meters")
31-
parser.add_argument("--bidirectional", type=bool, default=False, help="Use bidirectional RRT*")
32-
Optimized Kinodynamic RRT planning from start to goal
33-
3414
Args:
3515
start_world: (x, y, theta) start position in world coordinates
3616
goal_world: (x, y, theta) goal position in world coordinates
3717
occupancy_grid: Binary occupancy grid (1=obstacle, 0=free)
38-
metadata: Map metadata
3918
safety_margin: Safety margin in grid cells
4019
vehicle_width: Vehicle width in meters
4120
step_size: Step size for path sampling
@@ -77,18 +56,21 @@ def optimized_kinodynamic_rrt_planning(start_world, goal_world, occupancy_grid,
7756
# Initialize planner with optimized parameters
7857
planner = OptimizedKinodynamicRRT(
7958
occupancy_grid=occupancy_grid,
80-
collision_lookup=collision_lookup,
81-
start_pose=start_grid,
82-
goal_pose=goal_grid,
83-
max_iter=max_iterations,
59+
collision_lookup_table=collision_lookup,
60+
start_pose_tuple=start_grid,
61+
goal_pose_tuple=goal_grid,
62+
max_iterations=max_iterations,
63+
local_sampling_step_size=step_size,
64+
vehicle_width=vehicle_width,
65+
vehicle_length=vehicle_length
8466
# bidirectional=bidirectional
8567
)
8668

8769
## Testing Summoning RRT implementation
8870
# planner = BiRRT(start_grid, goal_grid, 1-occupancy_grid, [0,4384,0,4667])
8971
# grid_path = planner.search()
9072
t_rrt = time.time()
91-
grid_path = planner.plan(visualize_final=True)
73+
grid_path = planner.plan(visualize_planning_output=True)
9274
# anim = planner.animate_sampling()
9375
t_rrt_final = time.time() - t_rrt
9476
print(f"RRT planning completed in {t_rrt_final:.2f}s")
@@ -103,51 +85,31 @@ def optimized_kinodynamic_rrt_planning(start_world, goal_world, occupancy_grid,
10385

10486
return grid_path
10587

88+
# Used for local testing
10689
def main():
10790
"""Main function for running the planner."""
10891
parser = argparse.ArgumentParser(description="Optimized Kinodynamic RRT planner")
109-
parser.add_argument("--test", action="store_true", help="run unit tests and exit")
11092
parser.add_argument("--vis", default=True, action="store_true", help="show visualizations for tests or planning result")
11193
parser.add_argument("--animate", "-a", action="store_true", help="animate planned path")
112-
parser.add_argument("--max-iter", type=int, default=20000, help="maximum RRT iterations")
94+
parser.add_argument("--max-iter", type=int, default=10000, help="maximum RRT iterations")
11395
parser.add_argument("--pad", type=int, default=100, help="crop padding (cells)")
11496
parser.add_argument("--safety", type=int, default=2, help="safety margin (cells)")
11597
parser.add_argument("--map-pgm", type=str, default="occupancy_grid_after_>0.pgm", help="path to PGM map file")
116-
parser.add_argument("--map-yaml", type=str, default="rrt_occupancy_map.yaml", help="path to YAML metadata file")
117-
parser.add_argument("--step-size", type=float, default=1.0, help="step size for path sampling")
118-
parser.add_argument("--turning-radius", type=float, default=1.0, help="minimum turning radius")
119-
parser.add_argument("--vehicle-width", type=float, default=1.5, help="vehicle width in meters")
120-
parser.add_argument("--bidirectional", type=bool, default=False, help="Use bidirectional RRT*")
121-
122-
# parser = argparse.ArgumentParser(description="Optimized Kinodynamic RRT planner")
123-
# parser.add_argument("--test", action="store_true", help="run unit tests and exit")
124-
# parser.add_argument("--vis", action="store_true", help="show visualizations for tests or planning result")
125-
# parser.add_argument("--animate", "-a", action="store_true", help="animate planned path")
126-
# parser.add_argument("--max-iter", type=int, default=10000, help="maximum RRT iterations")
127-
# parser.add_argument("--pad", type=int, default=100, help="crop padding (cells)")
128-
# parser.add_argument("--safety", type=int, default=2, help="safety margin (cells)")
129-
# parser.add_argument("--map-pgm", type=str, default="rrt_occupancy_map.pgm", help="path to PGM map file")
130-
# parser.add_argument("--map-yaml", type=str, default="rrt_occupancy_map.yaml", help="path to YAML metadata file")
131-
# parser.add_argument("--step-size", type=float, default=5.0, help="step size for path sampling")
132-
# parser.add_argument("--turning-radius", type=float, default=100.0, help="minimum turning radius")
133-
# parser.add_argument("--vehicle-width", type=float, default=12.0, help="vehicle width in meters")
134-
# parser.add_argument("--bidirectional", type=bool, default=False, help="Use bidirectional RRT*")
98+
parser.add_argument("--step-size", type=float, default=100.0, help="step size for path sampling")
99+
parser.add_argument("--vehicle-width", type=float, default=20, help="vehicle width in meters")
100+
parser.add_argument("--vehicle-length", type=float, default=45, help="vehicle length in meters")
135101

136102
if "--vis" not in os.sys.argv and "--animate" not in os.sys.argv and "-a" not in os.sys.argv:
137103
matplotlib.use("Agg")
138104

139105
args = parser.parse_args()
140106

141-
if args.test:
142-
run_tests(show=args.vis)
143-
return
144-
145-
if not (os.path.exists(args.map_pgm) and os.path.exists(args.map_yaml)):
146-
print(f"Map files not found: {args.map_pgm} and/or {args.map_yaml}")
147-
print("Use --test for CI runs or provide map files with --map-pgm and --map-yaml")
107+
if not (os.path.exists(args.map_pgm)):
108+
print(f"Map files not found: {args.map_pgm}")
109+
print("Use --test for CI runs or provide map files with --map-pgm")
148110
return
149111

150-
occupancy_grid, meta = load_pgm_to_occupancy_grid(args.map_pgm, args.map_yaml)
112+
occupancy_grid = load_pgm_to_occupancy_grid(args.map_pgm)
151113

152114
# start_w = (-200.0, -137.0, 0 * math.pi / 2)
153115
# goal_w = (-100.0, -137.0, 3*math.pi/2)
@@ -160,24 +122,22 @@ def main():
160122
# goal_w = (15.0, 11.0, 2*math.pi/2)
161123
# goal_w = (.0, 5.0, 1*math.pi/2)
162124

163-
visualize_path(occupancy_grid, [], meta, start_w, goal_w)
125+
visualize_path(occupancy_grid, [], start_w, goal_w)
164126

165127
path = optimized_kinodynamic_rrt_planning(
166-
start_w, goal_w, occupancy_grid, meta,
128+
start_w, goal_w, occupancy_grid,
167129
safety_margin=args.safety,
168130
vehicle_width=args.vehicle_width,
169131
step_size=args.step_size,
170-
turning_radius=args.turning_radius,
171132
max_iterations=args.max_iter,
172-
bidirectional=args.bidirectional
173133
)
174134

175135
if path:
176136
print(f"Planned path with {len(path)} poses")
177137
if args.vis:
178-
visualize_path(occupancy_grid, path, meta, start_w, goal_w)
138+
visualize_path(occupancy_grid, path, start_w, goal_w)
179139
if args.animate:
180-
animate_path(occupancy_grid, path, meta, pad_cells=args.pad)
140+
animate_path(occupancy_grid, path, pad_cells=args.pad)
181141
else:
182142
print("Failed to find path")
183143

GEMstack/onboard/planning/utils.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
import math
2-
import numpy as np
32

4-
def normalize_angle(angle: float) -> float:
5-
"""Wrap angle to (‑π, π]."""
6-
return (angle + math.pi) % (2 * math.pi) - math.pi
7-
8-
def bresenham(x0, y0, x1, y1):
9-
"""Bresenham's line algorithm for grid traversal."""
10-
x0, y0, x1, y1 = map(int, map(round, (x0, y0, x1, y1)))
11-
dx, sx = abs(x1 - x0), (1 if x0 < x1 else -1)
12-
dy, sy = -abs(y1 - y0), (1 if y0 < y1 else -1)
13-
err = dx + dy
14-
while True:
15-
yield x0, y0
16-
if x0 == x1 and y0 == y1:
17-
break
18-
e2 = 2 * err
19-
if e2 >= dy:
20-
err += dy
21-
x0 += sx
22-
if e2 <= dx:
23-
err += dx
24-
y0 += sy
3+
def normalize_angle(angle):
4+
while angle > math.pi: angle -= 2.0 * math.pi
5+
while angle < -math.pi: angle += 2.0 * math.pi
6+
return angle

GEMstack/onboard/planning/visualization.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import math
22
import matplotlib.pyplot as plt
33
import matplotlib.animation as animation
4-
# from map_utils import world_to_grid
5-
from .map_utils import world_to_grid
64
import time
7-
def visualize_path(occupancy_grid, path, metadata, start_world, goal_world, show_headings=True):
5+
6+
def visualize_path(occupancy_grid, path, start_world, goal_world, show_headings=True):
87
"""
98
Visualize the planned path.
109
1110
Args:
1211
occupancy_grid: Binary occupancy grid (1=obstacle, 0=free)
1312
path: List of (x, y, theta) world coordinates
14-
metadata: Map metadata
1513
start_world: (x, y, theta) start position in world coordinates
1614
goal_world: (x, y, theta) goal position in world coordinates
1715
show_headings: Whether to show heading arrows along the path
@@ -23,13 +21,9 @@ def visualize_path(occupancy_grid, path, metadata, start_world, goal_world, show
2321

2422
# Extract path points
2523
if path:
26-
grid_path = []
27-
for world_pt in path:
28-
grid_pt = world_to_grid(*world_pt, metadata)
29-
grid_path.append(grid_pt)
3024

31-
xs = [p[0] for p in grid_path]
32-
ys = [p[1] for p in grid_path]
25+
xs = [p[0] for p in path]
26+
ys = [p[1] for p in path]
3327

3428
# Plot path
3529
plt.plot(xs, ys, 'g-', linewidth=2)
@@ -39,15 +33,15 @@ def visualize_path(occupancy_grid, path, metadata, start_world, goal_world, show
3933
arrow_interval = max(1, len(path) // 20) # Show ~20 arrows along path
4034
arrow_length = 10
4135

42-
for i in range(0, len(grid_path), arrow_interval):
43-
x, y, theta = grid_path[i]
36+
for i in range(0, len(path), arrow_interval):
37+
x, y, theta = path[i]
4438
dx = arrow_length * math.cos(theta)
4539
dy = arrow_length * math.sin(theta)
4640
plt.arrow(x, y, dx, dy, head_width=3, head_length=6, fc='blue', ec='blue')
4741

4842
# Show start and goal
49-
start_grid = world_to_grid(*start_world, metadata)
50-
goal_grid = world_to_grid(*goal_world, metadata)
43+
start_grid = start_world
44+
goal_grid = goal_world
5145

5246
plt.scatter(start_grid[0], start_grid[1], color='green', s=100, marker='o', label='Start')
5347
plt.scatter(goal_grid[0], goal_grid[1], color='red', s=100, marker='x', label='Goal')
@@ -70,15 +64,14 @@ def visualize_path(occupancy_grid, path, metadata, start_world, goal_world, show
7064
plt.savefig(f"path_planning_result_{time.time()}.png")
7165
# plt.show()
7266

73-
def animate_path(occupancy_grid, path, metadata, interval=60, pad_cells=20,
67+
def animate_path(occupancy_grid, path, interval=60, pad_cells=20,
7468
save="ani.gif", vehicle_len=10):
7569
"""
7670
Animate the drive and crop axes to the path region.
7771
7872
Args:
7973
occupancy_grid: Binary occupancy grid (1=obstacle, 0=free)
8074
path: List of (x, y, theta) world coordinates
81-
metadata: Map metadata
8275
interval: Animation interval in milliseconds
8376
pad_cells: Extra cells to pad around trajectory bbox
8477
save: If provided, save animation to this file
@@ -87,11 +80,8 @@ def animate_path(occupancy_grid, path, metadata, interval=60, pad_cells=20,
8780
Returns:
8881
Animation object
8982
"""
90-
def world_to_grid(x, y, th, meta):
91-
ox, oy, _ = meta['origin']; res = meta['resolution']
92-
return ((x-ox)/res, (y-oy)/res, th)
9383

94-
gpath = [world_to_grid(*pt, metadata) for pt in path]
84+
gpath = [pt for pt in path]
9585
xs = [p[0] for p in gpath]; ys = [p[1] for p in gpath]
9686

9787
xmin, xmax = min(xs)-pad_cells, max(xs)+pad_cells

0 commit comments

Comments
 (0)