
简介基于OpenCV模板匹配的车牌识别Python项目面向计算机相关专业学生的毕业设计或课程设计也适合想快速理解图像处理流程的初学者。项目采用Python 3.8与OpenCV 4.2编写完整覆盖车牌定位、矫正、颜色识别、字符分割与字符识别等步骤并提供简单GUI界面运行后可输出车牌号码与牌照颜色。压缩包共8个文件以Python算法脚本、GUI脚本、模板压缩包、配置与说明文档为主整体约6.43MB模板压缩包需先解压后使用算法与界面代码分离便于替换字符模板进行二次开发。受模板匹配策略影响单张车牌识别约需20秒且对输入图片分辨率和色偏较敏感项目定位偏向毕设原型与学习参考适合快速搭建整体架构而非工程级部署。目前已有431人浏览学习可从中掌握车牌识别各环节实现思路并据此扩展成自己的课程设计或毕业设计。1. 模板匹配车牌识别毕设选它稳在哪、坑有多深基于 OpenCV 模板匹配的车牌识别是 python 毕业设计里很常见的一个选题。它不依赖深度学习训练集不需要显卡一台普通笔记本就能跑完整个链路而且每个中间步骤都能在论文里单独展开写答辩时评委问起来也答得上。整个系统拆开就是三件事把车牌区域从整车图片里找出来把七个字符一个个切开再用模板匹配逐字符打分。看着简单实际做完你会发现真正卡人的不是识别那一行代码而是定位阈值和分割细节。这套方案适合没有深度学习基础、想快速把毕设做成型的同学也适合只想先跑通一个完整图像处理流程的从业者。下面按我实际做过的路线讲代码可以直接抄。2. 车牌定位用 HSV 颜色筛选剥出蓝色车牌区域OpenCV 图像处理里最基础也最常用的操作就是颜色空间转换。车牌定位这一步我建议直接用 HSV 颜色筛选而不是一上来就上模板匹配。原因后面展开讲这里先说结论在 OpenCV 里做车牌定位HSV 轮廓过滤是稳定性最高、代码量最小的组合。2.1 为什么定位车牌不直接上模板匹配模板匹配的原理是用一个小窗口在整张图上滑动逐像素计算相似度相似度最高的位置就是目标位置。这种方式在“目标尺寸固定、位置大致可预期”的工业场景很好用比如流水线上检测固定工位的零件。但车牌不满足这个条件拍摄距离不同车牌在画面里的宽度可能从 100 像素到 500 像素拍摄角度不同车牌还带透视形变。你可以准备多尺度模板但每多一个尺度就要整图多跑一遍速度翻倍不说视角一变相似度照样崩。常见做法是先用颜色先验锁定候选区。标准燃油车蓝牌是“蓝底白字”这个蓝色在整辆车里是最稳定的色块比找边缘、找文字都可靠。和 Halcon 这类商业视觉库相比OpenCV 的模板匹配模块确实朴素但胜在开源、免费、每一步都能可视化对毕设来说够用且好解释。颜色筛选之后再用轮廓的形状过滤就能把车牌区域从复杂背景里剥出来。2.2 HSV 定位的关键代码与参数选择安装时注意一下用 pip 装的是 opencv-pythonimport 名是cv2不是opencv。经常有人在环境这步卡住说“OpenCV 安装成功却找不到 cv2”其实多半是 import 名写错了。另外 opencv-python 会自动带上 numpy不需要单独安装 numpy 库。我这边用 Python 3.8 opencv-python 4.x 跑通4.x 的 API 和 3.x 差异不大但findContours返回值不一样下面代码按 4.x 写。import cv2 import numpy as np img cv2.imread(car.jpg) # 读入整车图片 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # BGR 转 HSV # 蓝色范围不要按理论值卡太死 lower_blue np.array([90, 80, 60]) upper_blue np.array([130, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 得到蓝色掩膜 # 闭运算把断裂的蓝色区域连成完整块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)这里有几个参数值得展开说。H 通道范围取 90 到 130而不是教科书上的 100 到 124是因为不同摄像头的色彩倾向不一样留余量能显著减少漏检。S 下限取 80目的是滤掉白色车身和灰色路面它们的饱和度很低。V 下限取 60防止夜间或逆光时蓝色变暗后被直接丢弃。闭运算核用 15×5 而不是方形核是因为车牌是横向长条横向连接效果好又不容易把旁边垂直的蓝色物体粘连进来。OpenCV 的 HSV 里 H 范围是 0 到 180不是 0 到 360这是新手最容易踩的坑。如果你按 0 到 360 去写蓝色范围那 inRange 结果基本是黑的。拿到轮廓之后下一步就是过滤。2.3 斜的车牌不要强行识别透视矫正车牌在照片里很少是完全水平的直接用正矩形裁剪会把大量背景包进来字符分割阶段会连环翻车。所以定位之后我一般先做透视矫正。用cv2.minAreaRect拿旋转矩形然后把四个顶点按坐标排序映射到正向矩形。for cnt in contours: rect cv2.minAreaRect(cnt) # 旋转矩形 box cv2.boxPoints(rect) # 获取四个顶点 box np.array(box, dtypefloat32) # 按 y 排序上半部分两个点、下半部分两个点 pts sorted(box, keylambda p: p[1]) top sorted(pts[:2], keylambda p: p[0]) # 左上、右上 bottom sorted(pts[2:], keylambda p: p[0]) # 左下、右下 src_pts np.array([top[0], top[1], bottom[1], bottom[0]], dtypefloat32) w int(rect[1][0]) h int(rect[1][1]) if w h: # 保证 w 是长边 w, h h, w dst_pts np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtypefloat32) M cv2.getPerspectiveTransform(src_pts, dst_pts) plate cv2.warpPerspective(img, M, (w, h))boxPoints返回的四个点顺序不固定所以不能直接按下标取“左上、右上、右下、左下”。按 y 排序再按 x 排序是常见做法对倾斜角度不太夸张的车牌都很稳。minAreaRect返回的宽高也不保证 w 是长边如果发现 w 小于 h交换 w 和 h否则矫正出来会是竖着的。透视变换的代码本身不难难的是顶点顺序这一步值得在论文里单独画一张示意图。2.4 定位结果的过滤条件面积、宽高比与填充率颜色筛选之后蓝色车身、蓝色广告牌、远处天空都可能混进来。我给候选轮廓设三道过滤按顺序判断全部通过才认为是车牌。过滤项建议范围设定理由轮廓面积大于整图面积的 1/2000太小的轮廓是噪点宽高比2.0 到 4.5标准蓝牌约 440:140透视后会有浮动轮廓填充率0.3 到 1.0蓝底上有白色字符掩膜不可能是实心的填充率是指轮廓面积除以最小外接矩形面积。车牌掩膜被白色字符断开填充率一般不会太高但也不会太低。这一步过滤完基本就只剩车牌区域了。如果多辆车并排会有多个候选轮廓这时可以把面积最大的那个当作目标或者按宽高比最接近 3.14 的优先。3. 字符分割垂直投影切出 7 个字符定位加矫正之后得到的是一个比较标准的正向车牌图。下一步把七个字符一个一个切出来。分割质量直接决定识别上限分割错一个字符后面识别再准也白搭。字符分割我常用垂直投影法原理很简单把二值图像的每一列做像素求和有字符的列和值大字符之间的空隙和值小按这个曲线切分。3.1 二值化固定阈值和自适应阈值怎么选先把车牌转灰度再做二值化。蓝底白字的车牌对比度本身就很高灰度直方图通常呈现双峰固定阈值 150 左右就能分得很干净。但车辆照片经常带反光、阴影局部亮度不均匀会让固定阈值失效。我一般这样处理先转灰度如果整块区域的平均亮度偏低就先做一次 CLAHE 自适应均衡化再用固定阈值二值化。gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) # 自适应均衡化改善反光和阴影 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray) _, binary cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)clipLimit控制对比度增强幅度调太大容易把字符笔画加粗调太小没效果2.0 是我平时用的起点。二值化之后会残留车牌边框和铆钉这些在投影时会形成额外的峰后面用宽度过滤处理。自适应阈值我也试过blockSize调小了笔画断裂调大了字符和背景粘连反而不如 CLAHE 固定阈值好控制。除非你的数据集里有大量强阴影照片否则优先用固定阈值。3.2 垂直投影切割代码与圆点处理二值化后白色字符在黑色背景上非常突出。按列统计白色像素数就能得到一条投影曲线。连续非零的列区间就是一个候选字符块。标准蓝牌是 7 个字符加一个小圆点所以投影后通常会得到 8 到 9 个候选段其中圆点是最窄的那一段。h, w binary.shape col_sum binary.sum(axis0) // 255 # 每列白色像素数量 char_blocks [] in_block False start 0 for x in range(w): if col_sum[x] 0 and not in_block: in_block True start x elif col_sum[x] 0 and in_block: in_block False end x if end - start 5: # 宽度小于5的段直接丢 char_blocks.append((start, end)) # 过滤圆点宽度明显小于字符平均宽度的段 char_blocks [b for b in char_blocks if b[1] - b[0] 10] # 按宽度排序或按位置顺序输出候选字符 for start, end in char_blocks: char_img binary[:, start:end]圆点“·”的宽度比正常字符窄很多过滤阈值取 10 像素基本能干掉。但要注意部分汉字的内部笔画较稀疏可能被投影切成两段。比如“京”字中间的横笔和下面的口部如果断开投影就会多出一段。常见做法是先看切出来的段数如果大于 7 段就把过窄的段丢掉如果小于 7 段说明有字符粘连了需要按字符先验位置切第二轮。3.3 字符宽度过滤与粘连字符的补救投影切出来的候选块宽度应该落在车牌总宽度的 8% 到 15% 之间。具体到像素级一个归一化到 440 像素宽的车牌字符宽度大约 40 到 50 像素。我按这个经验值做第二轮过滤太窄的是圆点或噪点太宽的是两个字符粘连。粘连是分割阶段最头疼的问题。出现粘连通常有两种原因一是车牌本身有倾斜透视矫正没做好上下边框和字符笔画焊在一起二是二值化时阈值太低笔画被加粗。对付粘连我的处理顺序是先检查透视矫正是否够正再检查二值化阈值是否偏高。如果都正常还粘连就用垂直投影的“谷底”二次分割找到整段投影曲线的最低点从那里切开。这一步做完每个字符块统一 resize 到 20×40 像素方便后面做模板匹配。分割结果一定要保存成图片人工看一眼这一步最值得可视化也是论文里很容易出图的地方。4. 字符识别matchTemplate 逐字符打分字符识别是整个项目的核心交付点但代码反而是最短的。模板匹配的思路是拿每个字符去和模板库里的所有模板比较取相似度最高的模板对应字符作为结果。OpenCV 的matchTemplate配合TM_CCOEFF_NORMED是标准做法。4.1 模板库的建立从真实车牌截模板模板库质量决定识别率的上限。这里有一个很多人踩过的坑不要用系统字体生成模板。黑体、宋体生成出来的“京”字和真实车牌的“京”字笔画结构有明显差异匹配分数会被误导。正确做法是从真实车牌照片里裁字符每个字符准备 3 到 5 张不同清晰度、不同拍摄角度的样本。字符集的构成我提一下省份简称 31 个字母 A 到 Z 去掉 I 和 O 剩下 24 个数字 0 到 9 共 10 个。为什么去掉 I 和 O因为标准车牌字符集中基本不出现这两个字母模板库里留着只会制造误判。文件命名建议用“字符_序号.png”的格式。templates/ ├── 京_01.png ├── 京_02.png ├── A_01.png ├── A_02.png └── 0_01.png这里没有捷径纯手工活但值得花时间。我自己做的时候从一百多张清晰车牌图里裁了大约五百个模板省份汉字多准备了几份因为汉字笔画复杂单一模板太脆弱。4.2 用 matchTemplate 打分核心代码识别时把待识别字符和所有模板都 resize 到 20×40逐张做匹配取最高分。import os def match_char(char_img, template_dir): # 统一尺寸 char_img cv2.resize(char_img, (20, 40)) best_score -1 best_name ? for tpl_name in os.listdir(template_dir): if not tpl_name.endswith(.png): continue tpl_path os.path.join(template_dir, tpl_name) tpl cv2.imread(tpl_path, cv2.IMREAD_GRAYSCALE) tpl cv2.resize(tpl, (20, 40)) # 归一化相关系数值越接近 1 越相似 res cv2.matchTemplate(char_img, tpl, cv2.TM_CCOEFF_NORMED) score res[0][0] if score best_score: best_score score best_name tpl_name.split(_)[0] return best_name, best_scoreTM_CCOEFF_NORMED的原理是先把两张图各自减去均值再做归一化相关所以它对亮度变化不敏感。字符模板和待识别字符都来自不同照片亮度不一致是常态这个模式最稳。如果分数低于 0.6我一般直接输出“无法识别”而不是硬猜一个结果答辩时更有说服力。matchTemplate在两张尺寸完全相同的图上计算时结果是一个 1×1 的数组所以res[0][0]就是分数。如果两张图尺寸不一致OpenCV 会直接报错这里resize放在匹配前就是为了避免这个坑。整个流程跑下来单字符识别耗时在毫秒级整张车牌识别不超过一百毫秒。4.3 提升识别率的几个土办法多模板投票是提升准确率最直接的手段。每个字符准备多张模板打分时不只取最高分而是取得分最高的前三个模板对应的字符出现次数最多的那个字符胜出。这个做法在论文里也很好写叫“多模板融合投票”。还有一个针对易混字符的土办法把 8 和 B、0 和 O 单独拎出来做几何校验。比如 8 的上半部分和下半部分各有一个封闭孔洞B 只有下半部分有一个数连通域就能区分。即使模板匹配分数接近几何校验也能给出正确结果。5. 避坑与参数调整模板匹配最容易被坑的五个细节这部分是我自己在毕设和项目里踩出来的经验照着调能少走很多弯路。5.1 车牌汉字识别错先查模板字体现象其他字符都识别对了唯独“京”字变成“示”或“就”而且分数还挺高。原因模板是网上找的图片或者系统字体生成的和真实车牌字体细节不一致。解决从真实车牌照片里裁模板把“京”字多裁几个版本替换掉原有模板。这一步做完汉字识别率通常立竿见影。5.2 蓝色定位把蓝色车身圈进来现象定位框比车牌大好几倍分割出来的字符全是乱码。原因inRange 的 H 范围太宽或者 S 下限设太低把蓝色车身、蓝色雨棚都算进来了。解决H 范围收窄到 100 到 124 试试S 下限提到 100 以上同时用宽高比和面积把大块蓝色物体排除。注意 H 范围不能一味收窄不然阴天拍的照片里蓝色偏灰会漏检。5.3 二值化后字符粘连投影切不开现象投影段数只剩 6 段有两个字符被当成一个块。原因最常见的是车牌倾斜后上下边框和字符粘连其次是二值化阈值太低导致笔画加粗。解决先做透视矫正再做二值化阈值从 150 往上调到 180 左右测试如果还粘连就用投影曲线的谷底位置强行切开。千万别一上来就调形态学核去腐蚀会把细笔画弄断。5.4 中文路径、中文显示、cv2 版本报错一起讲现象cv2.imread读带有中文名字的图片返回 Nonecv2.imshow窗口标题中文乱码api 版本不同的代码解包报错。原因OpenCV 在 Windows 下对中文路径支持不好且 4.x 的findContours返回值从三个变成两个。解决读图用cv2.imdecode(np.fromfile(img_path, dtypenp.uint8), cv2.IMREAD_COLOR)写识别结果用 PIL 的ImageDraw代替cv2.putText版本不确定的话在代码开头对findContours做一次兼容判断。答辩演示如果用英文文件名能规避一半问题但方法本身值得写在论文里体现严谨性。5.5 高分匹配也不可信0.95 分也可能是错的现象所有字符匹配分数都超过 0.9但 8 和 B 还是混。原因TM_CCOEFF_NORMED对细微形变的区分度有限而 8 和 B 本身结构接近。解决模板库里去掉车牌中不出现的字母 I 和 O这样 0/O 冲突直接消失8/B 这类保留冲突的字符单独写几何校验规则。还有一个经验不要因为分数高就把阈值调到 0.9分数在 0.6 到 0.8 之间反而是正常区间超过 0.95 要警惕是不是模板和待识别字符来自同一张图。6. 交付与验收把一个可跑的 demo 变成能答辩的系统毕业设计不能只交一堆脚本要能现场跑出结果。一个一键识别脚本加上一组评估数据基本就能撑起整个验收环节。6.1 一键识别脚本的结构把前面三步封装成三个函数识别入口保持简单。def recognize_plate(img_path): plate locate_plate(img_path) # 第2章定位 透视矫正 chars split_chars(plate) # 第3章垂直投影分割 result .join(match_char(c, templates)[0] for c in chars) return result print(recognize_plate(demo.jpg))演示时最好加一个可视化参数把定位框、分割后的字符图、最终识别结果一起画到原图上输出。答辩现场评委看到中间过程比只看到一个字符串有说服力得多。我当时就是现场换了几张自己拍的照片跑每一步都有图可看提问环节基本都围绕图像处理展开答起来很轻松。6.2 给自己准备一组评估数据找 20 到 30 张白天、顺光、清晰的标准蓝牌照片统计字符级准确率和整牌准确率。模板匹配在这样受限的数据集上字符级准确率做到 90% 到 95% 是正常水平整牌准确率在 70% 到 80% 之间。别在论文里写“准确率 99%”模板匹配没有这个能力写得离谱了评委反而追问不断。把测试集照片、每张图的中间结果、最终识别结果整理成表格放进论文这就是一个完整可复现的实验章节。6.3 值得继续扩展的方向如果时间充裕可以保留模板匹配做定位辅助把字符识别换成 SVM 或轻量 CNN整牌准确率会明显提升。这个方向在论文里叫“传统方法 深度学习”的对比实验结构上很完整。如果想往嵌入式走树莓派上也能跑 OpenCV把识别脚本跑在树莓派加摄像头上又是一个加分项。我第一次做这套东西时从网上下载模板识别结果惨不忍睹一度怀疑是模板匹配这方法不行。后来花了一下午从真实照片里裁模板把 HSV 阈值从 100-124 放宽到 90-130又把投影分割的圆点过滤加上准确率才从“答辩会翻车”变成“可以演示”。模板匹配不是黑匣子它的每一步结果都能可视化这是它作为毕业设计选题最大的价值。接受它的边界把数据集控制在标准蓝牌场景这套方案完全能撑起一个扎实的毕设。希望帮到你。本文还有配套的精品资源点击获取