Traffic Intersection Simulation using Pygame, Part 1 | by Mihir Gandhi | Towards Data Science

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.
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
Traffic Signals: Red, Yellow, and Green
Car :
Bike :
Bus
Truck
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.
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
Now let us dive into coding.
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
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.
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’ startx = {‘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
Next, we initialize Pygame with the following code:
pygame.init()simulation = pygame.sprite.Group()
Now let us build some classes whose objects we will generate in the simulation. We have 2 classes that we need to define.
Traffic Signal: We need to generate 4 traffic signals for our simulation. So we build a TrafficSignal class that has the following attributes:
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:
If it has not reached its stop point before the intersectionIf it has already crossed the intersectionIf the traffic signal controlling the direction in which the vehicle is moving is Green
If it has not reached its stop point before the intersection
If it has already crossed the intersection
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.
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()
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.
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
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)
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()
Time to see the results. Fire up a cmd/terminal and run the command:
$ python simulation.py
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:
Traffic Intersection Simulation using Pygame, Part 1
Traffic Intersection Simulation using Pygame, Part 2
Traffic Intersection Simulation using Pygame, Part 3
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.