Step By Step Guide to Simulate a Signalized Intersection by Python

Step By Step Guide to Simulate a Signalized Intersection by Python

Using Pygame Library

  • Define The Project

We're building a simulation from the scratch in Pygame to model vehicle movement through a traffic intersection with timed traffic signals. It has a four-way traffic intersection with traffic signals that control traffic flow in each direction. On top of each signal there is a timer that displays the length of time before the signal changes from phase to another.

The generation and movement of vehicles are governed by the signals and the vehicles in their close surroundings. This simulation was the initial stage in creating an AI traffic management model. The links below show the simulation's final outcome as well as our next step in developing an AI traffic system.

  • Install the project and the required libraries

  • step 1 :

Using SSH: git clone git@github.com:ayat93a/AI_Traffic_system.git Using HTTPS: https://github.com/ayat93a/AI_Traffic_system.git

  • step 2:

cd AI_Traffic_system

  • step 3: install the required packages:

pip install -r requirements.txt

  • step 4: Run the simulation

python main.py

  • Coding

    Firstly we start with establishing a set of constants that will be utilized in our code to simulate the vehicles movement at the intersection and the traffic signal's timings.
# Default values of signal timers in seconds
defaultGreen={0: 9, 1: 3, 2: 9, 3: 3}
#the keys of the dictionary represent the signal number (starting from the right hand leg and go ccw) while the value represents the green time at that signal --> {signal no.  :  green time}
defaultYellow=1 
defaultRed= 24

# signals
signals = []
currentGreen= 0
#to indicate which signal is green now
noOfSignals = 4 

nextGreen= (cureent_green + 1)%noOfSignals
# if cureent_green = 0 --> next = (0+1)%4 = 1 
# if cureent_green = 1 --> next = (1+1)%4 = 2
defaultGreen = {0: 9, 1: 3, 2: 9, 3: 3}

vehicleTypes = {0:'car'}
# assign each vehicle type with number
speeds = {'car':3 }
# assign each vehicle type with its speed -> {type : speed}

To locate the signals , timers , stop line coordination or any feature to the intersection, extract the coordination by using the Paint (or any similar program) to get the pixel values. here the intersection image was opened using Paint and the proper location for each feature was determined.


# Coordinates of signal image, timer
signalCoods = [(400, 430), (690, 110), (970, 305), (690, 610)]
signalTimerCoods = [(450, 480), (760, 160), (1020, 355), (740, 660)]

Assign where the vehicle will start its trip and when it will stop. We will declare x and y dictionaries for the start point locations and stop line and default stop line dictionaries for where the vehicle will stop, the default stop line is the line where a vehicle stops before the pedestrian crossing.

# assigning the legs start from the right hand and go ccw 
directionNumbers = {0:'right' , 1:'down', 2:'left', 3:'up'}

# coordinate of stop lines and default stop line -> stop before pedestrian crosse line
# define the stop line by x-coordinate  for right and left and the y-coordinate for up and down
stopLines = {"right": 350, "down": 197, "left": 1050, "up": 603}
defaultStop = {"right": 340, "down": 187, "left": 1060, "up": 610}

# determine the vehicle start from where to where
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'}

# gap between vehicls (moving and stoping gap)
moving_gap = 25
stoping_gap = 25

# for move() method
vehiclesTurned = {
    "right": {1: [], 2: []},
    "down": {1: [], 2: []},
    "left": {1: [], 2: []},
    "up": {1: [], 2: []},
}
vehiclesNotTurned = {
    "right": {1: [], 2: []},
    "down": {1: [], 2: []},
    "left": {1: [], 2: []},
    "up": {1: [], 2: []},
}

rotationAngle = 3  # rotate & drifting facrtor; 2 or 3 is best to have smooth rotation
mid = {
    "right": {"x": 560, "y": 465},
    "down": {"x": 560, "y": 310},
    "left": {"x": 860, "y": 310},
    "up": {"x": 815, "y": 495},
}
# set random or default green signal time here 
# if you decide to make a simulation for a randomly green time make it True else if you want your simulation to be based on a pre-defined default green time assign it to False
randomGreenSignalTimer = False
# set random green signal time range here
randomGreenSignalTimerRange = [10, 20]

