【计算机视觉】车牌分割定位识别 - 实践
目录
一、引言
车牌自动识别模块是现代社会智能交通系统(ITS)的重要组成部分,是图像处理和模式识别技术研究的热点,具有非常广泛的应用。车牌识别主要包括以下 4 个步骤:
・车牌图像处理;
・车牌定位处理;
・车牌字符处理;
・车牌字符识别。
本文通过对采集的车牌图像进行灰度变换、边缘检测、腐蚀及平滑等过程进行车牌图像预处理,并由此得到一种基于车牌颜色纹理特征的车牌定位方法,最终实现了车牌区域定位。车牌字符分割是为了方便后续对车牌字符进行匹配,从而对车牌进行识别。
本文用到的图片样例:
二、基本概述
车牌定位与字符识别技术以计算机图像处理、模式识别等技术为基础,通过对原图像进行预处理及边缘检测等过程来实现对车牌区域的定位,然后对车牌区域进行图像裁剪、归一化、字符分割及保存,最后将分割得到的字符图像与模板库的模板进行匹配识别,输出匹配结果。
在进行车牌识别时首先要正确分割车牌区域,为此人们已经提出了很多方法:使用霍夫变换检测直线来定位车牌边界获取车牌区域;使用灰度阈值分割、区域生长等方法进行区域分割;使用纹理特征分析技术检测车牌区域等。霍夫变换对图像噪声比较敏感,因此在检测车牌边界直线时容易受到车牌变形或噪声等因素的影响,具有较大的误检测概率。灰度阈值分割、区域生长等方法则比霍夫直线检测方法稳定,但当图像中包含某些与车牌灰度非常相似的区域时,便不再适用了。同理,纹理特征分析方法在遇到与车牌纹理特征相似的区域或其他干扰时,车牌定位的正确性也会受到影响。因此,仅采用单一的方法难以达到实际应用的要求。
车牌分割定位识别主要包括 4 个步骤:一是车牌图像处理;二是车牌定位处理;三是车牌字符处理;四是车牌字符识别。
三、车牌图像处理
原本的图像每个像素点都是 RGB 定义的,或者称为有 R/G/B 3 个通道。在这种情况下,很难区分谁是背景,谁是字符,所以需要对图像进行一些处理,把每个 RGB 定义的像素点都转化成一个 bit 位(即 0-1 代码),具体方法如下。
(一)图像灰度化
RGB 图像根据三基色原理,每种颜色都可以由红、绿、蓝 3 种基色按不同的比例构成,所以车牌图像的每个像素都由 3 个数值来指定红、绿、蓝的颜色分量。灰度图像实际上是一个数据矩阵 I,该矩阵中每个元素的数值都代表一定范围内的亮度值,矩阵 I 可以是整型、双精度,通常 0 代表黑色,255 代表白色。
在 RGB 模型中,如果 R=G=B,则表示一种灰度颜色。其中 R=G=B 的值叫作灰度值,由彩色转为灰度的过程称为图像灰度化处理。因此,灰度图像是指只有强度信息而没有颜色信息的图像。一般而言,可采用加权平均值法对原始 RGB 图像进行灰度化处理,该方法的主要思想是,从原始图像中取 R、G、B 各层的像素值经加权求和得到灰度图的亮度值。在现实生活中,人眼对绿色(G)敏感度最高,对红色(R)敏感度次之,对蓝色(B)敏感度最低,因此为了选择合适的权值对象输出合理的灰度图像,权值系数应该满足 G>R>B。实验和理论证明,当 R、G、B 的权值系数分别选择 0.299、0.587 和 0.114 时,能够得到最适合人眼观察的灰度图像。
(二)二值化
灰度图像二值化在图像处理的过程中有着很重要的作用,图像二值化处理不仅能使数据量大幅减少,还能突出图像的目标轮廓,便于进行后续的图像处理和分析。对车牌灰度图像而言,所谓的二值化处理就是将其像素点的灰度值设置为 0 或 255,从而让整幅图片呈现黑白效果。因此,对灰度图像进行适当的阈值选取,可以在图像二值化的过程中保留某些关键的图像特征。在车牌图像二值化的过程中,灰度大于或等于阈值的像素点被判定为目标区域,其灰度值用 255 表示;否则这些像素点被判定为背景或噪声而排除在目标区域以外,其灰度值用 0 表示。
图像二值化是指在整幅图像内仅保留黑、白二值的数值矩阵,每个像素都取两个离散数值(0 或 1)之一,其中 0 代表黑色,1 代表白色。在车牌图像处理系统中,进行图像二值化的关键是选择合适的阈值,使得车牌字符与背景能够得到有效分割。采用不同的阈值设定方法对车牌图像进行处理也会产生不同的二值化处理结果:阈值设置得过小,则容易误分割,产生噪声,影响二值变换的准确度;阈值设置得过大,则容易过分割,降低分辨率,使噪声信号被视为噪声而被过滤,造成二值变换的目标损失。
(三) 边缘检测
边缘是指图像局部亮度变化最显著的部分,主要存在于目标与目标、目标与背景、区域与区域、颜色与颜色之间,是图像分割、纹理特征提取和形状特征提取等图像分析的重要步骤之一。在车牌识别系统中,边缘提取对于车牌位置的检测有很重要的作用,常用的边缘检测算子有很多,如 Roberts、Sobel、Prewitt、Log 及 Canny 等。据实验分析,Canny 算子于边缘的检测相对精确,能更多地保留车牌区域的特征信息。Canny 算子在边缘检测中有以下明显的判别指标。
1.信噪比
信噪比越大,提取的边缘质量越高。信噪比(SNR)的定义为:
式中,G(x)代表边缘函数,h(x)代表宽度为 W 的滤波器的脉冲响应,σ代表高斯噪声的均方差。
2.定位精度
边缘的定位精度 L 的定义为:
式中,G′(x)、h′(x)分别是G(x)、h(x)的导数。L 越大,定位精度越高。
3.单边缘响应
为了保证单边缘只有一个响应,检测算子的脉冲响应导数的零交叉点的平均距离D(f′)应满足:
式中,h′′(x)是h(x)的二阶导数。
以上述指标和准则为前提,采用 Canny 算子的边缘检测算法步骤为:
(1)预处理。采用高斯滤波器进行图像平均。
(2)梯度计算。采用一阶偏导的有限差分来计算梯度,获取其幅值和方向。
(3)梯度处理。采用非极大值抑制方法对梯度幅值进行处理。
(4)边缘提取。采用双阈值算法检测和连接边缘。
(四)形态学运算
数学形态图像处理的基本运算有 4 个:膨胀(或扩张)、腐蚀(侵蚀)、开启和闭合。二值形态学中的运算对象是集合,通常给出了一个图像集合和一个结构元素集合,利用结构元素对图像集合进行形态学操作。
膨胀运算符号为⊕,图像集合 A 用结构元素 B 来膨胀,记作A⊕B,定义为:
式中,表示 B 的映像,即与 B 关于原点对称的集合。因此,用 B 对 A 进行膨胀的运算过程如下:首先做 B 关于原点的映射得到映像,再将其平移 x,当 A 与 B 映像的交集不为空时,B 的原点就是膨胀集合的像素。腐蚀运算的符号是Θ,图像集合 A 用结构元素 B 来腐蚀,记作AΘB,定义为:
因此,A 用 B 腐蚀的结果是所有满足将 B 平移 x 后 B 仍旧被全部包含在 A 中的集合,也就是结构元素 B 经过平移后全部被包含在集合 A 中原点所组成的集合中。
在一般情况下,由于受到噪声的影响,车牌图像在阈值化后得到的边界往往是不平滑的,在目标区域内部也有一些噪声孔洞,在背景区域会散布一些小的噪声干扰。通过连续的开运算和闭运算可以有效地改善这种情况,有时甚至需要经过多次腐蚀之后再加上相同次数的膨胀,才可以产生比较好的效果。
(五)滤波处理
图像滤波能够在尽量保留图像细节特征的条件下对噪声进行抑制,是图像预处理中常用的操作之一,其处理效果的好坏将直接影响后续的图像分割和识别的有效性和稳定性。
均值滤波也被称为线性滤波,采用的主要方法为邻域平均法。该方法对滤波像素的位置(x,y)选择一个模板,该模板由其邻近的若干像素组成,求出模板中所包含像素的均值,再把该均值赋予当前像素点(x,y),将其作为处理后的图像在该点上的灰度值g(x,y),即
M 为该模板中包含当前像素在内的像素总个数。
在一般情况下,在研究目标车牌时所出现的图像噪声都是无用的信息,而且会对目标车牌的检测和识别造成干扰,极大地降低了图像质量,影响图像增强、图像分割、特征提取、图像识别等后继工作的进行。因此,在程序实现中为了能有效地进行图像去噪,并且能有效地保存目标车牌的形状、大小及特定的几何和拓扑结构特征,需要对车牌进行均值滤波去噪处理。
四、定位处理
车牌区域具有明显的特点,因此根据车牌底色、字色等有关知识,可采用彩色像素点统计的方法分割出合理的车牌区域。下面以蓝底白字的普通车牌为例说明彩色像素点统计的分割方法。假设经数码相机或 CCD 摄像头拍摄采集到了包含车牌的 RGB 彩色图像,将水平方向记为 y,将垂直方向记为 x,则:首先,确定车牌底色 RGB 各分量分别对应的颜色范围;其次,在 y 方向统计此颜色范围内的像素点数量,设定合理的阈值,确定车牌在 y 方向的合理区域;再次,在分割出的 y 方向区域内统计 x 方向上此颜色范围内的像素点数量,设定合理的阈值进行定位;最后,根据 x、y 方向的范围来确定车牌区域,实现定位。
五、字符处理
(一)阈值分割
阈值分割算法是图像分割中应用场景最多的算法之一。简单地说,对灰度图像进行阈值分割就是先确定一个处于图像灰度取值范围内的阈值,然后将图像中各个像素的灰度值与这个阈值进行比较,并根据比较的结果将对应的像素划分为两类:像素灰度大于阈值的一类和像素灰度小于阈值的另一类,灰度值等于阈值的像素可以被归入这两类之一。分割后的两类像素一般分属图像的两个不同区域,所以对像素根据阈值分类达到了区域分割的目标。由此可见,阈值分割算法主要有以下两个步骤。
(1)确定需要分割的阈值。
(2)将阈值与像素点的灰度值进行比较,以分割图像的像素。
在以上步骤中,如果能确定一个合适的阈值,就可以准确地将图像进行分割。在阈值确定后,将阈值与像素点的灰度值进行比较和分割,就可对各像素点并行处理,通过分割的结果直接得到目标图像区域。一般选用最常用的图像双峰灰度模型进行阈值分割:假设图像目标和背景直方图具有单峰分布的特征,且处于目标和背景内部相邻像素间的灰度值是高度相关的,但处于目标和背景交界处两边的像素在灰度值上有很大的差别。如果一幅图像满足这些条件,则它的灰度直方图基本上可看作由分别对应目标和背景的两个单峰构成。如果这两个单峰部分的大小接近且均值相距足够远,两部分的均方差也足够小,则直方图在整体上呈现较明显的双峰现象。同理,如果在图像中有多个呈现单峰灰度分布的目标,则直方图在整体上可能呈现较明显的多峰现象。因此,对这类图像可用取多级阈值的方法来得到较好的分割效果。
如果要将图像中不同灰度的像素分成多个类,则需要选择一系列的阈值将像素分到合适的类别中。如果只用一个阈值分割,则称为单阈值分割算法;如果用多个阈值分割,则称为多阈值分割算法。因此,单阈值分割可看作多阈值分割的特例,许多单阈值分割算法可被推广到多阈值分割算法中。同理,在某些场景下也可将多阈值分割问题转化为一系列的单阈值分割问题来解决。以单阈值分割算法为例,对一幅原始图像f(x,y)取单阈值 T 分割得到二值图像可定义为:
这样得到的g(x,y)是一幅二值图像。
在一般的多阈值分割情况下,阈值分割输出的图像可表示为:
式中,是一系列分割阈值,k 表示赋予分割后图像的各个区域的不同标号。
(二)阈值化分割
车牌字符图像的分割目的是将车牌的整体区域分割成单字符区域,以便后续识别。其分割难点在于受字符与噪声粘连以及字符断裂等因素的影响。均值滤波是典型的线性滤波算法,指在图像上对图像进行模板移动扫描,该模板包括像素周围的近邻区域,通过模板与命中的近邻区域像素的平均值来代替原来的像素值,实现去噪的效果。为了从车牌图像中直接提取目标字符,最常用的方法是设定一个阈值 T,用 T 将图像的像素分成两部分:大于 T 像素集合和小于 T 的像素集合,得到二值化图像。
(三)归一化处理
字符图像归一化是简化计算的方式之一,在车牌字符分割后往往会出现大小不一致的情况,因此可采用基于图像放缩的归一化处理方式将字符图像进行大小放缩,以得到统一大小的字符像素,便于后续的字符识别。
六、字符识别
车牌字符识别方法基于模式识别理论,常用的有以下几类。
1.结构识别
结构识别主要由识别及分析两部分组成:识别部分主要包括预处理、基元抽取(包括基元和子图像之间的关系)和特征分析;分析部分包括基元选择及结构推理。
2.统计识别
统计识别用于确定已知样本所属的类别,以数学上的决策论为理论基础,并由此建立统计学识别模型。其基本方式是对所研究的图像实施大量的统计分析,寻找规律性认知,提取反映图像本质的特征并进行识别。
3.BP 神经网络
BP 神经网络以 B 神经网络模型为基础,属于误差后向传播的神经网络,是神经网络中使用最广泛的一类,采用了输入层、隐藏层和输出层 3 层网络的层间全互联方式,具有较高的运行效率和识别准确率。
4.模板匹配
模板匹配是数字图像处理中最常用的识别方法之一,通过建立已知的模式库,再将其应用到输入模式中寻找与之最佳匹配模式的处理步骤,得到对应的识别结果,具有很高的运行效率。基于模板匹配的字符识别方法的过程为:
(1)建库。建立已标准化的字符模板库。
(2)对比。将归一化的字符图像与模板库中的字符进行对比,在实际实验中充分考虑了我国普通小汽车牌照的特点,即:第 1 位字符是汉字,分别对应各个省的简称;第 2 位是 A~Z 的字母;后 5 位则是数字和字母的混合搭配。因此,为了提高对比的效率和准确性,分别对第 1 位、第 2 位和后 5 位字符进行识别。
(3)输出。在识别完成后输出所得到的车牌字符结果。其流程如图 5-3 所示。
(一)模板匹配的字符识别
模板匹配是图像识别方法中最具有代表性的基本方法之一,该方法首先根据已知条件建立模板库T(i,j),然后从待识别的图像或图像区域f(i,j)中提取若干特征量与T(i,j)相应的特征量进行对比,分别计算它们之间归一化的互相关量。其中,互相关量最大的一个表示二者的相似程度最高,可将图像划到该类别。此外,也可以计算图像与模板特征量之间的距离,采用最小距离法判定所属类别。但是,在实际情况下,用于匹配的图像的采集成像条件往往存在差异,可能会产生较大的噪声干扰。此外,图像经过预处理和归一化处理等步骤,其灰度或像素点的位置也可能会发生改变,进而影响识别效果。因此,在实际设计模板时,需要保持各区域形状的固有特点,突出不同区域的差别,并充分考虑处理过程可能会引起的噪声和位移等因素,按照基于图像不变的特性所对应的特征向量来构建模板,提高识别系统的稳定性。
(二)字符识别车牌经典应用
车牌自动识别系统以车牌的动态视频或静态图像作为输入,通过牌照颜色、牌照号码等关键内容的自动识别来提取车牌的详细信息。某些车牌识别系统具有通过视频图像判断车辆驶入监控区域的功能,一般被称为视频车辆检测,被广泛应用于道路车流量统计等方面。在现实生活中,一个完整的车牌识别系统应包括车辆检测、图像采集、车牌定位、车牌识别等模块。
车牌信息是一辆汽车独一无二的标识,所以车牌识别技术可以作为辨识一辆车最为有效的方法。车牌识别系统包括汽车图像的输入、车牌图像的预处理、车牌定位和字符检测、车牌字符的分割和车牌字符识别等部分。
七、Python代码展示字符分割和识别
(一)利用字符分割实现车牌的分割处理
1.实现步骤
准备工作:创建
segmented_chars
目录,用于保存分割出的字符图片;读取输入图像car.png
并检查是否读取成功,确保后续处理有有效图像数据。图像预处理:
- 将彩色图像转为灰度图:减少颜色通道干扰,简化后续处理。
- 灰度图二值化:使用
THRESH_BINARY_INV
(反相二值化)将图像转为黑白对比图(像素值仅 0 或 255),突出字符与背景的差异,并保存二值化结果。
像素分布统计:遍历二值化图像的每一列,统计每列的白色像素(255)和黑色像素(0)数量,记录两种像素的最大值。这一步是为了判断车牌底色(黑底白字 / 白底黑字)。
判断车牌底色:通过比较黑色像素最大值和白色像素最大值,判断车牌类型:若黑色像素更多,则为 “黑底白字”;否则为 “白底黑字”。这决定了后续以哪种像素作为字符的判断依据。
字符分割:
- 定义
find_end
函数:根据底色,从字符起始列向后寻找字符结束列(当有效像素占比超过 95% 时判定为结束)。 - 遍历图像列:当某列有效像素(黑底白字看白色像素,白底黑字看黑色像素)占比超过 5% 时,判定为字符起始列;调用
find_end
找到结束列,过滤过窄的噪声区域后,提取字符区域并保存到segmented_chars
目录,同时显示分割结果。
- 定义
收尾工作:关闭所有图像显示窗口,释放资源。
整体流程通过预处理增强字符与背景的对比,再基于列像素分布实现字符的自动分割,适用于简单场景下的车牌字符提取。
2.Python完整代码展示
import cv2
import os
# 创建保存分割结果的目录
if not os.path.exists('segmented_chars'):
os.makedirs('segmented_chars')
# 读取图像并检查是否成功
img = cv2.imread("car.png") # 读取图片
if img is None:
print("错误:无法读取图像,请检查文件路径和名称是否正确!")
exit()
# 转为灰度图并显示
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转为灰度图
cv2.imshow('gray', img_gray)
cv2.waitKey(0)
# 灰度图二值化(使用返回值接收结果,避免潜在的内存问题)
ret, img_thre = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY_INV)
cv2.imshow('threshold', img_thre)
cv2.waitKey(0)
# 保存二值化后的黑白图
cv2.imwrite('thre_res.png', img_thre)
print("二值化结果已保存为 'thre_res.png'")
# 分割字符:统计每列黑白像素
white = [] # 记录每列白色像素数
black = [] # 记录每列黑色像素数
height, width = img_thre.shape[:2] # 获取图像尺寸(简化写法)
white_max = 0
black_max = 0
# 计算每列的黑白像素总数
for i in range(width):
s = 0 # 白色像素计数(255)
t = 0 # 黑色像素计数(0)
for j in range(height):
if img_thre[j][i] == 255:
s += 1
else: # 非白色即黑色(二值化图像只有0和255)
t += 1
white_max = max(white_max, s)
black_max = max(black_max, t)
white.append(s)
black.append(t)
# print(f"列 {i}:白色像素 {s},黑色像素 {t}")
# 判断车牌底色(黑底白字/白底黑字)
arg = False # False=白底黑字;True=黑底白字
if black_max > white_max:
arg = True
print(f"车牌底色判断:{'黑底白字' if arg else '白底黑字'}")
# 定义“寻找字符结束列”的函数
def find_end(start_):
end_ = start_ + 1
# 遍历从start_+1到width-1的列,寻找字符结束位置
for m in range(start_ + 1, width):
# 根据底色判断有效像素阈值
if (black[m] if arg else white[m]) > (0.95 * black_max if arg else 0.95 * white_max):
end_ = m
break
return end_
# 遍历图像列,分割字符
n = 1 # 字符计数
char_count = 0 # 有效字符计数器
while n < width - 1: # 避免索引越界
n += 1
# 判断当前列是否为字符起始列
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)
n = end # 跳过当前字符区域,继续下一个
# 过滤过窄的区域(可能是噪声)
if end - start > 5:
char_count += 1
# 提取字符区域(避免索引从1开始导致顶部裁剪)
cj = img_thre[0:height, start:end] # 从第0行开始,保留完整字符
# 显示分割结果
cv2.imshow(f'char_{char_count}', cj)
# 保存分割的字符
save_path = f'segmented_chars/char_{char_count}.png'
cv2.imwrite(save_path, cj)
print(f"已保存分割字符:{save_path}")
cv2.waitKey(0)
# 关闭所有窗口,释放资源
cv2.destroyAllWindows()
3.程序运行结果展示
(二)利用字符识别车牌
1.实现步骤
1. 初始化与交互准备
- 定义全局变量用于存储鼠标交互时的矩形框参数,实现手动调整车牌区域的功能。
- 编写鼠标回调函数
draw_rectangle
,支持通过鼠标拖动绘制、调整矩形框,用于后续手动修正车牌定位。
2. 车牌预处理与定位
- 对比度增强:通过全局对比度拉伸和 CLAHE 局部增强,强化车牌区域字符与背景的差异。
- 二值化:使用自适应高斯阈值将灰度图转为黑白二值图,突出轮廓。
- 车牌定位:结合边缘检测(Canny)和形态学操作(开运算、闭运算)提取轮廓,再通过车牌的宽高比(2.0-4.0)和颜色特征(蓝、黄、白底色)筛选出最佳车牌区域。
3. 手动调整车牌区域
- 显示自动定位的车牌框,允许用户通过鼠标拖动调整框选范围,按 Enter 确认,提高定位准确性。
4. 车牌裁剪与二值化优化
- 根据调整后的矩形框裁剪出车牌区域,扩大边界 5 像素避免字符被截断。
- 对裁剪后的车牌再次进行灰度化、高斯去噪和二值化(OTSU 自适应阈值),并通过形态学操作(开运算去噪、膨胀连接断裂字符)优化图像。
5. 字符分割(核心步骤)
- 像素分布统计:统计二值化车牌中每列的白色和黑色像素数量,确定两种像素的最大值。
- 底色自动识别:通过比较白色 / 黑色像素最大值,判断车牌是 “黑底白字” 还是 “白底黑字”,确定字符对应的像素颜色。
- 动态分割字符:基于字符像素列分布,用低阈值(1% 字符像素最大值)检测字符起始列,用高阈值(80% 字符像素最大值)检测结束列,过滤过窄区域(噪声),提取并保存单个字符。
- 容错机制:若首次分割失败(0 个字符),自动反转底色逻辑重新尝试分割。
6. 结果输出
- 将分割出的字符保存到
result
目录,显示每个字符并输出总分割数量,最后关闭所有窗口释放资源。
整体流程结合了自动处理与人工交互,通过多步预处理和动态逻辑适配不同车牌类型,最终实现字符的有效分割。
2.Python代码完整展示
import cv2
import numpy as np
import os
from skimage import io
import matplotlib.pyplot as plt
# 全局变量用于手动调整方框
drawing = False # 是否正在绘制
start_x, start_y = -1, -1
current_rect = [0, 0, 0, 0] # 存储当前矩形 [x1, y1, x2, y2]
temp_rect = [0, 0, 0, 0] # 临时矩形用于拖动显示
image_copy = None # 图像副本用于绘制
# 鼠标回调函数:处理矩形框的绘制和调整
def draw_rectangle(event, x, y, flags, param):
global start_x, start_y, drawing, current_rect, temp_rect, image_copy
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
start_x, start_y = x, y
temp_rect = [x, y, x, y]
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
temp_rect[2], temp_rect[3] = x, y
img = image_copy.copy()
cv2.rectangle(img, (temp_rect[0], temp_rect[1]),
(temp_rect[2], temp_rect[3]), (0, 0, 255), 2)
cv2.imshow('手动调整车牌框 (拖动鼠标调整,按Enter确认)', img)
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
temp_rect[2], temp_rect[3] = x, y
current_rect[0] = min(temp_rect[0], temp_rect[2])
current_rect[1] = min(temp_rect[1], temp_rect[3])
current_rect[2] = max(temp_rect[0], temp_rect[2])
current_rect[3] = max(temp_rect[1], temp_rect[3])
img = image_copy.copy()
cv2.rectangle(img, (current_rect[0], current_rect[1]),
(current_rect[2], current_rect[3]), (0, 255, 0), 2)
cv2.imshow('手动调整车牌框 (拖动鼠标调整,按Enter确认)', img)
# 1. 增强对比度(强化字符与背景差异)
def enhance_contrast(img):
# 先进行全局对比度拉伸
min_val = np.min(img)
max_val = np.max(img)
stretched = ((img - min_val) / (max_val - min_val) * 255).astype(np.uint8)
# 再用CLAHE增强局部对比度
clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8, 8))
return clahe.apply(stretched)
# 2. 二值化(自适应阈值,保留更多细节)
def dobinaryzation(img):
return cv2.adaptiveThreshold(
img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# 3. 提取轮廓外接矩形
def find_rectangle(contour):
y_coords = [p[0][0] for p in contour]
x_coords = [p[0][1] for p in contour]
return [min(y_coords), min(x_coords), max(y_coords), max(x_coords)]
# 4. 定位车牌(多颜色+形状筛选)
def locate_license(edge_img, rgb_img):
contours, _ = cv2.findContours(edge_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
candidates = []
for c in contours:
rect = find_rectangle(c)
w, h = rect[2] - rect[0], rect[3] - rect[1]
if h == 0:
continue
if 2.0 < (w / h) < 4.0:
area = w * h
candidates.append([rect, area, w / h])
if not candidates:
return [0, 0, rgb_img.shape[1], rgb_img.shape[0]]
candidates = sorted(candidates, key=lambda x: x[1], reverse=True)[:3]
color_ranges = {
"blue": (np.array([100, 50, 50]), np.array([140, 255, 255])),
"yellow": (np.array([20, 100, 100]), np.array([30, 255, 255])),
"white": (np.array([0, 0, 200]), np.array([180, 30, 255]))
}
best_rect = candidates[0][0]
max_color_ratio = 0.3
for rect, _, _ in candidates:
x1, y1, x2, y2 = rect
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(rgb_img.shape[1], x2), min(rgb_img.shape[0], y2)
roi = rgb_img[y1:y2, x1:x2]
if roi.size == 0:
continue
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
for _, (lower, upper) in color_ranges.items():
mask = cv2.inRange(hsv_roi, lower, upper)
color_ratio = np.sum(mask == 255) / (roi.shape[0] * roi.shape[1] + 1e-6)
if color_ratio > max_color_ratio:
max_color_ratio = color_ratio
best_rect = rect
return best_rect
# 5. 车牌预处理(缩放+增强+形态学)
def find_license(img):
target_w = 400
scale = target_w / img.shape[1]
target_h = int(img.shape[0] * scale)
img_resized = cv2.resize(img, (target_w, target_h), interpolation=cv2.INTER_CUBIC)
gray_img = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
stretched = enhance_contrast(gray_img)
kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
opening = cv2.morphologyEx(stretched, cv2.MORPH_OPEN, kernel_open)
diff = cv2.absdiff(stretched, opening)
binary = dobinaryzation(diff)
canny = cv2.Canny(binary, 50, 150)
kernel_close = np.ones((5, 19), np.uint8)
closed = cv2.morphologyEx(canny, cv2.MORPH_CLOSE, kernel_close)
kernel_final = np.ones((11, 5), np.uint8)
edge_img = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel_final)
rect = locate_license(edge_img, img_resized)
return rect, img_resized
# 6. 裁剪车牌
def cut_license(rgb_img, rect):
x1, y1, x2, y2 = rect
x1 = max(0, x1 - 5)
y1 = max(0, y1 - 5)
x2 = min(rgb_img.shape[1], x2 + 5)
y2 = min(rgb_img.shape[0], y2 + 5)
return rgb_img[y1:y2, x1:x2]
# 7. 车牌二值化(自适应,不强制反转)
def deal_license(licenseimg):
gray_img = cv2.cvtColor(licenseimg, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray_img, (3, 3), 0)
# 不强制反转,让二值化结果自适应
_, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 形态学优化
kernel = np.ones((2, 2), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1) # 去噪
thresh = cv2.dilate(thresh, kernel, iterations=1) # 连接字符
return thresh
# 主函数
if __name__ == '__main__':
img_path = 'car.png'
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
if img is None:
print(f"错误:无法读取图像 {img_path},请检查路径!")
exit()
# 1. 预处理+车牌定位
rect, afterimg = find_license(img)
current_rect = rect.copy()
# 2. 手动调整车牌框
image_copy = afterimg.copy()
cv2.namedWindow('手动调整车牌框 (拖动鼠标调整,按Enter确认)')
cv2.setMouseCallback('手动调整车牌框 (拖动鼠标调整,按Enter确认)', draw_rectangle)
cv2.rectangle(image_copy, (current_rect[0], current_rect[1]),
(current_rect[2], current_rect[3]), (0, 255, 0), 2)
cv2.imshow('手动调整车牌框 (拖动鼠标调整,按Enter确认)', image_copy)
print("请拖动鼠标调整车牌框,调整完成后按Enter键确认")
while True:
key = cv2.waitKey(0)
if key == 13:
cv2.destroyWindow('手动调整车牌框 (拖动鼠标调整,按Enter确认)')
break
# 3. 显示调整后的定位
adjusted_img = afterimg.copy()
cv2.rectangle(adjusted_img, (current_rect[0], current_rect[1]),
(current_rect[2], current_rect[3]), (0, 0, 255), 2)
cv2.imshow('调整后的车牌定位', adjusted_img)
cv2.waitKey(0)
# 4. 裁剪车牌
cutimg = cut_license(afterimg, current_rect)
if cutimg.size == 0:
print("警告:未检测到有效车牌区域!")
exit()
cv2.imshow('裁剪后的车牌', cutimg)
cv2.waitKey(0)
# 5. 车牌二值化(不强制反转,保留原始对比)
thresh = deal_license(cutimg)
cv2.imshow('二值化车牌', thresh)
cv2.waitKey(0)
# 6. 字符分割(核心优化:自动识别底色并调整逻辑)
height, width = thresh.shape
# 统计字符和背景的像素分布
white = np.sum(thresh == 255, axis=0) # 白色列像素数
black = np.sum(thresh == 0, axis=0) # 黑色列像素数
white_max, black_max = white.max(), black.max()
print(f"白色像素最大值: {white_max}, 黑色像素最大值: {black_max}")
# 自动判断底色:字符颜色是像素值更高的颜色(更密集)
# 例如:白底黑字时,黑色是字符(black_max > white_max)
# 黑底白字时,白色是字符(white_max > black_max)
is_white_char = white_max > black_max
char_pixels = white if is_white_char else black
char_max = white_max if is_white_char else black_max
print(f"自动识别:{'黑底白字' if is_white_char else '白底黑字'}(字符为{'白色' if is_white_char else '黑色'})")
# 可视化像素分布
plt.figure(figsize=(10, 3))
plt.plot(white, label='White Pixels')
plt.plot(black, label='Black Pixels')
plt.title('Column-wise Pixel Distribution')
plt.xlabel('Column Index')
plt.ylabel('Pixel Count')
plt.legend()
plt.savefig('pixel_distribution.png')
plt.show()
# 创建保存目录
save_dir = 'result'
os.makedirs(save_dir, exist_ok=True)
# 分割字符(根据实际底色动态调整)
char_idx = 1
n = 0
while n < width:
n += 1
if n >= width:
break
# 字符起始阈值:字符像素的1%(超灵敏)
threshold = 0.01 * char_max if char_max != 0 else 1
current_col = char_pixels[n] # 使用字符像素列检测
if current_col > threshold:
start = n
end = start + 1
count = 0
# 寻找字符结束位置
for m in range(start + 1, width):
col_val = char_pixels[m]
# 结束阈值:字符像素的80%
if col_val > 0.8 * char_max:
count += 1
if count >= 1: # 连续1列即可判定结束
end = m
break
else:
count = 0
n = end # 跳过当前字符
# 宽度限制:适应不同字符(中文宽,字母数字窄)
if 3 < (end - start) < 100:
# 提取字符区域
cj = thresh[0:height, start:end]
# 统一字符尺寸并居中
cj_resized = cv2.resize(cj, (40, 80))
save_path = os.path.join(save_dir, f"char_{char_idx}.png")
io.imsave(save_path, cj_resized)
cv2.imshow(f'字符_{char_idx}', cj_resized)
print(f"已保存字符:{save_path},宽度:{end - start}")
char_idx += 1
cv2.waitKey(100)
# 容错机制:如果未检测到字符,尝试反转逻辑
if char_idx == 1:
print("首次分割失败,尝试反转逻辑...")
is_white_char = not is_white_char
char_pixels = white if is_white_char else black
char_max = white_max if is_white_char else black_max
n = 0
while n < width:
n += 1
if n >= width:
continue
threshold = 0.01 * char_max if char_max != 0 else 1
current_col = char_pixels[n]
if current_col > threshold:
start = n
end = start + 1
count = 0
for m in range(start + 1, width):
col_val = char_pixels[m]
if col_val > 0.8 * char_max:
count += 1
if count >= 1:
end = m
break
else:
count = 0
n = end
if 3 < (end - start) < 100:
cj = thresh[0:height, start:end]
cj_resized = cv2.resize(cj, (40, 80))
save_path = os.path.join(save_dir, f"char_{char_idx}.png")
io.imsave(save_path, cj_resized)
cv2.imshow(f'字符_{char_idx}', cj_resized)
print(f"已保存字符:{save_path},宽度:{end - start}")
char_idx += 1
cv2.waitKey(100)
print(f"共分割出 {char_idx - 1} 个字符")
cv2.destroyAllWindows()
3.程序运行结果展示
八、总结
本文研究了车牌自动识别技术,重点介绍了车牌定位与字符识别的关键步骤。系统通过图像灰度化、二值化、边缘检测等预处理方法优化车牌图像,结合颜色纹理特征实现车牌定位。在字符分割阶段,采用基于像素分布的动态阈值法,自动识别车牌底色并分割字符。最后通过模板匹配完成字符识别。研究还提供了Python实现代码,包括图像预处理、车牌定位、字符分割等功能模块,展示了完整的车牌识别流程。实验结果表明,该方法能有效处理不同背景和光照条件下的车牌图像,为智能交通系统提供了可靠的车牌识别解决方案。