Hands-on Tutorials

Traffic Intersection Simulation using Pygame, Part 1

This series of articles contains a step-by-step guide to develop a traffic intersection simulation from scratch using Pygame.

Mihir Gandhi
Towards Data Science
12 min readFeb 26, 2021

--

What are we building?

We are developing a simulation from scratch using Pygame to simulate the movement of vehicles across a traffic intersection having traffic lights with a timer. It contains a 4-way traffic intersection with traffic signals controlling the flow of traffic in each direction. Each signal has a timer on top of it which shows the time remaining for the signal to switch from green to yellow, yellow to red, or red to green. Vehicles such as cars, bikes, buses, and trucks are generated, and their movement is controlled according to the signals and the vehicles around them. This simulation can be further used for data analysis or to visualize AI or ML applications. The video below shows the final output of the simulation we will be building.

Final output of the simulation

Getting the images

Before diving into coding and seeing our beautiful simulation come to life, let us get some images that we will need to build the simulation. Here’s a list of what we need:

  • 4-way intersection
intersection.png
  • Traffic Signals: Red, Yellow, and Green
red.png
yellow.png
green.png
  • Car :
car.png
  • Bike :
bike.png
  • Bus
bus.png
  • Truck
truck.png

Please make sure that you rename the images you download according to the caption of the images above. Next, we need to resize the images of traffic signals and the vehicles according to the size of the 4-way intersection image. This is the only step that will involve some trial-and-error, but as we are building the simulation from scratch, this is necessary.

Open up the 4-way intersection image in an application like Paint for Windows or Preview for Mac. Select an area equal to what you want a vehicle to look like in your final simulation. Note down the dimensions.

Getting vehicle size and coordinates from intersection image

Now resize your vehicles to this size by opening them in any tool of your choice. Repeat the same process for the traffic signal images as well.

One last step remains before we move on to coding. As you may have noticed, we have images of vehicles facing towards right only. So rotate the images of vehicles and save each of them to get images facing all directions, as shown in the image below.

Create a folder ‘Traffic Intersection Simulation’ and within that create a folder ‘images’. Here we will store all of these images. The folder structure is as shown below where:

  • down: contains images of vehicles facing down
  • up: contains images of vehicles facing up
  • left: contains images of vehicles facing left
  • right: contains images of vehicles facing right
  • signals: contains the images of traffic signals

Coding

Now let us dive into coding.

Installing Pygame

Before installing PyGame, you need to have Python 3.1+ installed on your system. You can download Python from here. The easiest way to install Pygame is using pip. Simply run the following command in cmd/Terminal.

$ pip install pygame

Importing the required libraries

We start by creating a file named ‘simulation.py’ and importing the libraries that we will require for developing this simulation.

import randomimport timeimport threadingimport pygameimport sys

Note that your folder structure should look something like this at this point.

Folder structure of the project

Defining constants

Next, we define some constants that will be used in the movement of vehicles in the simulation as well as in control of traffic signal timers.

# Default values of signal timers in secondsdefaultGreen = {0:20, 1:20, 2:20, 3:20}defaultRed = 150defaultYellow = 5signals = []noOfSignals = 4currentGreen = 0   # Indicates which signal is green currentlynextGreen = (currentGreen+1)%noOfSignals currentYellow = 0   # Indicates whether yellow signal is on or offspeeds = {'car':2.25, 'bus':1.8, 'truck':1.8, 'bike':2.5}  # average speeds of vehicles

The coordinates below are also extracted by opening the 4-way intersection image in Paint/Preview and getting the pixel values.