The first step after importing the pygame library ,and define the constants here, is to use pygame.init() function to initialize PyGame . This function calls the separate init() functions of all the included pygame modules. Since these modules are abstractions for specific hardware, this startup step is essential to allow you to work with the same code on Linux, Windows, and Mac.

# initialize Pygame using pygame.init(). 
pygame.init()
# sprites are used for rendering
#  Sprite Group is an object that holds a group of Sprite objects
simulation = pygame.sprite.Group()

Defining Classes

Now let's create some classes that will generate objects in the simulation. There are two classes that need to be defined , Traffic Signal and Vehicle.

Class TrafficSignal

In our simulation we have 4 traffic signals, to simulate them we need to declare a class to initiate a signals object with 4 attributes:

  • red: red signal timer value.
  • yellow: yellow signal timer value.
  • green: green signal timer value.
  • timer_text: to display the value of timer in string format.
class Traffic_Signal :
    def __init__(self , red , yellow , green , timer_text):
        self.red = red
        self.yellow = yellow
        self.green = green
        self.timer_text = ''
        # timer_text to display the timer value

Class Vehicle

This is a class that represents vehicle objects that will be generated throughout the simulation. The attributes and methods of the Vehicle class are as follows:

  • lane : Represents the lane that the vehicle move on it (to identify the coordinates)
  • direction: Represents the direction in text format (to identify the coordinates)
  • x: Represents the current x-coordinate of the vehicle (the x-coordinates)
  • y: Represents the current y-coordinate of the vehicle (the y-coordinates)
  • vehicleClass: Represents the class of the vehicle such as car, bus, truck, or bike (will be used with direction to make the images path dynamin instead of hard coded)
  • crossed: Represents whether the vehicle has crossed the signal or not(to determine if the vehicle is before or after the stop line)
  • index: Represents the relative position of the vehicle among the vehicles moving in the same direction and the same lane (to calculate the stopping coordinates if it equal to the default stop or it must be calculated based on the relative location)
  • direction_number: Represents the direction by number starting from the right hand with the value of 0 and going CCW(to be used in generate vehicle method)
  • speed: Represents the speed of the vehicle according to its class(to be used in move method)
  • willTurn , turned and rotateAngle will be illustrated in the next part of the code.
  • image: Represents the image to be rendered
  • render(): To render -display- the image on screen
  • move(): To control the movement of the vehicle according to the traffic light and the vehicles ahead

The Vehicle class inherit from the base class for visible game objects pygame.sprite.Sprite. Derived classes will want to override the Sprite.update() and assign a Sprite.image and Sprite.rect attributes. The initializer can accept any number of Group instances to be added to. When subclassing the Sprite, you must be sure to call the base initializer before adding the Sprite to Groups pygame.sprite.Sprite.__init__(self).

class Vehicle(pygame.sprite.Sprite):
   class Vehicle(pygame.sprite.Sprite):
    def __init__(self, lane, vehicleClass, direction_number, direction, will_turn):
        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
        self.willTurn = will_turn 
        # will_turn is boolean
        self.turned = 0
        self.rotateAngle = 0
        vehicles[direction][lane].append(self)
        self.index = len(vehicles[direction][lane]) - 1
        self.crossedIndex = 0
        path = "ai_traffic_system/images/" + direction + "/" + vehicleClass + ".png"
        self.originalImage = pygame.image.load(path)
        self.image = pygame.image.load(path)

        #to determine self.stop 
        if (
            len(vehicles[direction][lane]) > 1
            and vehicles[direction][lane][self.index - 1].crossed == 0
         ):
            if direction == "left":
                self.stop = vehicles[direction][lane][self.index - 1].stop
                -vehicles[direction][lane][self.index - 1].image.get_rect().width
                -stoppingGap

            elif direction == "right":
                self.stop = vehicles[direction][lane][self.index - 1].stop
                +vehicles[direction][lane][self.index - 1].image.get_rect().width
                +stoppingGap
                print(vehicles[direction][lane][self.index - 1].stop
                +vehicles[direction][lane][self.index - 1].image.get_rect().width
                +stoppingGap)
            elif direction == "up":
                self.stop = vehicles[direction][lane][self.index - 1].stop
                -vehicles[direction][lane][self.index - 1].image.get_rect().height
                -stoppingGap
            elif direction == "down":
                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]

        # Set new starting and stopping coordinate
         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)

