使用 Python 中的机器学习开发车牌识别系统

犀利的毛毛虫 发布于 2025-03-08 463 次阅读


那么让我们开始吧......

LPR 有时称为 ALPR(自动车牌识别),有 3 个主要阶段。

  1. 车牌检测:这是系统的第一个阶段,也可能是最重要的阶段。正是在这个阶段,确定了车牌的位置。此阶段的输入是车辆的图像,输出是车牌。
  2. 字符分割: 在这个阶段,车牌上的字符被映射出来并分割成单独的图像。
  3. 字符识别:这是我们总结的地方。此处标识了前面分段的字符。为此,我们将使用机器学习。

理论已经讲得够多了,我们可以开始编码了吗?

当然,让我们准备一下环境。我们需要做的第一件事是创建一个虚拟环境。这使得管理我们的项目依赖项和包变得容易。您可以使用 virtualenv 包创建虚拟环境。

# install virtualenv if you don’t have the package already
pip install virtualenv
mkdir license-plate-recognition
cd license-plate-recognition
virtualenv lpr
source lpr/bin/activate

现在,名为 lpr 的文件夹应该位于您的项目目录中。

现在,让我们安装我们的第一个软件包 scikit-image。它是一个用于图像处理的 Python 包。要安装它,请运行。

pip install scikit-image

该软件包的一些关键依赖项是 scipy (用于一些复杂的科学计算)、numpy (用于 n 维数组作)和 matplotlib (用于绘制图形和显示图像)。另一个重要的包是 Pillow — 一个 python 图像库。

车牌检测 (车牌定位)

这是第一阶段,在这个阶段结束时,我们应该能够识别车牌在汽车上的位置。为此,我们需要读取图像并将其转换为灰度。在灰度图像中,每个像素都在 0 到 255 之间。我们现在需要将其转换为二进制图像,其中像素要么是完全黑色的,要么是白色的。

注意: 如果您在 mac-os 上使用 matplotlib 时遇到问题,请按照此说明进行作。

以下代码的输出将显示两个图像,一个是灰度图像,另一个是二进制图像。

from skimage.io import imread
from skimage.filters import threshold_otsu
import matplotlib.pyplot as plt

car_image = imread("car.jpg", as_gray=True)
# it should be a 2 dimensional array
print(car_image.shape)

# the next line is not compulsory however, a grey scale pixel
# in skimage ranges between 0 & 1. multiplying it with 255
# will make it range between 0 & 255 (something we can relate better with

gray_car_image = car_image * 255
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(gray_car_image, cmap="gray")
threshold_value = threshold_otsu(gray_car_image)
binary_car_image = gray_car_image > threshold_value
ax2.imshow(binary_car_image, cmap="gray")
plt.show()
灰度和二值图像

我们需要使用连通分量分析 (CCA) 的概念来识别图像中的所有连通区域。还可以探索其他方法,如边缘检测和形态处理。CCA 基本上可以帮助我们在前台对连接的区域进行分组和标记。如果一个像素具有相同的值并且彼此相邻,则认为它们已连接到另一个像素。

from skimage import measure
from skimage.measure import regionprops
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import localization

# this gets all the connected regions and groups them together
label_image = measure.label(localization.binary_car_image)
fig, (ax1) = plt.subplots(1)
ax1.imshow(localization.gray_car_image, cmap="gray");

# regionprops creates a list of properties of all the labelled regions
for region in regionprops(label_image):
    if region.area < 50:
        #if the region is so small then it's likely not a license plate
        continue

    # the bounding box coordinates
    minRow, minCol, maxRow, maxCol = region.bbox
    rectBorder = patches.Rectangle((minCol, minRow), maxCol-minCol, maxRow-minRow, edgecolor="red", linewidth=2, fill=False)
    ax1.add_patch(rectBorder)
    # let's draw a red rectangle over those regions

plt.show()

我们必须导入前一个文件,以便我们可以访问那里的值。measure.label 方法用于映射二进制图像中的所有连接区域并标记它们。在标记的图像上调用 regionprops 方法将返回所有区域及其属性(如 area、bounding box、label 等)的列表。我们用了补丁。Rectangle 方法在所有映射区域上绘制矩形。

从生成的图像中,我们可以看到其他不包含车牌的区域也被映射了。为了消除这些,我们将使用典型车牌的一些特性来去除它们。

  1. 它们的形状是矩形的。
  2. 宽度大于高度。
  3. 车牌区域宽度与完整图像的比例范围在 15% 到 40% 之间。
  4. 车牌区域高度占完整图像的比例在8%到20%之间。

如果这些特性不适用于您的车牌形状,请不要犹豫(尽管可能性很低)。

from skimage import measure
from skimage.measure import regionprops
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import localization

# this gets all the connected regions and groups them together
label_image = measure.label(localization.binary_car_image)

