目录
一. 车牌定位
0 流程
输入:原图像
输出:得到分割出车牌列表
得到原图二值化图像 -> 轮廓检测 -> 长宽面积等筛选 -> svm筛选 -> 倾斜矫正
四次检测: 1 按照蓝色 2 按照绿色 3 按照黄色 4 按照边缘 得到四个车牌列表
1 将原图像二值化得到黑白图像
基于颜色特征
以寻找蓝色车牌为例:选用BGR掩膜与S通道掩膜叠加 进行横向分量大的膨胀 得到二值图像
def get_BlueImg_bin(img):
# 掩膜:BGR通道,若像素B分量在 100~255 且 G分量在 0~190 且 G分量在 0~140 置255(白色) ,否则置0(黑色)
mask_gbr=cv2.inRange(img,(100,0,0),(255,190,140))
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 转换成 HSV 颜色空间
h, s, v = cv2.split(img_hsv) # 分离通道 色调(H),饱和度(S),明度(V)
mask_s = cv2.inRange(s, 80, 255) # 取饱和度通道进行掩膜得到二值图像
rgbs= mask_gbr & mask_s # 与操作,两个二值图像都为白色才保留,否则置黑
# 核的横向分量大,使车牌数字尽量连在一起
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 3))
img_rgbs_dilate = cv2.dilate(rgbs, kernel, 3) # 膨胀 ,减小车牌空洞
return img_rgbs_dilate
效果演示: mask_gbr 为 GBR通道掩膜所得二值图像
mask_s 为饱和度通道掩膜所得二值图像
image_bin 为 mask_gbr 和 mask_s与操作经腐蚀后最终结果
src 为原图像
基于边缘特征
增加一个颜色列表 判别按照边缘检测找出的车牌颜色 ,存入列表
(不同颜色车牌进行二值化操作、字符数目不同,需记录)
def get_EdgeImg_bin(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0)
sobelx = np.uint8(np.absolute(sobelx))
# kernel 横向分量大,使车牌数字尽量连在一起
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10, 3))
closing_img = cv2.morphologyEx(sobelx, cv2.MORPH_CLOSE, kernel, iterations=2)#闭运算
return cv2.inRange(closing_img,170,255)
问题 :
如果膨胀力度大,容易造成车牌字符与背景边缘粘连,识别失败。
膨胀力度较小,容易出现下图车牌分裂,只检测到右半部分的情况。
例
解决:
加大膨胀力会直接造成车牌失败,不予采取。
对于膨胀力度较小,车牌缺失,只能得到车牌右半部分的情况:
roimg为旋转校正图片,rect0(x0,y0,w0,h0) 为当前检测轮廓的位置信息
若 w0/h0<3:直接视为为非车牌区域,舍去
若 w0/h0>3.5:直接将rect0对应的区域投入支持向量机判断是否为车牌
若 3<=w0/h0<=3.5 :向左拓展当前区域,拓展长度为w/2,在拓展区域进行轮廓检测,若能找到轮廓外接矩形满足:
高>2/3h0, max(长/宽,宽/长)<1.5的轮廓,则将rect1(x0-int(w0/2),y0,w0+int(w0/2),h0), 位
置信息对应区域投入支持向量机判断是否为车牌。
例:
红色框代表rect0位置信息,绿色框为向左拓展区域,绿色框内容部
2 筛选车牌区域
在二值图像上 获取轮廓,并求得外接矩形
第一轮筛选(利用几何特征)
通过外接矩形长、宽、长宽比、轮廓包含面积占矩形面积占比,排除一部分轮廓
第二轮筛选(利用支持向量机)
准备若干车牌图像和非车牌图像用于SVM模型的训练, 将一轮筛选后保留下的轮廓,利用训练好的模型,判别当前区域是否为车牌
过程记录在 csdn :https://blog.csdn.net/afeiererer/article/details/88534905
效果演示: 从左到右依次为 原二值图像、一轮筛选后结果、二轮筛选后结果
3 车牌倾斜斜矫正
车牌字符能够正确分割的前提是车牌图像能够水平,以至于水平投影和垂直投影能够正常进行。
对于下面这种图像,我们可以明显看到车牌有倾斜角度,需要将图像校正,提取车牌。
得到 选出的车牌区域 的 最小外接矩形 ,得到偏转角度 ,旋转原图像 (旋转操作前需扩张原图,以防旋转出边界,造成车牌缺失及数组越界错误)
旋转效果:
抠取车牌图像 ,添加至车牌列表
ps: 经过旋转,大体保证上下平齐,但由于拍摄角度,可能如下图出现左右歪斜情况。
(后期将进行二次校正)
二. 车牌预处理
0 流程
输入:车牌原图像
输出:用于字符分割的二值图
二值化 (不同颜色车牌处理方式不同)-> 去除边框->二次校正(左右倾斜)
1 车牌预处理 方法一:基于颜色特征和大津法二值化
1.1 蓝色车牌预处理
采用了对比度增强的方法,车牌和数字的对比度得到提高。但部分图像对于汉字部分容易出现模糊情况
采用锐化方法汉字更加清晰,但图像杂质较多
改进方案:将经过锐化预处理的车牌二值化图像和经过对比度增强预处理的车牌二值化图像进行与操作。保留了锐 化的清晰优点,灰度拉伸的杂质少优点。
对比: 第一列为增强对比度后OTSU二值结果,第二列为锐化后OTSU二值结果,第三列为前二者进行与操作的最 终结果
def con(img): #增强对比度
h, w, ch = img.shape
src2 = np.zeros([h, w, ch], img.dtype)
con = cv2.addWeighted(img, 1.2, src2, 1 - 1.2, 0)
return con
def rui(img): #锐化
fil = np.array([[-1, -1, -1], [-1, 9, -1], [-1, 1, -1]])
res=cv2.filter2D(img,-1,fil)
return res
def get_BluePlate_bin(pai_src):
# 锐化 取 channl_h
pai_rui = rui(pai_src) #锐化
img_hsv = cv2.cvtColor(pai_rui, cv2.COLOR_BGR2HSV) # 转换成HSV颜色空间
h, s, v = cv2.split(img_hsv) # 分离 H、S、V 通道
_, rui_otsu = cv2.threshold(h, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 二值化
# 增强对比度 取 channl_s
pai_con = con(pai_src)
img_hsv = cv2.cvtColor(pai_con, cv2.COLOR_BGR2HSV) # 转换成HSV颜色空间
h, s, v = cv2.split(img_hsv) # 分离 H、S、V 通道
_, con_otsu = cv2.threshold(s, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 二值化
con_rui =rui_otsu & con_otsu #与操作 清晰,去噪
bin = cv2.inRange(eli_pai, 200, 255) #二值化
return bin
1.2 绿色车牌预处理
选用提取图像G通道进行大津法(OTSU)二值化的方法
效果:
def get_GreenPlate_bin(pai_src):
b, g, r = cv2.split(pai_src) #分离B、G、R通道
_, bin = cv2.threshold(g, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) #大津法二值化
return bin
1.3 黄色车牌预处理
选用将图像灰度化后进行大津法(OTSU)二值化的方法
效果:
def get_YellowPlate_bin(pai_src):
gray_img = cv2.cvtColor(pai_src, cv2.COLOR_BGR2GRAY) #灰度化图像
#大津法二值化
ret, gray_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
return gray_img
2 车牌预处理 方法二:基于k-means聚类
牌照主要分为背景色和文字颜色,其中蓝、黑、黄牌照背景色为单色,一般k=2的聚类即可;绿色牌照背景为白、绿双色,更适用于k=3的聚类(字符更突显)。
但由于光线、杂质等因素,部分车牌进行K=2的k-means聚类可能出现如下状况:
假定车牌原图 SRC 高宽分别为h、w, 我们设定一个高为h/2,宽为w/2的窗口,以一定步长在原图上滑动,若出现窗口中白色像素数量>窗口面积*90, 或窗口中黑色像素数量>窗口面积*90 ,视为失败聚类,将对此类车牌进行k=3的像素聚类。改进效果如下:
对于K=2的聚类,我们统计两类像素:像素值为0的像素个数n0、像素值为255的像素个数n255,认为两类像素中个数多的为背景。所以若n0<n255,将聚类得到的图像取反,保证字符为白色,背景为黑色。
对于K=3的聚类,我们统计三类像素:像素值为0的像素个数n0、像素值为255的像素个数n255,像素值为128的像素个数n128,首先抛除n0,n255,n128中最大值对应的像素种类,视为背景。再从剩余的两种像素中选择出字符代表的像素种类的值,根据这个值进行掩膜运算,筛选出字符去除背景,得到二值图像。
3 去除边框
去除上下边框:查找水平直方图波峰,认为水平方向,最大的波峰为车牌区域
# 查找水平直方图波峰
x_histogram = np.sum(gray_img, axis=1)
x_min = np.min(x_histogram)
x_average = np.sum(x_histogram) / x_histogram.shape[0]
x_threshold = (x_min + x_average) / 2
wave_peaks = find_waves(x_threshold, x_histogram)
if len(wave_peaks) == 0:
print("peak less 0:")
continue
# 认为水平方向,最大的波峰为车牌区域
wave = max(wave_peaks, key=lambda x: x[1] - x[0])
gray_img = gray_img[wave[0]:wave[1]]
去边框效果:
4 二次校正(左右偏斜)
利用霍夫直线检测,求检测到的直线的平均值作为车牌偏斜角度,进行仿射变换
三. 字符分割
1 方法一:垂直投影找波峰
阈值选取:
y_histogram = np.sum(gray_img, axis=0)
y_min = np.min(y_histogram)
y_average = np.sum(y_histogram) / y_histogram.shape[0]
y_threshold = (y_min + y_average) / 5 # U和0要求阈值偏小,否则U和0会被分成两半
如果得到波峰小于6(新能源汽车车牌小于7)则舍去
# 获取波峰
def get_wave_peaks(gray_img):
y_histogram = np.sum(gray_img, axis=0)
y_min = np.min(y_histogram)
y_average = np.sum(y_histogram) / y_histogram.shape[0]
y_threshold = (y_min + y_average) / 5 # U和0要求阈值偏小,否则U和0会被分成两半
wave_peaks = find_waves(y_threshold, y_histogram)
wave = max(wave_peaks, key=lambda x: x[1] - x[0])
max_wave_dis = wave[1] - wave[0]
# 判断是否是左侧车牌边缘
if wave_peaks[0][1] - wave_peaks[0][0] < max_wave_dis / 3 and wave_peaks[0][0] == 0:
wave_peaks.pop(0)
# 组合分离汉字
cur_dis = 0
for i, wave in enumerate(wave_peaks):
if wave[1] - wave[0] + cur_dis > max_wave_dis * 0.6:
break
else:
cur_dis += wave[1] - wave[0]
if i > 0:
wave = (wave_peaks[0][0], wave_peaks[i][1])
wave_peaks = wave_peaks[i + 1:]
wave_peaks.insert(0, wave)
# 去除车牌上的分隔点
if len( wave_peaks)>2:
point = wave_peaks[2]
if point[1] - point[0] < max_wave_dis / 3:
point_img = gray_img[:, point[0]:point[1]]
if np.mean(point_img) < 255 / 5:
wave_peaks.pop(2)
return wave_peaks
输出波峰:
分割效果:
绿线为每个波峰左端,红线为每个波峰右端。
两条线所夹区域为分割出的数字(汉字、字母),将用于投入支持向量机进行预测。
#分割数字(汉字、字母)
def seperate_card(img, waves):
part_cards = [] #分割出的图像列表
for wave in waves:
part_cards.append(img[:, wave[0]:wave[1]]) #按照波峰提取图像添加入列表
return part_cards
2 方法二:
找到确定的分界,然后补全界限。比如找到了 第1个字符与第2个字符分界,第3个和第4个分界 ,但是没找到 第2个和第3个分界,则取前两个界限中间。
过程:
1.找到不是字符的列
2.找到跳变
3.去杂线
4.找到分界
效果:
取每两条线之间区域,做轮廓检测,找到轮廓包含面积最大的,取其外接矩形部分,投入支持向量机进行预测。
3 两种方法对比
上方为方法一分割效果,下方为方法二分割效果。
可见,对于方法一的分割效果更好
而对于方法二的分割效果更好
可见两种方法各有优劣,可以互相补充。
四. 字符识别
- 采用RCNN方法。
- 分别训练数字字母模型和汉字模型 ,每个车牌分割出的第一位用汉字模型预测,其余用数字字母模型预测。
数字模型需要两套(7位车牌和8位车牌的数字 ‘2’ ,‘4’,‘6’ 字体不同)
- 采用三种方案对提取出的车牌原图进行预处理、分割、识别。效果各有优劣,互为补充。
Plan A:车牌预处理1+分割1 得到识别结果 r0
Plan B:车牌预处理1+分割2 得到识别结果 r1
Plan C:车牌预处理2+分割1 得到识别结果 r2
- 将得到的三个识别结果 r0 ,r1 ,r2 进行比较,得到最终结果。
比较规则:
设目标结果长度为n (新能源汽车车牌n=8,蓝黄等其余车牌n=7)
串长度 小于n-1 或 大于n+1 视为无效 ,不参与比较 ,所有有效串长度均为 n-1 或 n+1 或 n 。
若三个串最大长度<n ,返回空,识别失败
若三个串中只有一个长度为n ,返回长度为n的串
剩余情况,每位取出现频率最高的字符,返回前n位
def get_res(n,r0,r1,r2):
#n为目标长度,r0,r1,r2分别为三种方法返回的结果
res = [] # 最终结果
list=[[],[],[],[],[],[],[],[]] #存第n位字符
r_list=[] #参与比较的有效串列表 (串长度 <n-1 或 >n+1 视为无效 不参与比较)
for i in (r2,r1,r0):
if not (len(i)<n-1 or len(i)>n+1):
r_list.append(i)
num = len(r_list) # 参与比较的串数目
if num==0: #没有有效串,返回空
return []
max_num = max(len(r0), len(r1), len(r2))
if max_num < n:
r_list[0].append("1")
return r_list[0]
r_lenn = [] #长度为目标长度n的串列表
for i in r_list:
if len(i) == n:
r_lenn.append(i)
if len(r_lenn)==1: #如果只有一个长度为n,返回这个串
return r_lenn[0]
else: #剩余情况,每位取出现频率最高的字符,返回前n位
for r in r_list:
for i in range(len(r)):
list[i].append(r[i])
for i in range(n):
res.append(Counter(list[i]).most_common(1)[0][0])
return res
CNN:
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adadelta(),
metrics=['accuracy'])
五 输出演示