What dose vehicles[direction][lane].append(self) mean?

  • basically we create a list of vehicle for each direction and lane and by appending self we add the current vehicle to the related list.

How to understand the connections between this class and the set of constants previously determined?

  • when you initiate an object (vehicle) from the vehicle class, you must give that object 5 arguments : lane, vehicleClass, direction_number , direction and will_turn. will_turn is given as boolean.
  • To determine the speed, vehicleClass are utilized. The speed can be obtained by going to the previously declared dictionary speeds , give it the vehicleClass as key and then the speed of the vehicle is equal to the related value.
  • The lane and direction together are used to determine the coordination. who to determine the x- coordinates as well as the y-coordinates for the vehicle object? the direction represent the key of the predefined x and y dictionaries and the lane represent the index of the coordinates in the list that nested as value. for example if you initiate and object and call it n , where n = Vehicle(2, 'car' , 1, 'down') , the 2 represent the lane and 'down' represent the direction , if we print the self.x the result will be 697 and the self.y will be 0.
  • To determine the index which it represent the relative location of the vehicle to other vehicles in the lane, the length of the list where the vehicle was appended to it before must be obtained and subtract 1 from it to count the current number of the cars in that lane.

To understand the second part of the initiation take a glance at the following figure, it illustrates the concept that the determination of self.stop follows.

stop.png

To understand what happened let us analyze the case in the figure as example , firstly we will go with the first if statement in the code :

if (
      len(vehicles[direction][lane]) > 1
      and vehicles[direction][lane][self.index - 1].crossed == 0
         ):
       <something to do if the condition is true>
else:
       self.stop = defaultStop[direction]

here we check if the vehicle that entered the intersection in a specific direction and lane is the first vehicle in the queue or not by checking the length of the list of the vehicles that entered a specific direction by a specific lane . if the length is equal to 1 that mean that this vehicle is the first one in both of the direction and lane or at least is the first one in that lane , else if it greater than 1 that mean that there is a at least one vehicle ahead.

  • in the case that the vehicle is the first one in the queue , where the length of the list is equal to 1 , the self.stop will be equal to the value in defaultStop dictionary -previously declared - where the key is the direction.
  • in the case that its not the first one , here we will analyze the figure case. we can calculate the location by this simple equation :
    • location = default stop line - car length 'image width ' - gap the image width can be obtained by image.image.get_rect().width from pygame library , it also can measure the height of the image.
  • after check that we check if the vehicle cross the stop line or not , if not then we have 2 statment so we entre the if statment code block. let us take the left side code and analyze it briefly:
if direction == "left":
                self.stop = vehicles[direction][lane][self.index - 1].stop
                -vehicles[direction][lane][self.index - 1].image.get_rect().width
                -stoppingGap

we enter this condition when the direction is equal to left then self.stop is calculate by subtract the width of the vehicle vehicles[direction][lane][self.index - 1].image.get_rect().width and the gap between any 2 cars stoppingGap from the default line coordinate.

hint : manipulate the default stop line such as it locates in cartesian system , i.e. , for the left or right leg of the intersection the line is parallel to the y-axis , that means that coordinate of the stopping line is the y coordinate since it constant for any point locates at that line.

The last part of the initiation is to set new starting and stopping coordinate for the vehicles by updating the coordinates from where the vehicles are generated. This is done to prevent newly generated cars from overlapping with existing vehicles when a large number of vehicles are stopped at a red signal.

if direction == "left":
            temp = self.image.get_rect().width + stoppingGap
            x[direction][lane] += temp

the temp is a temporary variable save the magnitude of the width of the vehicle plus the gap, this temporary variable are used to modify the value of x dictionary for a specific key (direction and lane).

