基于模板匹配的车牌识别:从原理到代码实战
2026/9/13 13:32:05 网站建设 项目流程

简介:基于模板匹配的车牌识别工程包,面向计算机视觉初学者与图像处理爱好者,帮助理解如何通过图片匹配定位车牌区域、再借助模板匹配完成字符识别。包内共135个文件,含67个bmp车牌字符模板、66个jpg实景车牌图片,以及1个MATLAB脚本(.m)和1个数据库文件(.db),压缩包整体仅192KB,轻量且易于下载调试。已有140人学习下载,适合用于课程设计或入门实践。除了核心匹配代码,还提供了沪、宁、川、陕等不同省份车牌字符模板,可直接替换或扩展模板库;配合jpg样例图可快速验证灰度化、二值化、边缘检测等预处理效果,并进一步梳理字符分割与OCR识别的完整链路。通过学习这份工程,读者能掌握模板匹配的相似度计算与阈值设定思路,为后续更复杂的深度学习车牌识别打下基础。

1. 车牌识别模板匹配不是过时方案

现在聊车牌识别,很多文章默认跳到 YOLO、LPRNet 这类开源车牌检测识别模型,仿佛只有深度学习才能解决字符识别。但如果你手头只有一个 main.zip,里面是几张 BMP 模板和字符图片,这个包含模板匹配方案的小项目反而是理解整个识别链路最直接的入口。模板匹配是图片匹配里最简单的一种实现,它用已知的车牌字符模板在目标图像里滑窗搜索,通过归一化互相关这样的相似度算法找出最接近的位置。main.zip 中的 O.bmp、I.bmp 以及豫、辽、新、藏等省级简称模板,组成了一个最原始但完整的车牌字符识别示例。它适合两类人:一是刚接触图像处理、想搞懂像素级匹配原理的开发者;二是需要在离线、相机角度固定的闸机场景中快速验证识别效果的人。没有 GPU,不依赖深度学习框架,也能跑出可用的识别结果。

2. 模板匹配原理与相似度计算方法

2.1 从滑窗搜索到归一化互相关

模板匹配的核心操作是滑动窗口。假设模板 T 大小为 w×h,搜索图 I 大小为 W×H,模板从搜索图的左上角开始,以步长 1 向右下方向滑动。每移动到一个位置,就截取与模板同尺寸的子图 I(x,y),计算两者之间的相似度。所有位置计算完成后,会得到一张大小为 (W-w+1)×(H-h+1) 的响应图,响应值最大的点,就是模板最可能出现的位置。这个过程没有做任何特征提取,纯粹在像素灰度层面比较,因此它更接近一种“穷举式搜索”。

实际计算相似度时,OpenCV 的matchTemplate提供了多种算法。最常用的是归一化相关系数TM_CCOEFF_NORMED,计算公式本质上是对模板和子图各自减去均值后再做归一化相关。用公式表达就是:

R(x,y) = Σ(T'(x',y') · I'(x+x',y+y')) / sqrt(ΣT'(x',y')² · ΣI'(x+x',y+y')²)

其中 T' 是模板减去模板均值,I' 是子图减去子图均值。这个公式让结果对整体亮度偏移不敏感,所以模板匹配在光照变化明显的车牌场景中仍然可用。如果直接使用TM_SQDIFF,灰度均值一变,匹配值就会整体漂移,实际项目中很少单独依赖它。

OpenCV 中一次匹配调用非常简单:

