-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Built a reinforcement learning model for a self driving car
- Loading branch information
dptrup69
committed
May 18, 2024
1 parent
2411cbc
commit 6e7f945
Showing
8 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
|
||
import math | ||
import random | ||
import sys | ||
import os | ||
from neat import config | ||
|
||
import neat | ||
import pygame | ||
|
||
# Constants | ||
# WIDTH = 1600 | ||
# HEIGHT = 880 | ||
|
||
WIDTH = 2000 | ||
HEIGHT = 1200 | ||
|
||
CAR_SIZE_X = 60 | ||
CAR_SIZE_Y = 60 | ||
|
||
BORDER_COLOR = (255, 255, 255, 255) # Color To Crash on Hit | ||
|
||
current_generation = 0 # Generation counter | ||
|
||
class Car: | ||
|
||
def __init__(self): | ||
# Load Car Sprite and Rotate | ||
self.sprite = pygame.image.load('car.png').convert() # Convert Speeds Up A Lot | ||
self.sprite = pygame.transform.scale(self.sprite, (CAR_SIZE_X, CAR_SIZE_Y)) | ||
self.rotated_sprite = self.sprite | ||
|
||
# self.position = [690, 740] # Starting Position | ||
self.position = [830, 920] # Starting Position | ||
self.angle = 0 | ||
self.speed = 0 | ||
|
||
self.speed_set = False # Flag For Default Speed Later on | ||
|
||
self.center = [self.position[0] + CAR_SIZE_X / 2, self.position[1] + CAR_SIZE_Y / 2] # Calculate Center | ||
|
||
self.radars = [] # List For Sensors / Radars | ||
self.drawing_radars = [] # Radars To Be Drawn | ||
|
||
self.alive = True # Boolean To Check If Car is Crashed | ||
|
||
self.distance = 0 # Distance Driven | ||
self.time = 0 # Time Passed | ||
|
||
def draw(self, screen): | ||
screen.blit(self.rotated_sprite, self.position) # Draw Sprite | ||
self.draw_radar(screen) #OPTIONAL FOR SENSORS | ||
|
||
def draw_radar(self, screen): | ||
# Optionally Draw All Sensors / Radars | ||
for radar in self.radars: | ||
position = radar[0] | ||
pygame.draw.line(screen, (0, 255, 0), self.center, position, 1) | ||
pygame.draw.circle(screen, (0, 255, 0), position, 5) | ||
|
||
def check_collision(self, game_map): | ||
self.alive = True | ||
for point in self.corners: | ||
# If Any Corner Touches Border Color -> Crash | ||
# Assumes Rectangle | ||
if game_map.get_at((int(point[0]), int(point[1]))) == BORDER_COLOR: | ||
self.alive = False | ||
break | ||
|
||
def check_radar(self, degree, game_map): | ||
length = 0 | ||
x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length) | ||
y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length) | ||
|
||
# While We Don't Hit BORDER_COLOR AND length < 300 (just a max) -> go further and further | ||
while not game_map.get_at((x, y)) == BORDER_COLOR and length < 300: | ||
length = length + 1 | ||
x = int(self.center[0] + math.cos(math.radians(360 - (self.angle + degree))) * length) | ||
y = int(self.center[1] + math.sin(math.radians(360 - (self.angle + degree))) * length) | ||
|
||
# Calculate Distance To Border And Append To Radars List | ||
dist = int(math.sqrt(math.pow(x - self.center[0], 2) + math.pow(y - self.center[1], 2))) | ||
self.radars.append([(x, y), dist]) | ||
|
||
def update(self, game_map): | ||
# Set The Speed To 20 For The First Time | ||
# Only When Having 4 Output Nodes With Speed Up and Down | ||
if not self.speed_set: | ||
self.speed = 20 | ||
self.speed_set = True | ||
|
||
# Get Rotated Sprite And Move Into The Right X-Direction | ||
# Don't Let The Car Go Closer Than 20px To The Edge | ||
self.rotated_sprite = self.rotate_center(self.sprite, self.angle) | ||
self.position[0] += math.cos(math.radians(360 - self.angle)) * self.speed | ||
self.position[0] = max(self.position[0], 20) | ||
self.position[0] = min(self.position[0], WIDTH - 120) | ||
|
||
# Increase Distance and Time | ||
self.distance += self.speed | ||
self.time += 1 | ||
|
||
# Same For Y-Position | ||
self.position[1] += math.sin(math.radians(360 - self.angle)) * self.speed | ||
self.position[1] = max(self.position[1], 20) | ||
self.position[1] = min(self.position[1], WIDTH - 120) | ||
|
||
# Calculate New Center | ||
self.center = [int(self.position[0]) + CAR_SIZE_X / 2, int(self.position[1]) + CAR_SIZE_Y / 2] | ||
|
||
# Calculate Four Corners | ||
# Length Is Half The Side | ||
length = 0.5 * CAR_SIZE_X | ||
left_top = [self.center[0] + math.cos(math.radians(360 - (self.angle + 30))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 30))) * length] | ||
right_top = [self.center[0] + math.cos(math.radians(360 - (self.angle + 150))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 150))) * length] | ||
left_bottom = [self.center[0] + math.cos(math.radians(360 - (self.angle + 210))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 210))) * length] | ||
right_bottom = [self.center[0] + math.cos(math.radians(360 - (self.angle + 330))) * length, self.center[1] + math.sin(math.radians(360 - (self.angle + 330))) * length] | ||
self.corners = [left_top, right_top, left_bottom, right_bottom] | ||
|
||
# Check Collisions And Clear Radars | ||
self.check_collision(game_map) | ||
self.radars.clear() | ||
|
||
# From -90 To 120 With Step-Size 45 Check Radar | ||
for d in range(-90, 120, 45): | ||
self.check_radar(d, game_map) | ||
|
||
def get_data(self): | ||
# Get Distances To Border | ||
radars = self.radars | ||
return_values = [0, 0, 0, 0, 0] | ||
for i, radar in enumerate(radars): | ||
return_values[i] = int(radar[1] / 30) | ||
|
||
return return_values | ||
|
||
def is_alive(self): | ||
# Basic Alive Function | ||
return self.alive | ||
|
||
def get_reward(self): | ||
# Calculate Reward (Maybe Change?) | ||
# return self.distance / 50.0 | ||
return self.distance / (CAR_SIZE_X / 2) | ||
|
||
def rotate_center(self, image, angle): | ||
# Rotate The Rectangle | ||
rectangle = image.get_rect() | ||
rotated_image = pygame.transform.rotate(image, angle) | ||
rotated_rectangle = rectangle.copy() | ||
rotated_rectangle.center = rotated_image.get_rect().center | ||
rotated_image = rotated_image.subsurface(rotated_rectangle).copy() | ||
return rotated_image | ||
|
||
|
||
def run_simulation(genomes, config): | ||
|
||
# Empty Collections For Nets and Cars | ||
nets = [] | ||
cars = [] | ||
|
||
# Initialize PyGame And The Display | ||
pygame.init() | ||
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN) | ||
|
||
# For All Genomes Passed Create A New Neural Network | ||
for i, g in genomes: | ||
net = neat.nn.FeedForwardNetwork.create(g, config) | ||
nets.append(net) | ||
g.fitness = 0 | ||
|
||
cars.append(Car()) | ||
|
||
# Clock Settings | ||
# Font Settings & Loading Map | ||
clock = pygame.time.Clock() | ||
generation_font = pygame.font.SysFont("Arial", 30) | ||
alive_font = pygame.font.SysFont("Arial", 20) | ||
game_map = pygame.image.load('map3.png').convert() # Convert Speeds Up A Lot | ||
|
||
global current_generation | ||
current_generation += 1 | ||
|
||
# Simple Counter To Roughly Limit Time (Not Good Practice) | ||
counter = 0 | ||
|
||
while True: | ||
# Exit On Quit Event | ||
for event in pygame.event.get(): | ||
if event.type == pygame.QUIT: | ||
sys.exit(0) | ||
|
||
# For Each Car Get The Acton It Takes | ||
for i, car in enumerate(cars): | ||
output = nets[i].activate(car.get_data()) | ||
choice = output.index(max(output)) | ||
if choice == 0: | ||
car.angle += 10 # Left | ||
elif choice == 1: | ||
car.angle -= 10 # Right | ||
elif choice == 2: | ||
if(car.speed - 2 >= 12): | ||
car.speed -= 2 # Slow Down | ||
else: | ||
car.speed += 2 # Speed Up | ||
|
||
# Check If Car Is Still Alive | ||
# Increase Fitness If Yes And Break Loop If Not | ||
still_alive = 0 | ||
for i, car in enumerate(cars): | ||
if car.is_alive(): | ||
still_alive += 1 | ||
car.update(game_map) | ||
genomes[i][1].fitness += car.get_reward() | ||
|
||
if still_alive == 0: | ||
break | ||
|
||
counter += 1 | ||
if counter == 30 * 40: # Stop After About 20 Seconds | ||
break | ||
|
||
# Draw Map And All Cars That Are Alive | ||
screen.blit(game_map, (0, 0)) | ||
for car in cars: | ||
if car.is_alive(): | ||
car.draw(screen) | ||
|
||
# Display Info | ||
text = generation_font.render("Generation: " + str(current_generation), True, (0,0,0)) | ||
text_rect = text.get_rect() | ||
text_rect.center = (900, 450) | ||
screen.blit(text, text_rect) | ||
|
||
text = alive_font.render("Still Alive: " + str(still_alive), True, (0, 0, 0)) | ||
text_rect = text.get_rect() | ||
text_rect.center = (900, 490) | ||
screen.blit(text, text_rect) | ||
|
||
pygame.display.flip() | ||
clock.tick(60) # 60 FPS | ||
|
||
if __name__ == "__main__": | ||
|
||
# Load Config | ||
config_path = "./config.txt" | ||
config = neat.config.Config(neat.DefaultGenome, | ||
neat.DefaultReproduction, | ||
neat.DefaultSpeciesSet, | ||
neat.DefaultStagnation, | ||
config_path) | ||
|
||
# Create Population And Add Reporters | ||
population = neat.Population(config) | ||
population.add_reporter(neat.StdOutReporter(True)) | ||
stats = neat.StatisticsReporter() | ||
population.add_reporter(stats) | ||
|
||
# Run Simulation For A Maximum of 1000 Generations | ||
population.run(run_simulation, 1000) | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
png |