# Coordinates of vehicles’ start
x = {‘right’:[0,0,0], ‘down’:[755,727,697], ‘left’:[1400,1400,1400], ‘up’:[602,627,657]}
y = {‘right’:[348,370,398], ‘down’:[0,0,0], ‘left’:[498,466,436], ‘up’:[800,800,800]}vehicles = {‘right’: {0:[], 1:[], 2:[], ‘crossed’:0}, ‘down’: {0:[], 1:[], 2:[], ‘crossed’:0}, ‘left’: {0:[], 1:[], 2:[], ‘crossed’:0}, ‘up’: {0:[], 1:[], 2:[], ‘crossed’:0}}vehicleTypes = {0:’car’, 1:’bus’, 2:’truck’, 3:’bike’}directionNumbers = {0:’right’, 1:’down’, 2:’left’, 3:’up’}# Coordinates of signal image, timer, and vehicle countsignalCoods = [(530,230),(810,230),(810,570),(530,570)]signalTimerCoods = [(530,210),(810,210),(810,550),(530,550)]# Coordinates of stop linesstopLines = {‘right’: 590, ‘down’: 330, ‘left’: 800, ‘up’: 535}defaultStop = {‘right’: 580, ‘down’: 320, ‘left’: 810, ‘up’: 545}# Gap between vehiclesstoppingGap = 15 # stopping gapmovingGap = 15 # moving gap

Initializing Pygame

Next, we initialize Pygame with the following code:

pygame.init()simulation = pygame.sprite.Group()

Defining Classes

Now let us build some classes whose objects we will generate in the simulation. We have 2 classes that we need to define.

  1. Traffic Signal: We need to generate 4 traffic signals for our simulation. So we build a TrafficSignal class that has the following attributes:
  • red: Value of red signal timer
  • yellow: Value of yellow signal timer
  • green: Value of green signal timer
  • signalText: Value of timer to display
class TrafficSignal:
def __init__(self, red, yellow, green):
self.red = red
self.yellow = yellow
self.green = green
self.signalText = ""

2. Vehicle: This is a class that represents objects of vehicles that we will be generating in the simulation. The Vehicle class has the following attributes and methods:

  • vehicleClass: Represents the class of the vehicle such as car, bus, truck, or bike
  • speed: Represents the speed of the vehicle according to its class
  • direction_number: Represents the direction — 0 for right, 1 for down, 2 for left, and 3 for up
  • direction: Represents the direction in text format
  • x: Represents the current x-coordinate of the vehicle
  • y: Represents the current y-coordinate of the vehicle
  • crossed: Represents whether the vehicle has crossed the signal or not
  • index: Represents the relative position of the vehicle among the vehicles moving in the same direction and the same lane
  • image: Represents the image to be rendered
  • render(): To display the image on screen
  • move(): To control the movement of the vehicle according to the traffic light and the vehicles ahead
class Vehicle(pygame.sprite.Sprite):
def __init__(self, lane, vehicleClass, direction_number, direction):
pygame.sprite.Sprite.__init__(self)
self.lane = lane
self.vehicleClass = vehicleClass
self.speed = speeds[vehicleClass]
self.direction_number = direction_number
self.direction = direction
self.x = x[direction][lane]
self.y = y[direction][lane]
self.crossed = 0
vehicles[direction][lane].append(self)
self.index = len(vehicles[direction][lane]) - 1
path = "images/" + direction + "/" + vehicleClass + ".png"
self.image = pygame.image.load(path)
if(len(vehicles[direction][lane])>1
and vehicles[direction][lane][self.index-1].crossed==0):
if(direction=='right'):
self.stop =
vehicles[direction][lane][self.index-1].stop
- vehicles[direction][lane][self.index-1].image.get_rect().width
- stoppingGap
elif(direction=='left'):
self.stop =
vehicles[direction][lane][self.index-1].stop
+ vehicles[direction][lane][self.index-1].image.get_rect().width
+ stoppingGap
elif(direction=='down'):
self.stop =
vehicles[direction][lane][self.index-1].stop
- vehicles[direction][lane][self.index-1].image.get_rect().height
- stoppingGap
elif(direction=='up'):
self.stop =
vehicles[direction][lane][self.index-1].stop
+ vehicles[direction][lane][self.index-1].image.get_rect().height
+ stoppingGap
else:
self.stop = defaultStop[direction]

