车牌识别项目总结2019.9

最近学了Python感觉很强大,对计算机视觉有些兴趣,于是来做一个车牌识别项目,其中学习了OpenCV-Python、MATLAB等,收获颇丰,写一篇文章记录一下。

整体思路

车牌识别,LPR,License Plate Recognition,又叫ALPR。本项目使用蓝底白字,共7个字符加1个点的传统车牌。查阅资料发现实现的方式很多,本项目实现的流程为:

  1. 牌照检测:
    这一步是从图片中找到车牌区域,用OpenCV-Python实现。
  2. 字符分割:
    这里把牌照的每个字符分割开,用OpenCV-Python实现。
  3. 字符识别:
    功能如其名。用Scikit-Learn实现。

下面具体来看:

牌照检测

首先我初始图片是这样的:
车牌识别项目总结2019.9_第1张图片

开始我用的是网上找的真车实拍图,但是处理到后面发现图片全糊了,原因应该是户外的光照和阴影吧,我太难了。。于是换了这种低级版。

第一步导入图片并灰度化,我也不知道为啥,大家到这么做:

# 导入图片
img_raw = cv.imread('C:/Users/Administrator/Desktop/pic3.jpg')
# 灰度化
img = cv.cvtColor(img_raw, cv.COLOR_BGR2GRAY)

我这个图片显示出来太大了,所以把它调小:

# 等比缩放
rows, cols = img.shape
ratio = rows/cols
img = cv.resize(img, (800,int(800*ratio)))
img_raw = cv.resize(img_raw, (800,int(800*ratio)))

接下来就是平滑图像,为啥要平滑图像呢,我查了资料的理解是:平滑图像的作用是消除图像噪声,图像噪声是一些由于某些因素随机生成的像素点,留着他们会对之后的效果有影响。这里用了高斯平滑,代码就一行:

# 平滑图像
img = cv.GaussianBlur(img, (5,5), 0)

接下来是很重要的一步了——二值化,先上代码:

# 二值化
img = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)

官网提供了三种阈值化方法:简单阈值,自适应阈值和大津算法。
就我实验来看,在肉眼上自适应阈值的效果是最好的,但是并不代表对以后的操作就是友好的,开始在处理户外实拍照的时候用的是大津算法,在下文中还会用到简单阈值。
这里放一下二值化后的图像:车牌识别项目总结2019.9_第2张图片
接下来是Canny边缘检测,官网上有一句话:It is a multi-stage algorithm and we will go through each stages.意思就是在这一步之前你需要做很多“前戏”,就是为什么前面要做那么多步骤,算解开了我的疑惑吧,下面是代码:

# Canny边缘检测
img = cv.Canny(img, 100, 200)

就一行,很简单,然后是效果图:
车牌识别项目总结2019.9_第3张图片
画风变成暗黑系了,接下来这一步很重要,可以帮我们找出车牌的轮廓,叫做形态学处理,还是先上代码和效果图:

# 形态学处理
kernel = np.ones((40,40), np.uint8)
img = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
img = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)

车牌识别项目总结2019.9_第4张图片
这部分内容官网讲的很清楚,大家可以看一下:Morphological Transformations。这里说一下,kernel很关键,如果是换了一张风格和现在差别很大的图片(比如变成了户外实拍图,车牌位置变得很小),kernel的值是一定要变换的,找到最好的效果。

开运算:先腐蚀后膨胀,去除周围小点。
闭运算:先膨胀后腐蚀,不会去除周围小点,使距离近的图形相连。
至于为啥是先闭运算后开运算,我看别人是这么做的。。

可以看到车牌的轮廓已经很明显了,下面的步骤就是将它选出来。
这里贴一张官网的截图,消除了我当初的疑惑:
在这里插入图片描述
主要是第一条和第三条:

  • 为了更好的准确性,请使用二值图像。所以在查找轮廓之前,先阈值化或者Canny边缘检测。
  • 在OpenCV里,找到轮廓是从黑色背景里找白色对象,所以要记住,要被查找的对象应该是白色的,背景应该是黑色的。

当初听说可以轮廓检测,就纳闷为啥不能直接拿生图去检测,非要经过前面一系列处理。

好了,再回来,官网提供了函数可以帮你选出所有轮廓:

# 找出所有轮廓
contours, hierarchy = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

返回值contours是个数组,包含找到的所有轮廓,每一个元素都是轮廓的点集。找出来之后当然也要画出来看一下:

# 绘制所有轮廓
img = cv.drawContours(img_raw, contours, -1, (0,0,255), 3)

contourIdx参数如果是0、1、2、3等,就是绘制contours里面的指定轮廓,如果是-1,就是绘制所有轮廓。
这个函数不仅可以绘制轮廓,还可以把边界点进行连线。

cv.drawContours函数中的contourId参数为0时:
前一个参数如果是轮廓的集合,则打印第一条直线。
前一个参数如果是点的集合,则打印第一个点,如果想打印所有点连成的直线,则要加[]。

咱看一下效果吧(我是在原图上绘制的,这样方便看框定的区域是否准确):
车牌识别项目总结2019.9_第5张图片
可以看到车牌区域是完完整整地被框在某一个轮廓内,效果还是可以的,但计算机并不知道哪一个才是车牌轮廓,我们要把它筛选出来,这里我写了一个函数,是返回车牌图片,如图:
车牌识别项目总结2019.9_第6张图片
先上代码,再说一下我的思路:

def ContoursSelect(img_raw, contours):
    rectangle = []
    for cnt in contours:
        if cv.contourArea(cnt) > 15000:
            # img = cv.drawContours(img_raw, [cnt], 0, (0,0,255), 3)
            box_info = cv.minAreaRect(cnt)
            width, height = box_info[1]
            if width < height:
                width,height = height,width
            if 2 < width/height < 5:
                box_float = cv.boxPoints(box_info)
                box = np.intc(box_float)
                rectangle.append(box)
    if not len(rectangle) == 1:
        print('筛选错误!')
        return -1
    else:
        location_level = np.float32([[0,height],[0,0],[width,0],[width,height]])
        location_tilt = np.float32([[width,height],[0,height],[0,0],[width,0]])
        location = location_level if box_info[2] == 0 else location_tilt
        M = cv.getPerspectiveTransform(box_float, location)
        img_plate = cv.warpPerspective(img_raw, M, (int(width),int(height)))
        return img_plate
  1. 找到车牌轮廓

官网上给出了很多轮廓特征我们可以参考一下,比如说算轮廓的长度(也就是围成区域的周长)、轮廓围成的区域的面积等。

这里我用的两个条件去筛选(其实第一个经过第一个条件之后就只剩一个轮廓了,就是车牌轮廓):
第一个是面积,首先我查看所有轮廓的面积,确定合适的阈值,就是15000。
第二个我用到了边界矩形中的旋转矩形,这个特征非常适合本项目,因为车牌就是个矩形嘛。

下面我来具体解释一下,它会用一个矩形框住轮廓,并且是可以旋转的,因此可以保证面积最小。这不仅可以帮助我们筛选(车牌的长宽比是一定比例的),对截取车牌也是很方便的。可以去官网详细地看一下Contour Features

在实践过程中我遇到了一个问题:在我计算长宽比的时候,发现有时会出现小于0的情况,说明函数对于长宽的定义有他自己的想法,具体原因在这里:python opencv minAreaRect 生成最小外接矩形

  1. 把车牌截出来并返回

上一步已经用一个矩形把车牌位置框出来,由于他有可能是倾斜的,我们要把它摆正然后截出来返回。用到了透视变换,官网有实现步骤Geometric Transformations of Images,需要的边界点、中心点上面的旋转矩形都提供了,还是很方便的。

在实践过程中也遇到了问题:如果我车牌的外接矩形本来就是水平的,没错可以这么巧,我就遇到了,那么四个边界的定义顺序就和倾斜的不一样(详情还是见上面那篇文章),所以要分开讨论。

这样,这个函数就写完了,会返回一个摆正的车牌图:
车牌识别项目总结2019.9_第7张图片

字符分割

这一步的实现方法有很多,比如用机器学习的聚类算法,但是我没做出来,我用的算法生硬且暴力,大家看一下就知道了。

首先我们还是要得到车牌二值图,但是这里二值化的方法和上面就不一样了,这里采用简单阈值法,阈值是自己实验选择出来的:

# 车牌二值化
img_plate = cv.cvtColor(img_plate_raw, cv.COLOR_BGR2GRAY)
img_plate = cv.GaussianBlur(img_plate, (5,5), 0)
ret, img_plate = cv.threshold(img_plate, 225, 255, cv.THRESH_BINARY)

看一下效果:
车牌识别项目总结2019.9_第8张图片
还行吧,接着我进行了一步开运算,因为我实验发现车牌号周围有许多小点:
车牌识别项目总结2019.9_第9张图片