to add sprites 'the object' to a PyGame Group `pygame.sprite.Group().add(self)' method is utilized.

the second method in class Vehicle is render() , to render surface in pygame we use blit() method. the simplest description ever for what is the usage of the blit function is that blitting is drawing. When we need to get this surface (background) and draw it onto the window we call the blit function.

def render(self, screen):
     screen.blit(self.image, (self.x, self.y))

Now let's have a look at the move() method,the last method in this class and one of the most crucial parts of our simulation. This method is a component of the Vehicle class created above, thus it must be indented accordingly. this method contains all the logic behind the moving vehicle in the simulation. to make the tutorial simple and short i will illustrate only one direction ,the left direction , if you get the idea from it you can simply go with the other. The full version of the code is available here AI_Traffic_system.

   def move(self):
      if self.direction == "left":
            if self.crossed == 0 and self.x < stopLines[self.direction]:
                self.crossed = 1
                vehicles[self.direction]["crossed"] += 1
                if self.willTurn == 0:
                    vehiclesNotTurned[self.direction][self.lane].append(self)
                    self.crossedIndex = (
                        len(vehiclesNotTurned[self.direction][self.lane]) - 1
                    )
            if self.willTurn == 1:
                if self.lane == 2:
                    if self.crossed == 0 or self.x > stopLines[self.direction] - 440:
                        if (
                            self.x >= self.stop
                            or (currentGreen == 2 and currentYellow == 0)
                            or self.crossed == 1
                        ) 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
                            )
                            or vehicles[self.direction][self.lane][
                                self.index - 1
                            ].turned
                            == 1
                        ):
                            self.x -= self.speed
                    else:
                        if self.turned == 0:
                            self.rotateAngle += rotationAngle
                            self.image = pygame.transform.rotate(
                                self.originalImage, self.rotateAngle
                            )
                            self.x -= 1
                            self.y += 1.2
                            if self.rotateAngle == 90:
                                self.turned = 1
                                vehiclesTurned[self.direction][self.lane].append(self)
                                self.crossedIndex = (
                                    len(vehiclesTurned[self.direction][self.lane]) - 1
                                )
                        else:
                            if self.crossedIndex == 0 or (
                                (self.y + self.image.get_rect().height)
                                < (
                                    vehiclesTurned[self.direction][self.lane][
                                        self.crossedIndex - 1
                                    ].y
                                    - movingGap
                                )
                            ):
                                self.y += self.speed
                elif self.lane == 1:
                    if self.crossed == 0 or self.x > mid[self.direction]["x"]:
                        if (
                            self.x >= self.stop
                            or (currentGreen == 2 and currentYellow == 0)
                            or self.crossed == 1
                        ) 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
                            )
                            or vehicles[self.direction][self.lane][
                                self.index - 1
                            ].turned
                            == 1
                        ):
                            self.x -= self.speed
                    else:
                        if self.turned == 0:
                            self.rotateAngle += rotationAngle
                            self.image = pygame.transform.rotate(
                                self.originalImage, -self.rotateAngle
                            )
                            self.x -= 1.8
                            self.y -= 2.5
                            if self.rotateAngle == 90:
                                self.turned = 1
                                vehiclesTurned[self.direction][self.lane].append(self)
                                self.crossedIndex = (
                                    len(vehiclesTurned[self.direction][self.lane]) - 1
                                )
                        else:
                            if self.crossedIndex == 0 or (
                                self.y
                                > (
                                    vehiclesTurned[self.direction][self.lane][
                                        self.crossedIndex - 1
                                    ].y
                                    + vehiclesTurned[self.direction][self.lane][
                                        self.crossedIndex - 1
                                    ]
                                    .image.get_rect()
                                    .height
                                    + movingGap
                                )
                            ):
                                self.y -= self.speed
            else:
                if self.crossed == 0:
                    if (
                        self.x >= self.stop
                        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
                else:
                    if (self.crossedIndex == 0) or (
                        self.x
                        > (
                            vehiclesNotTurned[self.direction][self.lane][
                                self.crossedIndex - 1
                            ].x
                            + vehiclesNotTurned[self.direction][self.lane][
                                self.crossedIndex - 1
                            ]
                            .image.get_rect()
                            .width
                            + movingGap
                        )
                    ):
                        self.x -= self.speed

For each direction, we first check if the vehicle has crossed the intersection or not. This is essential because, regardless of whether the light is green or red, if the car has already crossed, it can continue to drive. We set the value of crossed to 1 when the car crossed the intersection.

The next step is to select when the vehicle will move and when it will stop. When the vehicle moves, there are three scenarios:

  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 three situations are the vehicle's coordinates updated by incrementing or decrementing them by the vehicle's speed, depending on their direction of movement. However, there is another possibility: there is a car ahead of us traveling in the same direction and lane. In this situation, the vehicle can only move if there is a sufficient gap between it and the vehicle ahead of it, which is determined by taking into account the coordinates, width/height, and movingGap of the vehicle ahead of it.

Functions

initialize()function

Creating objects of TrafficSignal class From top left to bottom left in a clockwise sequence, we initialize 4 TrafficSignal objects with 2 options for the signal timing values based on the predefined constan randomGreenSignalTimer. if the value of it equals to True then a simulation with a random green time will be done ,else a pre-defined time will be used.

def initialize():
    minTime = randomGreenSignalTimerRange[0]
    maxTime = randomGreenSignalTimerRange[1]
    if randomGreenSignalTimer:
        ts1 = TrafficSignal(0, defaultYellow, random.randint(minTime, maxTime))
        signals.append(ts1)
        ts2 = TrafficSignal(
            ts1.red + ts1.yellow + ts1.green,
            defaultYellow,
            random.randint(minTime, maxTime),
        )
        signals.append(ts2)
        ts3 = TrafficSignal(defaultRed, defaultYellow, random.randint(minTime, maxTime))
        signals.append(ts3)
        ts4 = TrafficSignal(defaultRed, defaultYellow, random.randint(minTime, maxTime))
        signals.append(ts4)
    else:
        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()

In the random green time case , the random.randint() method returns an integer number from low (inclusive) to high (exclusive) numbers in the range. The red time selection is based on the default red time , where the default red was the summation of the green time for the signals in the intersection. repeat() is called at the end of the initialize() method.

repeat() function

repeat() is a recursive function that runs our entire simulation. This is the engine that drives the signals timing.

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()

when you create a variable inside a function, that variable is local, and can only be used inside that function. here to change the status for the variables : currentGreen, currentYellow, nextGreen to a global variable the global keyword is used , so we allows to use and modify them outside of the current scope.

Firstly we check if the current signal is in green phase by a while loop by the while loop condition , inside it we call 2 functions : updateValues() and time.sleep(1). what that means ?

The time.sleep() takes a number as an argument the which represent the time in seconds that you want to pause the function before executing the next line .

when the while loop condition becomes False , that mean the signal move from one phase to another, and we should to change the status of the vehicles from moving to stop in there stop values.

for the static model , 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.

For the random green time approach , the only difference is that if randomGreenSignalTimer is set to True, a random value between randomGreenSignalTimerRange[0] and randomGreenSignalTimerRange[1] is generated and set as the green signal time.

updateValues() function

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

def `
    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

