ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于模板匹配的车牌识别:从原理到代码实战

基于模板匹配的车牌识别:从原理到代码实战 简介基于模板匹配的车牌识别工程包面向计算机视觉初学者与图像处理爱好者帮助理解如何通过图片匹配定位车牌区域、再借助模板匹配完成字符识别。包内共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-w1)×(H-h1) 的响应图响应值最大的点就是模板最可能出现的位置。这个过程没有做任何特征提取纯粹在像素灰度层面比较因此它更接近一种“穷举式搜索”。实际计算相似度时OpenCV 的matchTemplate提供了多种算法。最常用的是归一化相关系数TM_CCOEFF_NORMED计算公式本质上是对模板和子图各自减去均值后再做归一化相关。用公式表达就是R(x,y) Σ(T(x,y) · I(xx,yy)) / sqrt(ΣT(x,y)² · ΣI(xx,yy)²)其中 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, axis0) // 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并指定interpolationcv2.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), interpolationcv2.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, axis0) // 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, axis1) 0)[0] if len(rows) 0: continue region region[rows[0]:rows[-1]1, :] region cv2.resize(region, (24, 48), interpolationcv2.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低于该值直接丢弃能有效减少误识别。本文还有配套的精品资源点击获取
返回列表