Skip to content

Commit 35fc6ea

Browse files
authored
Merge pull request #22 from shantanuparabumd/astar
Implementation Of Astar Path Planning
2 parents 3ddebcc + a9d8f51 commit 35fc6ea

File tree

10 files changed

+620
-2
lines changed

10 files changed

+620
-2
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ src/simulations/perception/sensor_auto_calibration/__pycache__
3939
test/__pycache__
4040
Test/__pycache__
4141
.pytest_cache
42-
.coverage
42+
.coverage
43+
__pycache__
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""
2+
binary_occupancy_grid.py
3+
4+
Author: Shantanu Parab
5+
"""
6+
7+
import numpy as np
8+
import sys
9+
from pathlib import Path
10+
from collections import defaultdict
11+
import matplotlib.pyplot as plt
12+
from matplotlib.colors import ListedColormap
13+
14+
15+
abs_dir_path = str(Path(__file__).absolute().parent)
16+
relative_path = "/../../../components/"
17+
18+
19+
sys.path.append(abs_dir_path + relative_path + "visualization")
20+
sys.path.append(abs_dir_path + relative_path + "obstacle")
21+
sys.path.append(abs_dir_path + relative_path + "state")
22+
23+
24+
from min_max import MinMax
25+
from obstacle_list import ObstacleList
26+
from obstacle import Obstacle
27+
from state import State
28+
import json
29+
30+
31+
# Define RGB colors for each grid value
32+
# Colors in the format [R, G, B], where values are in the range [0, 1]
33+
colors = [
34+
[1.0, 1.0, 1.0], # Free space (white)
35+
[0.4, 0.8, 1.0], # Explored nodes (light blue)
36+
[0.0, 1.0, 0.0], # Path (green)
37+
[0.5, 0.5, 0.5], # Clearance space (yellow-orange)
38+
[0.0, 0.0, 0.0], # Obstacles (red)
39+
]
40+
41+
# Create a colormap
42+
custom_cmap = ListedColormap(colors)
43+
44+
class BinaryOccupancyGrid:
45+
def __init__(self, x_lim , y_lim, resolution, clearance, map_path):
46+
47+
self.x_min, self.x_max = x_lim.min_value(), x_lim.max_value()
48+
self.y_min, self.y_max = y_lim.min_value(), y_lim.max_value()
49+
self.resolution = resolution
50+
self.clearance = clearance
51+
52+
self.map, self.x_range, self.y_range = self.create_grid()
53+
self.map_path = map_path
54+
55+
56+
def create_grid(self):
57+
"""Create a grid based on the specified or derived limits."""
58+
59+
x_range = np.arange(self.x_min, self.x_max, self.resolution)
60+
y_range = np.arange(self.y_min, self.y_max, self.resolution)
61+
62+
map = np.zeros((len(y_range), len(x_range))) # Initialize map as free space
63+
64+
return map, x_range, y_range
65+
66+
def add_object(self, obtacle_list: ObstacleList):
67+
"""Mark obstacles and their clearance on the map, considering rotation (yaw)."""
68+
for obs in obtacle_list.list:
69+
# Get obstacle parameters
70+
x_c = obs.state.x_m
71+
y_c = obs.state.y_m
72+
yaw = obs.state.yaw_rad
73+
length = obs.length_m
74+
width = obs.width_m
75+
76+
# Calculate the clearance dimensions
77+
clearance_length = length + self.clearance
78+
clearance_width = width + self.clearance
79+
80+
# Define corners for the clearance area
81+
clearance_corners = np.array([
82+
[-clearance_length, -clearance_width],
83+
[-clearance_length, clearance_width],
84+
[clearance_length, clearance_width],
85+
[clearance_length, -clearance_width]
86+
])
87+
88+
# Define corners for the actual obstacle
89+
obstacle_corners = np.array([
90+
[-length, -width],
91+
[-length, width],
92+
[length, width],
93+
[length, -width]
94+
])
95+
96+
# Apply rotation to both obstacle and clearance corners
97+
rotation_matrix = np.array([
98+
[np.cos(yaw), -np.sin(yaw)],
99+
[np.sin(yaw), np.cos(yaw)]
100+
])
101+
rotated_clearance_corners = np.dot(clearance_corners, rotation_matrix.T) + np.array([x_c, y_c])
102+
rotated_obstacle_corners = np.dot(obstacle_corners, rotation_matrix.T) + np.array([x_c, y_c])
103+
104+
# Mark the clearance area
105+
self._mark_area(rotated_clearance_corners, value=0.75) # 0.5 for clearance
106+
107+
# Mark the actual obstacle area
108+
self._mark_area(rotated_obstacle_corners, value=1.0) # 1.0 for obstacles
109+
110+
def _point_in_polygon(self, x, y, corners):
111+
"""
112+
Check if a point (x, y) is inside a polygon defined by corners.
113+
Args:
114+
x: X-coordinate of the point.
115+
y: Y-coordinate of the point.
116+
corners: Array of polygon corners in global coordinates.
117+
Returns:
118+
True if the point is inside the polygon, False otherwise.
119+
"""
120+
n = len(corners)
121+
inside = False
122+
px, py = x, y
123+
for i in range(n):
124+
x1, y1 = corners[i]
125+
x2, y2 = corners[(i + 1) % n]
126+
if ((y1 > py) != (y2 > py)) and \
127+
(px < (x2 - x1) * (py - y1) / (y2 - y1 + 1e-6) + x1):
128+
inside = not inside
129+
return inside
130+
131+
132+
def _mark_area(self, corners, value):
133+
"""
134+
Mark a rectangular area on the map based on the given rotated corners.
135+
Args:
136+
corners: The rotated corners of the area in global coordinates.
137+
value: The value to mark in the map (e.g., 0.5 for clearance, 1.0 for obstacles).
138+
"""
139+
# Get the bounding box of the corners
140+
x_min = max(0, int((min(corners[:, 0]) - self.x_range[0]) / self.resolution))
141+
x_max = min(self.map.shape[1], int((max(corners[:, 0]) - self.x_range[0]) / self.resolution))
142+
y_min = max(0, int((min(corners[:, 1]) - self.y_range[0]) / self.resolution))
143+
y_max = min(self.map.shape[0], int((max(corners[:, 1]) - self.y_range[0]) / self.resolution))
144+
145+
# Iterate through the map cells in the bounding box
146+
for x in range(x_min, x_max):
147+
for y in range(y_min, y_max):
148+
# Get the center of the current cell
149+
cell_x = self.x_range[0] + x * self.resolution + self.resolution / 2
150+
cell_y = self.y_range[0] + y * self.resolution + self.resolution / 2
151+
152+
# Check if the cell center is inside the rotated polygon
153+
if self._point_in_polygon(cell_x, cell_y, corners):
154+
self.map[y, x] = max(self.map[y, x], value) # Mark the cell
155+
156+
# Save the map to a file as an image/json
157+
def save_map(self):
158+
"""
159+
Save the map to a file.
160+
"""
161+
162+
if self.map_path.endswith('.npy'):
163+
np.save(self.map_path, self.map)
164+
elif self.map_path.endswith('.png'):
165+
plt.imsave(self.map_path, self.map, cmap=custom_cmap, origin='lower')
166+
elif self.map_path.endswith('.json'):
167+
map_list = self.map.tolist()
168+
with open(self.map_path, 'w') as f:
169+
json.dump(map_list, f)
170+
else:
171+
raise ValueError("Unsupported file format. Use .npy, .png, or .json")
172+
173+
174+
175+
if __name__ == "__main__":
176+
177+
obst_list = ObstacleList()
178+
obst_list.add_obstacle(Obstacle(State(x_m=10.0, y_m=15.0), length_m=10, width_m=8))
179+
obst_list.add_obstacle(Obstacle(State(x_m=40.0, y_m=0.0), length_m=2, width_m=10))
180+
obst_list.add_obstacle(Obstacle(State(x_m=10.0, y_m=-10.0, yaw_rad=np.rad2deg(45)), length_m=5, width_m=5))
181+
obst_list.add_obstacle(Obstacle(State(x_m=30.0, y_m=15.0, yaw_rad=np.rad2deg(10)), length_m=5, width_m=2))
182+
183+
bin_occ_grid = BinaryOccupancyGrid(MinMax(-5, 55), MinMax(-20, 25), 0.5, 1.5)
184+
bin_occ_grid.add_object(obst_list)
185+
186+
bin_occ_grid.save_map("map.json")
187+
188+
plt.figure(figsize=(10, 8))
189+
plt.imshow(bin_occ_grid.map, extent=[bin_occ_grid.x_range[0], bin_occ_grid.x_range[-1], bin_occ_grid.y_range[0], bin_occ_grid.y_range[-1]],
190+
origin='lower', cmap=custom_cmap)
191+
192+
plt.legend()
193+
plt.show()

src/components/obstacle/obstacle.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ def __init__(self, state, accel_mps2=0.0, yaw_rate_rps=0.0,
3131
self.state = state
3232
self.accel_mps2 = accel_mps2
3333
self.yaw_rate_rps = yaw_rate_rps
34+
self.length_m = length_m
35+
self.width_m = width_m
3436

3537
contour = np.array([[length_m, -length_m, -length_m, length_m, length_m],
3638
[width_m, width_m, -width_m, -width_m, width_m]])
39+
3740
self.array = XYArray(contour)
38-
41+
3942
def update(self, time_s):
4043
"""
4144
Function to update obstacle's state
Binary file not shown.

0 commit comments

Comments
 (0)