
简介这是一套基于Python与OpenCV的车牌识别系统毕业设计源码面向计算机相关专业学生适合作为毕业设计、期末大作业或课程设计的实战参考。系统涵盖车牌定位、字符分割与识别等完整流程结合OpenCV图像处理与CNN、UNet深度学习模型并配有GUI交互界面能够实现从静态图片到视频流的车牌检测与识别。资源包共15个文件包含9个Python源文件、2个训练好的H5模型文件、1个MP4演示视频及3个GIF效果展示压缩包仅26.7MB部署轻量下载后简单配置即可运行。目前已有635人学习使用项目已获导师认可并高分通过对于需要快速上手完整项目的初学者尤其友好。通过源码注释与演示文件读者可清晰理解核心算法和工程实现既能直接用于毕设答辩也可在此基础上进行二次开发。1. 先说清楚这套车牌识别源码能做什么、适不适合你毕业设计做车牌识别最常遇到的场景是答辩前一周老师发来三张现场照片一张傍晚逆光一张车身倾斜还有一张蓝底广告牌。你用压缩包自带的测试图跑得好好的换成这几张识别结果要么是空列表要么把“粤B 5D123”读成“田B 5D123”。这套基于Python与OpenCV的车牌识别系统本质是把识别拆成定位、分割、字符识别三条串行流程用颜色掩膜、边缘检测、模板匹配这类传统视觉手段把车牌从画面里摘出来。它适合想快速跑通一个完整视觉项目、答辩时能讲清楚原理的毕设场景也适合拿OpenCV图像处理链路练手。但请你先把“下载即用”理解成“环境配好代码能跑起来”而不是“任何照片都能百分百识别”后者要靠后面几章的预处理和参数调试。2. 车牌识别的完整链路定位、分割、字符识别每一步的原理2.1 车牌定位先做颜色掩膜还是先做边缘检测车牌在画面里最稳的特征不是“上面有字”而是“一块长宽比固定、底色相对统一的矩形块”。中国蓝底白字车牌的HSV色域相对独立所以我会先做颜色筛选再做轮廓校验Canny边缘检测作为补充不放在第一步。原因是边缘检测对复杂背景太敏感停车场里栏杆、车窗、路面纹理都会检出大量矩形轮廓单靠边缘找车牌会把后续步骤拖垮。import cv2 import numpy as np # 用imdecode兼容中文路径读取图片 def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) img imread_unicode(test.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌的常用HSV范围实际需要按光照微调 lower np.array([100, 100, 60]) upper np.array([130, 255, 255]) mask cv2.inRange(hsv, lower, upper) # 闭运算把字符之间的白色间隙和蓝色底连成一块整体 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for c in contours: x, y, w, h cv2.boundingRect(c) area cv2.contourArea(c) ratio w / h if h 0 else 0 # 车牌宽高比约3.7:1放宽容差到2.5-4.5面积过滤掉小噪声 if 2.5 ratio 4.5 and area 2000: print(候选车牌区域:, x, y, w, h)HSV三个通道里H是色相S是饱和度V是明度。蓝色车牌的H大致落在100到130之间S不能太低否则偏白V控制暗部夜间要把下限从60降到40左右。MORPH_CLOSE用的是核宽17、高5的矩形结构元宽度大于字符间缝高度又不会把车牌上下边缘一起并进来。轮廓筛选的宽高比是这套流程里最重要的一个参数标准车牌宽520像素高140像素比例约3.7车窗、后视镜和广告牌很难同时满足这个比例和面积条件。颜色掩膜还有个边界要提前讲清楚新能源车牌是渐变绿底黑字蓝底掩膜对它完全失效。如果毕设老师要求兼容新能源车需要额外加一条绿色HSV分支比如H落在60到90之间S和V适当放宽然后把两路候选矩形做一次去重合并。2.2 字符分割垂直投影的适用条件与两个必踩的坑定位拿到的是“包含车牌的矩形截图”下一步要从这块图里切出单个字符。常见做法是对二值化结果做垂直投影也就是统计每一列里前景像素的数量像素数接近0的列就是字符间隔连续非零的列合并成一个字符框。def segment_chars(plate_gray): # plate_gray 是从定位框裁剪并二值化后的图像 h, w plate_gray.shape col_sum np.sum(plate_gray 255, axis0) ranges [] in_char False start 0 for i in range(w): if col_sum[i] 0 and not in_char: in_char True start i elif col_sum[i] 0 and in_char: in_char False ranges.append((start, i)) chars [] for r in ranges: width r[1] - r[0] # 过滤过宽和过窄的列段过宽可能是边框过窄可能是分隔符或噪声 if 10 width 100: chars.append(r) return chars垂直投影成立的假设是字符从左到右排列车牌恰好满足这一点。但有两个坑在第一次跑时几乎必现。一是车牌上下边框在二值化后连成一条横线导致所有字符被横向连通投影分不出间隙解决方法是先把车牌区域上下各裁掉5到8像素或者对二值图先做一次开运算去掉细线。二是车牌第二位后面的圆点分隔符它的投影宽度通常只有2到4像素会被过滤阈值直接丢掉但如果你把过滤阈值设置过小比如图里字符本身也只有6像素宽那分隔符就混进来了识别结果里多出一个“点”需要按投影区间宽度和间距做二次过滤。2.3 字符识别模板匹配为什么在毕设里够用它的上限在哪分割出字符后每个字符都是一小块二值图。最省事的识别方式是模板匹配不需要训练、不依赖GPU也不需要在答辩时解释什么是反向传播。停车场里那些车牌识别一体机走的是嵌入式相机加深度模型和纯OpenCV是两条技术路线后者在毕设场景里胜在每一步都能拿中间图像出来讲。def match_char(char_img, templates): char_resized cv2.resize(char_img, (20, 40), interpolationcv2.INTER_AREA) best_score -1 best_label None for label, tmp in templates.items(): tmp cv2.resize(tmp, (20, 40), interpolationcv2.INTER_AREA) res cv2.matchTemplate(char_resized, tmp, cv2.TM_CCOEFF_NORMED) _, score, _, _ cv2.minMaxLoc(res) if score best_score: best_score score best_label label return best_label, best_score模板匹配的逻辑是把字符图缩放成固定尺寸再和每个模板做归一化相关计算分数最高的模板标签就是识别结果。这里要说明两点一是字符归一化到20×40不是拍脑袋定的车牌字符的宽高比接近1:2正方形模板会挤压笔画二是TM_CCOEFF_NORMED对光照和对比度不太敏感比平方差系列更稳。模板匹配的软肋在汉字。省份简称笔画复杂模板数量直接决定准确率上限。常见做法是为高频汉字各准备两到三套不同字号的模板比如“京、津、冀、苏、浙、粤、鲁、川”这些出现频率最高的再从测试图里截取真实字符补充模板库。数字和字母因为字形简单准确率相对高但“0”和“O”、“8”和“B”这种相似字符的混淆主要通过保持二值化后笔画粗细一致来缓解这个细节放到第四章展开。3. 下载即用到跑通环境安装、源码结构和第一张图的识别3.1 环境准备pip安装opencv之前先定Python版本网上搜opencv安装教程最常见的结果是让你直接跑pip install opencv-python但装完一运行控制台报ModuleNotFoundError: No module named cv2。这类问题一大半出在环境错位pip装到了系统Python而VSCode里选了解释器是虚拟环境或者反过来。我的建议是先用固定版本搭好组合再谈跑代码。# 创建虚拟环境避免污染系统Python python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate pip install opencv-python4.8.1.78 pip install numpy1.24.3 Pillow matplotlibPython版本建议用3.8到3.10之间的稳定版。opencv-python 4.8系列对Python 3.11的兼容没有问题但老车牌代码里如果用了cv2.findContours的旧返回值写法新版OpenCV只返回两个值而不是三个就会直接报错。把opencv-python钉在4.8.1.78既避开4.9里某些模块改名导致的兼容问题也避开太老版本没有cv2.imdecode参数支持的情况。numpy不要随意升到2.x很多OpenCV函数在numpy 2.x下处理uint8数组时会触发类型判断异常。装完后验证环境是否真的可用跑下面这行能打印版本号才算数python -c import cv2; print(cv2.__version__)3.2 从压缩包里找主流程源码结构通常长什么样标题说“下载即用”但压缩包解压后先不要急着双击某个文件。常见做法是看有没有requirements.txt或main.py如果源码把功能拆成了多个模块文件名一般会体现“定位”“分割”“识别”这三个阶段模板图会放在单独目录里。我用这套结构时习惯按下面这种模块划分project/ ├── main.py # 主入口串起整个识别流程 ├── plate_utils.py # 定位、分割、识别函数 ├── templates/ # 字符模板图 │ ├── hanzi/ │ └── chars/ ├── test_imgs/ # 测试图片 └── requirements.txt# main.py 的主流程骨架 from plate_utils import locate_plate, segment_chars, recognize_chars img imread_unicode(test_imgs/car1.jpg) crop locate_plate(img) if crop is None: print(未定位到车牌) exit(1) char_boxes segment_chars(crop) result recognize_chars(char_boxes) print(识别结果:, result)主流程拆成三个独立函数最大的好处是排查问题不用从头读代码。定位不准就先看locate_plate的输出图分割不对就看segment_chars的中间图识别错才去管模板。答辩时评委问哪一步你就能现场把中间图像打开来演示。3.3 最小可用代码跑通第一张图并且画出定位框不急着一次到位做全识别先把“输入图片到画出车牌框”这个小闭环跑通。这段代码不用模板不涉及字符是判断环境和解压包是否完好的试金石。import cv2 import numpy as np img cv2.imdecode( np.fromfile(test.jpg, dtypenp.uint8), cv2.IMREAD_COLOR ) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (100, 100, 60), (130, 255, 255)) mask cv2.morphologyEx( mask, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) ) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for c in contours: x, y, w, h cv2.boundingRect(c) if 2.5 w / max(h, 1) 4.5: cv2.rectangle(img, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imshow(result, img) cv2.waitKey(0) cv2.destroyAllWindows()这段代码只做两件事把蓝色区域筛选出来再按宽高比画框。如果它能稳定框住测试图里的车牌说明环境没问题、图像读取没问题、HSV阈值对这张图适用。之后再去套分割和识别不要一上来就追完整准确率那样出了问题分不清是环境还是算法。4. 把识别率从“能动”调到“能用”预处理顺序与参数调试4.1 蓝色车牌的HSV阈值按光照场景分档调参HSV阈值没有一组定死全天下通用的值我按场景分成三档实际调参时以此为基准做微调。场景H下限H上限S下限V下限正常日光10013010060傍晚/阴天1001308040夜间偏黄路灯951408040调参时不要只改数字要看掩膜图。cv2.imshow(mask, mask)直接显示二值掩膜蓝色车牌区域应该是完整的一块白色矩形如果白色区域有黑洞说明V下限太高把暗部滤掉了如果白色区域蔓延到天空或车身说明H范围太宽。记住一个规律H管颜色S管浓淡V管亮度。夜间最常犯的错是把V下限也当成和白天一样60结果车牌区域全部变黑识别直接失败。4.2 二值化顺序为什么先高斯模糊自适应阈值为什么比固定阈值稳定位框裁剪下来后进入字符分割之前要先灰度化和二值化。直接对原图做固定阈值通常效果很差因为车牌区域存在光照不均靠左上角的字符和靠右下角的字符明暗差异明显一个固定阈值必然牺牲掉一半笔画。gray cv2.cvtColor(plate_crop, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, # 邻域块大小必须为奇数 2 # 从均值减去的常数 )GaussianBlur的(5, 5)是卷积核尺寸标准差填0表示自动计算。为什么先模糊再做二值化因为传感器噪点会在二值图上形成大量孤立白色像素分割时被当成字符框。adaptiveThreshold的blockSize是核心参数11表示每个像素参考周围11×11区域的加权均值太小会把笔画内部空洞放大太大会把背景阴影也带进来C值2表示阈值比均值低2数值越小字符越容易被保留。如果二值化后字符边缘出现锯齿优先调大模糊核而不是改C值。4.3 形态学操作的kernel尺寸闭运算和开运算的顺序不能反形态学是预处理里最容易被忽略但翻车最狠的一环。闭运算先膨胀后腐蚀负责把字符之间的空隙填起来让蓝色区域连成整块开运算先腐蚀后膨胀负责去掉孤立噪点。顺序一定是先开后闭反过来的话开运算会把闭运算刚连上的缝隙又腐蚀断。车牌区域高度闭运算核宽闭运算核高用途约40像素175连起字符间隙约80像素339大图或近距离拍摄约20像素93小目标或远景核宽度要覆盖字符间缝隙的最大宽度核高度不能超过字符高度否则会把上下边框也并进来。判断核尺寸是否合适看定位框是否刚好包住蓝底边缘而不是把车灯或保险杠也框进来。4.4 批量验证写个循环统计准确率而不是一张一张肉眼看调参最怕凭感觉一张图调好换一张又挂。我习惯把测试图文件名直接命名成真实车牌号然后写脚本批量跑用文件名的字符串当标准答案。import glob import os def batch_validate(foldertest_imgs): files glob.glob(folder /*.jpg) correct 0 total len(files) for f in files: label os.path.basename(f).split(.)[0] pred recognize(f) # 返回完整车牌字符串 if label.upper() pred.upper(): correct 1 else: print(识别失败:, f, 期望, label, 实际, pred) print(正确率: %d/%d %.2f%% % (correct, total, correct / total * 100))注意这里比较时把预期和结果都转大写避免“粤”的大小写和车牌字符串中字母大小写不一致影响统计。每次改一个参数就重跑一遍观察失败集是否缩小。这个习惯能帮你一小时完成几十轮的参数试验而不是把时间花在来回打开图片窗口上。5. 避坑车牌识别项目里最常见的6类翻车现场5.1 图片路径带中文cv2.imread返回None现象代码在英文路径下能跑一旦把测试图放进“车牌识别”之类的文件夹cv2.imread直接返回None程序后续全崩。原因OpenCV在Windows下读取文件走的是窄字符API中文路径会读取失败。这是OpenCV自带的老问题不是代码逻辑问题。解决放弃cv2.imread改用np.fromfile读文件字节再交给cv2.imdecode解码。import numpy as np import cv2 def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)5.2 装完opencv-python还是报No module named cv2现象按照教程执行了pip install opencv-python控制台仍然报ModuleNotFoundError: No module named cv2。原因绝大多数情况是pip装到系统Python或另一个虚拟环境VSCode或IDE里选中的解释器却不是当前环境。少部分是Python 3.13这类太新版本没有对应的opencv wheel。解决先确认解释器路径再看包路径。在终端执行python -c import sys; print(sys.executable)然后在VSCode右下角点解释器选中同一个路径。如果是在Jupyter里跑同样要确认kernel对应的是哪个环境。5.3 汉字识别全乱数字和字母却很准现象结果里的“粤B 5D123”中“粤”被识别成“田”或“月”但“B”“5”“D”“123”基本正确。原因模板库里汉字模板太少或者模板本身是从印刷体截取的和实际车牌字体差异大。汉字笔画密集模板匹配对形近字的分辨能力本来就弱。解决收集二三十张测试图里的真实省份汉字切出来补充到模板库同一个汉字保持两份以上不同对比度的模板。另外给匹配分数加一个阈值低于0.5的字符输出“?”而不是硬猜至少让老师知道这里模型没有把握。5.4 蓝色广告牌被当成车牌框出来现象定位阶段在蓝色招牌、蓝色货车车身上画了一堆框分割和识别在后面对这些框做无效运算。原因HSV掩膜只认颜色宽高比只是粗筛蓝色广告牌的宽高比完全可能落在2.5到4.5之间。解决在定位后面加字符数验证对这个候选区域跑分割如果切出的有效字符数不在6到7个之间直接判定不是车牌并丢弃。车牌的字符结构是省份汉字加字母加数字一共7位旧式车牌加分隔符也还是7个有效字符。这个验证成本极低但过滤效果最明显。5.5 字符分割把“0”和“O”搞混模板匹配分数高达0.85却仍然错现象识别结果里“0”经常被当成“O”或者“8”被当成“B”。检查模板图和字符图发现字形差异确实不明显。原因模板匹配对像素重叠度敏感而“0”和“O”在同一个字体下笔画粗细一致时几乎完全重叠。真正能区分它们的是上下文——车牌第二位是字母后面第四位开始是数字但模板匹配是逐字符独立的不利用这个规则。解决在识别结果后处理阶段加位置规则校验。第二位强制当作字母处理第三到第七位强制当作数字处理不允许出现“O”“I”“Z”这类数字中不存在的字符。这一步能用最少的代码解决大量混淆问题。5.6 夜间图片识别率骤降白天一切正常现象白天测试图识别率超过八成换上夜间图片后定位框直接消失或框住一片黑乎乎的区域。原因HSV掩膜的V通道明度阈值把夜间暗部全部滤掉了轮廓检测根本找不到蓝底区域。解决降低V下限并在二值化前用CLAHE提升局部对比度。gray cv2.cvtColor(plate_crop, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)clipLimit控制对比度增强强度2.0是保守值继续调大会放大噪点tileGridSize是分块大小8×8表示把图像分成8×8的网格分别做直方图均衡能保住暗部细节的同时不把整个画面提亮到过曝。夜间识别没有银弹先把V下限降到40再看车灯反光区域是否干扰定位框必要时加一步按面积排序取最大候选区域。6. 把识别结果变成可用的东西结果导出、GUI封装和验收准备识别准确率稳定以后别再让结果只停留在控制台。毕设验收时把识别结果写进CSV是成本最低的效果提升手段。import csv with open(result.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([文件名, 识别结果]) writer.writerow([test_imgs/car1.jpg, 粤B5D123])编码一定要用utf-8-sig否则Excel直接打开会乱码。加上BOM头至少让老师双击文件时第一眼是正常的这个细节在验收现场很加分。同一份CSV如果要做数据统计改成追加模式每识别一张图就写一行多轮测试后可以直接按列排序筛选失败案例。GUI封装方面不需要上PyQt用OpenCV自带的窗口加一个结果文字就够了。定位框画完以后用cv2.putText把识别字符串覆盖在框上方再imshow出来既能看中间结果又能在答辩时现场演示完整流程。如果老师问“你这套识别率多少”不要只答数字直接切到批量验证脚本把失败图列表展示出来然后解释每条失败原因——是倾斜过大、是夜间低照度、还是模板缺失。模板库的维护要趁早。我现在的习惯是每收集一张新测试图就顺便把它的真实字符切进模板目录对照上一次识别结果做一次增量替换然后重跑批量脚本看整体准确率是上升还是下降。模板匹配这种方案的天花板就在模板覆盖度上模板越贴近真实车牌字体实测准确率越接近你演示时的数据。答辩前最后一天不要调参数了把失败图按原因分类整理成一张表格比多调出一个百分点的准确率更能说明工作量。希望帮到你。本文还有配套的精品资源点击获取