
简介基于 OpenCV 的车牌识别 Python 项目源自学长的毕业设计作业定位为计算机相关专业学生的课程设计或毕设参考。项目实现“定位车牌 → 矫正车牌 → 识别颜色 → 分割字符 → 识别字符”的完整流程并附带简单 GUI可识别车牌号码与牌照颜色适合入门学习图像处理与字符匹配。包内共 8 个文件以 Python 源码、模板压缩包、配置文件及说明文档为主其中 function.py 为算法核心gui.py 为界面程序refer1.zip 为识别用模板另含 pycache 编译文件和 gdf 依赖关系图等资源包约 6.43MB。作者在描述中明确提示该方案采用模板匹配单张车牌识别耗时较长且准确率有限使用时可针对性优化。已有 429 人学习下载。通过该资源可掌握 OpenCV 车牌定位、形态学处理、模板匹配识别等基础代码思路也能学习到如何将算法封装为简单桌面应用适合作为课程设计快速改造的起点。1. 基于opencv模板匹配的车牌识别先想清楚这套路为什么能过答辩很多人拿到“基于opencv模板匹配的车牌识别-python毕业设计”这个题目第一反应是把车牌照片和模板库做全局模板匹配然后直接输出结果。这个思路其实会在答辩时被问倒真实场景下车牌在画面中的尺度、倾斜角度、光照都会变模板匹配对尺度和旋转变换并不鲁棒。常见做法是把模板匹配当“识别器”而不是“定位器”先靠颜色把蓝色车牌区域从整张图里找出来再做字符分割最后只对单个字符做模板匹配。这个流程不但可靠而且每一步都能讲出原理代码也可以控制在几百行以内。本文面向需要用Python快速跑通算法、并能在论文里把过程讲清楚的读者覆盖从环境安装、定位、分割到模板匹配调参的完整链路。2. 车牌定位用 HSV 颜色阈值还是边缘检测先定方案再写代码2.1 为什么在 HSV 空间里做颜色筛选比 RGB 判断更稳蓝底白字车牌在RGB空间里蓝色分量通常占优势但不同时段、不同天气下同一个车牌的RGB值波动很大。直接对B分量做固定阈值要么误把深色车身框进来要么在夜间漏检。HSV把色调H、饱和度S和亮度V分开车牌蓝色在H通道上有相对稳定的取值范围把S和V作为辅助条件可以过滤掉大部分背景干扰。这里我一般用OpenCV的cv2.cvtColor把BGR转到HSV然后用cv2.inRange取蓝色区域。需要注意OpenCV中H的取值范围是0到180不是色相环的0到360。import cv2 import numpy as np img cv2.imread(car.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌的HSV阈值以实际采集画面为准微调 lower_blue np.array([100, 80, 50]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) cv2.imshow(mask, mask) cv2.waitKey(0)inRange输出的是一个二值图白色区域代表命中车牌蓝底。S的下限设为80是为了排除像素饱和度不足的浅蓝灰色物体V下限设为50是为了在夜间或阴影区域保住车牌基本轮廓。这两个值不是死参数实际调试时如果mask中车牌区域有空洞需要适当降低V的下限如果背景大面积命中则提高S的下限。2.2 形态学开闭运算把字符区域连接成一个整体颜色筛选做完后车牌内部的白色字符、铆钉、反光点会导致蓝色区域破碎成多个小区域。直接用轮廓查找会得到一堆碎片。常见做法是先做一次开运算去掉细小的噪点再做一次闭运算把字符之间的空隙填上让整个车牌连成一个完整的连通域。kernel_open cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3)) kernel_close cv2.getStructuringElement(cv2.MORPH_RECT, (19, 7)) mask_open cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open, iterations1) mask_close cv2.morphologyEx(mask_open, cv2.MORPH_CLOSE, kernel_close, iterations1)开运算的核宽度选择7像素用来去掉单个孤立噪点闭运算核宽度19像素覆盖一个正常字符在水平方向的宽度让蓝色底能“跨过”白色字符连起来。核太小时字缝补不上核太大时旁边车身的蓝色区域会和车牌粘连破坏后续轮廓筛选。2.3 轮廓筛选与透视纠正拿到干净的车牌矩形得到完整的mask后用cv2.findContours找所有连通域再通过长宽比、面积占比、矩形度三个条件过滤把最像车牌的区域挑选出来。车牌的标准长宽比约为440比140即3.14左右实际拍摄角度会略高。contours, _ cv2.findContours(mask_close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) box None for c in contours: x, y, w, h cv2.boundingRect(c) aspect w / h area_ratio (w * h) / (img.shape[0] * img.shape[1]) # 车牌长宽比一般在2.5~4.5之间面积占比0.004~0.06 if 2.5 aspect 4.5 and 0.004 area_ratio 0.06: box (x, y, w, h) break面积占比的上下限和图像分辨率直接相关。对于1080p画面车牌通常占全图的0.5%到5%低于0.004容易命中小蓝色车标高于0.06容易误收车身大面积蓝色喷漆。选出候选矩形后如果车牌在画面中有明显倾斜仅用boundingRect会把字符截歪这时候需要先提取最小外接矩形、求四角点再用cv2.getPerspectiveTransform加cv2.warpPerspective做透视拉正。rect cv2.minAreaRect(points) # 输入轮廓点集 angle rect[2] # 角度超过±3度时建议拉正否则后续匹配误差会放大3. 字符分割的细节垂直投影配合启发式规则把车牌拆成七个字符3.1 从灰度图到二值图的阈值选择车牌识别对二值化的要求比通用OCR更高。全局固定阈值在强光照下很容易把字符细节吞掉自适应阈值能保留局部细节但车牌边缘和铆钉干扰也同时被保留。常见做法是先在HSV的V通道上做cv2.threshold再利用“蓝底白字”的先验做一次反色校正。比如光线均匀的车牌图可以直接取阈值120作为亮字符和暗底色分界。gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 蓝底车牌中白字亮度高Otsu分割后字符应为白色255实践中Otsu按灰度直方图自动找谷底适合字符占比稳定的图像。但夜间补光不足时Otsu会把半个车牌压成黑色这时改用固定阈值或先对V通道做直方图均衡化。3.2 垂直投影切分字符的算法主体分割的思路是按列统计白点个数画投影曲线然后找波峰和波谷。字符区域白点密集成带字符间缝隙白点掉到极低。一次完整的垂直投影分割逻辑如下。def vertical_projection(binary_img): h, w binary_img.shape proj [0] * w for col in range(w): proj[col] np.sum(binary_img[:, col] // 255) return proj proj vertical_projection(binary) col_nums len(proj) split_points [] in_char False for i in range(col_nums): if proj[i] 0 and not in_char: start i in_char True elif proj[i] 0 and in_char: end i in_char False split_points.append((start, end)) # 过滤掉宽度小于5像素的细小噪声段 split_points [(s, e) for s, e in split_points if e - s 5]这一步的缺陷是汉字“京”“苏”等左右结构笔画间偶尔断裂会被切为两段字符和车牌边框之间若没有完全断开又会合并成一段。因此不能只依赖投影还要叠加宽度偏差判断。# 一段的宽度和平均字符宽度比较过宽则可能是粘连需要在中间找低谷再切一刀 avg_w np.mean([e - s for s, e in split_points]) for s, e in split_points: if e - s avg_w * 1.5: # 在该区间内找白点数最小值位置重新分割 min_pos np.argmin(proj[s:e]) s # 判断min_pos两侧是否都有像素这种启发式规则在毕业设计答辩场合非常讨喜它表明你处理过真实数据不是只在理想图上跑通。3.3 分割结果检查的三个硬指标字符切完后至少做三个判断数量是否为7单个字符宽高比是否落在0.4到0.8之间字符水平方向间距是否均匀。第一个判断用于捕获汉字断成两截的情况第二个判断用于捕获“鲁”字这种笔画多、粘连严重的案例第三个判断用于识别螺栓孔干扰。if len(split_points) ! 7: # 常见处理把相邻过窄的段合并或者把过宽段重新切分 pass螺栓孔常出现在车牌四角如果它被误判为一个字符宽高比会比正常字符矮很多。处理方式是从分割列表中剔除面积明显异常的小块保证后续匹配模板的输入统一。4. 模板匹配识别的核心matchTemplate 方法选型与多峰值去重4.1 TM_CCOEFF_NORMED 与 TM_SQDIFF_NORMED 的取舍对比cv2.matchTemplate提供六种匹配方式在车牌场景最常用的是归一化相关系数法TM_CCOEFF_NORMED和归一化平方差法TM_SQDIFF_NORMED。前者输出数值越大越匹配后者数值越小越匹配。方法输出含义对光照变化的敏感度车牌识别推荐度TM_SQDIFF像素差平方和高背景亮度直接影响阈值不推荐TM_SQDIFF_NORMED归一化差异0为完全一致中受二值化质量影响可作为辅助判断TM_CCORR_NORMED相关值亮区易产生高响应高白色背景会盖过字符不推荐TM_CCOEFF_NORMED去均值后的相关系数1为最佳低对亮度偏移有抑制作用推荐实际调参经验是二值化后的字符模板和待匹配字符都是黑白二值图背景占比高用TM_CCOEFF_NORMED最稳定它的阈值可以放心设到0.7以上。下面的代码完成单字符匹配。res cv2.matchTemplate(char_img, template_img, cv2.TM_CCOEFF_NORMED) _, max_score, _, max_loc cv2.minMaxLoc(res)4.2 模板库的组织方式和训练来源模板匹配的效果上限取决于模板本身。最稳的模板来源是真实车牌截图自己采集几张车牌切出7个字符保存成和识别端相同尺寸的黑白图。模板文件命名格式可以简单直接方便加载。templates/ hanzi/ 京.png 津.png 沪.png letters/ A.png B.png digits/ 0.png 1.png加载时先统一模板尺寸。汉字模板宽高比约0.6建议统一到48×80字母和数字模板统一到32×64。过大模板不仅匹配慢而且对分割字符的微小位置偏移更敏感。templates {} categories [hanzi, letters, digits] for cat in categories: files os.listdir(ftemplates/{cat}) for f in files: if f.endswith(.png): tm cv2.imread(ftemplates/{cat}/{f}, 0) label f.split(.)[0] # 字母和数字统一到 32x64中文汉字统一到 48x80 tag_size (48, 80) if cat hanzi else (32, 64) tm cv2.resize(tm, tag_size) tm cv2.threshold(tm, 127, 255, cv2.THRESH_BINARY)[1] templates[label] tm模板必须保证字符笔画是白色、背景为黑色。如果采集模板时存了彩色图cv2.imread用0读灰度后一定要再做一次二值化否则灰度渐变会拉低匹配得分。4.3 候选框重叠问题阈值扫描后的一组矩形怎么合并cv2.minMaxLoc只能给出得分最高的点但模板匹配在真实分割字符上经常会拿到多个局部极大值。比如字符“0”内部空洞或背景纹理导致同一区域出现多个高亮点如果直接把所有超过阈值的点全部画在图上会输出一大堆重叠框。常见做法是设定低阈值收集所有候选位置然后按非极大值抑制NMS思路保留最强框。threshold 0.75 loc np.where(res threshold) boxes [] for pt in zip(*loc[::-1]): boxes.append((pt[0], pt[1], char_w, char_h)) # 按得分降序排 boxes.sort(keylambda b: res[b[1], b[0]], reverseTrue) keep [] while boxes: best boxes.pop(0) keep.append(best) boxes [b for b in boxes if not iou(best, b)] # iou是两个矩形交并比超过0.4的认为是同一个字符位置直接丢弃NMS这一步在论文里可以作为一个细化点着重描述。它能证明你理解了模板匹配会产生多响应不是查一个最大值就完事。阈值设在0.75左右如果结果频繁出现错识别可以尝试把阈值升到0.8观察得分分布。4.4 置信度不足时留一个“不确定”分支车牌字符库里包含31个省简称、24个字母I和O通常不用和10个数字每个字符都做一遍完整匹配最终取最高分。但如果最高分低于0.6说明该字符在二值化阶段或者分割阶段出了问题。稳妥做法是让它输出“?”而不是硬猜一个错误字符后续可以用连通域分析结果兜底。def match_char(char_img): best_label ? best_score 0 char_resized cv2.resize(char_img, (32, 64)) for label, tm in templates.items(): res cv2.matchTemplate(char_resized, tm, cv2.TM_CCOEFF_NORMED) _, score, _, _ cv2.minMaxLoc(res) if score best_score: best_score score best_label label if best_score 0.6: return ?, best_score return best_label, best_score5. 工程化落地的鲁棒性提升模板库自建、环境安装与性能开销5.1 模板库别偷懒直接下载自己拍素材最稳网上找的字体模板和实际拍摄车牌的笔画宽度、内核渲染方式差异很大即使字型相同匹配得分也会明显偏低。我一般会在白天、傍晚两个时段各采集10张不同省份车牌切出字符后人工校验一遍把笔画模糊的模板删掉。这样做还有一个好处覆盖了“京”“鲁”“苏”等不同省份字符的笔画细节而不是只用几个常见省份测试。5.2 光照、倾斜和图像分辨率三个常见干扰处理车牌识别在室外环境中最大的敌人不是模板库不够大而是图像质量波动。强光导致的字符反光会让二值化后字符出现断点处理方式是识别前对灰度图做一次直方图均衡化。gray_eq cv2.equalizeHist(gray)模板匹配对角度极其敏感超过正负2度匹配分就会明显下降。前期定位阶段如果检测到车牌旋转不能省去透视纠正这一步。分辨率方面apeng 录像是1080p如果直接对整幅图做模板匹配matchTemplate在大尺寸图像上的计算量会线性增加。更合理的做法是只对裁剪出的字符区域做匹配且车牌区域超过300像素宽时先缩小到统一宽度再处理。5.3 在本地Python环境安装OpenCV的最稳命令序列有相当一部分同学在环境安装阶段就卡住了常见的报错是ModuleNotFoundError: No module named cv2。这通常是pip装进了系统全局环境而运行代码用的是虚拟环境。推荐用venv或conda建一个干净环境再安装opencv-python。python -m venv plate_env # Windows 下激活环境 plate_env\Scripts\activate # Linux/macOS 下激活环境 # source plate_env/bin/activate pip install opencv-python numpy matplotlib如果下载速度慢可以临时用清华镜像源加速。这个源在整个Python生态里都很常用和OpenCV官方包不冲突。pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple装完跑一个最小验证命令确认版本可用再往下写代码。import cv2 print(cv2.__version__)这里能看到类似4.x.x的输出就能保证后边的API调用不会因为环境问题报错。如果使用Anaconda也可以直接执行conda install opencv-python效果等同。5.4 性能分析与降耗策略在1080p图像上色域转换加颜色阈值大约耗时5到8毫秒轮廓筛选和形态学操作耗时约3毫秒全字符模板匹配在1000个模板规模下耗时约20到40毫秒。实际项目如果有实时帧率要求优先使用ROI缩小匹配范围车牌定位完成后直接把车牌区域裁剪出来后面的二值化和模板匹配都基于裁剪图完成。start time.perf_counter() plate_img img[y:yh, x:xw] # 后续处理都用plate_img而不是整帧 elapsed_ms (time.perf_counter() - start) * 1000这段计时在论文里面作为性能指标数据是有效的。如果帧率还是不够考虑把模板图先转成统一尺寸并提前加载到内存中而不是每次识别都读一次磁盘文件。6. 把匹配得分矩阵可视化一眼定位是模板问题还是预处理问题调试车牌识别时最难的一点是识别结果错了但不知道错在哪个环节。其实cv2.matchTemplate返回的结果矩阵就是一张得分图它可以直接用热力图的方式展示出来。把热力图叠加到被匹配的字符图上就可以直观判断匹配失败是发生在字符分割、模板选择还是阈值筛选阶段。res cv2.matchTemplate(char_img, tm_plate, cv2.TM_CCOEFF_NORMED) # 将得分矩阵归一化到 0~255 显示 res_norm cv2.normalize(res, None, 0, 255, cv2.NORM_MINMAX) heatmap cv2.applyColorMap(res_norm.astype(np.uint8), cv2.COLORMAP_JET) # 和原始字符图等比显示 resized cv2.resize(char_img, (res.shape[1], res.shape[0])) overlay cv2.addWeighted(resized, 0.4, heatmap, 0.6, 0) cv2.imshow(match_heatmap, overlay)如果字符图上正确位置的得分区域没有出现亮块说明二值化后的字符和模板在笔画粗细上差异太大优先检查模板是否被cv2.resize压变形或者模板二值化阈值是否选错。如果正确位置出现了亮块但最终输出字符不对问题就在NMS或候选框选择逻辑上。这个可视化技巧同时适用于汉字、字母和数字是整个项目落地排查效率最高的一个步骤。它不用额外调试信息看到亮斑就能决定下一刀该切在哪里。本文还有配套的精品资源点击获取