# getting the maximum width, height and minimum width and height that a license plate can be
plate_dimensions = (0.08*label_image.shape[0], 0.2*label_image.shape[0], 0.15*label_image.shape[1], 0.4*label_image.shape[1])
min_height, max_height, min_width, max_width = plate_dimensions
plate_objects_cordinates = []
plate_like_objects = []
fig, (ax1) = plt.subplots(1)
ax1.imshow(localization.gray_car_image, cmap="gray");

# regionprops creates a list of properties of all the labelled regions
for region in regionprops(label_image):
    if region.area < 50:
        #if the region is so small then it's likely not a license plate
        continue

    # the bounding box coordinates
    min_row, min_col, max_row, max_col = region.bbox
    region_height = max_row - min_row
    region_width = max_col - min_col
    # ensuring that the region identified satisfies the condition of a typical license plate
    if region_height >= min_height and region_height <= max_height and region_width >= min_width and region_width <= max_width and region_width > region_height:
        plate_like_objects.append(localization.binary_car_image[min_row:max_row,
                                  min_col:max_col])
        plate_objects_cordinates.append((min_row, min_col,
                                              max_row, max_col))
        rectBorder = patches.Rectangle((min_col, min_row), max_col-min_col, max_row-min_row, edgecolor="red", linewidth=2, fill=False)
        ax1.add_patch(rectBorder)
    # let's draw a red rectangle over those regions

plt.show()

从 cca.py 脚本的修订版本中,将消除可能不是车牌的其他区域。但是,某些看起来与车牌一模一样的区域(前照灯、贴纸等)仍有可能也被标记。要消除这些其他区域,我们需要进行垂直投影。该概念是通过添加每列中的所有像素来实现的。假设车牌区域将具有大量像素值,因为车牌区域上写有字符。

字符分割

这是我们绘制车牌上所有字符的阶段。我们在这里也将使用 CCA 的概念。

import numpy as np
from skimage.transform import resize
from skimage import measure
from skimage.measure import regionprops
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import cca2

# on the image I'm using, the headlamps were categorized as a license plate
# because their shapes were similar
# for now I'll just use the plate_like_objects[2] since I know that's the
# license plate. We'll fix this later

# The invert was done so as to convert the black pixel to white pixel and vice versa
license_plate = np.invert(cca2.plate_like_objects[2])

labelled_plate = measure.label(license_plate)

fig, ax1 = plt.subplots(1)
ax1.imshow(license_plate, cmap="gray")
# the next two lines is based on the assumptions that the width of
# a license plate should be between 5% and 15% of the license plate,
# and height should be between 35% and 60%
# this will eliminate some
character_dimensions = (0.35*license_plate.shape[0], 0.60*license_plate.shape[0], 0.05*license_plate.shape[1], 0.15*license_plate.shape[1])
min_height, max_height, min_width, max_width = character_dimensions

characters = []
counter=0
column_list = []
for regions in regionprops(labelled_plate):
    y0, x0, y1, x1 = regions.bbox
    region_height = y1 - y0
    region_width = x1 - x0

    if region_height > min_height and region_height < max_height and region_width > min_width and region_width < max_width:
        roi = license_plate[y0:y1, x0:x1]

        # draw a red bordered rectangle over the character.
        rect_border = patches.Rectangle((x0, y0), x1 - x0, y1 - y0, edgecolor="red",
                                       linewidth=2, fill=False)
        ax1.add_patch(rect_border)

        # resize the characters to 20X20 and then append each character into the characters list
        resized_char = resize(roi, (20, 20))
        characters.append(resized_char)

        # this is just to keep track of the arrangement of the characters
        column_list.append(x0)

plt.show()

plate_like_objects 是汽车上看起来像车牌的所有区域的列表。从我使用的图像中,三个区域被确定为车牌的候选项。为了节省时间,我对索引 2 进行了硬编码,因为这是带有车牌的索引。我将共享的最终代码将包含车牌验证技术,以消除实际上不包含车牌的其他区域。

然后在车牌上完成 CCA,并将每个字符的大小调整为 20 像素 x 20 像素。这样做是因为下一个阶段与角色的识别有关。

为了跟踪字符的顺序,引入了 column_list 变量来记下每个区域的起始 x 轴。然后可以对其进行排序以了解字符的正确顺序。

字符识别

这将是最后一个阶段,在这个阶段,我们引入了机器学习的概念。机器学习可以简单地定义为 AI 的一个分支,它处理数据并对其进行处理以发现可用于未来预测的模式。机器学习的主要类别是监督学习、无监督学习和强化学习。监督式学习利用已知的数据集(称为训练数据集)进行预测。我们将走监督学习的道路,因为我们已经对 As、B 和所有字母的样子有所了解。监督学习可分为两类;分类和回归。字符识别属于分类类别。