这样看还不如不搞这一步。。

接下来就是切割了,我写了两个函数:

  1. LevelCut

这个函数的作用是把车牌号上下面的多余部分去掉。原理是找到图像中间的一行像素,从左向右检测,遇到白色就向上(下)移动,重复,直到有一行完全没有白色,认为是边界。上代码:

def LevelCut(img_plate):
    rows, cols = img_plate.shape
    line = rows//2 # line是图片中水平中线
    boundaries = []
    for boundary in range(line, 0, -1): # 向上扫描找到上边界
        flag = 1
        for point in img_plate[boundary]:
            if point == 255:
                flag = 0
                break
        if flag:
            boundaries.append(boundary)
            break
    for boundary in range(line, rows-1, 1): # 向下扫描找到下边界
        flag = 1
        for point in img_plate[boundary]:
            if all(point == [255,255,255]): # point == 255
                flag = 0
                break
        if flag == 1:
            boundaries.append(boundary)
            break
    img_plate_temp = img_plate[boundaries[0]:boundaries[1],:]
    return img_plate_temp

效果如图:
车牌识别项目总结2019.9_第10张图片
其实这个算法可以优化,把“遇到白色就向上(下)移动”改为统计每一行的像素值和从而找到一个阈值,毕竟一行像素完全完全是黑色这个条件有点太苛刻了。

这一步操作之后要调整一下图片大小,因为下面操作会用到字符间和字符本身的宽度,不然阈值就用不了了。

  1. VerticalCut

垂直切割要考虑的情况更多,原理和水平切割类似,统计每一列的像素值和,相当于把图片垂直压缩成一行,如果前后两值从0变成非0为左边界,或从非0变0为右边界,每一对左右边界框定出一个字符。算法会遇到以下问题:
第一个问题是,省份简写中例如“沪”、“浙”、“津”等,在图片预处理的过程中很容易把偏旁腐蚀,这个算法就会把一个字的偏旁当做一个字符框起来,更不要说“川”这种字,算法会把它拆成三块,这里我解决的办法是判断两个字符的间距,如果小于某个阈值就认为是一个字,进行合并。
第二个问题是,车牌的组成里有个点,我的算法会把也会把它框出来当做一个字符,方法是,这个判断字符本身的宽度,很小的就剔除。
还会有其他细节问题,大家自行看代码:

