简易车牌识别-paddlepaddle版

简要介绍

基于百度AI Studio的《深度学习7日入门-CV疫情特辑》作业,通过CNN来识别车牌号。基本上代码在平台上都有了,完成以后我又写了个Pytorch版本,可以在此查看。

本项目车牌数据为20x20的灰度图片,共分为65类,即0-9的数字,A-Z的字母以及“京津冀”等省市自治区的简称。想下载跑一跑的可以在下面下载。

数据集下载地址:
链接:https://pan.baidu.com/s/10Z3GKuoTC5lMPab-SgCPMg
提取码:s3il

框架使用的是paddlepaddle 1.7版本,使用动态图编写,在百度的AI studio平台上运行(在此安利一下,这个平台白嫖的Tesla V100 GPU是真的香,但是只能用paddlepaddle)。

代码主要流程如下:

  • 数据处理:生成训练集、测试集图像列表,自定义数据提供器
  • 网络定义:使用动态图构建CNN网络
  • 训练
  • 测试:正确率计算与车牌图片识别例子

总的来说还是比较简单的,适合初上手paddlepaddle的小伙伴练习,使用过程中发现paddlepaddle还是挺有意思的。

数据处理

首先来看图片数据在文件夹中的格式,文件夹下就是图片,文件夹名则为标签,65类,包含数字、字母和行政区简称,共16000多张。
图占位
为了将它们分成测试集与训练集,使用一个列表文件保存训练与测试集图片的路径与标签,分别为train_data.listtest_data.list通过以下代码可以得到训练集大小14506,测试集大小1645个。

''' 生成车牌字符图像列表  测试集与训练集之比约10:1
'''
data_path = './data'  # 文件路径
character_folders = os.listdir(data_path)
label = 0
LABEL_temp = {}
if(os.path.exists('./train_data.list')):
    os.remove('./train_data.list')
if(os.path.exists('./test_data.list')):
    os.remove('./test_data.list')
for character_folder in character_folders:
    with open('./train_data.list', 'a') as f_train:
        with open('./test_data.list', 'a') as f_test:
            if character_folder == '.DS_Store' or character_folder == '.ipynb_checkpoints' or character_folder == 'data23617':
                continue
            print(character_folder + " " + str(label))
            LABEL_temp[str(label)] = character_folder #存储一下标签的对应关系
            character_imgs = os.listdir(os.path.join(data_path, character_folder))
            for i in range(len(character_imgs)):
                if i%10 == 0:  # 通过此可以调节训练集 测试集 比例
                    f_test.write(os.path.join(os.path.join(data_path, character_folder), character_imgs[i]) + "\t" + str(label) + '\n')
                else:
                    f_train.write(os.path.join(os.path.join(data_path, character_folder), character_imgs[i]) + "\t" + str(label) + '\n')
    label = label + 1
print('图像列表已生成')

输出如下: 共65个label,在本地的话,由于文件夹会按照名称排序,下面的列表会有顺序,但在AI Studio上没有没有。

...
J 50
G 51
V 52
S 53
M 54
gan 55
Z 56
xiang 57
hu 58
K 59
jl 60
jing 61
e1 62
zhe 63
W 64
图像列表已生成

生成好的list文件如下,路径与标签之间用一个\t分隔。
在这里插入图片描述
然后使用数据提供器,在训练时提供图片与标签数据,类似Pytorch中的DataLoader。需要注意的是,可能是数据集中混了一些RGB的彩色图像,会导致后续网络中通道数错误,所以需要使用灰度图像读取。这里的实现还是比较有意思的,感兴趣的可以看看paddle的文档,xmap_readers怎么用。

xmp_reader API: https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/xmap_readers_cn.html#xmap-readers

def data_mapper(sample):
    img, label = sample
    img = paddle.dataset.image.load_image(file=img, is_color=False)  # 注意,转为灰度图像!!
    img = img.flatten().astype('float32') / 255.0
    return img, label
    
def data_reader(data_list_path):
    def reader():
        with open(data_list_path, 'r') as f:
            lines = f.readlines()
            for line in lines:
                img, label = line.split('\t')
                yield img, int(label)
    return paddle.reader.xmap_readers(data_mapper, reader, cpu_count(), 1024)

# 用于训练的数据提供器
train_reader = paddle.batch(reader=paddle.reader.shuffle(reader=data_reader('./train_data.list'), buf_size=512), batch_size=128)
# 用于测试的数据提供器
test_reader = paddle.batch(reader=data_reader('./test_data.list'), batch_size=128)

网络定义

由于paddlepaddle在1.6版本中更新了动态图,变得兼具动态图与静态图,所以实现上与Pytorch很像,这里构建了2个卷积层和3个全连接层,加入dropout来防止过拟合。构建网络时我加入了一个print_size变量来看看网络结构,看看输出维度是否正确,毕竟懒得算了。。。


