简介这是一套基于OpenCV模板匹配的车牌识别Python项目面向计算机相关专业学生完成毕业设计或课程设计核心流程包含车牌定位、几何矫正、颜色识别、字符分割与模板匹配识别配有简单GUI界面开发环境为Python 3.8与OpenCV 4.2。资源共8个文件以Python源码为主辅以编译缓存、模板压缩包及说明文档整体大小约6.43MB算法与界面代码分离便于单独修改和调试。已有429人学习下载项目代码经测试可运行答辩评分较高。由于采用多模板匹配方式单张车牌识别约需20秒且精度有限但对理解传统图像处理流程、模板匹配原理以及GUI搭建仍具参考价值适合作为入门级车牌识别项目的起步模板。1. 模板匹配做车牌识别为什么毕业设计里它最多把“基于 opencv 模板匹配的车牌识别”作为 python 毕业设计题目几乎是每年都会被重新选一次的老面孔。这个方案用 OpenCV 的 matchTemplate 滑动匹配思路配合字符分割把车牌上的汉字、字母、数字逐个认出来不依赖深度学习框架也不用 GPU一台普通笔记本就能把流程完整跑通。它的识别率上限不高做不了停车场道闸那种复杂场景但作为毕业设计它把“图像预处理—定位—分割—识别—结果展示”五个环节串得非常清楚每一步都能可视化、能调试、能在答辩时讲出原理。适合 python 基础一般、想让项目既有代码量又能自圆其说的同学。2. 先把模板匹配原理和边界说透再搭运行环境2.1 模板匹配的滑动窗口原理与三个硬边界模板匹配的思路很直白准备一张小图当模板在另一张待搜索的大图上从左到右、从上到下逐像素滑动每滑动到一个位置就计算一次模板和当前窗口的相似度最后取相似度最高的位置作为匹配结果。OpenCV 的 cv2.matchTemplate 就是干这个的相似度算法有好几种常见的是 TM_CCOEFF_NORMED输出值范围在 0 到 1 之间越接近 1 说明越像。这个算法看着简单但它有三个硬边界直接影响整个车牌识别项目的设计走向。第一是旋转敏感模板是正着的待匹配的字符只要偏转几度得分就明显下降所以车牌定位后一定要做倾斜校正。第二是尺度敏感模板 45x90 像素实际字符如果是 80x160匹配效果会大打折扣所以字符送入匹配前必须先统一 resize 到模板尺寸。第三是光照敏感逆光、过曝、阴影都会让灰度分布改变模板匹配能靠归一化算法抵消一部分但不能根治预处理里就必须把光照问题考虑进去。理解了这三个边界就能明白这个项目的整体结构为什么是“先定位、再校正、后分割、最后匹配”。定位负责把车牌从整张图里抠出来校正负责把倾斜的车牌摆正分割负责把连贯的车牌切成单个字符匹配才是最后一步。前面任何一步做不好后面识别分数都会很难看。这也是这个毕设项目真正花时间的地方。2.2 环境搭建conda 创建独立环境与 opencv-python 最小安装环境这块不用纠结太多。网上搜“linux 安装 cuda 版本 opencv”或者“opencv cmake 编译步骤”这类内容对毕设来说属于性能溢出模板匹配是纯 CPU 操作跑一张图几十毫秒根本用不到 GPU。我一般建议直接用 conda 建一个独立环境避免把系统 Python 搞乱。conda create -n plate python3.9 -y conda activate plate pip install opencv-python numpy matplotlib如果你在国内容器里下载慢pip 后面加-i https://pypi.tuna.tsinghua.edu.cn/simple这种国内镜像源几分钟就能装完。这里装的是 opencv-python不带 contrib 扩展模块对模板匹配、边缘检测、形态学操作完全够用。如果以后想试 SIFT、SURF 这类特征点算法才需要换 opencv-contrib-python这个项目用不到。numpy 是 OpenCV 的底层依赖装 opencv-python 的时候会自动带上不需要单独担心。matplotlib 主要用来画分割结果和识别结果的对比图写论文的时候很有用。装完之后在 python 里执行import cv2; print(cv2.__version__)能输出版本号就说明环境没问题。2.3 最小验证用 matchTemplate 跑通一次完整匹配环境装好后先别急着写整个识别流程用一个最小脚本验证模板匹配本身能跑通。随便找一张场景图和一张从场景里截出来的小图跑下面的代码import cv2 img cv2.imread(scene.jpg) # 场景大图 tpl cv2.imread(tpl.jpg) # 模板小图必须小于场景图 h, w tpl.shape[:2] res cv2.matchTemplate(img, tpl, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(res) top_left max_loc bottom_right (top_left[0] w, top_left[1] h) cv2.rectangle(img, top_left, bottom_right, (0, 0, 255), 2) cv2.imwrite(result.jpg, img) print(score , max_val)这段代码的逻辑很简单读入两张图调用一次 matchTemplate然后用 minMaxLoc 找到得分最高的位置最后画框存图。如果 result.jpg 里框的位置正好落在模板原本的位置上且 score 接近 1.0说明 OpenCV 工作正常。这里要特别留意一个参数层面的坑matchTemplate 要求模板尺寸必须小于等于输入图尺寸否则会直接报错。这在后面的字符匹配阶段会成为高频踩坑点因为分割出来的字符区域可能比模板还小届时要先 resize 再匹配。现在先留个印象。这个脚本跑通之后就可以正式进入车牌子图定位的环节了。3. 车牌定位整个项目里对识别准确率影响最大的一步3.1 为什么选边缘检测而不是 HSV 颜色筛选做车牌定位最常见的两条路是边缘检测和颜色筛选。颜色筛选的思路是先把 BGR 转成 HSV提取蓝色区域蓝底白字车牌再在二值图上找轮廓。听起来很直接但实际调参时很痛苦蓝色的 HSV 范围受光照影响极大同一个车牌在晴天阴影和夜间灯光下的色相值能差出几十阈值稍微不对要么把蓝天白云框进来要么把深色车身也算进去。我一般推荐用边缘检测。车牌区域的字符和底色之间存在高频边缘而且车牌本身有矩形边框边缘信息非常稳定。思路是灰度化 滤波去噪 边缘检测 形态学闭运算把相邻字符连成一个整体矩形最后找轮廓并按几何特征过滤。这个流程对光照的敏感度比 HSV 低很多在毕业设计这种以“让流程跑通并讲清原理”为目标的场景里是性价比最高的选择。HSV 颜色筛选不是不能用它适合背景干净的场景比如地库入口、固定机位。如果你手头测试图恰好都是这种场景可以把 HSV 作为辅助过滤条件加在边缘检测之后用来确认候选区域内部确实是蓝色而不是拿它做主定位手段。3.2 定位流程完整代码灰度、双边滤波、Sobel、Canny、形态学下面这段代码是定位模块的核心输入的是一张带车牌的整车图输出的是一个候选车牌区域的坐标列表。每一步都有明确的参数含义先按这个跑通再根据你自己的测试图微调。import cv2 import numpy as np img cv2.imread(car.jpg) # 输入整车图 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 双边滤波保边去噪比高斯滤波更适合理清边缘 gray cv2.bilateralFilter(gray, 11, 17, 17) # Sobel 检测 x 方向梯度车牌字符横向排列x 方向边缘最明显 sob cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize3) sob cv2.convertScaleAbs(sob) # Canny 双阈值高阈值开大一点滤掉车身弱纹理 edges cv2.Canny(sob, 80, 180) # 横向长核闭运算把相邻字符边缘连成一个连通域 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 3)) close cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations2) # 找轮廓并做几何过滤 contours, _ cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) H, W img.shape[:2] candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / h # 车牌宽高比约 3.14 area w * h if ratio 2.5 or ratio 4.5: continue if area H * W * 0.002 or area H * W * 0.05: continue if cv2.contourArea(cnt) / area 0.5: # 矩形度过滤排除不规则噪声 continue candidates.append((x, y, w, h))逻辑上分四段。先转灰度并双边滤波这是为了降低噪声同时保留字符边缘的锐利度接着 Sobel 只取 x 方向梯度因为车牌是横向排列的字符横向边缘比纵向边缘信息量大很多Canny 接在 Sobel 结果上做非极大值抑制和双阈值连接边缘图会比直接对灰度图做 Canny 干净很多最后闭运算用横向长核把每个字符各自产生的边缘片段连成一个整体矩形区域。闭运算的核大小 (17, 3) 是个经验值。核太短字符边缘连不到一起车牌会被切成两三个小块核太长会把车灯、保险杠上的横条也连进来产生大块误检。iterations2 是让连接更稳固但如果你的测试图分辨率特别高可以适当把核宽加到 21 到 25。3.3 轮廓过滤的宽高比、面积、矩形度参数怎么调过滤条件里最核心的是宽高比。标准燃油车牌尺寸 440x140 毫米宽高比约 3.14但经过边缘检测和闭运算后检测到的轮廓往往包含车牌边框实际比例会浮动在 2.5 到 4.5 之间。区间设得太窄比如 2.8 到 3.5会把一些稍微变形的车牌漏掉设太宽又会混入车身的矩形物体。我习惯先用宽松区间拿到候选再在候选列表里按得分排序。面积过滤要结合输入图分辨率来定。0.002 到 0.05 这个比例意思是车牌区域面积不能小于整张图的千分之二也不能大于百分之五。如果你拍的整车图里车牌只占很小一块要把下限往下调一个数量级。矩形度过滤用的是轮廓面积除以外接矩形面积车牌是标准矩形这个比值通常在 0.7 以上设 0.5 已经比较宽容了主要是滤掉那些外形像矩形但内部空洞很多的噪声轮廓。到这里得到的 candidates 可能不止一个不要急这是正常现象。接下来把每个候选区域单独截出来按“蓝色像素占比”做一个二次确认确认是车牌才进入下一步。这样能滤掉大量误检。3.4 候选车牌倾斜校正minAreaRect 取旋转角并透视摆正定位框拿到了但很多情况下车牌在画面里是歪的。模板匹配对旋转很敏感歪个三五度字符识别率就会明显下降所以识别前必须有一个校正步骤。rect cv2.minAreaRect(cnt) # 返回 ((cx, cy), (w, h), angle) angle rect[2] cx, cy rect[0] # angle 绝对值大于 1.5 度时做旋转纠偏 if abs(angle) 1.5: M cv2.getRotationMatrix2D((cx, cy), angle, 1.0) rotated cv2.warpAffine(img, M, (W, H), flagscv2.INTER_CUBIC)minAreaRect 返回的 angle 方向定义比较绕角度正负在不同宽高比下含义不一致但没关系我们只关心绝对值大小旋转后通过视觉确认车牌是否水平。校正后再从 rotated 图里按原来的 boundingRect 坐标截取子图就能得到一张接近水平的车牌图。这一步是整个定位模块里最容易被忽略、但对识别率影响最大的环节。我见过很多毕设代码里定位框画得很准但车牌在框里是歪的送进识别模块后连续识别错四五位字符。问题不在匹配算法而是在旋转校正这一步省掉了。4. 字符分割与模板匹配识别顺序决定准确率4.1 垂直投影分割把七位车牌拆成单个字符车牌子图拿到后首先要解决“从哪里切一刀”的问题。最常见的做法是垂直投影。思路是把车牌子图转灰度、二值化然后按列求白色像素的统计值字符所在的列会有明显响应字符之间的间隔处响应接近零按这个波形找出连续的字符区间。gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 车牌是深底浅字用 BINARY_INV 把字符变成白色前景 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 左右边框会干扰投影各裁掉 6 像素 binary[:, :6] 0 binary[:, -6:] 0 col_sum np.sum(binary, axis0) / 255.0 # 每列白色像素总数 # 找到连续非零列的起止区间 ranges [] start None for i, v in enumerate(col_sum): if v 0 and start is None: start i elif v 0 and start is not None: ranges.append((start, i)) start None if start is not None: ranges.append((start, len(col_sum)))OTSU 自适应阈值是这里的关键。它根据整张图的灰度直方图自动计算分割阈值能适应不同光照条件下车牌底色和字符的对比度差异比固定阈值稳得多。如果你发现分割结果里字符连成一片或者碎成小段优先怀疑的是二值化阈值而不是投影逻辑本身。分割完成后要对 ranges 做一次过滤标准车牌字符高约为车牌子图高度的 80% 以上那些高度明显不足的区间通常是车牌中间的间隔符小圆点或者噪声直接丢弃。字符宽度方面汉字和字母数字的宽度差异不小但一般不会低于子图宽度的 8%低于这个比例的区间基本是边框残留。4.2 模板库怎么生成从真实车牌子图里抠别用字体渲染模板匹配的模板来源是个容易走偏的地方。很多人图省事用 PIL 的字体文件直接渲染字符图片当模板比如用 simhei.ttf 渲染一个“京”字再渲染一个“A”然后拿去做匹配。这样做出来的模板和真实车牌字符的形状差异非常大因为车牌用的是专门的国标字体笔画粗细、转折角度都和普通黑体不一样。结果就是匹配分数普遍偏低而且不同字符之间的分数差很小识别结果不稳定。正确的做法是从真实车牌子图里抠字符当模板。用前面写的定位和分割模块收集几十张车牌子图把分割出来的字符图片按文件名归类比如“京.png”“A.png”“5.png”。每个字符尽量收集 2 到 3 个不同来源的样本盖住不同光照和磨损情况。字体、笔画粗细的细微差异会让匹配分数更真实。模板数量和准确率不是正比关系。同一字符放太多张模板反而会引入噪声增加误匹配风险。我一般每个字符最多保留 3 张宁缺毋滥。模板图统一 resize 到 45x90 像素这是按标准车牌字符宽高比 1:2 来的。注意是拉伸缩放不做等比留白因为后续识别时字符区域也会做同样的 resize尺度保持一致即可。4.3 匹配打分代码统一尺寸、TM_CCOEFF_NORMED、minMaxLoc模板准备好之后识别就变成了一件很简单的事对分割出的每个字符区间依次和模板库里的所有模板做一次匹配得分最高的模板对应的标签就是识别结果。TEMPLATE_W, TEMPLATE_H 45, 90 def match_char(char_img, template_db): # 统一尺寸模板匹配要求模板不大于输入图 char_img cv2.resize(char_img, (TEMPLATE_W, TEMPLATE_H)) best_label, best_score None, -1.0 for label, tpl_list in template_db.items(): for tpl in tpl_list: res cv2.matchTemplate(char_img, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(res) if max_val best_score: best_score, best_label max_val, label return best_label, best_score函数逻辑不复杂外层遍历字符标签内层遍历同一标签下的多张模板每次匹配取最高分最后把全局最高分对应的标签返回。这里用 TM_CCOEFF_NORMED因为它对亮度和对比度的变化有一定鲁棒性是模板匹配里最适合真实图像的一种度量方式。尺寸统一是最容易忽略的细节。如果 char_img 只有 30x60 像素直接拿去和 45x90 的模板做 matchTemplateOpenCV 会抛异常报错信息是模板尺寸必须小于等于输入尺寸。先 resize 再匹配虽然把字符拉伸了但因为模板也是同样方式 resize 出来的相对比例一致不影响排序结果。把整个识别循环写成函数的好处是后面做批量测试时可以直接复用。4.4 识别结果格式化中文车牌号的输出与显示问题识别完成后把所有字符按分割顺序拼接就得到了完整的车牌号。这里有一个细节要处理标准蓝牌是 7 位绿牌是 8 位第二位是字母 D 或 F。如果分割时把小圆点也算成了一个字符结果里会混进一个点拼接前要按字符宽度过滤掉。OpenCV 自带的 cv2.putText 不支持中文往输出图像上直接画“京A12345”这种带汉字的结果时汉字会显示成问号。常见的做法是识别结果先在控制台用 print 输出写论文时用 matplotlib 的 imshow 展示原图和识别文本把 text 作为标题打印。如果需要在图上画中文可以转 PIL 的 ImageDraw 配合中文字体绘制但这属于锦上添花不是核心流程。到这里整个“定位—分割—识别”主链路已经完整了。接下来把我在实际调试中反复踩过的坑集中列出来每条都按现象、原因、解决的顺序写清楚。5. 车牌识别避坑现象、原因、解决5.1 报错 “size of input image should be smaller than template”现象matchTemplate 在识别阶段直接抛异常程序中断报错信息里提到模板尺寸大于输入图。原因分割出来的字符区间宽度或高度小于模板尺寸。常见于数字“1”这种窄字符或者分割时把字符边缘切掉了一部分。OpenCV 的模板匹配机制决定了模板必须小于等于输入图不满足就直接抛错。解决在匹配函数入口对 char_img 做强制定位 resize就像 4.3 节代码里写的那样。先 resize 再匹配不要试图用条件判断跳过窄字符因为“1”和“7”这类窄字符恰好是识别重点跳过就会丢位。5.2 定位框锁定了车灯或车尾装饰条现象候选区域画出来位置不对不是车牌而是车灯、镀铬条或者其他矩形物体最后识别结果完全乱码。原因轮廓过滤只用了宽高比、面积、矩形度三个几何特征。车灯和镀铬条同样是矩形宽高比也能落在 2.5 到 4.5 区间内单靠几何特征无法区隔。解决在几何过滤后加一道颜色确认把候选区域的 BGR 图像转成 HSV统计蓝色像素在区域内的占比。燃油车牌蓝色占比通常在 30% 以上低于 25% 的直接排除。这个额外步骤成本很低但能把误检率降一个量级。注意 BGR 和 RGB 通道顺序用 OpenCV 读图时是 BGR转 HSV 前不要弄混。5.3 同一个定位框换一张图识别率骤降现象测试集里前几张图识别得好好的换到不同光照环境下的图就大面积翻车有时候连定位都定不准。原因光照变化导致二值化阈值失效或者边缘检测把车牌区域淹没在阴影里。模板匹配的归一化算法能缓解对比度变化但不能消除它。解决二值化改用 OTSU 自适应阈值不要用固定值 127定位前先对灰度图做一次直方图均衡化让整体亮度分布更均匀。另外一个容易被忽略的点是车牌定位后要确认输出矩形是水平的如果存在 2 度以上的倾斜先旋转校正再走后续流程。5.4 字符库模板加多了识别率反而下降现象为了提升覆盖率同一字符放了 5 张以上的模板结果把“京”认成“苏”把“0”认成“O”。原因模板库中相似字符的模板互相干扰。matchTemplate 每次只取最高分如果某张模板本身噪声大或拍摄角度特殊它的分数可能虚高把正确模板的得分压下去。解决每个字符最多保留 2 到 3 张模板且模板必须来源真实车牌子图。匹配时除了取最高分还要记录最高分和第二高分的差值差值低于 0.1 时判定为低置信度宁可输出“无法识别”也不硬猜。这个阈值看起来简单但在答辩时能拿出来讲原因比盲目加模板有说服力。5.5 垂直投影把间隔符和边框算成了字符现象分割出来的字符区间数量不对蓝牌 7 位变成了 9 位 10 位多出来的段是车牌中间的小圆点或者两侧边框。原因二值化时没把边框去掉或者小圆点的灰度值高于阈值被投影算法识别成了独立字符段。解决二值化后先对图像左右两侧各 6 像素区域填零把边框抹掉。对分割出的区间做宽度和高度双重过滤字符高度应大于子图高度的 70%宽度应小于子图宽度的 25%、大于 5%。小圆点的高度和宽度都明显小于真实字符用这个条件就能滤掉。6. 从“流程能跑”到“答辩能过”验证方法和一个省时间的技巧前面五章把模板匹配车牌识别的完整链路搭起来了但如果只是在自己电脑上跑通几张测试图就去答辩很容易被问到“识别率到底多少”“换一批图还行不行”。我在做这个方向时养成的一个习惯是建一个分层统计的小脚本把每张测试图的定位、分割、识别三个环节的成败分别记录下来。统计口径可以这样定定位正确指的是候选框和真实车牌区域的 IOU 大于 0.6分割正确指的是分割出的字符数量和位置与真实车牌一致识别正确指的是最终字符序列完全匹配。只统计最终识别率是不够的因为定位错了后面全错你根本不知道问题出在哪一环。分层统计能让你一眼看到瓶颈在定位还是在分割答辩时被问到也答得上。# 测试集目录结构 testset/ images/ # 整车图 labels.txt # 每张图对应的真实车牌号labels.txt 每行格式是“文件名 车牌号”脚本读取后依次跑定位、分割、识别把每一层的对错记录到一张结果表里。表头建议做成文件名、真实车牌、定位是否成功、分割位数、识别结果、单字符平均置信度。这张表印在论文附录里比任何文字描述都有说服力。最后分享一个提升鲁棒性的小技巧对定位出来的车牌子图做一次三档缩放分别按原始尺寸、放大 1.1 倍、缩小 0.9 倍进行分割和识别取平均置信度最高的一组作为最终结果。这个技巧能掩盖掉一部分因拍摄距离不同导致的尺度偏差代码量不大但对整体准确率的提升往往比换匹配算法更明显。模板匹配的方案上限摆在那里跑不过深度学习模型但它的优势在于每一步都透明、可控、讲得出理由。先把这套流程跑稳答辩时带着分层统计表去比单纯贴一段代码扎实得多。希望帮到你。本文还有配套的精品资源点击获取