By updateValues() we loop through the signals list to update the value remine for each phase each secound, for example if the current signal now in the green phase , subtract 1 from the green and so on.

generateVehicles() function

def generateVehicles():

    while True:
        vehicle_type = random.choice(allowedVehicleTypesList)
        lane_number = random.randint(1, 2)
        will_turn = 0

        if lane_number == 1:
            temp = random.randint(0, 99)
            if temp < 35:
                will_turn = 1
        elif lane_number == 2:
            temp = random.randint(0, 99)
            if temp < 35:
                will_turn = 1
        temp = random.randint(0, 100)

        direction_number = 0
        dist = [25, 50, 75, 105]
        if temp < dist[0]:
            direction_number = 1  # west to east (Right)
        elif temp < dist[1]:
            direction_number = 3  # north to south (Down)
        elif temp < dist[2]:
            direction_number = 0  # east to west  (Left)
        elif temp < dist[3]:
            direction_number = 2  # south to north (Up)
        Vehicle(
            lane_number,
            vehicleTypes[vehicle_type],
            direction_number,
            directionNumbers[direction_number],
            will_turn,
        )
        time.sleep(0.2)

The vehicles are generated using the generateVehicles() method. Random numbers determine the lane number (1 or 2), and the direction the vehicle drives in. The percentage distribution of vehicles is represented by the variable distribution. Every 0.2 second, a new car is introduced to the simulation.