class CNNNet(fluid.dygraph.Layer):
    def __init__(self, print_size=False, num_classes=65):
        super(YZNet, self).__init__()
        #卷积层,卷积核大小为3*3,步长是1,一共有32个卷积核
        self.conv_1 = Conv2D(num_channels=1, num_filters=50,filter_size=3,stride=1, act='relu')
        #池化层,池化核大小为2*2,步长是1,最大池化
        self.pool_1 = Pool2D(pool_size=2,pool_stride=1,pool_type='max')
        #第二个卷积层,卷积核大小为3*3,步长1,一共有64个卷积核
        self.conv_2 = Conv2D(num_channels=50, num_filters=32,filter_size=3,stride=1, act='relu')
        #第二个池化层,池化核大小是2*2,步长1,最大池化
        self.pool_2 = Pool2D(pool_size=2,pool_stride=1,pool_type='max')
        
        self.fc1 = Linear(input_dim=6272, output_dim=1000 , act='relu')
        self.drop_ratio1 = 0.3 #0.5
        self.fc2 = Linear(1000, 128, act='relu')
        self.drop_ratio2 = 0.3 #0.5
        self.fc3 = Linear(128, num_classes)
        self.print_size = print_size

    def forward(self, inputs):
        #print("inputs shape: ", inputs.shape)
        conv_1 = self.conv_1(inputs)
        pool_1 = self.pool_1(conv_1)

        conv_2 = self.conv_2(pool_1)
        pool_2 = self.pool_2(conv_2)
        x = fluid.layers.reshape(pool_2, [pool_2.shape[0], -1])
        #print("x shape: ", x.shape)  # 6272
        fc1 = self.fc1(x)
        fc1 = fluid.layers.dropout(fc1, self.drop_ratio1)
        fc2 = self.fc2(fc1)
        fc2 = fluid.layers.dropout(fc2, self.drop_ratio2)
        y = self.fc3(fc2)
        
        if self.print_size:
            print("inputs shape: ", inputs.shape)
            print("CP 1 shape: ", pool_1.shape)
            print("Cp 2 shape: ", pool_2.shape)
            print("x shape: ", x.shape)
            print("y shape: ", y.shape)
        return y

当然也可以使用AlexNet这些网络,但是因为数据大小只有20x20,所以我还是使用浅一些的,结果也还不错。

训练

接下来就是训练,我觉得比Pytorch方便一些,它只需要在训练开始指明CUDA还是CPU,不需要像Pytorch一样网络、Tensor都要to(device)。过程都大同小异,在这里我每10次迭代保存一次网络参数。迭代100次效果就挺不错了。

place = fluid.CUDAPlace(0)
with fluid.dygraph.guard(place):
    model = CNNNet()
    model.train() #训练模式
    #opt=fluid.optimizer.SGDOptimizer(learning_rate=0.001, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
    opt = fluid.optimizer.Momentum(learning_rate=0.001, momentum=0.9, parameter_list=model.parameters())
    epochs_num= 100 
    
    for pass_num in range(epochs_num):
        
        for batch_id,data in enumerate(train_reader()):
            images=np.array([x[0].reshape(1,20,20) for x in data],np.float32)
            labels = np.array([x[1] for x in data]).astype('int64')
            labels = labels[:, np.newaxis]
            image=fluid.dygraph.to_variable(images)
            label=fluid.dygraph.to_variable(labels)
            
            predict=model(image)#预测
            
            #loss = fluid.layers.cross_entropy(predict,label)
            loss = fluid.layers.softmax_with_cross_entropy(predict, label)

            avg_loss=fluid.layers.mean(loss)#获取loss值
            
            acc=fluid.layers.accuracy(predict,label)#计算精度
            
            if batch_id != 0 and batch_id % 50 == 0:
                print("train_pass:{},batch_id:{},train_loss:{},train_acc:{}".format(pass_num,batch_id,avg_loss.numpy(),acc.numpy()))
            
            avg_loss.backward()
            opt.minimize(avg_loss)
            model.clear_gradients()            
        
        if pass_num > 0 and pass_num % 10 == 0:  # 每10次保存一次网络参数
            name = "CNNNet-" + str(pass_num)
            fluid.save_dygraph(model.state_dict(), name)
            print("save ", name)
...
train_pass:98,batch_id:100,train_loss:[0.0148773],train_acc:[1.]
train_pass:99,batch_id:50,train_loss:[0.09262685],train_acc:[0.96875]
train_pass:99,batch_id:100,train_loss:[0.02442267],train_acc:[0.9921875]

模型校验

训练完成后,使用测试集测试一下准确率,需要设置成评估模式。这里可以载入刚才保存的模型参数,如果刚刚训练完,也可以不加。

#模型校验
with fluid.dygraph.guard():
    accs = []
    model=CNNNet()#模型实例化
    model_dict,_=fluid.load_dygraph('CNNNet-100')
    model.load_dict(model_dict) # 加载模型参数
    model.eval()  # 评估模式
    for batch_id,data in enumerate(test_reader()):#测试集
        images=np.array([x[0].reshape(1,20,20) for x in data],np.float32)
        labels = np.array([x[1] for x in data]).astype('int64')
        labels = labels[:, np.newaxis]
            
        image=fluid.dygraph.to_variable(images)
        label=fluid.dygraph.to_variable(labels)
            
        predict=model(image) 
        acc=fluid.layers.accuracy(predict,label)
        accs.append(acc.numpy()[0])
        avg_acc = np.mean(accs)
    print(avg_acc)