import cv2 result = cv2.matchTemplate(search_img, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

逻辑说明:search_img是待搜索图像,template是模板图,两者必须是灰度图或相同通道数的彩色图。cv2.minMaxLoc返回响应图的最值和位置。对于TM_CCOEFF_NORMEDmax_loc就是最佳匹配位置。参数上需要注意的是,search_img的宽高必须大于等于模板,否则函数会直接报错。

2.2 为什么车牌字符匹配优先选 TM_CCOEFF_NORMED

在车牌识别场景中,字符区域往往先被二值化,但二值化后的图仍然会残留孤立的噪声点。如果使用直接计算灰度距离的方法,噪声点会以相同权重参与匹配,从而拉低真实字符位置的分数。而TM_CCOEFF_NORMED因为做了均值减除,等于在频域上滤掉了低频背景成分,字符边缘和结构信息的权重被放大,对二值噪声的容忍度更高。我一般会同时跑一次TM_CCORR_NORMED,如果两个方法的峰值位置一致,说明匹配结果可信;如果不一致,大概率是目标字符和模板的尺寸差太多。

下表列出了几种匹配方法在车牌字符场景下的表现,方便按需选取:

方法最优值车牌场景特点
TM_SQDIFF最小值对灰度偏移敏感,建议提前做归一化
TM_SQDIFF_NORMED最小值二值图可用,但边缘噪声影响中等
TM_CCORR最大值对整体亮度敏感,容易误匹配
TM_CCORR_NORMED最大值亮度整体偏高时仍可用
TM_CCOEFF最大值均值减除,抗光照偏移,但结果无界
TM_CCOEFF_NORMED最大值推荐用于车牌模板,结果近似相关系数

注意:响应值不是概率。TM_CCOEFF_NORMED的结果虽然理论上在 -1 到 1 之间,但实际会因为插值和浮点误差略超边界。阈值设置时不要要求无限接近 1。

2.3 阈值选取与多尺度模板匹配

对于归一化相关系数,0.8 通常可以作为“高置信度”的分界线。但车牌字符中存在大量外形接近的样本,比如“豫”和“预”在低分辨率下差别很小,仅靠固定阈值判断容易漏掉正确字符。常见做法是先对模板做多尺度缩放,例如 0.9 倍、1.0 倍、1.1 倍,分别与目标子图匹配,取三个尺度中最高分作为结果。这样可以在一定程度上补偿拍摄距离不同造成的尺寸差异。

另一个容易被忽略的点是模板文件的干净程度。main.zip 里的模板直接以字符名命名,比如 豫.bmp,这种方式省去了额外维护标签映射表的麻烦。但如果模板图像中包含了字符周围的边框或噪点,匹配分数会被明显拉低。处理方法是入库前做一次裁剪,用行投影和列投影把字符主体从模板中切出来,再统一归一化尺寸。我在实际项目里还会把模板中的纯黑边去掉,只保留字符笔画,这样能减少模板内部背景对相似度计算的干扰。

3. 车牌定位与字符分割的预处理流程

3.1 用颜色特征定位车牌区域

模板匹配回答的是“这个字符是谁”,但不负责回答“车牌在哪里”。main.zip 里的 I.bmp 可能是一张已经包含车牌的图像,但真实摄像头抓拍的图片通常是一个完整场景,需要先定位车牌区域。最稳定的特征是车牌底色,常见蓝色车牌的 HSV 色相范围大约在 100 到 124,饱和度在 43 到 255,明度在 46 到 255。使用cv2.inRange做颜色过滤后,再执行一次闭运算,就能连接相邻的字符区域形成候选连通域。

定位到候选区域后,还需要对车牌做透视矫正。很多人直接使用仿射变换,但实际拍摄时前挡风玻璃倾斜、相机仰角会造成平行四边形畸变,模板匹配对这类几何变形非常敏感,几个像素的倾斜就可能让相关系数从 0.85 降到 0.6。矫正思路是先通过边缘检测找到车牌四边形的四个顶点,再用cv2.getPerspectiveTransform计算变换矩阵,最后用cv2.warpPerspective输出正视角的车牌图。模板匹配本身只能容忍很小的旋转角度,因此透视矫正是必要的前置步骤。

3.2 字符分割:水平投影与垂直投影

定位并矫正后,接下来要把 7 个字符独立切出来。最常用的方法是投影法:对二值化后的车牌图统计每一列的非零像素数量,得到垂直投影直方图。字符之间的空白区域投影值接近 0,形成波谷,根据波谷位置确定字符左右边界。每个字符块内部再统计行方向投影,切掉上下多余背景。

下面是一段可用的垂直投影分割示例,输入是二值化后的车牌图,输出是每个字符的左右边界:

import cv2 import numpy as np def vertical_projection_split(plate_bin): h, w = plate_bin.shape # 每一列的白色像素数量 col_sum = np.sum(plate_bin, axis=0) // 255 # 阈值取最大投影值的5%,用于过滤微弱噪声 threshold = 0.05 * np.max(col_sum) chars = [] in_char = False start = 0 for x in range(w): if col_sum[x] > threshold and not in_char: in_char = True start = x elif col_sum[x] <= threshold and in_char: in_char = False chars.append((start, x)) return chars

逻辑说明:plate_bin是 0 和 255 组成的二值图,col_sum计算每一列白色像素总数。threshold 用于排除轻微的斑点噪声,避免把单个噪点误认为字符边界。函数遍历每一列,记录进入字符区和离开字符区的 x 坐标,最终返回边界元组列表。

参数上需要注意:如果字符之间粘连,投影波谷不够深,可以把 threshold 提高到 0.1 或 0.15;如果字符本身笔画断裂,投影值下降明显,则需要降低到 0.02。分割前对二值图做一次 3×3 的开运算可以去掉字符周围的孤立点,但要控制核大小,否则会把细笔画一起抹掉。

3.3 模板归一化:保持相同尺寸和笔画宽度

分割出的字符尺寸和 main.zip 中的模板尺寸大概率不同。模板匹配要求模板与目标子图尺度接近,因此每个字符都需要resize到统一尺寸。典型值是宽 24 像素、高 48 像素,这和标准车牌字符的比例接近。统一尺寸时要注意插值方式。字符图本质上是二值图,如果使用默认的双线性插值,放大后会产生灰色过渡带,字符边缘变模糊,而模板库里的字符通常是硬边缘,两边不一致会直接降低匹配分数。

我一般先把字符图固定为 0 和 255 的掩码,再用cv2.resize并指定interpolation=cv2.INTER_NEAREST,保证放大后边缘仍然锐利。如果模板字符笔画偏粗,目标字符笔画偏细,还需要做一次形态学膨胀或腐蚀来补偿。可以先用一个简单的连通域分析统计字符的笔画面积占比,如果目标字符面积明显小于模板,就膨胀一圈。这一步在反色或光照不均匀时尤其重要。

4. 基于 main.zip 模板库的识别实现

4.1 工程文件与模板库结构

main.zip 解压后的核心文件包括 O.bmp、I.bmp 和多个省级简称模板。文件名就是字符内容,识别时无需额外映射表。下面是一个典型的目录结构和文件作用:

文件/目录作用
O.bmp字母 O 的模板,二值化字符图
I.bmp字母 I 的模板
豫.bmp / 辽.bmp / 新.bmp / 藏.bmp省级行政区简称模板
鲁.bmp / 晋.bmp / 川.bmp / 皖.bmp其他省级简称模板
待识别图像.jpg用户准备的测试图,包含车牌区域

注意:main.zip 的模板并不完整,缺少数字和大部分字母,所以只能覆盖部分字符类型。实际使用时需要补齐 31 个省简称、24 个字母和 10 个数字。补模板的方法是从标准车牌图片中截取单个字符,二值化后按同名保存,再用下一节中的加载函数统一读取。

4.2 核心识别代码:从读图到输出文本

下面是一个最小可运行的 Python 识别脚本,完全基于 main.zip 中的模板做匹配,不依赖任何深度学习框架。

import cv2 import numpy as np import glob def load_templates(template_dir): templates = [] labels = [] for path in glob.glob(f"{template_dir}/*.bmp"): tag = path.split("/")[-1].split(".")[0] img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 统一二值化,确保模板只有0和255 _, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # 统一尺寸,避免模板间尺度不一致 img = cv2.resize(img, (24, 48), interpolation=cv2.INTER_NEAREST) templates.append(img) labels.append(tag) return templates, labels def match_char(char_img, templates, labels): best_score = -1.0 best_label = None for tpl, label in zip(templates, labels): res = cv2.matchTemplate(char_img, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ = cv2.minMaxLoc(res) if max_val > best_score: best_score = max_val best_label = label return best_label, best_score def recognize_plate(plate_img, template_dir="./templates"): gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) col_sum = np.sum(binary, axis=0) // 255 threshold = 0.05 * np.max(col_sum) chars = [] in_char = False start = 0 for x in range(binary.shape[1]): if col_sum[x] > threshold and not in_char: in_char = True start = x elif col_sum[x] <= threshold and in_char: in_char = False chars.append((start, x)) templates, labels = load_templates(template_dir) result = [] for start, end in chars: region = binary[:, start:end] if region.shape[1] < 5: continue # 按行投影切掉上下空白 rows = np.where(np.sum(region, axis=1) > 0)[0] if len(rows) == 0: continue region = region[rows[0]:rows[-1]+1, :] region = cv2.resize(region, (24, 48), interpolation=cv2.INTER_NEAREST) label, score = match_char(region, templates, labels) result.append((label, score)) return result if __name__ == "__main__": img = cv2.imread("test_plate.jpg") plate_chars = recognize_plate(img) print([item[0] for item in plate_chars])

逻辑说明:load_templates读取所有 BMP 模板,统一二值化并缩放到 24×48。match_char遍历模板,用TM_CCOEFF_NORMED计算响应,取最高分对应的字符作为识别结果。recognize_plate负责整张车牌的预处理、分割和识别,最终输出一个包含字符和置信度的列表。

参数说明:模板尺寸 24×48 可以根据模板库比例调整,但不能拉伸比例过大。一个更好的做法是将高度固定为 48,宽度按原始宽高比缩放并在两侧补零到 24,这样可以避免字符字形变形。投影阈值 0.05 只适合干净的二值图,如果车牌区域带有边框或铆钉干扰,建议先做开运算再投影。

4.3 模板残缺时的降级方案

main.zip 缺少数字和大部分字母模板,碰到未知字符时,代码会给出一个错误的高频匹配结果。一个可行的降级策略是:当最高匹配分数低于 0.7 时,不输出该字符,而是标记为“未知”,避免把“鲁”误识别成“晋”。更完整的做法是利用车牌编码规则做约束:第一位是省简称,第二位是字母,后面是字母或数字。根据位置缩小候选模板集合,既能提高准确率,又能减少匹配次数。

性能方面,如果模板库扩充到 70 个字符,线性遍历次数会明显增加。按位置分组后,省简称组只需要匹配 31 个模板,字母组匹配 24 个模板,数字混合组匹配 34 个模板,整体耗时只有原来的一半。对嵌入式设备来说,这种优化比换更高分辨率的输入图更有效。

提示:如果待识别车牌区域有蓝色底色,建议先用颜色遮罩把背景置黑,再提取白色字符,这样二值化后噪声更少。

5. 参数调优与误识别排查技巧

5.1 匹配分数高但结果是错的?

先检查模板和字符图的极性是否一致。如果模板是白底黑字,目标字符是黑底白字,TM_CCOEFF_NORMED的结果会接近 0 甚至负数。加载模板时可以判断白色像素占比:如果模板白色比例超过 80%,说明背景是白色,识别前需要反色。这个检查只要放在load_templates里一次,就能规避整批模板的极性问题。

5.2 处理车牌倾斜和照度不均

车牌图像有透视变形时,模板匹配几乎必然失败。可以先用cv2.Canny检测边缘,再用 Hough 变换找长直线,定位车牌四条边框后做透视变换。变换后的图像会引入插值模糊,需要再做一次 Otsu 二值化。若是逆光场景,先用 CLAHE 限制对比度增强,而不是直接做全局直方图均衡化,否则阴影区域会被放大成黑色团块,导致字符粘连。

5.3 用置信度分布做快速诊断

对同一张测试图,把所有模板的最高分数打印出来,观察分数分布。如果最高分和次高分非常接近,说明模板字符之间存在外形相似的问题。此时可以把相似度分数低于 0.85 但高于 0.7 的结果输出为“候选字符列表”,交给后面的业务逻辑处理。把best_score的阈值设在 0.75,低于该值直接丢弃,能有效减少误识别。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询