前言:本案例的车牌图像来源于互联网,如有侵权请尽快联系我,立删。
文章目录
- 一、概述
- 二、车牌图像分析
- 三、车牌定位
- 1. 基本处理
- 2. 图像降噪
- 3. 灰度拉伸
- 4. 图像差分
- 5. 二值化
- 6. 边缘检测
- 7. 形态学处理
- 8. 定位车牌
- 四、字符分割
- 1. 去除上下边缘
- 2. 分割并保存字符
- 五、测试其它图片
- 六、总结
- 七、附上完整代码
一、概述
在智能交通系统中,汽车牌照识别发挥了巨大的作用。其实现是将图像处理技术与计算机软件技术相连接在一起,以准确识别出车牌牌照的字符为目的,将识别出的数据传送至交通实时管理系统,以最终实现交通监管的功能。在车牌自动识别系统中,从汽车图像的获取到车牌字符处理是一个复杂的过程,主要分为四个阶段:图像获取、车牌定位、字符分割以及字符识别。本文主要通过OpenCV的各种图像处理方法实现车牌定位以及字符分割。
二、车牌图像分析
我国汽车牌照一般由七个字符和一个点组成(参考下图),车牌字符的高度和宽度是固定的,分别为90mm和45mm,七个字符之间的距离也是固定的12mm,中间分割符圆点的直径是10mm,但是真实车牌图像会因为透视原因造成字符间的距离变化。在民用车牌中,字符排列位置遵循以下规律:第一个字符通常是我国各省区的简称,共31个,用汉字表示;第二个字符通常是发证机关的代码号,最后五个字符由英文字母和数字组合而成,字母是24个大写字母(除去 I 和 O)的组合,数字用"0-9"之间的数字表示。
从图像处理角度看,汽车牌照具有以下几个特征:
- 车牌的几何特征,即车牌形状统一为高宽比固定的矩形;
- 车牌的灰度分布呈现出连续的波谷-波峰-波谷分布,这是因为我国车牌颜色单一,字符直线排列;
- 车牌直方图呈现出双峰状的特点,即车牌直方图中可以看到双个波峰;
- 车牌具有强边缘信息,这是因为车牌的字符相对集中在车牌的中心,而车牌边缘无字符,因此车牌的边缘信息感较强;
- 车牌的字符颜色和车牌背景颜色对比鲜明。目前,我国国内的车牌大致可分为蓝底白字和黄底黑字,特殊用车采用白底黑字或黑底白字,有时辅以红色字体等。为简化处理,本文只考虑蓝底白字的车牌。
三、车牌定位
1. 基本处理
调整尺寸和转灰度图
为了确保输入的车牌图像不能太大或太小,需要对图像进行尺寸调整,一般的照片高宽比是3:4,此次我们限制图像最大宽度为400像素,函数如下:
def resize_img(img):
""" resize图像 """
h, w = img.shape[:-1]
scale = 400 / max(h, w)
img_resized = cv.resize(img, None, fx=scale, fy=scale,
interpolation=cv.INTER_CUBIC)
# print(img_resized.shape)
return img_resized
因图像后续处理输入要求,在此需将图像转为灰度图
img_gray = cv.cvtColor(img_resized, cv.COLOR_BGR2GRAY)
cv.imshow('Gray', img_gray)
效果展示:
2. 图像降噪
噪声是由一种或者多种原因造成的灰度值的随机变化,通常需要平滑技术(也常称为滤波或者降噪技术)进行抑制或者去除。图像降噪即通过滤波器增强图像中某个波段或频率并阻塞(或降低)其他频率波段。常见的图像滤波方式有均值滤波、高斯滤波、中值滤波、双边滤波等。一般采用高斯滤波来对图像进行降噪。(本案例车牌图像质量较好,此次没有进行此操作)
img_gaussian = cv.GaussianBlur(img_gray, (3, 3), 0)
cv.imshow("Gaussian_Blur2", img_gaussian)
3. 灰度拉伸
图像拉伸主要用来改善图像显示的对比度,道路提取流程中往往首先要对图像进行拉伸的预处理。图像拉伸主要有三种方式:灰度拉伸、直方图均衡化和直方图规定化,此次使用灰度拉伸,将灰度值拉伸到整个0-255的区间,那么其对比度显然是大幅增强的。可以用如下的公式来将某个像素的灰度值映射到更大的灰度空间:
其中Imin,Imax是原始图像的最小灰度值和最大灰度值,MIN和MAX是要拉伸到的灰度空间的灰度最小值和最大值。
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)
img_stretched = img
return img_stretched
效果展示:
4. 图像差分
图像差分,就是把两幅图像的对应像素值相减,以削弱图像的相似部分,突出显示图像的变化部分。在进行差分前,需要对图像进行开运算,即先腐蚀后膨胀。
# 进行开运算,去除噪声
r = 14
h = w = r * 2 + 1
kernel = np.zeros((h, w), np.uint8)
cv.circle(kernel, (r, r), r, 1, -1)
# 开运算
img_opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
效果展示:
再对开运算前后图像进行差分,使用cv.absdiff函数
效果展示:
5. 二值化
图像二值化处理就是将图像上点的灰度置为0或255,即整个图像呈现出明显的黑白效果。将256个亮度等级的灰度图像通过适当的阀值选取而获得仍然可以反映图像整体和局部特征的二值化图像。
def binarization(img):
""" 二值化处理函数 """
maxi = float(img.max())
mini = float(img.min())
x = maxi - ((maxi - mini) / 2)
# 二值化, 返回阈值ret和二值化操作后的图像img_binary
ret, img_binary = cv.threshold(img, x, 255, cv.THRESH_BINARY)
# img_binary = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 5, 2)
return img_binary
二值化后可以明显看到车牌区域,效果展示:
6. 边缘检测
边缘检测的目的是找到图像中亮度变化剧烈的像素点构成的集合,表现出来往往是轮廓。边缘检测有很多检测器,其中最常用的是canny边检测器,不容易受到噪声的影响。
def canny(img):
""" canny边缘检测 """
img_canny = cv.Canny(img, img.shape[0], img.shape[1])
return img_canny
效果展示:
7. 形态学处理
开运算和闭运算是形态学常用的图像处理方式,开运算可以消除亮度较高的细小区域,在纤细点处分离物体,对于较大物体,可以在不明显改变其面积的情况下平滑其边界。闭运算具有填充白色物体内细小黑色空洞的区域、连接临近物体、平滑边界等作用。
def opening_closing(img):
""" 开闭运算,保留车牌区域,消除其他区域,从而定位车牌 """
# 进行闭运算
kernel = np.ones((5, 23), np.uint8)
img_closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
cv.imshow("Closing", img_closing)
# 进行开运算
img_opening1 = cv.morphologyEx(img_closing, cv.MORPH_OPEN, kernel)
cv.imshow("Opening_1", img_opening1)
# 再次进行开运算
kernel = np.ones((11, 6), np.uint8)
img_opening2 = cv.morphologyEx(img_opening1, cv.MORPH_OPEN, kernel)
return img_opening2
三次运算效果展示:
8. 定位车牌
先对上一步的图像 ‘img_opening2’ 检测轮廓,使用的是cv.findContours,该函数会返回图像的轮廓信息,然后对轮廓信息进行大小,高宽比,颜色筛选出最符合车牌的矩形轮廓,从而定位车牌区域。
def find_rectangle(contour):
""" 寻找矩形轮廓 """
y, x = [], []
for p in contour:
y.append(p[0][0])
x.append(p[0][1])
return [min(y), min(x), max(y), max(x)]
def locate_license(original, img):
""" 定位车牌号 """
_, contours, hierarchy = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
img_cont = original.copy()
img_cont = cv.drawContours(img_cont, contours, -1, (255, 0, 0), 6)
cv.imshow("Contours", img_cont)
# 计算轮廓面积及高宽比
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])
# 选出面积最大的五个区域
block = sorted(block, key=lambda b: b[1])[-5:]
# 使用颜色识别判断找出最像车牌的区域
maxweight, maxindex=0, -1
for i in range(len(block)):
# print('block', block[i])
if 2 <= block[i][2] <=4 and 1000 <= block[i][1] <= 20000: # 对矩形区域高宽比及面积进行限制
b = original[block[i][0][1]: block[i][0][3], block[i][0][0]: block[i][0][2]]
# BGR转HSV
hsv = cv.cvtColor(b, cv.COLOR_BGR2HSV)
lower = np.array([100, 50, 50])
upper = np.array([140, 255, 255])
# 根据阈值构建掩膜
mask = cv.inRange(hsv, lower, upper)
# 统计权值
w1 = 0
for m in mask:
w1 += m / 255
print(w1)
w2 = 0
for n in w1:
w2 += n
# 选出最大权值的区域
if w2 > maxweight:
maxindex = i
maxweight = w2
rect = block[maxindex][0]
return rect
在原图中框出车牌,效果展示:
四、字符分割
1. 去除上下边缘
将车牌区域从图像中裁剪出来,如下图:
车牌的上下边界通常都是不规范的,其中拉铆螺母的位置也会干扰字符分割,我们需要去除边缘没用的部分。
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 remove_upanddown_border(img):
""" 去除车牌上下无用的边缘部分,确定上下边界 """
plate_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, plate_binary_img = cv.threshold(plate_gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
row_histogram = np.sum(plate_binary_img, axis=1) # 数组的每一行求和
row_min = np.min(row_histogram)
row_average = np.sum(row_histogram) / plate_binary_img.shape[0]
row_threshold = (row_min + row_average) / 2
wave_peaks = find_waves(row_threshold, row_histogram)
# 挑选跨度最大的波峰
wave_span = 0.0
for wave_peak in wave_peaks:
span = wave_peak[1] - wave_peak[0]
if span > wave_span:
wave_span = span
selected_wave = wave_peak
plate_binary_img = plate_binary_img[selected_wave[0]:selected_wave[1], :]
#cv.imshow("plate_binary_img", plate_binary_img)
return plate_binary_img
效果展示:
2. 分割并保存字符
从左往右开始检测匹配字符,若宽度(end - start)大于5则认为是字符,将其裁剪并保存下来。
def find_end(start, arg, black, white, width, black_max, white_max):
end = start + 1
for m in range(start + 1, width - 1):
if (black[m] if arg else white[m]) > (0.95*black_max if arg else 0.95*white_max):
end = m
break
return end
def char_segmentation(thresh):
""" 分割字符 """
white, black = [], [] # list记录每一列的黑/白色像素总和
height, width = thresh.shape
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
while n < width - 2:
n += 1
# 判断是白底黑字还是黑底白字 0.05参数对应上面的0.95 可作调整
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
if end - start > 5 or end > (width * 3 / 7):
cropImg = thresh[0:height, start-1:end+1]
# 对分割出的数字、字母进行resize并保存
cropImg = cv.resize(cropImg, (34, 56))
cv.imwrite(save_path + '\\{}.bmp'.format(n), cropImg)
cv.imshow('Char_{}'.format(n), cropImg)
最终分割的字符保存至文件夹中,效果展示:
五、测试其它图片
测试另外两张汽车正视图,字符分割效果较好,展示如下:
六、总结
案例思路总结起来是先对图像做预处理,包含调整图像尺寸、转换灰度图、图像降噪、灰度拉伸、图像差分、图像二值化、canny边缘检测、形态学开闭运算,从而定位车牌区域,然后裁剪车牌,去除上下无用边缘部分,最后进行字符分割并将其保存至特定文件夹。
本案例对汽车正视图的车牌定位以及字符分割的效果较为成功,如果图像中车牌有一定的倾斜度以及透视变形,则其中还需对车牌进行倾斜矫正以及透视变换,图像处理也将更为复杂。后续的字符识别只要拥有足够的数据集训练,其过程也与手写数字识别案例一样简单。
七、附上完整代码
import cv2 as cv
import numpy as np
img_path = 'data\\img\\test_005.jpg'
save_path = 'Chars\\test'
def resize_img(img, max_size):
""" resize图像 """
h, w = img.shape[0:2]
scale = max_size / max(h, w)
img_resized = cv.resize(img, None, fx=scale, fy=scale,
interpolation=cv.INTER_CUBIC)
# print(img_resized.shape)
return img_resized
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)
img_stretched = img
return img_stretched
def absdiff(img):
""" 对开运算前后图像做差分 """
# 进行开运算,用来去除噪声
r = 15
h = w = r * 2 + 1
kernel = np.zeros((h, w), np.uint8)
cv.circle(kernel, (r, r), r, 1, -1)
# 开运算
img_opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
# 获取差分图
img_absdiff = cv.absdiff(img, img_opening)
cv.imshow("Opening", img_opening)
return img_absdiff
def binarization(img):
""" 二值化处理函数 """
maxi = float(img.max())
mini = float(img.min())
x = maxi - ((maxi - mini) / 2)
# 二值化, 返回阈值ret和二值化操作后的图像img_binary
ret, img_binary = cv.threshold(img, x, 255, cv.THRESH_BINARY)
# img_binary = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 5, 2)
# 返回二值化后的黑白图像
return img_binary
def canny(img):
""" canny边缘检测 """
img_canny = cv.Canny(img, img.shape[0], img.shape[1])
return img_canny
def opening_closing(img):
""" 开闭运算,保留车牌区域,消除其他区域,从而定位车牌 """
# 进行闭运算
kernel = np.ones((5, 23), np.uint8)
img_closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
cv.imshow("Closing", img_closing)
# 进行开运算
img_opening1 = cv.morphologyEx(img_closing, cv.MORPH_OPEN, kernel)
cv.imshow("Opening_1", img_opening1)
# 再次进行开运算
kernel = np.ones((11, 6), np.uint8)
img_opening2 = cv.morphologyEx(img_opening1, cv.MORPH_OPEN, kernel)
return img_opening2
def find_rectangle(contour):
""" 寻找矩形轮廓 """
y, x = [], []
for p in contour:
y.append(p[0][0])
x.append(p[0][1])
return [min(y), min(x), max(y), max(x)]
def locate_license(original, img):
""" 定位车牌号 """
_, contours, hierarchy = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
img_cont = original.copy()
img_cont = cv.drawContours(img_cont, contours, -1, (255, 0, 0), 6)
cv.imshow("Contours", img_cont)
# 计算轮廓面积及高宽比
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])
# 选出面积最大的五个区域
block = sorted(block, key=lambda bl: bl[1])[-5:]
# 使用颜色识别判断找出最像车牌的区域
maxweight, maxindex=0, -1
for i in range(len(block)):
# print('block', block[i])
if 2 <= block[i][2] <=4 and 1000 <= block[i][1] <= 20000: # 对矩形区域高宽比及面积进行限制
b = original[block[i][0][1]: block[i][0][3], block[i][0][0]: block[i][0][2]]
# BGR转HSV
hsv = cv.cvtColor(b, cv.COLOR_BGR2HSV)
lower = np.array([100, 50, 50])
upper = np.array([140, 255, 255])
# 根据阈值构建掩膜
mask = cv.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
rect = block[maxindex][0]
return rect
def preprocessing(img):
# resize图像至300 * 400
img_resized = resize_img(img, 400)
cv.imshow('Original', img_resized)
# 转灰度图
img_gray = cv.cvtColor(img_resized, cv.COLOR_BGR2GRAY)
cv.imshow('Gray', img_gray)
# 高斯滤波
# img_gaussian = cv.GaussianBlur(img_gray, (3,3), 0)
# cv.imshow("Gaussian_Blur", img_gaussian)
# 灰度拉伸,提升图像对比度
img_stretched = stretching(img_gray)
cv.imshow('Stretching', img_stretched)
# 差分开运算前后图像
img_absdiff = absdiff(img_stretched)
cv.imshow("Absdiff", img_absdiff)
# 图像二值化
img_binary = binarization(img_absdiff)
cv.imshow('Binarization', img_binary)
# 边缘检测
img_canny = canny(img_binary)
cv.imshow("Canny", img_canny)
# 开闭运算,保留车牌区域,消除其他区域
img_opening2 = opening_closing(img_canny)
cv.imshow("Opening_2", img_opening2)
# 定位车牌号所在矩形区域
rect = locate_license(img_resized, img_opening2)
print("rect:", rect)
# 框出并显示车牌
img_copy = img_resized.copy()
cv.rectangle(img_copy, (rect[0], rect[1]), (rect[2], rect[3]), (0, 255, 0), 2)
cv.imshow('License', img_copy)
return rect, img_resized
def cut_license(original, rect):
""" 裁剪车牌 """
license_img = original[rect[1]:rect[3], rect[0]:rect[2]]
return license_img
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 remove_upanddown_border(img):
""" 去除车牌上下无用的边缘部分,确定上下边界 """
plate_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, plate_binary_img = cv.threshold(plate_gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
row_histogram = np.sum(plate_binary_img, axis=1) # 数组的每一行求和
row_min = np.min(row_histogram)
row_average = np.sum(row_histogram) / plate_binary_img.shape[0]
row_threshold = (row_min + row_average) / 2
wave_peaks = find_waves(row_threshold, row_histogram)
# 挑选跨度最大的波峰
wave_span = 0.0
selected_wave = []
for wave_peak in wave_peaks:
span = wave_peak[1] - wave_peak[0]
if span > wave_span:
wave_span = span
selected_wave = wave_peak
plate_binary_img = plate_binary_img[selected_wave[0]:selected_wave[1], :]
return plate_binary_img
def find_end(start, arg, black, white, width, black_max, white_max):
end = start + 1
for m in range(start + 1, width - 1):
if (black[m] if arg else white[m]) > (0.95*black_max if arg else 0.95*white_max):
end = m
break
return end
def char_segmentation(thresh):
""" 分割字符 """
white, black = [], [] # list记录每一列的黑/白色像素总和
height, width = thresh.shape
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
while n < width - 2:
n += 1
# 判断是白底黑字还是黑底白字 0.05参数对应上面的0.95 可作调整
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
if end - start > 5 or end > (width * 3 / 7):
cropImg = thresh[0:height, start-1:end+1]
# 对分割出的数字、字母进行resize并保存
cropImg = cv.resize(cropImg, (34, 56))
cv.imwrite(save_path + '\\{}.bmp'.format(n), cropImg)
cv.imshow('Char_{}'.format(n), cropImg)
def main():
# 读取图像
image = cv.imread(img_path)
# 图像预处理,返回img_resized和定位的车牌矩形区域
rect, img_resized = preprocessing(image)
# 裁剪出车牌
license_img = cut_license(img_resized, rect)
cv.imshow('License', license_img)
# 去除车牌上下无用边缘
plate_b_img = remove_upanddown_border(license_img)
cv.imshow('plate_binary', plate_b_img)
# 字符分割,保存至文件夹
char_segmentation(plate_b_img)
cv.waitKey(0)
cv.destroyAllWindows()
if __name__ == '__main__':
main()