本文参考博客使用opencv进行车牌提取及识别进行。程序部分为网上获取程序修改而来,并在其中加入了自己的注释和理解
采用python+opencv进行程序编写。
课程设计内容分享
程序下载请前往https://download.csdn.net/download/chenkz123/10841956
一个典型的车辆牌照识别系统一般包括以下4个部分:车辆图像获取、车牌定位、车牌字符分割和车牌字符识别 。
车辆图像获取
现有的车辆图像获取方式主要有两种:一种是由彩色摄像机和图像采集卡组成,其工作过程是:当车辆检测器(如地感线圈、红外线等)检测到车辆进入拍摄范围时,向主机发送启动信号,主机通过采集卡采集一幅车辆图像,为了提高系统对天气、环境、光线等的适应性,摄像机一般采用自动对焦和自动光圈的一体化机,同时光照不足时还可以自动补光照明,保证拍摄图片的质量;另一种是由数码照相机构成,其工作过程是:当车辆检测器检测到车辆进入拍摄范围时,直接给数码照相机发送一个信号,数码相机自动拍摄一幅车辆图像,再传到主机上,数码相机的一些技术参数可以通过与数码相机相连的主机进行设置,光照不足时也需要自动开启补光照明,保证拍摄图片的质量。
为了方便起见,这里选用网上获取的图片。
车牌定位
车牌定位的主要工作是从获取的车辆图像中找到汽车牌照所在位置,并把车牌从该区域中准确地分割出来,供字符分割使用。因此,牌照区域的确定是影响系统性能的重要因素之一,牌照的定位与否直接影响到字符分割和字符识别的准确率。目前车牌定位的方法很多,但总的来说可以分为以下4类:(1)基于颜色的分割方法,这种方法主要利用颜色空间的信息,实现车牌分割,包括彩色边缘算法、颜色距离和相似度算法等;(2)基于纹理的分割方法,这种方法主要利用车牌区域水平方向的纹理特征进行分割,包括小波纹理、水平梯度差分纹理等;(3)基于边缘检测的分割方法;(4)基于数学形态法的分割方法。 为了代码实现上的方便,采用的是基于边缘检测的分割方法。
在读入图片之后,通过resize限制图片的长和高。再对利用高斯滤波对图像进行去噪,之后利用灰度化将图像转为单通道灰度图,为之后的图像处理做准备。
if type(car_pic) == type(""):
img = imreadex(car_pic) # 读入图片
else:
img = car_pic
pic_hight, pic_width = img.shape[:2] # 获取图片大小
if pic_width > MAX_WIDTH: # 限制大小
resize_rate = MAX_WIDTH / pic_width
img = cv2.resize(img, (MAX_WIDTH, int(pic_hight * resize_rate)), interpolation=cv2.INTER_AREA)
if pic_hight > MAX_HEIGHT: # 限制大小
resize_rate = MAX_HEIGHT / pic_hight
img = cv2.resize(img, (int(pic_width * resize_rate), MAX_HEIGHT), interpolation=cv2.INTER_AREA)
# cv2.imshow('resize', img) # 显示resize后图像
blur = self.cfg["blur"]
# 高斯去噪
if blur > 0:
img = cv2.GaussianBlur(img, (blur, blur), 0) # 图片分辨率调整(高斯滤波)
oldimg = img
# cv2.imshow('GaussianBlur', oldimg) # 显示高斯去燥后图像
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转灰度
# cv2.imshow('Gray', img) # 高斯去燥转灰度后图像
# 读取图片文件
def imreadex(filename):
return cv2.imdecode(np.fromfile(filename, dtype=np.uint8), cv2.IMREAD_COLOR)
采用开运算断开较窄的狭颈和消除细的突出物,采用图像叠加(灰度图-开操作图)突显字符等部分,效果如下图所示。随后进行二值化,再利用Canny算子进行边缘检测,利用闭运算及开运算使图像边缘成为一个整体。
kernel = np.ones((20, 20), np.uint8)
img_opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
img_opening = cv2.addWeighted(img, 1, img_opening, -1, 0)
# cv2.imshow('Image superposition', img_opening) # 开操作、图像叠加之后图像
# 找到图像边缘
ret, img_thresh = cv2.threshold(img_opening, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
img_edge = cv2.Canny(img_thresh, 100, 200)
# cv2.imshow('Canny', img_edge) # Canny边缘检测后图像
# 使用开运算和闭运算让图像边缘成为一个整体
kernel = np.ones((self.cfg["morphologyr"], self.cfg["morphologyc"]), np.uint8)
img_edge1 = cv2.morphologyEx(img_edge, cv2.MORPH_CLOSE, kernel)
img_edge2 = cv2.morphologyEx(img_edge1, cv2.MORPH_OPEN, kernel)
然后进行轮廓检测并删除掉面积过小的轮廓,考虑到车牌的几何特征统一为长宽高固定的矩形,利用轮廓最小邻接矩形长宽比再进行筛选,此时已经得到了几个长宽比合适,且轮廓面积大于一定阈值的轮廓了。因为得到的轮廓外接矩形可能具有一定的倾斜,所以利用仿射变化来进行矫正。
# 查找图像边缘整体形成的矩形区域,可能有很多,车牌就在其中一个矩形区域中
image, contours, hierarchy = cv2.findContours(img_edge2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = [cnt for cnt in contours if cv2.contourArea(cnt) > Min_Area]
print('待选区域个数', len(contours))
drawimg = img
cv2.drawContours(drawimg, contours, -1, (0, 0, 255), 3)
# cv2.imshow('drawimg', drawimg)
# 一一排除不是车牌的矩形区域
car_contours = []
for cnt in contours:
rect = cv2.minAreaRect(cnt)
area_width, area_height = rect[1]
if area_width < area_height:
area_width, area_height = area_height, area_width
wh_ratio = area_width / area_height # 计算长宽比
# print(wh_ratio)
# 要求矩形区域长宽比在2到5.5之间,2到5.5是车牌的长宽比,其余的矩形排除
if wh_ratio > 2 and wh_ratio < 5.5:
car_contours.append(rect)
box = cv2.boxPoints(rect)
box = np.int0(box)
# oldimg = cv2.drawContours(oldimg, [box], 0, (0, 0, 255), 2)
# cv2.imshow("edge4", oldimg)
# print(rect)
print('筛选后待选矩形个数:', len(car_contours))
print("精确定位")
card_imgs = []
# 矩形区域可能是倾斜的矩形,需要矫正,以便使用颜色定位
for rect in car_contours:
if rect[2] > -1 and rect[2] < 1: # 创造角度,使得左、高、右、低拿到正确的值
angle = 1
else:
angle = rect[2]
rect = (rect[0], (rect[1][0] + 5, rect[1][1] + 5), angle) # 扩大范围,避免车牌边缘被排除
box = cv2.boxPoints(rect)
heigth_point = right_point = [0, 0]
left_point = low_point = [pic_width, pic_hight]
for point in box:
if left_point[0] > point[0]:
left_point = point
if low_point[1] > point[1]:
low_point = point
if heigth_point[1] < point[1]:
heigth_point = point
if right_point[0] < point[0]:
right_point = point
if left_point[1] <= right_point[1]: # 正角度
new_right_point = [right_point[0], heigth_point[1]]
pts2 = np.float32([left_point, heigth_point, new_right_point]) # 字符只是高度需要改变
pts1 = np.float32([left_point, heigth_point, right_point])
M = cv2.getAffineTransform(pts1, pts2)
dst = cv2.warpAffine(oldimg, M, (pic_width, pic_hight))
point_limit(new_right_point)
point_limit(heigth_point)
point_limit(left_point)
card_img = dst[int(left_point[1]):int(heigth_point[1]), int(left_point[0]):int(new_right_point[0])]
card_imgs.append(card_img)
# cv2.imshow("card", card_img)
# cv2.waitKey(0)
elif left_point[1] > right_point[1]: # 负角度
new_left_point = [left_point[0], heigth_point[1]]
pts2 = np.float32([new_left_point, heigth_point, right_point]) # 字符只是高度需要改变
pts1 = np.float32([left_point, heigth_point, right_point])
M = cv2.getAffineTransform(pts1, pts2)
dst = cv2.warpAffine(oldimg, M, (pic_width, pic_hight))
point_limit(right_point)
point_limit(heigth_point)
point_limit(new_left_point)
card_img = dst[int(right_point[1]):int(heigth_point[1]), int(new_left_point[0]):int(right_point[0])]
card_imgs.append(card_img)
# cv2.imshow("card", card_img)
# cv2.waitKey(0)
考虑到中国大陆上通用的汽车牌照底色有如下几种:蓝色、黄色、白色、黑色、绿色、渐变绿色、黄绿双拼色(蓝、黄、绿、白、黑)。蓝色是小车车牌(包括小吨位的货车);黄色牌照是大车或农用车用的车牌及教练车的车牌,还有新产品未定型的试验车;白色是特种车车牌(如军车警车车牌及赛车车牌);黑色是外商及外商的企业由国外自带车的车牌;小型新能源汽车:底色采用渐变绿色;大型新能源汽车:底色采用黄绿双拼色。因此,我们可以统计几个选择的矩形部分的颜色来作为该部分是否为车牌部分,在流程图上我们称之为颜色定位,我们简要统计颜色包括四种(蓝、绿、黄和黑白)。车牌具有强边缘信息,这是因为车牌的字符相对集中在车牌的中心,而车牌边缘无字符,因此我们通过寻找字符的上下左右边界来缩小车牌区域,避免不必要的干扰。最后便可以截取车牌区域的图片用以下一阶段的字符分割
3.车牌字符分割
第3部分是对汽车牌照字符的分割,当对车牌进行定位以后我们需要将车牌分割出来,然后对车牌部分进行字符分割,将车牌分为字符用于后续的识别。
车牌的字符颜色和车牌背景颜色对比鲜明。目前,我国国内的车牌大致可分为蓝底白字和黄底黑字,新能源汽车则采用绿底黑字,特殊用车采用白底黑字或黑底白字,有时辅以红色字体等。为了实现方便,我们只对主流的三种车牌:蓝底白字、黄底黑字,绿底黑字等三种车牌进行识别。首先对车牌图像进行灰度化,为了使字符部分高亮,我们对黄绿底色的车牌进行取反操作(使字符为白),再进行二值化。
# 以上为车牌定位
# 以下为识别车牌中的字符
predict_result = []
roi = None
card_color = None
for i, color in enumerate(colors):
if color in ("blue", "yello", "green"):
card_img = card_imgs[i]
gray_img = cv2.cvtColor(card_img, cv2.COLOR_BGR2GRAY)
# 黄、绿车牌字符比背景暗、与蓝车牌刚好相反,所以黄、绿车牌需要反向
if color == "green" or color == "yello":
gray_img = cv2.bitwise_not(gray_img)
# bitwise_not是对二进制数据进行“非”操作,即对图像(灰度图像或彩色图像均可)
# 每个像素值进行二进制“非”操作,~1=0,~0=1
ret, gray_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# cv2.imshow("gray_img", gray_img)
因为我国车牌颜色单一,字符直线排列车牌的灰度分布呈现出连续的波谷-波峰-波谷分布。在水平方向计算直方图,得到的最大波峰区域为字符区域。在垂直方向计算直方图,得到连续的波谷-波峰-波谷,初步可认为波峰部分为字符、波谷部分为间隔。
# 查找水平直方图波峰
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]]
# 查找垂直直方图波峰
row_num, col_num = gray_img.shape[:2]
# 去掉车牌上下边缘1个像素,避免白边影响阈值判断
gray_img = gray_img[1:row_num - 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会被分成两半
wave_peaks = find_waves(y_threshold, y_histogram)
# 根据设定的阈值和图片直方图,找出波峰,用于分隔字符
def find_waves(threshold, histogram):
up_point = -1 # 上升点
is_peak = False
if histogram[0] > threshold:
up_point = 0
is_peak = True
wave_peaks = []
for i, x in enumerate(histogram):
if is_peak and x < threshold:
if i - up_point > 2:
is_peak = False
wave_peaks.append((up_point, i))
elif not is_peak and x >= threshold:
is_peak = True
up_point = i
if is_peak and up_point != -1 and i - up_point > 4:
wave_peaks.append((up_point, i))
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
因为我国车牌字符高宽、间隔大小都是明确的。且汽车车牌字符数至少为7个,所以波谷-波峰对不够的部分不是车牌。通过波峰波谷的间距,我们可以很好的分割出字符(并在其中排除间隔点、铆钉、边缘部分)。至此我们已经得到了车牌的字符,我们将其resize至固定大小(20*20)。
# 车牌字符数应大于6
if len(wave_peaks) <= 6:
print("peak less 1:", len(wave_peaks))
continue
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)
# 去除车牌上的分隔点
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)
if len(wave_peaks) <= 6:
print("peak less 2:", len(wave_peaks))
continue
part_cards = seperate_card(gray_img, wave_peaks)
for i, part_card in enumerate(part_cards):
# 可能是固定车牌的铆钉
if np.mean(part_card) < 255 / 5:
print("a point")
continue
part_card_old = part_card
w = abs(part_card.shape[1] - SZ) // 2
part_card = cv2.copyMakeBorder(part_card, 0, 0, w, w, cv2.BORDER_CONSTANT, value=[0, 0, 0])
# 扩充src的边缘,将图像变大,然后以各种外插方式自动填充图像边界
part_card = cv2.resize(part_card, (SZ, SZ), interpolation=cv2.INTER_AREA)
4.车牌字符识别
第4部分就是对分离出来的单个字符进行识别,让机器告诉我们车牌的值。我们采用了支持向量机来进行训练,训练数据集分别采用网上查找到的汉字及字符图片训练集,字符图片大小为20*20,采用HOG作为支持向量机的特征向量。
我们分别对汉字和字符训练不同的模型,并对其分别进行预测。我国汽车牌照字符的排列位置遵循以下规律:第一个字符通常是我国各省区的简称,用汉字表示;第二个字符通常是发证机关的代码号,用字母表示;第二个字符之后为点(可能不存在,但依然存在较大间隔)最后5或6个字符由英文字母和数字组合而成,字母是二十四个大写字母(除去I和O这两个字母)的组合,数字用"0-9"之间的数字表示。
# part_card = deskew(part_card)
part_card = preprocess_hog([part_card]) # 方向梯度直方图
if i == 0:
resp = self.modelchinese.predict(part_card)
charactor = provinces[int(resp[0]) - PROVINCE_START]
else:
resp = self.model.predict(part_card)
charactor = chr(resp[0])
# 判断最后一个数是否是车牌边缘,假设车牌边缘被认为是1
if charactor == "1" and i == len(part_cards) - 1:
if part_card_old.shape[0] / part_card_old.shape[1] >= 7: # 1太细,认为是边缘
continue
predict_result.append(charactor)