Skip to the content.

License Plate Detection using YOLOv4

The primary objective of this project is to implement a YOLOv4 neural network to detect and identify license plates in vehicle images. The implementation involves loading the pre-trained YOLOv4 model, processing the images, and visualizing the detection results.

Dataset

The dataset consists of images of Moroccan vehicle license plates. Each image is accompanied by a file that specifies the position of the plate in YOLO format. The images were labeled using LabelImg, a graphical image annotation tool.

YOLO Model

YOLO (You Only Look Once) is a state-of-the-art, real-time object detection system. YOLOv4, the model used in this project, is an improvement over previous versions and offers enhanced accuracy and speed.

YOLO Model Comparison

Model Version Description Speed (FPS) mAP (Mean Average Precision)
YOLOv1 Initial version with basic features ~45 FPS ~63.4%
YOLOv2 Improved accuracy and speed, multi-scale training ~40 FPS ~76.8%
YOLOv3 Further improvements in accuracy, deeper architecture ~30 FPS ~80.0%
YOLOv4 Latest version with advanced features like CSPDarknet53 ~50 FPS ~89.7%

Code Explanation

1. Importing Libraries

import numpy as np
import cv2
import matplotlib.pyplot as plt
import time

This cell imports necessary libraries: numpy for numerical operations, cv2 for image processing, matplotlib for plotting, and time for measuring execution time.

2. Reading Class Labels

labels = open("obj.names").read().strip().split('\n')  # list of class names
print(labels)

Reads class names from the obj.names file and prints them.
Output:

['license_plate']

3. Defining Paths and Parameters

weights_path = "yolov4-obj.weights"
configuration_path = "yolov4-obj.cfg"
probability_minimum = 0.5
threshold = 0.3

Sets the paths to the YOLO weights and configuration files and defines the minimum probability and threshold for non-maximum suppression.

4. Loading YOLO Model

network = cv2.dnn.readNetFromDarknet(configuration_path, weights_path)

Loads the YOLO model with the specified configuration and weights.

5. Getting Layer Names

layers_names_all = network.getLayerNames()  # list of layers names
print(layers_names_all)

Gets and prints all layer names in the YOLO model.

Output:

('conv_0', 'bn_0', 'mish_1', 'conv_1', 'bn_1', 'mish_2', 'conv_2', 'bn_2', 'mish_3', 'identity_3', 'conv_4', 'bn_4', 'mish_5', 'conv_5', 'bn_5', 'mish_6', 'conv_6', 'bn_6', 'mish_7', 'shortcut_7', 'conv_8', 'bn_8', 'mish_9', 'concat_9', 'conv_10', 'bn_10', 'mish_11', 'conv_11', 'bn_11', 'mish_12', 'conv_12', 'bn_12', 'mish_13', 'identity_13', 'conv_14', 'bn_14', 'mish_15', 'conv_15', 'bn_15', 'mish_16', 'conv_16', 'bn_16', 'mish_17', 'shortcut_17', 'conv_18', 'bn_18', 'mish_19', 'conv_19', 'bn_19', 'mish_20', 'shortcut_20', 'conv_21', 'bn_21', 'mish_22', 'concat_22', 'conv_23', 'bn_23', 'mish_24', 'conv_24', 'bn_24', 'mish_25', 'conv_25', 'bn_25', 'mish_26', 'identity_26', 'conv_27', 'bn_27', 'mish_28', 'conv_28', 'bn_28', 'mish_29', 'conv_29', 'bn_29', 'mish_30', 'shortcut_30', 'conv_31', 'bn_31', 'mish_32', 'conv_32', 'bn_32', 'mish_33', 'shortcut_33', 'conv_34', 'bn_34', 'mish_35', 'conv_35', 'bn_35', 'mish_36', 'shortcut_36', 'conv_37', 'bn_37', 'mish_38', 'conv_38', 'bn_38', 'mish_39', 'shortcut_39', 'conv_40', 'bn_40', 'mish_41', 'conv_41', 'bn_41', 'mish_42', 'shortcut_42', 'conv_43', 'bn_43', 'mish_44', 'conv_44', 'bn_44', 'mish_45', 'shortcut_45', 'conv_46', 'bn_46', 'mish_47', 'conv_47', 'bn_47', 'mish_48', 'shortcut_48', 'conv_49', 'bn_49', 'mish_50', 'conv_50', 'bn_50', 'mish_51', 'shortcut_51', 'conv_52', 'bn_52', 'mish_53', 'concat_53', 'conv_54', 'bn_54', 'mish_55', 'conv_55', 'bn_55', 'mish_56', 'conv_56', 'bn_56', 'mish_57', 'identity_57', 'conv_58', 'bn_58', 'mish_59', 'conv_59', 'bn_59', 'mish_60', 'conv_60', 'bn_60', 'mish_61', 'shortcut_61', 'conv_62', 'bn_62', 'mish_63', 'conv_63', 'bn_63', 'mish_64', 'shortcut_64', 'conv_65', 'bn_65', 'mish_66', 'conv_66', 'bn_66', 'mish_67', 'shortcut_67', 'conv_68', 'bn_68', 'mish_69', 'conv_69', 'bn_69', 'mish_70', 'shortcut_70', 'conv_71', 'bn_71', 'mish_72', 'conv_72', 'bn_72', 'mish_73', 'shortcut_73', 'conv_74', 'bn_74', 'mish_75', 'conv_75', 'bn_75', 'mish_76', 'shortcut_76', 'conv_77', 'bn_77', 'mish_78', 'conv_78', 'bn_78', 'mish_79', 'shortcut_79', 'conv_80', 'bn_80', 'mish_81', 'conv_81', 'bn_81', 'mish_82', 'shortcut_82', 'conv_83', 'bn_83', 'mish_84', 'concat_84', 'conv_85', 'bn_85', 'mish_86', 'conv_86', 'bn_86', 'mish_87', 'conv_87', 'bn_87', 'mish_88', 'identity_88', 'conv_89', 'bn_89', 'mish_90', 'conv_90', 'bn_90', 'mish_91', 'conv_91', 'bn_91', 'mish_92', 'shortcut_92', 'conv_93', 'bn_93', 'mish_94', 'conv_94', 'bn_94', 'mish_95', 'shortcut_95', 'conv_96', 'bn_96', 'mish_97', 'conv_97', 'bn_97', 'mish_98', 'shortcut_98', 'conv_99', 'bn_99', 'mish_100', 'conv_100', 'bn_100', 'mish_101', 'shortcut_101', 'conv_102', 'bn_102', 'mish_103', 'concat_103', 'conv_104', 'bn_104', 'mish_105', 'conv_105', 'bn_105', 'leaky_106', 'conv_106', 'bn_106', 'leaky_107', 'conv_107', 'bn_107', 'leaky_108', 'pool_108', 'identity_109', 'pool_110', 'identity_111', 'pool_112', 'concat_113', 'conv_114', 'bn_114', 'leaky_115', 'conv_115', 'bn_115', 'leaky_116', 'conv_116', 'bn_116', 'leaky_117', 'conv_117', 'bn_117', 'leaky_118', 'upsample_118', 'identity_119', 'conv_120', 'bn_120', 'leaky_121', 'concat_121', 'conv_122', 'bn_122', 'leaky_123', 'conv_123', 'bn_123', 'leaky_124', 'conv_124', 'bn_124', 'leaky_125', 'conv_125', 'bn_125', 'leaky_126', 'conv_126', 'bn_126', 'leaky_127', 'conv_127', 'bn_127', 'leaky_128', 'upsample_128', 'identity_129', 'conv_130', 'bn_130', 'leaky_131', 'concat_131', 'conv_132', 'bn_132', 'leaky_133', 'conv_133', 'bn_133', 'leaky_134', 'conv_134', 'bn_134', 'leaky_135', 'conv_135', 'bn_135', 'leaky_136', 'conv_136', 'bn_136', 'leaky_137', 'conv_137', 'bn_137', 'leaky_138', 'conv_138', 'permute_139', 'yolo_139', 'identity_140', 'conv_141', 'bn_141', 'leaky_142', 'concat_142', 'conv_143', 'bn_143', 'leaky_144', 'conv_144', 'bn_144', 'leaky_145', 'conv_145', 'bn_145', 'leaky_146', 'conv_146', 'bn_146', 'leaky_147', 'conv_147', 'bn_147', 'leaky_148', 'conv_148', 'bn_148', 'leaky_149', 'conv_149', 'permute_150', 'yolo_150', 'identity_151', 'conv_152', 'bn_152', 'leaky_153', 'concat_153', 'conv_154', 'bn_154', 'leaky_155', 'conv_155', 'bn_155', 'leaky_156', 'conv_156', 'bn_156', 'leaky_157', 'conv_157', 'bn_157', 'leaky_158', 'conv_158', 'bn_158', 'leaky_159', 'conv_159', 'bn_159', 'leaky_160', 'conv_160', 'permute_161', 'yolo_161')

