写在开头:许久之前的项目了,也是人工智能和图像处理的入门吧,想了想还是整理出来比较好
为什么使用python进行车牌识别?
1.首先是python最近两年的发展形势不错,python语言简单明了,容易入门,很重要的一点是他的拓展库比较丰富
2.他在人工智能,图像识别的方面的应用性很强,因为有强大的库的支撑
准确率是多少?
如何提高准确率?
https://www.cnblogs.com/bonelee/p/8528863.html
车牌图像处理原理
一、是读取图像,对图像进行预处理,包括(具有先后顺序):压缩图像、转换为灰度图像、灰度拉伸、开运算(去噪声)、将灰度图像和开运算后图像取差分图、整张图像二值化、canny边缘检测、闭运算、开运算、再次开运算(这三步是为了保留车牌区域,并消除其他区域)、定位车牌位置(找轮廓、画轮廓、取前三个轮廓进行排序、找出最大的区域);
二、是框处车牌号;
三、分割车牌号和背景,分割包括:创建掩膜、创建背景和前景、分割;
四、将分割出来的车牌进行二值化,生成黑白图像;
五、分割出车牌号码中的文字、数字和字母,放入特定的文件夹;
六、对分割出来的文字、数字和字母图像尺寸进行处理,以方便后面测试。
卷积神经网络进行训练和测试
一、定义卷积层;
二、定义全连接层;
三、创建模型和权重参数的文件夹;
五、再次遍历图片文件夹,生成图片输入数据和标签;
六、构建训练模型,该模型包括两个卷积层和一个全连接层,采用Adam梯度下降优化算法;
七、创建图,进行迭代训练;
八、通过tensorflow提供的API接口tf.train.Saver()来保存训练好的模型以及权重参数等;
九、将分割出来的文字、数字和字母图像,通过训练模型和权重进行测试。
1.缩放,转化为灰度图像
意义:
m=400 * img.shape[0] / img.shape[1]
#shape[3] 长 宽 通道
#压缩图像
img=cv2.resize(img,(400,int(m)),interpolation=cv2.INTER_CUBIC)
'''
nterpolation 选项 所用的插值方法
INTER_NEAREST 最近邻插值
INTER_LINEAR 双线性插值(默认设置),简单,但容易出现锯齿
INTER_AREA 使用像素区域关系进行重采样。 它可能是图像抽取的首选方法,
因为它会产生无云纹理的结果。 但是当图像缩放时,它类似于INTER_NEAREST方法。
INTER_CUBIC 4x4像素邻域的双三次插值,放大的常用插值方法
INTER_LANCZOS4 8x8像素邻域的Lanczos插值
'''
#BGR转换为灰度图像
gray_img=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
print('gray_img.shape',gray_img.shape)
cv2.imshow("gray_img",gray_img)
易考察点:图像缩放所用的插值方法及其比较
2.图像拉伸
意义:#如果一幅图像的灰度集中在较暗的区域而导致图像偏暗,可以用灰度拉伸功能来拉伸(斜率>1)物体灰度区间以改善图像;
# 同样如果图像灰度集中在较亮的区域而导致图像偏亮,也可以用灰度拉伸功能来压缩(斜率<1)物体灰度区间以改善图像质量
#灰度拉伸
#如果一幅图像的灰度集中在较暗的区域而导致图像偏暗,可以用灰度拉伸功能来拉伸(斜率>1)物体灰度区间以改善图像;
# 同样如果图像灰度集中在较亮的区域而导致图像偏亮,也可以用灰度拉伸功能来压缩(斜率<1)物体灰度区间以改善图像质量
stretchedimg=stretching(gray_img)#进行灰度拉伸,是因为可以改善图像的质量
print('stretchedimg.shape',stretchedimg.shape)
def stretching(img):
'''
图像拉伸函数:图像增强算法
'''
maxi=float(img.max())
mini=float(img.min())
for i in range(img.shape[0]):
for j in range(img.shape[1]):
img[i,j]=(255/(maxi-mini)*img[i,j]-(255*mini)/(maxi-mini))
return img
3.开运算
意义:
'''进行开运算,用来去除噪声'''
r=15
h=w=r*2+1
kernel=np.zeros((h,w),np.uint8)
cv2.circle(kernel,(r,r),r,1,-1)
#开运算
openingimg=cv2.morphologyEx(stretchedimg,cv2.MORPH_OPEN,kernel)
#获取差分图,两幅图像做差 cv2.absdiff('图像1','图像2')
strtimg=cv2.absdiff(stretchedimg,openingimg)
cv2.imshow("stretchedimg",stretchedimg)
cv2.imshow("openingimg1",openingimg)
cv2.imshow("strtimg",strtimg)
#cv2.waitKey(0)
顶帽(Top Hat):原图与开运算结果图之差
开运算放大了裂缝或者局部低亮度的区域,所以,从原图中减去开运算后的图,得到的结果突出了比原图轮廓周围的区域更明亮的区域,这个操作与选择的核的大小有关。TopHat运算一般用来分离比邻近点亮一些的斑块,可以使用这个运算提取背景。
易考察点:开运算:先腐蚀后膨胀,开运算能够除去孤立的小点,毛刺和小桥,而总的位置和形状不便。
闭运算:先膨胀后腐蚀,闭运算能够填平小湖(即小孔),弥合小裂缝,而总的位置和形状不变;闭运算是通过填充图像的凹角来滤波图像的。
也算是一种滤波器,滤波器有哪些?
图像处理相关滤波器的介绍https://www.cnblogs.com/ISGuXing/p/7654572.html
4.图像二值化
图像二值化处理就是将图像上点的灰度置为0或255,即整个图像呈现出明显的黑白效果。将256个亮度等级的灰度图像通过适当的阀值选取而获得仍然可以反映图像整体和局部特征的二值化图像。
#图像二值化
binaryimg=allbinaryzation(strtimg)
cv2.imshow("binaryimg",binaryimg)
#cv2.waitKey(0)
#整个图像(包括车身)二值化处理
def allbinaryzation(img):
'''
二值化处理函数
'''
maxi=float(img.max())
mini=float(img.min())
x=maxi-((maxi-mini)/2)
#二值化,返回阈值ret 和 二值化操作后的图像thresh
ret,thresh=cv2.threshold(img,x,255,cv2.THRESH_BINARY)
# thresh = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,5,2)
#返回二值化后的黑白图像
return thresh
二值化一般分为全局阈值和局部阈值,此处用的是全局固定阈值
全局阈值就是一幅图像包括目标物体、背景还有噪声,要想从多值的数字图像中直接提取出目标物体;常用的方法就是设定一个阈值T,用T将图像的数据分成两部分:大于T的像素群和小于T的像素群。这是研究灰度变换的最特殊的方法,称为图像的二值化(Binarization)
局部阈值就是当同一幅图像上的不同部分的具有不同亮度时。这种情况下我们需要采用自适应阈值。此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。因此在同一幅图像上的不同区域采用的是不同的阈值,从而使我们能在亮度不同的情况下得到更好的结果。此种方式更能显示细节
5. 边缘检测
边缘检测的目的是找到图像中亮度变化剧烈的像素点构成的集合,表现出来往往是轮廓。边缘检测有很多检测器,其中最常用的是canny边检测器,不容易受到噪声的影响。
#canny边缘检测
canny=cv2.Canny(binaryimg,binaryimg.shape[0],binaryimg.shape[1])
cv2.imshow("canny",canny)
#cv2.waitKey(0)
图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波。我们知道微分运算是求信号的变化率,具有加强高频分量的作用。在空域运算中来说,对图像的锐化就是计算微分。由于数字图像的离散信号,微分运算就变成计算差分或梯度。图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零点检测。通过计算梯度,设置阀值,得到边缘图像。
canny边缘检测原理:
Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
最优定位:检测的边缘点应该精确地定位于边缘的中心。
图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
Canny算法出现以后一直是作为一种标准的边缘检测算法,此后也出现了各种基于Canny算法的改进算法。时至今日,Canny算法及其各种变种依旧是一种优秀的边缘检测算法。而且除非前提条件很适合,你很难找到一种边缘检测算子能显著地比Canny算子做的更好。
6.形态学处理
开运算和闭运算是形态学常用的图像处理方式,开运算可以消除亮度较高的细小区域,在纤细点处分离物体,对于较大物体,可以在不明显改变其面积的情况下平滑其边界。闭运算具有填充白色物体内细小黑色空洞的区域、连接临近物体、平滑边界等作用。
#进行闭运算
kernel=np.ones((5,23),np.uint8)
closingimg=cv2.morphologyEx(canny,cv2.MORPH_CLOSE,kernel)
cv2.imshow("closingimg",closingimg)
#进行开运算
openingimg=cv2.morphologyEx(closingimg,cv2.MORPH_OPEN,kernel)
cv2.imshow("openingimg2",openingimg)
#再次进行开运算
kernel=np.ones((11,6),np.uint8)
openingimg=cv2.morphologyEx(openingimg,cv2.MORPH_OPEN,kernel)
cv2.imshow("openingimg3",openingimg)
#cv2.waitKey(0)
7.定位车牌
先对上一步的图像 ‘img_opening2’ 检测轮廓,使用的是cv.findContours,该函数会返回图像的轮廓信息,然后对轮廓信息进行大小,高宽比,颜色筛选出最符合车牌的矩形轮廓,从而定位车牌区域。
#车牌号定位
def locate_license(img,afterimg):
'''
定位车牌号
'''
img, contours,hierarchy=cv2.findContours(img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#cv2.waitKey(0)
img_copy = afterimg.copy()
img_cont = cv2.drawContours(img_copy,contours,-1,(255,0,0),6)
cv2.imshow("img_cont",img_cont)
#cv2.waitKey(0)
#找出最大的三个区域
block=[]
for c in contours:
#找出轮廓的左上点和右下点,由此计算它的面积和长度比
r=find_rectangle(c)#里面是轮廓的左上点和右下点
a=(r[2]-r[0])*(r[3]-r[1]) #面积
s=(r[2]-r[0])/(r[3]-r[1]) #长度比
block.append([r,a,s])
#选出面积最大的3个区域
block=sorted(block,key=lambda b: b[1])[-3:]
#使用颜色识别判断找出最像车牌的区域
maxweight,maxindex=0,-1
for i in range(len(block)):#len(block)=3
b=afterimg[block[i][0][1]:block[i][0][3],block[i][0][0]:block[i][0][2]]
#BGR转HSV
hsv=cv2.cvtColor(b,cv2.COLOR_BGR2HSV)
#蓝色车牌的范围
lower=np.array([100,50,50])
upper=np.array([140,255,255])
#根据阈值构建掩膜
mask=cv2.inRange(hsv,lower,upper)
#统计权值
w1=0
for m in mask:
w1+=m/255
w2=0
for n in w1:
w2+=n
#选出最大权值的区域
if w2>maxweight:
maxindex=i
maxweight=w2
return block[maxindex][0]
检测函数说明:
第一个参数表示输入图像,必须为一个8位的二值图像。第二参数表示轮廓检测的模式,有如下取值:
CV_RETR_EXTERNAL:只检索最外面的轮廓;
CV_RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中;
CV_RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界;
CV_RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次,可以参见下图。第三个参数用来表示轮廓边缘的近似方法的,常用值如下所示:CV_CHAIN_CODE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。CV_CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分。
将相近颜色的内容放入掩膜中,遍历每个区域,找颜色符合内容最多的一块
8.框出车牌位置
# 框出车牌号
cv2.rectangle(afterimg, (rect[0], rect[1]), (rect[2], rect[3]), (0, 255, 0), 2)
cv2.imshow('afterimg1', afterimg)
9.分割车牌和背景
# 分割车牌与背景
cutimg = cut_license(afterimg, rect)
cv2.imshow('cutimg', cutimg)
def cut_license(afterimg,rect):
'''
图像分割函数
'''
#转换为宽度和高度
rect[2]=rect[2]-rect[0]
rect[3]=rect[3]-rect[1]
rect_copy=tuple(rect.copy())#tuple是一个元组
print("rect_copy",rect_copy)
rect=[0,0,0,0]
#创建掩膜
mask=np.zeros(afterimg.shape[:2],np.uint8)
#创建背景模型 大小只能为13*5,行数只能为1,单通道浮点型
bgdModel=np.zeros((1,65),np.float64)
#创建前景模型
fgdModel=np.zeros((1,65),np.float64)
#分割图像
cv2.grabCut(afterimg,mask,rect_copy,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
mask2=np.where((mask==2)|(mask==0),0,1).astype('uint8')
img_show=afterimg*mask2[:,:,np.newaxis]
# newmask = img_show
# mask[newmask == 0] = 0
# mask[newmask == 255] = 1
# mask , bgdModel,fgdModel = cv2.grabCut(afterimg,mask,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
# mask1 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
# img_show = afterimg*mask1[:,:,np.newaxis]
# plt.imshow(img_show), plt.colorbar(), plt.show()
return img_show
此处用的是grabcut分割方法,还有基于阈值的分割方法和分水岭份分割方法,这里的grabcut分割方式是根据框选矩形区域实现的,矩阵内部为前景,外部为后景,还有一种是分水岭算法,我记得是依据边缘检测去做的
10图像二值化
# 二值化生成黑白图
thresh = lice_binarization(cutimg)
cv2.imshow('thresh', thresh)
cv2.waitKey(0)
11,分割字符
# 分割字符
'''
判断底色和字色
'''
# 记录黑白像素总和
white = [] # 记录每一列的白色像素总和
black = [] # 记录每一列的黑色像素总和
height = thresh.shape[0] # 263
width = thresh.shape[1] # 400
white_max = 0 # 仅保存每列,取列中白色最多的像素总数
black_max = 0 # 仅保存每列,取列中黑色最多的像素总数
# 计算每一列的黑白像素总和
for i in range(width):
line_white = 0 # 这一列白色总数
line_black = 0 # 这一列黑色总数
for j in range(height):
if thresh[j][i] == 255:
line_white += 1
if thresh[j][i] == 0:
line_black += 1
white_max = max(white_max, line_white)
black_max = max(black_max, line_black)
white.append(line_white)
black.append(line_black)
print('white_max', white_max)
print('black_max', black_max)
# arg为true表示黑底白字,False为白底黑字
arg = True
if black_max < white_max:
arg = False
# 分割车牌的数字
n = 1
start = 1
end = 2
s_width = 28
s_height = 28
temp = 1
while n < width - 2:
n += 1
# 判断是白底黑字还是黑底白字 0.05参数对应上面的0.95 可作调整
#某列字的像素大于
print("n" + str(n) + "arg" + str(arg) + "white" + str(white[n]) + "black"+str(black[n])+"white_max"+str(white_max)+"black_max"+str(black_max))
if (white[n] if arg else black[n]) > (0.05 * white_max if arg else 0.05 * black_max):#这点没有理解透彻
start = n
end = find_end(start, arg, black, white, width, black_max, white_max)
n = end
print("start" + str(start))
print("end" + str(end))
# 思路就是从左开始检测匹配字符,若宽度(end - start)小与20则认为是左侧白条 pass掉 继续向右识别,否则说明是
# 省份简称,剪切,压缩 保存,还有一个当后五位有数字 1 时,他的宽度也是很窄的,所以就直接认为是数字 1 不需要再
# 做预测了(不然很窄的 1 截切 压缩后宽度是被拉伸的),shutil.copy()函数是当检测
# 到这个所谓的 1 时,从样本库中拷贝一张 1 的图片给当前temp下标下的字符
# if end - start > 5:
# print("end - start" + str(end - start))
if end - start > 5:
cj = thresh[1:height, start:end]
print("result/%s.jpg" % (n))
#指定图片存储路径
cv2.imwrite('img/{0}.bmp'.format(n), cj)
#对分割出的数字、字母进行裁剪
b_img = cv2.resize(cj, None, fx=5, fy=3)
b_img, contours, hierarchy = cv2.findContours(b_img.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.imshow('b_img', b_img)
block = []
for c in contours:
# 找出轮廓的左上点和右下点,由此计算它的面积和长度比
r = find_rectangle(c) # 里面是轮廓的左上点和右下点
a = (r[2] - r[0]) * (r[3] - r[1]) # 面积
s = (r[2] - r[0]) / (r[3] - r[1]) # 长度比
block.append([c, r, a, s])
block1 = sorted(block, key=lambda block: block[2])[-1:]
# rect = cv2.minAreaRect(block2)
# box1 = np.int0(cv2.boxPoints(rect))
box = block1[0][1]
y_mia = box[0] # y_mia
x_min = box[1] # x_min
y_max = box[2] # y_max
x_max = box[3] # x_max
cropImg = b_img[x_min:x_max, y_mia:y_max] # crop the image
cropImg = cv2.resize(cropImg,(32,40),interpolation=cv2.INTER_CUBIC)
cv2.imwrite('img_test/{0}.bmp'.format(n), cropImg)
cv2.imshow('cutlicense', cj)
cv2.imshow("charecter",cropImg)
cv2.waitKey(0)