0.9831731

最后得到的平均准确率大于98%,效果还是不错的。

不过,冷冰冰的数字没什么意思,下面来预测一个车牌图片。使用opencv-python来分隔车牌。比起前面的来,这一段难度大些,需要懂一些opencv。

注意,cv2 4.0以上版本不可这么做,可以用3.x来做
车牌中间第2位会有一个分隔点(如京A·888888),在此不需要

license_plate = cv2.imread('./车牌.png')
gray_plate = cv2.cvtColor(license_plate, cv2.COLOR_RGB2GRAY)  # 需要是灰度图
ret, binary_plate = cv2.threshold(gray_plate, 175, 255, cv2.THRESH_BINARY)
result = []
for col in range(binary_plate.shape[1]):
    result.append(0)
    for row in range(binary_plate.shape[0]):
        result[col] = result[col] + binary_plate[row][col]/255
character_dict = {}
num = 0
i = 0
while i < len(result):
    if result[i] == 0:
        i += 1
    else:
        index = i + 1
        while result[index] != 0:
            index += 1
        character_dict[num] = [i, index-1]
        num += 1
        i = index

for i in range(8):
    if i==2:  # 分隔点
        continue
    padding = (170 - (character_dict[i][1] - character_dict[i][0])) / 2
    ndarray = np.pad(binary_plate[:,character_dict[i][0]:character_dict[i][1]], ((0,0), (int(padding), int(padding))), 'constant', constant_values=(0,0))
    ndarray = cv2.resize(ndarray, (20,20))
    cv2.imwrite('./' + str(i) + '.png', ndarray)
    
def load_image(path):
    img = paddle.dataset.image.load_image(file=path, is_color=False)
    img = img.astype('float32')
    img = img[np.newaxis, ] / 255.0
    return img

分割好了以后,需要对label进行一下处理,将其转化为汉字。LABEL_temp就是第一步里面生成的存储标签与数字对应关系的列表。接下来将列表中的“yun”换成"云"等,做出车牌的样子。

#将标签进行转换
print('Label:',LABEL_temp)
match = {'A':'A','B':'B','C':'C','D':'D','E':'E','F':'F','G':'G','H':'H','I':'I','J':'J','K':'K','L':'L','M':'M','N':'N',
        'O':'O','P':'P','Q':'Q','R':'R','S':'S','T':'T','U':'U','V':'V','W':'W','X':'X','Y':'Y','Z':'Z',
        'yun':'云','cuan':'川','hei':'黑','zhe':'浙','ning':'宁','jin':'津','gan':'赣','hu':'沪','liao':'辽','jl':'吉','qing':'青','zang':'藏',
        'e1':'鄂','meng':'蒙','gan1':'甘','qiong':'琼','shan':'陕','min':'闽','su':'苏','xin':'新','wan':'皖','jing':'京','xiang':'湘','gui':'贵',
        'yu1':'渝','yu':'豫','ji':'冀','yue':'粤','gui1':'桂','sx':'晋','lu':'鲁',
        '0':'0','1':'1','2':'2','3':'3','4':'4','5':'5','6':'6','7':'7','8':'8','9':'9'}
L = 0
LABEL ={}

for V in LABEL_temp.values():
    LABEL[str(L)] = match[V]
    L += 1
print(LABEL)

然后对车牌图片进行识别,用刚才分隔好的车牌图片(8个),依次放入网络预测。

with fluid.dygraph.guard():
    model=CNNNet()#模型实例化
    model_dict,_=fluid.load_dygraph('CNNNet-100')
    model.load_dict(model_dict)
    model.eval()  # 评估模式
    lab=[]  # 存储预测结果
    for i in range(8):
        if i==2:  # 车牌中间的点
            continue
        infer_imgs = []
        infer_imgs.append(load_image('./' + str(i) + '.png'))
        infer_imgs = np.array(infer_imgs)
        infer_imgs = fluid.dygraph.to_variable(infer_imgs)
        result=model(infer_imgs)
        lab.append(np.argmax(result.numpy()))
print(lab)


display(Image.open('./车牌.png'))
print('\n车牌识别结果为:',end='')
for i in range(len(lab)):
    print(LABEL[str(lab[i])],end='')

在这里插入图片描述
最后识别结果正确(也不知道有没有经过车主同意。。。),感兴趣的可以自己拍个车牌照片试一试。

总结

这个简单的车牌识别案例有助于快速上手paddlepaddle,通过使用发现其也有挺多可取之处,推荐大家用一用这个国货,以及AI studio平台,GPU真的挺香。我用Pytorch重新实现了一遍,主要不同点就在于数据的读入部分,感兴趣的可以点击上方链接查看。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