Update app.py
Browse files
app.py
CHANGED
|
@@ -18,15 +18,8 @@ SCAN_ANGLE = 60
|
|
| 18 |
SCAN_RESOLUTION = 30
|
| 19 |
SCAN_RANGE_DEFAULT = 6.0
|
| 20 |
OBSTACLE_COUNT = 10
|
| 21 |
-
scan_range = 5.0 # or whatever range your LiDAR/scanner is expected to use
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# State variables
|
| 25 |
-
robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0}
|
| 26 |
-
trajectory = []
|
| 27 |
-
slam_points = []
|
| 28 |
-
obstacles = []
|
| 29 |
|
|
|
|
| 30 |
def to_canvas_coords(x, y):
|
| 31 |
cx = int((x + WORLD_UNITS / 2) * SCALE)
|
| 32 |
cy = int((WORLD_UNITS / 2 - y) * SCALE)
|
|
@@ -87,25 +80,30 @@ def move_robot(robot_pose, obstacles):
|
|
| 87 |
def draw_environment(robot_pose, trajectory, obstacles):
|
| 88 |
img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white")
|
| 89 |
draw = ImageDraw.Draw(img)
|
| 90 |
-
|
| 91 |
-
|
|
|
|
| 92 |
x = int(i * SCALE)
|
| 93 |
-
draw.line([(x,0), (x, SPACE_SIZE)], fill=(230,230,230))
|
| 94 |
-
draw.line([(0,x), (SPACE_SIZE, x)], fill=(230,230,230))
|
| 95 |
-
|
|
|
|
| 96 |
for ox, oy in obstacles:
|
| 97 |
cx, cy = to_canvas_coords(ox, oy)
|
| 98 |
r = int(OBSTACLE_RADIUS * SCALE)
|
| 99 |
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill="gray")
|
| 100 |
-
|
|
|
|
| 101 |
if len(trajectory) > 1:
|
| 102 |
points = [to_canvas_coords(x, y) for x, y in trajectory]
|
| 103 |
draw.line(points, fill="blue", width=2)
|
| 104 |
-
|
|
|
|
| 105 |
rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 106 |
r = int(ROBOT_RADIUS * SCALE)
|
| 107 |
draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red")
|
| 108 |
-
|
|
|
|
| 109 |
dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 110 |
dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 111 |
draw.line([rx, ry, rx + dx, ry - dy], fill="darkred", width=2)
|
|
@@ -114,48 +112,49 @@ def draw_environment(robot_pose, trajectory, obstacles):
|
|
| 114 |
def draw_slam_map(robot_pose, slam_points, scan_range):
|
| 115 |
img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white")
|
| 116 |
draw = ImageDraw.Draw(img)
|
| 117 |
-
|
| 118 |
-
|
|
|
|
| 119 |
x = int(i * SCALE)
|
| 120 |
-
draw.line([(x,0), (x, SPACE_SIZE)], fill=(230,230,230))
|
| 121 |
-
draw.line([(0,x), (SPACE_SIZE, x)], fill=(230,230,230))
|
| 122 |
-
|
|
|
|
| 123 |
angles = np.linspace(-SCAN_ANGLE / 2, SCAN_ANGLE / 2, SCAN_RESOLUTION) + robot_pose['angle']
|
| 124 |
for a in angles:
|
| 125 |
rad = math.radians(a)
|
| 126 |
dist = scan_range
|
| 127 |
-
# Find closest slam point along this angle
|
| 128 |
-
closest_dist = None
|
| 129 |
for px, py in slam_points:
|
| 130 |
vx, vy = px - robot_pose['x'], py - robot_pose['y']
|
| 131 |
point_dist = math.hypot(vx, vy)
|
| 132 |
point_angle = math.degrees(math.atan2(vy, vx))
|
| 133 |
angle_diff = abs((point_angle - a + 180) % 360 - 180)
|
| 134 |
if angle_diff < SCAN_ANGLE / SCAN_RESOLUTION:
|
| 135 |
-
if
|
| 136 |
-
|
| 137 |
-
if closest_dist is not None:
|
| 138 |
-
dist = closest_dist
|
| 139 |
x_end = robot_pose['x'] + dist * math.cos(rad)
|
| 140 |
y_end = robot_pose['y'] + dist * math.sin(rad)
|
| 141 |
cx_start, cy_start = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 142 |
cx_end, cy_end = to_canvas_coords(x_end, y_end)
|
| 143 |
draw.line([cx_start, cy_start, cx_end, cy_end], fill="green")
|
| 144 |
-
|
|
|
|
| 145 |
for px, py in slam_points:
|
| 146 |
cx, cy = to_canvas_coords(px, py)
|
| 147 |
-
draw.ellipse([cx-2, cy-2, cx+2, cy+2], fill="black")
|
| 148 |
-
|
|
|
|
| 149 |
rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 150 |
r = int(ROBOT_RADIUS * SCALE)
|
| 151 |
draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red")
|
| 152 |
-
|
|
|
|
| 153 |
dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 154 |
dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 155 |
draw.line([rx, ry, rx + dx, ry - dy], fill="red", width=2)
|
| 156 |
return img
|
| 157 |
|
| 158 |
-
#
|
| 159 |
class SimulationState:
|
| 160 |
def __init__(self):
|
| 161 |
self.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0}
|
|
@@ -167,6 +166,7 @@ class SimulationState:
|
|
| 167 |
|
| 168 |
state = SimulationState()
|
| 169 |
|
|
|
|
| 170 |
def reset_simulation(obstacle_count):
|
| 171 |
state.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0}
|
| 172 |
state.trajectory = []
|
|
@@ -179,43 +179,39 @@ def reset_simulation(obstacle_count):
|
|
| 179 |
return env_img, slam_img, "Simulation reset"
|
| 180 |
|
| 181 |
def step_simulation(scan_range):
|
| 182 |
-
# Move robot and update trajectory
|
| 183 |
pose_before = state.robot_pose.copy()
|
| 184 |
new_pose, moved = move_robot(state.robot_pose, state.obstacles)
|
| 185 |
if moved:
|
| 186 |
state.trajectory.append((new_pose['x'], new_pose['y']))
|
| 187 |
-
|
| 188 |
new_points = scan_environment(state.robot_pose, state.obstacles, scan_range)
|
| 189 |
-
# Add only new points that are not close to existing points
|
| 190 |
for pt in new_points:
|
| 191 |
if all(math.hypot(pt[0] - p[0], pt[1] - p[1]) > 0.1 for p in state.slam_points):
|
| 192 |
state.slam_points.append(pt)
|
| 193 |
-
|
| 194 |
env_img = draw_environment(state.robot_pose, state.trajectory, state.obstacles)
|
| 195 |
-
|
| 196 |
-
#slam_img = draw_slam_map(state.robot_pose, state.slam_points, scan_range)
|
| 197 |
-
slam_img = draw_slam_map(state.robot_pose, state.slam_points)
|
| 198 |
|
| 199 |
-
# If recording, save frames
|
| 200 |
if state.recording:
|
| 201 |
combined = Image.new('RGB', (SPACE_SIZE * 2, SPACE_SIZE))
|
| 202 |
combined.paste(env_img, (0, 0))
|
| 203 |
combined.paste(slam_img, (SPACE_SIZE, 0))
|
| 204 |
state.video_frames.append(np.array(combined))
|
|
|
|
| 205 |
return env_img, slam_img, f"Robot moved: {moved}, Position: ({state.robot_pose['x']:.2f}, {state.robot_pose['y']:.2f})"
|
| 206 |
|
| 207 |
def toggle_recording(record):
|
| 208 |
state.recording = record
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
|
| 220 |
# Default obstacle count
|
| 221 |
OBSTACLE_COUNT = 10
|
|
|
|
| 18 |
SCAN_RESOLUTION = 30
|
| 19 |
SCAN_RANGE_DEFAULT = 6.0
|
| 20 |
OBSTACLE_COUNT = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
# Utilities
|
| 23 |
def to_canvas_coords(x, y):
|
| 24 |
cx = int((x + WORLD_UNITS / 2) * SCALE)
|
| 25 |
cy = int((WORLD_UNITS / 2 - y) * SCALE)
|
|
|
|
| 80 |
def draw_environment(robot_pose, trajectory, obstacles):
|
| 81 |
img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white")
|
| 82 |
draw = ImageDraw.Draw(img)
|
| 83 |
+
|
| 84 |
+
# Grid
|
| 85 |
+
for i in range(int(WORLD_UNITS) + 1):
|
| 86 |
x = int(i * SCALE)
|
| 87 |
+
draw.line([(x, 0), (x, SPACE_SIZE)], fill=(230, 230, 230))
|
| 88 |
+
draw.line([(0, x), (SPACE_SIZE, x)], fill=(230, 230, 230))
|
| 89 |
+
|
| 90 |
+
# Obstacles
|
| 91 |
for ox, oy in obstacles:
|
| 92 |
cx, cy = to_canvas_coords(ox, oy)
|
| 93 |
r = int(OBSTACLE_RADIUS * SCALE)
|
| 94 |
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill="gray")
|
| 95 |
+
|
| 96 |
+
# Trajectory
|
| 97 |
if len(trajectory) > 1:
|
| 98 |
points = [to_canvas_coords(x, y) for x, y in trajectory]
|
| 99 |
draw.line(points, fill="blue", width=2)
|
| 100 |
+
|
| 101 |
+
# Robot
|
| 102 |
rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 103 |
r = int(ROBOT_RADIUS * SCALE)
|
| 104 |
draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red")
|
| 105 |
+
|
| 106 |
+
# Heading
|
| 107 |
dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 108 |
dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 109 |
draw.line([rx, ry, rx + dx, ry - dy], fill="darkred", width=2)
|
|
|
|
| 112 |
def draw_slam_map(robot_pose, slam_points, scan_range):
|
| 113 |
img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white")
|
| 114 |
draw = ImageDraw.Draw(img)
|
| 115 |
+
|
| 116 |
+
# Grid
|
| 117 |
+
for i in range(int(WORLD_UNITS) + 1):
|
| 118 |
x = int(i * SCALE)
|
| 119 |
+
draw.line([(x, 0), (x, SPACE_SIZE)], fill=(230, 230, 230))
|
| 120 |
+
draw.line([(0, x), (SPACE_SIZE, x)], fill=(230, 230, 230))
|
| 121 |
+
|
| 122 |
+
# Laser scan lines
|
| 123 |
angles = np.linspace(-SCAN_ANGLE / 2, SCAN_ANGLE / 2, SCAN_RESOLUTION) + robot_pose['angle']
|
| 124 |
for a in angles:
|
| 125 |
rad = math.radians(a)
|
| 126 |
dist = scan_range
|
|
|
|
|
|
|
| 127 |
for px, py in slam_points:
|
| 128 |
vx, vy = px - robot_pose['x'], py - robot_pose['y']
|
| 129 |
point_dist = math.hypot(vx, vy)
|
| 130 |
point_angle = math.degrees(math.atan2(vy, vx))
|
| 131 |
angle_diff = abs((point_angle - a + 180) % 360 - 180)
|
| 132 |
if angle_diff < SCAN_ANGLE / SCAN_RESOLUTION:
|
| 133 |
+
if point_dist < dist:
|
| 134 |
+
dist = point_dist
|
|
|
|
|
|
|
| 135 |
x_end = robot_pose['x'] + dist * math.cos(rad)
|
| 136 |
y_end = robot_pose['y'] + dist * math.sin(rad)
|
| 137 |
cx_start, cy_start = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 138 |
cx_end, cy_end = to_canvas_coords(x_end, y_end)
|
| 139 |
draw.line([cx_start, cy_start, cx_end, cy_end], fill="green")
|
| 140 |
+
|
| 141 |
+
# SLAM points
|
| 142 |
for px, py in slam_points:
|
| 143 |
cx, cy = to_canvas_coords(px, py)
|
| 144 |
+
draw.ellipse([cx - 2, cy - 2, cx + 2, cy + 2], fill="black")
|
| 145 |
+
|
| 146 |
+
# Robot
|
| 147 |
rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y'])
|
| 148 |
r = int(ROBOT_RADIUS * SCALE)
|
| 149 |
draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red")
|
| 150 |
+
|
| 151 |
+
# Heading
|
| 152 |
dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 153 |
dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE
|
| 154 |
draw.line([rx, ry, rx + dx, ry - dy], fill="red", width=2)
|
| 155 |
return img
|
| 156 |
|
| 157 |
+
# Simulation State
|
| 158 |
class SimulationState:
|
| 159 |
def __init__(self):
|
| 160 |
self.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0}
|
|
|
|
| 166 |
|
| 167 |
state = SimulationState()
|
| 168 |
|
| 169 |
+
# Handlers
|
| 170 |
def reset_simulation(obstacle_count):
|
| 171 |
state.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0}
|
| 172 |
state.trajectory = []
|
|
|
|
| 179 |
return env_img, slam_img, "Simulation reset"
|
| 180 |
|
| 181 |
def step_simulation(scan_range):
|
|
|
|
| 182 |
pose_before = state.robot_pose.copy()
|
| 183 |
new_pose, moved = move_robot(state.robot_pose, state.obstacles)
|
| 184 |
if moved:
|
| 185 |
state.trajectory.append((new_pose['x'], new_pose['y']))
|
| 186 |
+
|
| 187 |
new_points = scan_environment(state.robot_pose, state.obstacles, scan_range)
|
|
|
|
| 188 |
for pt in new_points:
|
| 189 |
if all(math.hypot(pt[0] - p[0], pt[1] - p[1]) > 0.1 for p in state.slam_points):
|
| 190 |
state.slam_points.append(pt)
|
| 191 |
+
|
| 192 |
env_img = draw_environment(state.robot_pose, state.trajectory, state.obstacles)
|
| 193 |
+
slam_img = draw_slam_map(state.robot_pose, state.slam_points, scan_range)
|
|
|
|
|
|
|
| 194 |
|
|
|
|
| 195 |
if state.recording:
|
| 196 |
combined = Image.new('RGB', (SPACE_SIZE * 2, SPACE_SIZE))
|
| 197 |
combined.paste(env_img, (0, 0))
|
| 198 |
combined.paste(slam_img, (SPACE_SIZE, 0))
|
| 199 |
state.video_frames.append(np.array(combined))
|
| 200 |
+
|
| 201 |
return env_img, slam_img, f"Robot moved: {moved}, Position: ({state.robot_pose['x']:.2f}, {state.robot_pose['y']:.2f})"
|
| 202 |
|
| 203 |
def toggle_recording(record):
|
| 204 |
state.recording = record
|
| 205 |
+
return f"Recording {'started' if record else 'stopped'}."
|
| 206 |
+
|
| 207 |
+
def export_video():
|
| 208 |
+
if not state.video_frames:
|
| 209 |
+
return None, "No frames recorded."
|
| 210 |
+
buffer = io.BytesIO()
|
| 211 |
+
imageio.mimsave(buffer, state.video_frames, format="GIF", duration=0.1)
|
| 212 |
+
buffer.seek(0)
|
| 213 |
+
return buffer, "Video exported."
|
| 214 |
+
|
| 215 |
|
| 216 |
# Default obstacle count
|
| 217 |
OBSTACLE_COUNT = 10
|