if(direction=='right'):
temp = self.image.get_rect().width + stoppingGap
x[direction][lane] -= temp
elif(direction=='left'):
temp = self.image.get_rect().width + stoppingGap
x[direction][lane] += temp
elif(direction=='down'):
temp = self.image.get_rect().height + stoppingGap
y[direction][lane] -= temp
elif(direction=='up'):
temp = self.image.get_rect().height + stoppingGap
y[direction][lane] += temp
simulation.add(self)
def render(self, screen):
screen.blit(self.image, (self.x, self.y))

Let us understand the latter part of the constructor.

In the constructor, after initializing all the variables, we are checking if there are vehicles already present in the same direction and lane as the current vehicle. If yes, we need to set the value of ‘stop’ of the current vehicle taking into consideration the value of ‘stop’ and the width/height of the vehicle ahead of it, as well as the stoppingGap. If there is no vehicle ahead already, then the stop value is set equal to defaultStop. This value of stop is used to control where the vehicles will stop when the signal is red. Once this is done, we update the coordinates from where the vehicles are generated. This is done to avoid overlapping of newly generated vehicles with the existing vehicles when there are a lot of vehicles stopped at a red light.

Now let’s talk about the move() function, which is one of the most important piece of code in our simulation. Note that this function is also a part of the Vehicle class defined above and needs to be indented accordingly.

    def move(self):
if(self.direction=='right'):
if(self.crossed==0 and self.x+self.image.get_rect().width>stopLines[self.direction]):
self.crossed = 1
if((self.x+self.image.get_rect().width<=self.stop
or self.crossed == 1 or (currentGreen==0 and currentYellow==0))
and (self.index==0 or self.x+self.image.get_rect().width
<(vehicles[self.direction][self.lane][self.index-1].x - movingGap))):
self.x += self.speed
elif(self.direction=='down'):
if(self.crossed==0 and
self.y+self.image.get_rect().height>stopLines[self.direction]):
self.crossed = 1
if((self.y+self.image.get_rect().height<=self.stop
or self.crossed == 1 or (currentGreen==1 and currentYellow==0))
and (self.index==0 or self.y+self.image.get_rect().height
<(vehicles[self.direction][self.lane][self.index-1].y - movingGap))):
self.y += self.speed
elif(self.direction=='left'):
if(self.crossed==0 and
self.x<stopLines[self.direction]):
self.crossed = 1
if((self.x>=self.stop or self.crossed == 1
or (currentGreen==2 and currentYellow==0))
and (self.index==0 or self.x
>(vehicles[self.direction][self.lane][self.index-1].x
+ vehicles[self.direction][self.lane][self.index-1].image.get_rect().width
+ movingGap))):
self.x -= self.speed
elif(self.direction=='up'):
if(self.crossed==0 and
self.y<stopLines[self.direction]):
self.crossed = 1
if((self.y>=self.stop or self.crossed == 1
or (currentGreen==3 and currentYellow==0))
and (self.index==0 or self.y
>(vehicles[self.direction][self.lane][self.index-1].y
+ vehicles[self.direction][self.lane][self.index-1].image.get_rect().height
+ movingGap))):
self.y -= self.speed

For each direction, we first check if the vehicle has crossed the intersection or not. This is important because if the vehicle has already crossed, then it can keep moving regardless of the signal being green or red. So when the vehicle crossed the intersection, we set the value of crossed to 1. Next, we decide when the vehicle moves and when it stops. There are 3 cases when the vehicle moves:

  1. If it has not reached its stop point before the intersection
  2. If it has already crossed the intersection
  3. If the traffic signal controlling the direction in which the vehicle is moving is Green