我们现在需要做的就是获取训练数据集,选择监督学习分类器,训练模型,测试模型并查看其准确性,然后使用该模型进行预测。

让我们从训练模型开始。我有两个不同的数据集,一个是 10 像素 x 20 像素,另一个是 20 像素 x 20 像素。我们将使用 20px x 20px,因为我们已经将每个字符的大小调整为该大小。除 O 和 I 之外的每个字母(典型的尼日利亚车牌没有这些字母,因为它们与 0 和 1 相似)都有 10 个不同的图像。

我们可以使用几个分类器,每个分类器都有其优点和缺点。我们将使用 SVC (support vector classifiers) 来完成此任务。我选择使用 SVC,因为它为我提供了最佳性能。但是,这并不一定意味着 SVC 是最好的分类器。

我们必须为这个阶段安装 scikit-learn 包。

pip install scikit-learn
import os
import numpy as np
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
from sklearn.externals import joblib
from skimage.io import imread
from skimage.filters import threshold_otsu

letters = [
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
            'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
            'U', 'V', 'W', 'X', 'Y', 'Z'
        ]

def read_training_data(training_directory):
    image_data = []
    target_data = []
    for each_letter in letters:
        for each in range(10):
            image_path = os.path.join(training_directory, each_letter, each_letter + '_' + str(each) + '.jpg')
            # read each image of each character
            img_details = imread(image_path, as_gray=True)
            # converts each character image to binary image
            binary_image = img_details < threshold_otsu(img_details)
            # the 2D array of each image is flattened because the machine learning
            # classifier requires that each sample is a 1D array
            # therefore the 20*20 image becomes 1*400
            # in machine learning terms that's 400 features with each pixel
            # representing a feature
            flat_bin_image = binary_image.reshape(-1)
            image_data.append(flat_bin_image)
            target_data.append(each_letter)

    return (np.array(image_data), np.array(target_data))

def cross_validation(model, num_of_fold, train_data, train_label):
    # this uses the concept of cross validation to measure the accuracy
    # of a model, the num_of_fold determines the type of validation
    # e.g if num_of_fold is 4, then we are performing a 4-fold cross validation
    # it will divide the dataset into 4 and use 1/4 of it for testing
    # and the remaining 3/4 for the training
    accuracy_result = cross_val_score(model, train_data, train_label,
                                      cv=num_of_fold)
    print("Cross Validation Result for ", str(num_of_fold), " -fold")

    print(accuracy_result * 100)


current_dir = os.path.dirname(os.path.realpath(__file__))

training_dataset_dir = os.path.join(current_dir, 'train')

image_data, target_data = read_training_data(training_dataset_dir)

# the kernel can be 'linear', 'poly' or 'rbf'
# the probability was set to True so as to show
# how sure the model is of it's prediction
svc_model = SVC(kernel='linear', probability=True)

cross_validation(svc_model, 4, image_data, target_data)

# let's train the model with all the input data
svc_model.fit(image_data, target_data)

# we will use the joblib module to persist the model
# into files. This means that the next time we need to
# predict, we don't need to train the model again
save_directory = os.path.join(current_dir, 'models/svc/')
if not os.path.exists(save_directory):
    os.makedirs(save_directory)
joblib.dump(svc_model, save_directory+'/svc.pkl')

在上面的要点中,训练数据集中的每个字符都用于训练 svc 模型。还进行了 4 倍交叉验证以确定模型的准确性,然后将模型持久化到文件中,以便无需再训练模型即可进行预测。

现在我们有一个经过训练的模型,我们可以尝试预测我们之前分割的字符。

import os
import segmentation
from sklearn.externals import joblib

# load the model
current_dir = os.path.dirname(os.path.realpath(__file__))
model_dir = os.path.join(current_dir, 'models/svc/svc.pkl')
model = joblib.load(model_dir)

classification_result = []
for each_character in segmentation.characters:
    # converts it to a 1D array
    each_character = each_character.reshape(1, -1);
    result = model.predict(each_character)
    classification_result.append(result)

print(classification_result)

plate_string = ''
for eachPredict in classification_result:
    plate_string += eachPredict[0]

print(plate_string)

# it's possible the characters are wrongly arranged
# since that's a possibility, the column_list will be
# used to sort the letters in the right order

column_list_copy = segmentation.column_list[:]
segmentation.column_list.sort()
rightplate_string = ''
for each in segmentation.column_list:
    rightplate_string += plate_string[column_list_copy.index(each)]

print(rightplate_string)

注意

该系统最重要的事情之一是确保使用的图像清晰。还要确保图片的大小不要太大,600px 的宽度就足够了。如果您有任何疑问,请将其放在评论部分。谢谢

完整的应用程序可以在 https://github.com/femioladeji/License-Plate-Recognition-Nigerian-vehicles 上找到