任务描述:
本次实践是一个多分类任务,需要将照片中的每个字符分别进行识别,完成车牌的识别。
数据集介绍
-
数据集文件名为characterData.zip,其中有65个文件夹
-
包含0-9,A-Z,以及各省简称
-
图片为1*20*20的灰度图像
-
本次实验中,取其中的10%作为测试集,90%作为训练集
#导入需要的包 import os import zipfile #Python zipfile模块用来做zip格式编码的压缩和解压缩的 import random #生成随机数 import json import cv2 #使用cv2来表示调用的是 C++ 开发的 opencv 的接口 import numpy as np from PIL import Image import paddle import paddle.fluid as fluid from paddle.fluid.dygraph import Linear,Conv2D,Pool2D import matplotlib.pyplot as plt #面向对象的绘图
1、数据准备
'''
参数配置
'''
train_parameters = {
"input_size": [1, 20, 20], #输入图片的shape
"class_dim": -1, #分类数
"src_path":"data/data23617/characterData.zip", #原始数据集路径
"target_path":"/home/aistudio/data/dataset", #要解压到的路径
"train_list_path": "./train_data.txt", #train_data.txt路径
"eval_list_path": "./val_data.txt", #eval_data.txt路径
"label_dict":{}, #标签字典
"readme_path": "/home/aistudio/data/readme.json", #readme.json路径
"num_epochs": 1, #训练轮数
"train_batch_size": 32, #批次的大小
"learning_strategy": { #优化函数相关的配置
"lr": 0.001 #超参数学习率
}
}
def unzip_data(src_path,target_path):
'''
解压原始数据集,将src_path路径下的zip包解压至data/dataset目录下
'''
if(not os.path.isdir(target_path)): #判断绝对路径是否为目录
z = zipfile.ZipFile(src_path, 'r') #打开一个ZIP文件。返回的也是一个类似文件的ZipFile对象,可以读写。
z.extractall(path=target_path) #批量解压文件。默认是全部解压。
z.close()
else:
print("文件已解压")
def get_data_list(target_path,train_list_path,eval_list_path):
'''
生成数据列表
'''
#存放所有类别的信息
class_detail = []
#获取所有类别保存的文件夹名称
data_list_path=target_path
class_dirs = os.listdir(data_list_path)
if '__MACOSX' in class_dirs:
class_dirs.remove('__MACOSX')
# #总的图像数量
all_class_images = 0
# #存放类别标签
class_label=0
# #存放类别数目
class_dim = 0
# #存储要写进eval.txt和train.txt中的内容
trainer_list=[]
eval_list=[]
#读取每个类别
for class_dir in class_dirs:
if class_dir != ".DS_Store":
class_dim += 1
#每个类别的信息
class_detail_list = {}
eval_sum = 0
trainer_sum = 0
#统计每个类别有多少张图片
class_sum = 0
#获取类别路径
path = os.path.join(data_list_path,class_dir)
# print(path)
# 获取所有图片
img_paths = os.listdir(path)
for img_path in img_paths: # 遍历文件夹下的每个图片
if img_path =='.DS_Store':
continue
name_path = os.path.join(path,img_path) # 每张图片的路径
if class_sum % 10 == 0: # 每10张图片取一个做验证数据
eval_sum += 1 # eval_sum为测试数据的数目
eval_list.append(name_path + "\t%d" % class_label + "\n")
else:
trainer_sum += 1
trainer_list.append(name_path + "\t%d" % class_label + "\n")#trainer_sum测试数据的数目
class_sum += 1 #每类图片的数目
all_class_images += 1 #所有类图片的数目
# 说明的json文件的class_detail数据
class_detail_list['class_name'] = class_dir #类别名称
class_detail_list['class_label'] = class_label #类别标签
class_detail_list['class_eval_images'] = eval_sum #该类数据的测试集数目
class_detail_list['class_trainer_images'] = trainer_sum #该类数据的训练集数目
class_detail.append(class_detail_list)
#初始化标签列表
train_parameters['label_dict'][str(class_label)] = class_dir
class_label += 1
#初始化分类数
train_parameters['class_dim'] = class_dim
print(train_parameters)
#乱序
random.shuffle(eval_list) #shuffle() 方法将序列的所有元素随机排序。shuffle()是不能直接访问的,需要导入 random 模块.
with open(eval_list_path, 'a') as f:
for eval_image in eval_list:
f.write(eval_image) #write()方法可将任何字符串写入一个打开的文件,
#write()方法不会在字符串的结尾添加换行符('\n')
#乱序
random.shuffle(trainer_list)
with open(train_list_path, 'a') as f2:
for train_image in trainer_list:
f2.write(train_image)
# 说明的json文件信息
readjson = {}
readjson['all_class_name'] = data_list_path #文件父目录
readjson['all_class_images'] = all_class_images
readjson['class_detail'] = class_detail
jsons = json.dumps(readjson, sort_keys=True, indent=4, separators=(',', ': '))
##将Python对象编码成json字符串
with open(train_parameters['readme_path'],'w') as f: #建立一个json文件
f.write(jsons)
print ('生成数据列表完成!')
补充:json.dumps()
- indent:应该是一个非负的整型,如果是0,或者为空,则一行显示数据,否则会换行且按照indent的数量显示前面的空白,这样打印出来的json数据也叫pretty-printed json
- separators:分隔符,实际上是(item_separator, dict_separator)的一个元组,默认的就是(‘,’,’:’);这表示dictionary内keys之间用“,”隔开,而KEY和value之间用“:”隔开。
- encoding:默认是UTF-8,设置json数据的编码方式。
- sort_keys:将数据根据keys的值进行排序。
def data_reader(file_list):
'''
自定义data_reader
'''
def reader():
with open(file_list, 'r') as f:
lines = [line.strip() for line in f]
for line in lines:
img_path, lab = line.strip().split('\t')
img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = np.array(img).astype('float32')
img = img/255.0
yield img, int(lab)
return reader
补充:
1、cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255
需要特别注意的是图片读出来的格式是BGR,不是我们最常见的RGB格式,颜色肯定有区别。
2、cv2.cvtColor(p1,p2) 是颜色空间转换函数,p1是需要转换的图片,p2是转换成何种格式。
cv2.COLOR_BGR2RGB 将BGR格式转换成RGB格式
cv2.COLOR_BGR2GRAY 将BGR格式转换成灰度图片
'''
参数初始化
'''
src_path=train_parameters['src_path']
target_path=train_parameters['target_path']
train_list_path=train_parameters['train_list_path']
eval_list_path=train_parameters['eval_list_path']
batch_size=train_parameters['train_batch_size']
'''
解压原始数据到指定路径
'''
unzip_data(src_path,target_path)
#每次生成数据列表前,首先清空train.txt和eval.txt
with open(train_list_path, 'w') as f:
f.seek(0)
f.truncate()
with open(eval_list_path, 'w') as f:
f.seek(0)
f.truncate()
#生成数据列表
get_data_list(target_path,train_list_path,eval_list_path)
'''
构造数据提供器
'''
train_reader = paddle.batch(data_reader(train_list_path),
batch_size=batch_size,
drop_last=True)
eval_reader = paddle.batch(data_reader(eval_list_path),
batch_size=batch_size,
drop_last=True)
补充:
- truncate() 方法用于截断文件,如果指定了可选参数 size,则表示截断文件为 size 个字符。 如果没有指定 size,则从当前位置起截断;截断之后 size 后面的所有字符被删除。
-
file.seek()方法标准格式是:seek(offset,whence=0)offset:开始的偏移量,也就是代表需要移动偏移的字节数whence:给offset参数一个定义,表示要从哪个位置开始偏移;0代表从文件开头开始算起,1代表从当前位置开始算起,2代表从文件末尾算起。默认为0
whence 的默认参数是0。
-
drop_last
告诉如何处理"数据集长度"除于batch_size余下的数据。True就抛弃,否则保留.
Batch=0
Batchs=[]
all_train_accs=[]
def draw_train_acc(Batchs, train_accs):
title="training accs"
plt.title(title, fontsize=24)
plt.xlabel("batch", fontsize=14)
plt.ylabel("acc", fontsize=14)
plt.plot(Batchs, train_accs, color='green', label='training accs') #label为线条的标签
plt.legend() #显示标签
plt.grid()
plt.show()
all_train_loss=[]
def draw_train_loss(Batchs, train_loss):
title="training loss"
plt.title(title, fontsize=24)
plt.xlabel("batch", fontsize=14)
plt.ylabel("loss", fontsize=14)
plt.plot(Batchs, train_loss, color='red', label='training loss')
plt.legend()
plt.grid()
plt.show() #可以将多条线画在同一张图上
2、定义模型
class MyLeNet(fluid.dygraph.Layer):
def __init__(self):
super(MyLeNet,self).__init__()
self.hidden1_1 = Conv2D(1,28,5,1) #通道数、卷积核个数、卷积核大小
#卷积核的输出通道数就是卷积核个数
self.hidden1_2 = Pool2D(pool_size=2,pool_type='max',pool_stride=1)
self.hidden2_1 = Conv2D(28,32,3,1)
self.hidden2_2 = Pool2D(pool_size=2,pool_type='max',pool_stride=1)
self.hidden3 = Conv2D(32,32,3,1)
self.hidden4 = Linear(32*10*10,65,act='softmax')
def forward(self,input):
#print(input.shape) #32*1*20*20
x = self.hidden1_1(input)
#print(x.shape) #32*28*16*16
x = self.hidden1_2(x)
#print(x.shape) #32*28*15*15
x = self.hidden2_1(x)
#print(x.shape) #32*32*13*13
x = self.hidden2_2(x)
#print(x.shape) #32*32*12*12
x = self.hidden3(x)
#print(x.shape) #32*32*10*10
x = fluid.layers.reshape(x, shape=[-1, 32*10*10]) #32*3200
y = self.hidden4(x) #32*65
return y
#定义DNN网络
'''
class MyDNN(fluid.dygraph.Layer):
def __init__(self):
super(MyDNN,self).__init__()
self.hidden1 = Linear(20*20,200,act='relu')
self.hidden2 = Linear(200,100,act='relu')
self.hidden3 = Linear(100,100,act='relu')
self.out = Linear(100,65,act='softmax')
def forward(self,input): # forward 定义执行实际运行时网络的执行逻辑
x = fluid.layers.reshape(input, shape=[-1,20*20]) #-1 表示这个维度的值是从x的元素总数和剩余维度推断出来的,有且只能有一个维度设置为-1
# print(x.shape)
x = self.hidden1(x)
# print('1', x.shape)
x = self.hidden2(x)
# print('2',x.shape)
x = self.hidden3(x)
# print('3',x.shape)
y = self.out(x)
# print('4',y.shape)
return y
'''
3、训练模型
with fluid.dygraph.guard():
model=MyLeNet() #模型实例化
model.train() #训练模式
opt=fluid.optimizer.SGDOptimizer(learning_rate=train_parameters['learning_strategy']['lr'], parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
epochs_num=train_parameters['num_epochs'] #迭代次数
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) #32*1*20*20
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) #数据传入model
loss=fluid.layers.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:
Batch = Batch+50
Batchs.append(Batch)
all_train_loss.append(avg_loss.numpy()[0])
all_train_accs.append(acc.numpy()[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) #优化器对象的minimize方法对参数进行更新
model.clear_gradients() #model.clear_gradients()来重置梯度
fluid.save_dygraph(model.state_dict(),'MyLeNet')#保存模型
draw_train_acc(Batchs,all_train_accs)
draw_train_loss(Batchs,all_train_loss)
4、模型评估
#模型评估
with fluid.dygraph.guard():
accs = []
model_dict, _ = fluid.load_dygraph('MyLeNet')
model = MyLeNet()
model.load_dict(model_dict) #加载模型参数
model.eval() #训练模式
for batch_id,data in enumerate(eval_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)
5、使用模型
5.1对车牌图像进行预处理
# 对车牌图片进行处理,分割出车牌中的每一个字符并保存
license_plate = cv2.imread('work/车牌.png')
gray_plate = cv2.cvtColor(license_plate, cv2.COLOR_RGB2GRAY)
ret, binary_plate = cv2.threshold(gray_plate, 175, 255, cv2.THRESH_BINARY) #ret:阈值,binary_plate:根据阈值处理后的图像数据
#这里把阈值设置成了175,对于BINARY方法,当图像中的灰度值大于175的重置像素值为255. 0是黑,255是白
# 按列统计像素分布
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
# print(result)
#记录车牌中字符的位置
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
# print(character_dict)
#将每个字符填充,并存储
characters = []
for i in range(8):
if i==2:
continue
padding = (170 - (character_dict[i][1] - character_dict[i][0])) / 2
#将单个字符图像填充为170*170
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('work/' + str(i) + '.png', ndarray)
characters.append(ndarray)
def load_image(path):
img = paddle.dataset.image.load_image(file=path, is_color=False) #读取图像,返回的是numpy数组
img = img.astype('float32')
img = img[np.newaxis, ] / 255.0
return img
补充:
- cv2.threshold()这个函数有四个参数,第一个原图像,第二个进行分类的阈值,第三个是高于(低于)阈值时赋予的新值,第四个是一个方法选择参数,常用的cv2.THRESH_BINARY(黑白二值)。该函数有两个返回值,第一个retVal(得到的阈值值(在后面一个方法中会用到)),第二个就是阈值化后的图像。
- 缩放到原来的二分之一,输出尺寸格式为(宽,高) img_test1 = cv2.resize(img, (int(y / 2), int(x / 2)))
5.2 对标签进行转换
#将标签进行转换
print('Label:',train_parameters['label_dict'])
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 train_parameters['label_dict'].values():
LABEL[str(L)] = match[V]
L += 1
print(LABEL)
5.3 使用模型进行预测
#构建预测动态图过程
with fluid.dygraph.guard():
model=MyLeNet()#模型实例化
model_dict,_=fluid.load_dygraph('MyLeNet')
model.load_dict(model_dict)#加载模型参数
model.eval()#评估模式
lab=[]
for i in range(8):
if i==2:
continue
infer_imgs = []
infer_imgs.append(load_image('work/' + 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('work/车牌.png'))
for i in range(len(lab)):
print(LABEL[str(lab[i])],end='')