Only in these 3 cases, the coordinates of the vehicle is updated by incrementing/decrementing them by the speed of the vehicle, depending on their direction of motion. However, we need to consider one more possibility that there is a vehicle ahead moving in the same direction and lane. In this case, the vehicle can move only if there is a sufficient gap to the vehicle ahead, and this is decided by taking into consideration the coordinate and the width/height of the vehicle ahead of it, as well as the movingGap.

Creating objects of TrafficSignal class

Next, we initialize 4 TrafficSignal objects, from top left to bottom left in a clockwise direction, with default values of signal timers. The red signal timer of ts2 is set equal to the sum of the yellow and green signal timer of ts1.

def initialize():
ts1 = TrafficSignal(0, defaultYellow, defaultGreen[0])
signals.append(ts1)
ts2 = TrafficSignal(ts1.yellow+ts1.green, defaultYellow, defaultGreen[1])
signals.append(ts2)
ts3 = TrafficSignal(defaultRed, defaultYellow, defaultGreen[2])
signals.append(ts3)
ts4 = TrafficSignal(defaultRed, defaultYellow, defaultGreen[3])
signals.append(ts4)
repeat()

repeat() function

The function repeat() that is called at the end of the initialize() function above is a recursive function that runs our entire simulation. This is the driving force of our simulation.

def repeat():
global currentGreen, currentYellow, nextGreen
while(signals[currentGreen].green>0):
updateValues()
time.sleep(1)
currentYellow = 1
for i in range(0,3):
for vehicle in vehicles[directionNumbers[currentGreen]][i]:
vehicle.stop=defaultStop[directionNumbers[currentGreen]]
while(signals[currentGreen].yellow>0):
updateValues()
time.sleep(1)
currentYellow = 0

signals[currentGreen].green = defaultGreen[currentGreen]
signals[currentGreen].yellow = defaultYellow
signals[currentGreen].red = defaultRed

currentGreen = nextGreen
nextGreen = (currentGreen+1)%noOfSignals
signals[nextGreen].red = signals[currentGreen].yellow+signals[currentGreen].green
repeat()

The repeat() function first calls the updateValues() function every second to update the signal timers until the green timer of the currentGreen signal reaches 0. It then sets that signal to yellow and resets the stop value of all vehicles moving in the direction controlled by the currentGreen signal. It then calls the updateValues() function again after every second until the yellow timer of the currentGreen signal reaches 0. The currentYellow value is now set to 0 as this signal will turn red now. Lastly, the values of the currentGreen signal are restored to the default values, the value of currentGreen and nextGreen is updated to point to the next signals in the cycle, and the value of nextGreen signal’s red timer is updated according to yellow and green of the updated currentGreen signal. The repeat() function then calls itself, and the process is repeated with the updated currentGreen signal.

updateValues() function

The function updateValues() updates the timers of all signals after every second.

def updateValues():
for i in range(0, noOfSignals):
if(i==currentGreen):
if(currentYellow==0):
signals[i].green-=1
else:
signals[i].yellow-=1
else:
signals[i].red-=1

generateVehicles() function

The generateVehicles() function is used to generate the vehicles. The type of vehicle (car, bus, truck, or bike), the lane number (1 or 2) as well as the direction the vehicle moves towards is decided by using random numbers. The variable dist represents the cumulative distribution of vehicles in percentage. So a distribution of [25,50,75,100] means that there is an equal distribution (25% each) of vehicles across all 4 directions. Some other distributions can be [20,50,70,100], [10,20,30,100], and so on. A new vehicle is added to the simulation after every 1 second.

def generateVehicles():
while(True):
vehicle_type = random.randint(0,3)
lane_number = random.randint(1,2)
temp = random.randint(0,99)
direction_number = 0
dist= [25,50,75,100]
if(temp<dist[0]):
direction_number = 0
elif(temp<dist[1]):
direction_number = 1
elif(temp<dist[2]):
direction_number = 2
elif(temp<dist[3]):
direction_number = 3
Vehicle(lane_number, vehicleTypes[vehicle_type], direction_number, directionNumbers[direction_number])
time.sleep(1)

