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

In this article, we will be adding some more features to the simulation developed in my previous article. This updated simulation will represent real-life scenario more closely and can be used effectively in Data Analysis tasks or AI applications. First, we will be adding turning functionality to the vehicles being generated. So unlike the previous simulation where all the vehicles went straight through the intersection, some of the vehicles will be turning left, some right and some will go straight in the modified simulation. Second, we will be adding a feature that lets us choose which vehicle types among car, bus, truck, and bike we want in our simulation. Third, we will be adding a random green signal timer function, which sets the green signal time equal to a random number generated within a given range and helps in modeling tasks. The video below shows the final output of the simulation we will be building.
We are starting from where we left off in the previous article. Follow the steps in the article to set up the environment and build a basic simulation, which looks something like this. In this article, we will be modifying the basic simulation to include the aforementioned additional features. Alternatively, you can find the source code for the basic simulation along with setup instructions here.
To code the additional functionalities into our existing simulation, we need to add some new variables, define some new functions, and modify some of the existing functions as well.
Here, we are increasing the value of 2 variables, stoppingGap and movingGap. This is done to prevent overlap of vehicles when they turn.
# modify existing variablesstoppingGap = 25    movingGap = 25
Next, we define some new variables that will be required to implement our desired functionalities.
# add new variablesallowedVehicleTypes = {'car': True, 'bus': True, 'truck': True, 'bike': True}allowedVehicleTypesList = []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 = 3mid = {'right': {'x':705, 'y':445}, 'down': {'x':695, 'y':450}, 'left': {'x':695, 'y':425}, 'up': {'x':695, 'y':400}}randomGreenSignalTimer = TruerandomGreenSignalTimerRange = [10,20]
The allowedVehicleTypes dictionary is used to control which vehicle types will be generated in the simulation. The mid dictionary represents the coordinates of the midpoint of the intersection image, from where the vehicles would turn right to cross the intersection. randomGreenSignalTimer is used to control whether the green signal timer will be set randomly or equal to default values. If randomGreenSignalTimer is set to True, then a random number between the range specified by randomGreenSignalTimerRange is set as green signal time.
We need to add some new attributes to the Vehicle class and update the move() function. The Vehicle class now has the following additional attributes:
willTurn: Represents whether the vehicle will turn at the intersection or not
turned: Represents whether the vehicle has turned or not
rotateAngle: Represents the angle by which the vehicle rotates in each move() call
crossedIndex: Represents the relative position of the vehicle among the vehicles moving in the same direction and the same lane after crossing the intersection
index: Represents the relative position of the vehicle among the vehicles moving in the same direction and the same lane
originalImage: Represents the original image of the vehicle
The constructor also needs to be modified to accommodate these new attributes. The modified Vehicle class looks like this.
The latter part of the constructor and the render() function remain the same.
Now comes the most important and complicated part of our code, the move() function. Let us focus on the code for vehicles moving towards the right, as the same conditions are checked for other directions, and the code is quite similar.
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, depending only on the vehicle ahead. So when the vehicle crosses the intersection, we set the value of crossed to 1. Now, if the vehicle goes straight i.e. it does not turn, then we add it to the vehiclesNotTurned list and update its crossedIndex, which is its index in the vehiclesNotTurned list. This is done because the order of the vehicles changes after the intersection as some turn and some do not, and the vehicles list defined earlier is not useful to prevent them from overlapping.
Next, if the vehicle is going to turn, it moves straight until the turning point which is the middle of the intersection. This movement can happen in 3 cases:
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
Along with this, we need to ensure that there is no overlap with the vehicle ahead when it moves. This is decided by taking into consideration the following three parameters: the coordinate of the vehicle ahead, the width/height of the vehicle ahead, and the movingGap. Also, if the vehicle ahead has already turned, then we need not worry about overlap. Only when any of these three conditions are satisfied along with no overlap, the coordinate of the vehicle is updated by incrementing/decrementing it by the speed of the vehicle, depending on its direction of motion.
Once the vehicle crosses its turning point, if the turned value is 0, it turns as it rotates while moving along both the x and y-axis. Once the rotationAngle is 90 degrees, the turned variable is set to 1, the vehicle is added to the vehiclesTurned list, and its crossedIndex is updated. Else if the turned value is 1, the vehicle moves only if there is a sufficient gap to the vehicle ahead, found using the vehiclesTurned list. This is decided based on the same three parameters mentioned above. This turning logic is coded separately for the two lanes to have more control over the simulation.
This is where the originalImage is used. Rotating an image is considered a destructive transform. This means that every time it is performed, the image loses pixel data. For this reason, it is better to re-transform the original image than to keep transforming an image multiple times. Thus, we rotate the originalImage by rotationAngle and update the image by this modified image, while keeping the originalImage unchanged.
Lastly, if the vehicle is not going to turn, then the first part remains the same as the turning vehicles, and they move straight until they cross the intersection. However, after crossing the intersection, they simply move if there is a sufficient gap to the vehicle ahead, found using the vehiclesNotTurned list. Again, this is decided based on the three parameters mentioned above.
The entire move() function is given below. Note that this function is also a part of the Vehicle class defined above and needs to be indented accordingly.
We need to modify the initialize() function so that the 4 TrafficSignal objects are initialized with random values between the range specified by randomGreenSignalTimerRange if randomGreenSignalTime is set to True.
def initialize():    minTime = randomGreenSignalTimerRange[0]    maxTime = randomGreenSignalTimerRange[1]    if(randomGreenSignalTimer):        ts1 = TrafficSignal(0, defaultYellow, random.randint(minTime,maxTime))        signals.append(ts1)        ts2 = TrafficSignal(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()
The same applies to the repeat() function as well. The only change is that if randomGreenSignalTimer is set to True, we generate a random number between randomGreenSignalTimerRange[0] and randomGreenSignalTimerRange[1], and set it as the green signal time.
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      minTime = randomGreenSignalTimerRange[0]    maxTime = randomGreenSignalTimerRange[1]    if(randomGreenSignalTimer):        signals[currentGreen].green=random.randint(minTime, maxTime)    else:        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 generateVehicles() function is modified as shown below. The vehicle type is set by generating a random number from allowedVehicleTypesList. This list is populated in the Main class below. We define a new variable will_turn, which is initially set to 0. will_turn is then set to 1 with a 40% chance using random numbers. Lastly, in addition to the existing variables, this will_turn variable is also passed in the constructor while creating an object of the Vehicle class.
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<40):                will_turn = 1        elif(lane_number == 2):            temp = random.randint(0,99)            if(temp<40):                will_turn = 1        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], will_turn)        time.sleep(1)
There is only one small addition here. We just need to populate the allowedVehicleTypesList according to the allowedVehicleTypes dictionary. For this, the following code needs to be added right at the beginning of the Main class, as shown below. The rest of the Main class remains the same.
class Main:    global allowedVehicleTypesList    i = 0    for vehicleType in allowedVehicleTypes:        if(allowedVehicleTypes[vehicleType]):            allowedVehicleTypesList.append(i)        i += 1
Time to see the results. Fire up a cmd/terminal and run the command:
$ python simulation.py
And we are done! We have added the three additional features — turning functionality, vehicle type controller, and random green signal timer function — to our simulation. This makes the simulation more representative of the real-life scenarios and gives us more control to customize it, thus serving as a handy tool for data analysis as well as AI or ML applications.
Source code: https://github.com/mihir-m-gandhi/Traffic-Intersection-Simulation-with-Turns
This is the second 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.