Main class

class Main:
    global allowedVehicleTypesList
    i = 0
    for vehicleType in allowedVehicleTypes:
        if allowedVehicleTypes[vehicleType]:
            allowedVehicleTypesList.append(i)
        i += 1
    thread1 = threading.Thread(
        name="initialization", target=initialize, args=()
    )  # initialization
    thread1.daemon = True
    thread1.start()

    # Colours
    black = (0, 0, 0)
    white = (255, 255, 255)

    # Screensize
    screenWidth = 1400
    screenHeight = 800
    screenSize = (screenWidth, screenHeight)

    # Setting background image i.e. image of intersection
    background = pygame.image.load("assist/intersection.png")
    # background = pygame.transform.scale(background, (1600, 920))
    # background = pygame.transform.scale(background, (1400, 800)) OR Resize with google`s help
    screen = pygame.display.set_mode(screenSize)
    pygame.display.set_caption("SIMULATION")

    # Loading signal images and font
    redSignal = pygame.image.load("assist/signals/red.png")
    yellowSignal = pygame.image.load("assist/signals/yellow.png")
    greenSignal = pygame.image.load("assist/signals/green.png")
    font = pygame.font.Font(None, 30)
    thread2 = threading.Thread(
        name="generateVehicles", target=generateVehicles, args=()
    )  # Generating vehicles
    # thread3 = threading.Thread(
    #     name="generateVehicles", target=generateVehicles, args=()
    # )
    # thread4 = threading.Thread(
    #     name="generateVehicles", target=generateVehicles, args=()
    # )
    thread2.daemon = True
    thread2.start()
    # thread3.start()
    # thread4.start()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        screen.blit(background, (0, 0))  # display background in simulation
        for i in range(
            0, noOfSignals
        ):  # display signal and set timer according to current status: green, yellow, or red
            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:
                signals[i].signalText = signals[i].red
                screen.blit(redSignal, signalCoods[i])
                if signals[i].red <= 10:
                    signals[i].signalText = signals[i].red
                else:
                    signals[i].signalText = "---"
                screen.blit(redSignal, signalCoods[i])
        signalTexts = ["", "", "", ""]

        # display signal timer
        for i in range(0, noOfSignals):
            signalTexts[i] = font.render(str(signals[i].signalText), True, white, black)
            screen.blit(signalTexts[i], signalTimerCoods[i])

        # display the vehicles
        for vehicle in simulation:
            screen.blit(vehicle.image, [vehicle.x, vehicle.y])
            vehicle.move()
        pygame.display.update()
        # updateValues() update Values of TrafficSignal Timers
        # time.sleep(0.5) # will break the code dont use  it here :D
        # print(signals[0].green)

Main()

We begin by establishing a separate thread for the initialize() function, which instantiates the 4 TrafficSignal objects, in the Main method. The simulation window's screen width and size, as well as the background and caption to be displayed, are all defined next. The images of the three signals, red, yellow, and green, are then loaded. We'll now make a new thread for generateVehicles ().

A thread is a separate execution flow. This indicates that two things will be happening at the same time in your program. However, the separate threads do not really execute at the same time in most Python 3 implementations; they just appear to.

It's easy to conceive of threading as having two (or more) processors working at the same time on your application, each doing its own thing. That's nearly correct. The threads may operate on multiple processors, but only one of them will be active at a time.

After that, we execute an infinite loop that keeps updating our simulation screen. We start by defining the exit criteria within the loop. For each of the four traffic lights, we draw the appropriate signal graphic and signal timing in the next step. Finally, we display the cars on the screen and perform the move() method on each of them. In the next update, this function causes the cars to move.

This projects was done by me and my colleges :