def VerticalCut(img_plate_temp):
    imgs = [] # 用于存放7张图片
    boundaries = [] # 用于存放边界
    img_one_dim = np.sum(img_plate_temp//255, axis=0) # 这是一个一维矩阵,表示二值图压缩成一行,每个元素表示一列像素的像素值之和(除255使矩阵中只有0和1,方便观察)
    left_boundary = -1
    for val in enumerate(img_one_dim):
        if val[0] == 0:
            before = val
            continue
        elif before[1] == 0 and val[1] != 0:
            left_boundary = before[0]
        elif before[1] != 0 and val[1] == 0:
            right_boundary = val[0]
            if not left_boundary == -1:
                boundaries.append([left_boundary,right_boundary])
        before = val
    distance = [(boundaries[i+1][0]-boundaries[i][1]) for i in range(len(boundaries)-1)] # 查看每两组区域间的距离
    for dis in enumerate(distance):
        if dis[1] < 8: # 两组区域间距离小于8的认为是一片区域,进行合并
            x = boundaries[dis[0]][0]
            y = boundaries[dis[0]+1][1]
            del boundaries[dis[0]], boundaries[dis[0]]
            boundaries.insert(dis[0], [x,y])
    length = [(j-i) for i,j in boundaries]
    if len(boundaries) == 7:
        imgs = Cut(img_plate_temp, boundaries)
    else:
        for i in range(len(boundaries)-7):
            index = length.index(min(length))
            del length[index], boundaries[index]
        imgs = Cut(img_plate_temp, boundaries)
    return imgs

def Cut(img_plate_temp, boundaries):
    imgs = []
    rows, cols = img_plate_temp.shape
    for boundary in boundaries:
        imgs.append(img_plate_temp[0:rows,boundary[0]:boundary[1]])
    return imgs

在测试过程中我犯了个不细心的错误,这里记录一下:
a = [1,2,3,4,5,6,7] # 想删除前两个元素
del a[0],a[0] # 不要写成 del a[0],a[1]

上效果图:
车牌识别项目总结2019.9_第11张图片
看效果还是不错的,不过不得不说,我这算法太傻了,大家尽量别用。

还有一个问题自己遇到了,在这里提一下:有时候我喜欢去Jupyter里测试代码,比较方便,但是二值图保存到本地再在Jupyter里面读就不是二维矩阵了,又变成三维了。导致我有的代码在Jupyter里跑地好好的,移到主程序里就出错,都是教训啊!

字符识别

学习机器学习的时候做过MNIST手写数字识别,用的是神经网络算法,我觉得和这个很类似,决定如法炮制。
在网上找了个数据集如图,里面包括数字0到9,24个大写英文字母(I和O没有,太容易和1和0搞混吧)还有部分省份简称,大小是40*32,共5062张。
车牌识别项目总结2019.9_第12张图片
拿到图片之后就要打标签了,这个以前没做过,于是我打算看看MNIST数据集长什么样子,是一个mat文件,于是下载了MATLAB,打开后长这样:
车牌识别项目总结2019.9_第13张图片
我决定按它的格式复刻一份,5000多张图片挨个打标签肯定是不现实的,那就来写个脚本:

main_path = uigetdir('请选择文件夹:');
cd (main_path);
folders = dir('.');
for i = 1:length(folders);
    if i >= 3;
        cd (folders(i).name);
        imgs = dir('*.bmp');
         for j = 1:length(imgs);
             path = fullfile((main_path), (folders(i).name), (imgs(j).name));
             img = imread(path);
             lb = str2num(folders(i).name);
             img = img(:);
             data = [data,img];
             pause(1);
             label = [label,lb];
         end
         cd ..;
    end
end

我把我的数据集的文件夹做了下整理,一共是3层,第二层是40个文件夹,文件夹名就是标签,所以打标签的过程就完全不用人参与了,为了防止跑乱,中间加了1秒的延时。

第一次写MATLAB脚本遇到点问题,这里记录一下:

  1. cd 变量,一定要给变量加上括号,不然出错;
  2. 文件夹展开第三项才是第一个文件夹,不然就跑乱了;
  3. 打完标签后记得返回上一层。。

程序跑了一个多小时,成功了,还是很开心的,如下:
车牌识别项目总结2019.9_第14张图片
一模一样复制了一份,名字都没改,可见我求生欲有多强。下面就可以开始训练了:

from sklearn.datasets import fetch_mldata
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.externals import joblib
ALPR = fetch_mldata('MLSET', data_home='C:\\Users\\Administrator\\Desktop\\datasets\\MLSET\\')
X = ALPR.data/255
y = ALPR.target
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size = 4602, test_size=1000, random_state=62)
mlp_alpr = MLPClassifier(solver='lbfgs',hidden_layer_sizes=[100,100], activation='relu', alpha=1e-5, random_state=62)
mlp_alpr.fit(X_train, y_train)
joblib.dump(mlp_alpr, 'C:\\Users\\Administrator\\Desktop\\mlp_alpr.pkl')
print('测试数据集得分:{:.2f}%'.format(mlp_alpr.score(X_test,y_test)*100))

打印出来测试集得分99.5%,把我高兴坏了,但是实际上效果很差。下面是预测代码:

label = ['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 Predict(nums):
    mlp_alpr = joblib.load('C:\\Users\\Administrator\\Desktop\\mlp_alpr.pkl')
    plate_number = ''
    for num in nums:
        num = num/255
        arr = []
        for j in range(32):
            for i in range(40):
                arr.append(num[i][j])
        arr1 = np.array(arr).reshape(1,-1)
        c = label[mlp_alpr.predict(arr1)[0]]
        plate_number = plate_number + c
    return plate_number

第一,要注意把测试图片resize成数据集中图片一样的大小,就是一样的特征数。
第二,图片的展开方式一定要相同,我的MATLAB脚本上是这样展开的:
车牌识别项目总结2019.9_第15张图片
但是测试图片如果这样展开就肯定错了:
车牌识别项目总结2019.9_第16张图片
好了,我的测试结果为:

(root) C:\Users\Administrator>D:/mypython/Anaconda3/python.exe c:/Users/Administrator/Desktop/ALPR.py
U6P7087

一共7个字符对了4个,结果挺失望的,问了下朋友,他建议数据集的数据自己采集。唉,不管怎么说这一条线也算走下来了。

如有错误,欢迎指正!

你可能感兴趣的:(车牌识别项目总结2019.9)