import math import numpy as np import random import imageio from PIL import Image, ImageDraw import io import gradio as gr # Parameters SPACE_SIZE = 600 WORLD_UNITS = 20 SCALE = SPACE_SIZE / WORLD_UNITS OBSTACLE_RADIUS = 0.5 ROBOT_RADIUS = 0.3 STEP_SIZE = 0.3 TURN_ANGLE = 30 SCAN_ANGLE = 60 SCAN_RESOLUTION = 30 SCAN_RANGE_DEFAULT = 6.0 OBSTACLE_COUNT = 10 # Utilities def to_canvas_coords(x, y): cx = int((x + WORLD_UNITS / 2) * SCALE) cy = int((WORLD_UNITS / 2 - y) * SCALE) return cx, cy def generate_obstacles(count): obs = [] for _ in range(count): while True: x = random.uniform(-WORLD_UNITS / 2 + OBSTACLE_RADIUS, WORLD_UNITS / 2 - OBSTACLE_RADIUS) y = random.uniform(-WORLD_UNITS / 2 + OBSTACLE_RADIUS, WORLD_UNITS / 2 - OBSTACLE_RADIUS) if math.hypot(x, y) > 2: obs.append((x, y)) break return obs def check_collision(x, y, obstacles): if not (-WORLD_UNITS / 2 + ROBOT_RADIUS <= x <= WORLD_UNITS / 2 - ROBOT_RADIUS): return True if not (-WORLD_UNITS / 2 + ROBOT_RADIUS <= y <= WORLD_UNITS / 2 - ROBOT_RADIUS): return True for ox, oy in obstacles: if math.hypot(ox - x, oy - y) <= OBSTACLE_RADIUS + ROBOT_RADIUS: return True return False def scan_environment(robot_pose, obstacles, scan_range): points = [] angles = np.linspace(-SCAN_ANGLE / 2, SCAN_ANGLE / 2, SCAN_RESOLUTION) + robot_pose['angle'] for a in angles: rad = math.radians(a) for d in np.linspace(0, scan_range, 100): x = robot_pose['x'] + d * math.cos(rad) y = robot_pose['y'] + d * math.sin(rad) if any(math.hypot(ox - x, oy - y) <= OBSTACLE_RADIUS for ox, oy in obstacles): points.append((x, y)) break return points def move_robot(robot_pose, obstacles): rad = math.radians(robot_pose['angle']) for d in np.linspace(0, SCAN_RANGE_DEFAULT, 30): x = robot_pose['x'] + d * math.cos(rad) y = robot_pose['y'] + d * math.sin(rad) if check_collision(x, y, obstacles): robot_pose['angle'] -= TURN_ANGLE return robot_pose, False new_x = robot_pose['x'] + STEP_SIZE * math.cos(rad) new_y = robot_pose['y'] + STEP_SIZE * math.sin(rad) if not check_collision(new_x, new_y, obstacles): robot_pose['x'] = new_x robot_pose['y'] = new_y return robot_pose, True else: robot_pose['angle'] -= TURN_ANGLE return robot_pose, False def draw_environment(robot_pose, trajectory, obstacles): img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white") draw = ImageDraw.Draw(img) # Grid for i in range(int(WORLD_UNITS) + 1): x = int(i * SCALE) draw.line([(x, 0), (x, SPACE_SIZE)], fill=(230, 230, 230)) draw.line([(0, x), (SPACE_SIZE, x)], fill=(230, 230, 230)) # Obstacles for ox, oy in obstacles: cx, cy = to_canvas_coords(ox, oy) r = int(OBSTACLE_RADIUS * SCALE) draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill="gray") # Trajectory if len(trajectory) > 1: points = [to_canvas_coords(x, y) for x, y in trajectory] draw.line(points, fill="blue", width=2) # Robot rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y']) r = int(ROBOT_RADIUS * SCALE) draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red") # Heading dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE draw.line([rx, ry, rx + dx, ry - dy], fill="darkred", width=2) return img def draw_slam_map(robot_pose, slam_points, scan_range): img = Image.new("RGB", (SPACE_SIZE, SPACE_SIZE), "white") draw = ImageDraw.Draw(img) # Grid for i in range(int(WORLD_UNITS) + 1): x = int(i * SCALE) draw.line([(x, 0), (x, SPACE_SIZE)], fill=(230, 230, 230)) draw.line([(0, x), (SPACE_SIZE, x)], fill=(230, 230, 230)) # Laser scan lines angles = np.linspace(-SCAN_ANGLE / 2, SCAN_ANGLE / 2, SCAN_RESOLUTION) + robot_pose['angle'] for a in angles: rad = math.radians(a) dist = scan_range for px, py in slam_points: vx, vy = px - robot_pose['x'], py - robot_pose['y'] point_dist = math.hypot(vx, vy) point_angle = math.degrees(math.atan2(vy, vx)) angle_diff = abs((point_angle - a + 180) % 360 - 180) if angle_diff < SCAN_ANGLE / SCAN_RESOLUTION: if point_dist < dist: dist = point_dist x_end = robot_pose['x'] + dist * math.cos(rad) y_end = robot_pose['y'] + dist * math.sin(rad) cx_start, cy_start = to_canvas_coords(robot_pose['x'], robot_pose['y']) cx_end, cy_end = to_canvas_coords(x_end, y_end) draw.line([cx_start, cy_start, cx_end, cy_end], fill="green") # SLAM points for px, py in slam_points: cx, cy = to_canvas_coords(px, py) draw.ellipse([cx - 2, cy - 2, cx + 2, cy + 2], fill="black") # Robot rx, ry = to_canvas_coords(robot_pose['x'], robot_pose['y']) r = int(ROBOT_RADIUS * SCALE) draw.ellipse([rx - r, ry - r, rx + r, ry + r], fill="red") # Heading dx = math.cos(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE dy = math.sin(math.radians(robot_pose['angle'])) * ROBOT_RADIUS * 2 * SCALE draw.line([rx, ry, rx + dx, ry - dy], fill="red", width=2) return img # Simulation State class SimulationState: def __init__(self): self.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0} self.trajectory = [] self.slam_points = [] self.obstacles = generate_obstacles(OBSTACLE_COUNT) self.recording = False self.video_frames = [] state = SimulationState() # Handlers def reset_simulation(obstacle_count): state.robot_pose = {'x': 0.0, 'y': 0.0, 'angle': 0.0} state.trajectory = [] state.slam_points = [] state.obstacles = generate_obstacles(obstacle_count) state.recording = False state.video_frames = [] env_img = draw_environment(state.robot_pose, state.trajectory, state.obstacles) slam_img = draw_slam_map(state.robot_pose, state.slam_points, SCAN_RANGE_DEFAULT) return env_img, slam_img, "Simulation reset" def step_simulation(scan_range): pose_before = state.robot_pose.copy() new_pose, moved = move_robot(state.robot_pose, state.obstacles) if moved: state.trajectory.append((new_pose['x'], new_pose['y'])) new_points = scan_environment(state.robot_pose, state.obstacles, scan_range) for pt in new_points: if all(math.hypot(pt[0] - p[0], pt[1] - p[1]) > 0.1 for p in state.slam_points): state.slam_points.append(pt) env_img = draw_environment(state.robot_pose, state.trajectory, state.obstacles) slam_img = draw_slam_map(state.robot_pose, state.slam_points, scan_range) if state.recording: combined = Image.new('RGB', (SPACE_SIZE * 2, SPACE_SIZE)) combined.paste(env_img, (0, 0)) combined.paste(slam_img, (SPACE_SIZE, 0)) state.video_frames.append(np.array(combined)) return env_img, slam_img, f"Robot moved: {moved}, Position: ({state.robot_pose['x']:.2f}, {state.robot_pose['y']:.2f})" def toggle_recording(record): state.recording = record return f"Recording {'started' if record else 'stopped'}." def export_video(): if not state.video_frames: return None, "No frames recorded." buffer = io.BytesIO() imageio.mimsave(buffer, state.video_frames, format="GIF", duration=0.1) buffer.seek(0) return buffer, "Video exported." # Default obstacle count OBSTACLE_COUNT = 10 with gr.Blocks() as demo: gr.Markdown("# 2D SLAM Simulation") with gr.Row(): env_img = gr.Image(label="Environment View", height=SPACE_SIZE, width=SPACE_SIZE) slam_img = gr.Image(label="SLAM Map View", height=SPACE_SIZE, width=SPACE_SIZE) with gr.Row(): scan_range_slider = gr.Slider(1.0, 10.0, value=SCAN_RANGE_DEFAULT, step=0.1, label="Laser Scan Range") obstacle_count_slider = gr.Slider(0, 30, value=OBSTACLE_COUNT, step=1, label="Obstacle Count") with gr.Row(): step_btn = gr.Button("Step") reset_btn = gr.Button("Reset") record_toggle = gr.Checkbox(label="Record Video") status_text = gr.Textbox(value="Simulation ready.", interactive=False) reset_btn.click(reset_simulation, inputs=obstacle_count_slider, outputs=[env_img, slam_img, status_text]) step_btn.click(step_simulation, inputs=scan_range_slider, outputs=[env_img, slam_img, status_text]) record_toggle.change(toggle_recording, inputs=record_toggle, outputs=None) demo.launch()