6. Output Layers

print(network.getUnconnectedOutLayers())
layers_names_output = [layers_names_all[i - 1] for i in network.getUnconnectedOutLayers()]
print(layers_names_output)

Identify the names of the output layers required by the YOLO algorithm.

Output:

[327 353 379]
['yolo_139', 'yolo_150', 'yolo_161']

7. Reading and Displaying the Image

image_input = cv2.imread("WhatsApp Image 2023-10-28 at 18.15.55_8d3d7646.jpg")
image_input_shape = image_input.shape
print(image_input_shape)
plt.imshow(cv2.cvtColor(image_input, cv2.COLOR_BGR2RGB))
plt.show()

Reads the input image and displays its shape. Then, it displays the image.

Output shape:

(1280, 1024, 3)

8. Creating Blob from Image

blob = cv2.dnn.blobFromImage(image_input, 1 / 255.0, (416, 416), swapRB=True, crop=False)
print(image_input.shape)
print(blob.shape)

Creates a blob from the image to pass to the YOLO model and prints the shapes.

Output:

(1280, 1024, 3)
(1, 3, 416, 416)

9. Displaying the Blob

blob_to_show = blob[0, :, :, :].transpose(1, 2, 0)
print(blob_to_show.shape)
plt.imshow(blob_to_show)
plt.show()

Displays the blob.

Output shape:

(416, 416, 3)

10. Forward Pass through YOLO

network.setInput(blob)
start = time.time()
output_from_network = network.forward(layers_names_output)
end = time.time()
print('YOLO v4 took {:.3f} seconds'.format(end - start))

Performs a forward pass through the YOLO network and measures the time taken.

Output:

YOLO v4 took 1.935 seconds

11. Processing YOLO Output

print(type(output_from_network))
print(type(output_from_network[0]))

Prints the types of the outputs.

Output:

<class 'tuple'>
<class 'numpy.ndarray'>

12. Generating Colors for Classes

np.random.seed(42)
colours = np.random.randint(0, 255, size=(len(labels), 3), dtype='uint8')
print(colours.shape)
print(colours[0])

Generates random colors for bounding boxes.

Output:

(1, 3)
[102 220 225]

13. Detecting Objects

bounding_boxes = []
confidences = []
class_numbers = []
h, w = image_input_shape[:2]
print(h, w)

Initializes lists for bounding boxes, confidences, and class numbers and prints the image dimensions.

Output:

1280 1024

14. Extracting Bounding Boxes and Confidences

for result in output_from_network:
    for detection in result:
        scores = detection[5:]
        class_current = np.argmax(scores)
        confidence_current = scores[class_current]
        if confidence_current > probability_minimum:
            box_current = detection[0:4] * np.array([w, h, w, h])
            x_center, y_center, box_width, box_height = box_current.astype('int')
            x_min = int(x_center - (box_width / 2))
            y_min = int(y_center - (box_height / 2))
            bounding_boxes.append([x_min, y_min, int(box_width), int(box_height)])
            confidences.append(float(confidence_current))
            class_numbers.append(class_current)

Extracts bounding boxes and confidences for detected objects.

15. Applying Non-Maximum Suppression

results = cv2.dnn.NMSBoxes(bounding_boxes, confidences, probability_minimum, threshold)

Applies Non-Maximum Suppression to filter out overlapping bounding boxes.

16. Displaying Detected Labels

for i in range(len(class_numbers)):
    print(labels[int(class_numbers[i])])
with open('found_labels.txt', 'w') as f:
    for i in range(len(class_numbers)):
        f.write(labels[int(class_numbers[i])])

Prints and saves detected labels.

Output:

license_plate
license_plate

17. Drawing Bounding Boxes

if len(results) > 0:
    for i in results.flatten():
        x_min, y_min = bounding_boxes[i][0], bounding_boxes[i][1]
        box_width, box_height = bounding_boxes[i][2], bounding_boxes[i][3]
        colour_box_current = [int(j) for j in colours[class_numbers[i]]]
        cv2.rectangle(image_input, (x_min, y_min), (x_min + box_width, y_min + box_height), colour_box_current, 2)
        text_box_current = '{} : {:.2f}%'.format(labels[int(class_numbers[i])], confidences[i])
        cv2.putText(image_input, text_box_current, (x_min, y_min - 7), cv2.FONT_HERSHEY_SIMPLEX, 0.5, colour_box_current, 2)
plt.imshow(cv2.cvtColor(image_input, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()

Draws bounding boxes and labels on the image and displays it.