Main class

And we have reached our last piece of code, the Main class, after which we can see our simulation in action.

class Main:
thread1 = threading.Thread(name="initialization",target=initialize, args=())
thread1.daemon = True
thread1.start()

black = (0, 0, 0)
white = (255, 255, 255)
screenWidth = 1400
screenHeight = 800
screenSize = (screenWidth, screenHeight)
background = pygame.image.load('images/intersection.png')
screen = pygame.display.set_mode(screenSize)
pygame.display.set_caption("SIMULATION")
redSignal = pygame.image.load('images/signals/red.png')
yellowSignal = pygame.image.load('images/signals/yellow.png')
greenSignal = pygame.image.load('images/signals/green.png')
font = pygame.font.Font(None, 30)
thread2 = threading.Thread(name="generateVehicles",target=generateVehicles, args=())
thread2.daemon = True
thread2.start()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.blit(background,(0,0))
for i in range(0,noOfSignals):
if(i==currentGreen):
if(currentYellow==1):
signals[i].signalText = signals[i].yellow
screen.blit(yellowSignal, signalCoods[i])
else:
signals[i].signalText = signals[i].green
screen.blit(greenSignal, signalCoods[i])
else:
if(signals[i].red<=10):
signals[i].signalText = signals[i].red
else:
signals[i].signalText = "---"
screen.blit(redSignal, signalCoods[i])
signalTexts = ["","","",""]
for i in range(0,noOfSignals):
signalTexts[i] = font.render(str(signals[i].signalText), True, white, black)
screen.blit(signalTexts[i],signalTimerCoods[i])

for vehicle in simulation:
screen.blit(vehicle.image, [vehicle.x, vehicle.y])
vehicle.move()
pygame.display.update()

Let us understand the Main() function by breaking it down into smaller pieces. We start by creating a separate thread for initialize() method, which instantiates the 4 TrafficSignal objects. Then we define 2 colours, white and black, that we will be using in our display. Next, we define the screen width and screen size, as well as the background and caption to be displayed in the simulation window. We then load the images of the 3 signals, i.e. red, yellow, and green. Now we create another thread for generateVehicles().

Next, we run an infinite loop that updates our simulation screen continuously. Within the loop, we first define the exit criteria. In the next section, we render the appropriate signal image and signal timer for each of the 4 traffic signals. Finally, we render the vehicles on the screen and call the function move() on each vehicle. This function causes the vehicles to move in the next update.

The blit() function is used to render the objects on the screen. It takes 2 arguments, the image to render and the coordinates. The coordinates point to the top-left of the image.

Almost done! Now we just need to call the Main() program, and our code is complete.

Main()

Running the code

Time to see the results. Fire up a cmd/terminal and run the command:

$ python simulation.py
Snapshot of final simulation output

And voila! We have a fully functional simulation of a 4-way intersection that we have built from scratch, right from getting the image of the intersection, signals, and vehicles, to coding the logic for signal switching and vehicle movement.

Source code: https://github.com/mihir-m-gandhi/Basic-Traffic-Intersection-Simulation

This is the first part in a series of articles:

This simulation was developed as part of a research project titled ‘Smart Control of Traffic Lights using Artificial Intelligence’. Check out its demonstration video here. This research work was presented at IEEE International Conference on Recent Advances and Innovations in Engineering (ICRAIE) 2020 and published in IEEE Xplore. Read the paper here.

Thanks for reading! I hope this article was helpful. If you have any doubts or need further clarification, feel free to reach out to me on LinkedIn.

--

--

Software Engineer | MS CS @ Georgia Tech | Machine Learning | Data Science | Travel Entisiast | Connect with me: https://www.linkedin.com/in/mihir-m-gandhi/