|
| 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() |
0 commit comments