基于深度学习与OpenCV的车牌识别Python源码全流程解析
2026/9/24 18:34:50 网站建设 项目流程

简介:基于深度学习与OpenCV的Python车牌识别系统,是一套经过导师审核的毕业设计源码,评审分数95分以上,已调试至可直接运行。资源面向计算机相关专业学生和从业者,尤其适合作为毕业设计、期末课程设计或课程大作业参考项目,可帮助解决车牌检测、字符分割与识别等实际问题。压缩包共包含47个文件,6个Python源码模块覆盖系统入口、图像处理、数学工具、配置和识别流程;21张jpg与15张png图片提供多场景测试样本;2个7z压缩包存放字符训练数据,2个dat文件为SVM模型参数,整体大小28.81MB,结构清晰便于查阅。目前已有297人学习下载。使用者直接运行GUI.py即可启动系统,借助内置模型和测试图片快速查看识别效果;代码模块划分明确,便于理解车牌定位、字符分割与字符识别等关键步骤,也支持在此基础上扩展数据集或改进算法,完成课设报告与答辩演示。

1. 基于深度学习+OpenCV的车牌识别系统:这套Python源码应该怎么用

车牌识别是计算机视觉方向里最适合打通全流程的毕设选题之一。这套基于深度学习思路 + OpenCV的Python车牌识别源码,从图像输入到车牌定位、字符分割、字符识别输出是一条完成的链路,运行GUI.py就能看到最终效果,评审分能到95分以上说明整体完成度不低。它不是那种只贴了一个模型文件的黑匣子,定位、分割、识别的中间步骤都拆在main_VLPR.py、config.py等脚本里,适合正在做Python毕业设计或期末课程设计的学生,也适合想快速拿到一个可改可跑的基线系统的人。不过需要提前说明,这套实现主要针对蓝底白字车牌,换到新能源绿牌、大型车黄牌,或者夜间强光场景,大概率要调整参数。下文按我拆这个源码包的顺序,把定位、分割、识别三条链路的关键逻辑和踩坑点讲清楚。

2. 车牌定位链路:先搞清楚HSV粗筛和轮廓几何约束的价值

车牌定位是整个系统里最影响成败的一步。实际拍摄的图片里,车身、路面、标牌、阴影都会进入画面,如果第一步定位不准,后面分割和识别做得再精细也是白搭。常规思路是把定位拆成“颜色粗筛 → 形态学处理 → 轮廓几何校验”三段,这套源码的主流程也符合这个套路,但它的参数集中在config.py里,改起来比散写在各函数里顺手得多。

2.1 定位思路对比:为什么颜色域粗筛比边缘检测更稳

做车牌定位时,很多人第一反应是Canny边缘检测加轮廓查找。但在室外场景下,车身边缘、路面标线都会产生大量强边缘,边缘检测结果非常碎,后续要花很多精力做轮廓合并。相比之下,颜色域筛选更贴合车牌本身的结构特点:蓝底白字的车牌,底色是固定的蓝色系,在HSV空间里落在相对稳定的色相范围内,受光线明暗影响比BGR空间小很多。

常见的做法是先转到HSV,再对H、S、V三个通道做阈值截取,生成二值掩膜。核心代码大致如下:

import cv2 import numpy as np def locate_by_color(img_bgr): # 转HSV色彩空间,把色相和亮度分离 hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 蓝色车牌的常用HSV区间:H 100~124,S 90~255,V 90~255 lower_blue = np.array([100, 90, 90]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算,把车牌内部字符造成的空洞连成整体 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 提取外轮廓,作为候选车牌区域 contours, _ = cv2.findContours( closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) return contours, hsv

参数说明:

  • cv2.COLOR_BGR2HSV不能省。直接用BGR做固定阈值,早晚光照变化的场景下很容易失效
  • inRange输出单通道二值图,后续所有轮廓运算都基于它
  • MORPH_RECT内核尺寸(17, 5)与车牌宽扁结构对应,宽度大于高度
  • RETR_EXTERNAL只取最外层轮廓,避免字符轮廓混入候选

关于HSV阈值,我的经验是色相H的范围最稳定,但饱和度和亮度要按环境微调。S下限90比较适合白天正常光照;傍晚光线不足时,S和V下限可以一起下调到60左右,否则车牌区域因为饱和度不足会被直接滤掉。

2.2 config.py先验参数:改哪几个值能明显改变识别率

打开这个项目先看config.py是对的。定位相关的常量集中定义在这里,比在代码里到处搜数字好调得多。典型配置项如下:

参数项常见初始值作用调整场景
HSV蓝色下界[100, 90, 90]限定色相/饱和度/亮度的最低值傍晚漏检时调低V下限
HSV蓝色上界[124, 255, 255]限定候选色范围深蓝车身误检时压缩S上限
最小轮廓面积2000 像素过滤远处小噪点摄像头分辨率变化时按比例缩放
宽高比范围2.5 ~ 5.5筛掉方形或竖长框俯拍变形明显时放宽到2.2~5.8
形态学核尺寸(17, 5)连接车牌内部断裂区域车牌斜切严重时适当加宽

这些参数不是定死就能用。比如最小轮廓面积,如果直接用固定像素值,车辆距离摄像头10米和30米时,同一个车牌的面积差出好几倍。我的习惯是按输入图像高度做等比缩放,例如把阈值设为img_height * 3.0左右,再根据实证结果微调。

宽高比参数同样要留余量。真实道路图片里,车牌通常不是正对镜头,存在轻微透视形变,宽高比落在2.2到5.8之间都属于正常。如果一开始就卡到2.5到5.5,一个轻微俯拍的车牌可能直接被滤掉。

2.3 定位失败的三种中间态:没有框、错框、半框

第一种现象是整张图找不到候选框。先输出mask中间结果看一眼:如果mask全黑,说明HSV范围没有覆盖到车牌的色域,或者输入图像尺寸被Resize后颜色信息发生了变化;如果mask有大面积白色噪点,说明S和V的下限太低,把灰色路面也纳入了范围。

第二种现象是框住了车身或路牌。多数组件在几何校验环节没有做“填充率”检查。填充率指轮廓面积除以外接矩形面积,车牌的字符和边框结构让这个值通常在0.45到0.85之间,而车身光面区域与路牌文字区域的填充率往往明显偏离这个区间。把面积阈值和填充率同时用上,能滤掉一大类误检。

第三种现象是候选框位置偏移,边缘压到字符上。常见于斜拍场景。此时不要急着调HSV参数,问题不在颜色,而在于没有做透视矫正。倾斜车牌的字符在像素层面已经变形,二值化后也无法修正,必须先进行投影变换,这部分逻辑直接关系到下一章字符分割的质量。

定位这一步做扎实,后续识别错误率会肉眼可见地下降。建议每次换新测试图时,先保存定位框的可视化结果,确认框的位置准了再进入分割环节,不要跳过中间态直接看最终识别结果。

3. 字符分割链路:从候选框到能喂给分类器的单字符

定位拿到车牌区域后,下一步是把车牌拆成单个字符。蓝底白字车牌的结构是固定的:1个省份汉字 + 1个字母 + 5个字母数字混合,共7个字符。字符间距相对均匀,但汉字和字母的笔画复杂度差异很大,分割方案需要兼顾两者。常用方案是垂直投影法和连通域分析法,两者各有适用场景,也可以配合使用。

3.1 先透视矫正,再谈二值化:倾斜车牌的救法

实际拍摄时,车牌和镜头很少完全正对。透视形变会让字符宽度不一致,靠近镜头的字符宽,远离镜头的字符窄,直接按固定宽度切分必然出错。因此分割前必须做透视矫正,把倾斜车牌拉正。

常规做法是提取车牌的四角点,映射到固定尺寸的矩形。这里有两种情况:如果定位阶段已经拿到最小外接矩形,可以直接用矩形的四个角做变换;如果用的是轮廓外接正矩形,则需要额外拟合最小外接矩形,得到带旋转角度的四个点。

def correct_perspective(img, box_points): # box_points 为定位阶段得到的四个角点,顺序:左上、右上、右下、左下 src = np.float32(box_points) dst = np.float32([[0, 0], [440, 0], [440, 140], [0, 140]]) # 计算透视变换矩阵并执行变换 matrix = cv2.getPerspectiveTransform(src, dst) result = cv2.warpPerspective(img, matrix, (440, 140)) return result

代码逻辑说明:

  • src是源码中四个角点的坐标,dst是希望映射到的目标尺寸
  • 目标尺寸440×140是常用值,比例接近真实车牌440:140
  • warpPerspective输出就是矫正后的车牌图,后续二值化和分割都基于这张图

参数说明:目标尺寸不固定时,要注意保持宽高比在3:1左右,否则字符会被拉扁或拉长,直接影响后续分割效果。如果输入图像分辨率较高,440×140可以等比放大,比如880×280,字符细节保留更多,但分割耗时也会增加。

3.2 垂直投影分割:把车牌拆成7段的经典算法

透视矫正后得到的是规整矩形,此时用垂直投影法切分字符非常合适。原理是对二值图按列统计白色像素数,字符所在列的像素数明显大于字符间隙,通过阈值即可切出每段。

def split_chars_by_projection(bin_img): h, w = bin_img.shape # 按列统计白色像素数量,除以255归一化到像素个数 col_sum = bin_img.sum(axis=0) / 255.0 # 用行高的12%作为判定阈值 threshold = h * 0.12 flag_char = col_sum > threshold segments = [] start = None for i, is_char in enumerate(flag_char): if is_char and start is None: start = i elif not is_char and start is not None: segments.append((start, i)) start = None if start is not None: segments.append((start, w)) return segments

参数说明:

  • h * 0.12这个阈值决定哪些列被识别为字符列。阈值太高会漏掉笔画较细的数字“1”,阈值太低会把边框和铆钉噪声带进来
  • 返回的segments是左右边界列表,后续按边界切割原图即可

这个算法的坑在于字间黏连。如果车牌的边框没有完全去除,左边框和第一个字符会连在一起,形成一个超宽段;字符“川”和“B”的笔画也容易在低阈值下黏连。我的处理习惯是分割后检查每段宽度,如果某段宽度超过平均宽度的1.5倍,就把这段重新按连通域拆分,这就是下一小节要讲的二次校验。

3.3 连通域分割与二次校验:解决边框粘连和字符黏连

连通域分析法的思路是直接对二值图提取轮廓,每个轮廓的外接矩形就是一个字符候选区域。它更依赖二值图的质量,但灵活性更高。

def split_chars_by_contour(bin_img): contours, _ = cv2.findContours( bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [] for c in contours: x, y, w, h = cv2.boundingRect(c) # 过滤噪点:太小的点、过窄的竖条、过扁的横条都排除 if h > 12 and w > 4 and 0.2 < (w / h) < 1.2: boxes.append((x, y, w, h)) # 按x坐标从左到右排好顺序,保证输出和阅读顺序一致 boxes.sort(key=lambda b: b[0]) return boxes

参数说明:

  • h > 12是最小高度阈值,过滤掉细碎噪声;分辨率提高时该值要等比例放大
  • 0.2 < w/h < 1.2用来排除竖长条和横长条,字符的正常宽高比都落在这个范围附近
  • sort这步很重要,轮廓提取顺序不保证从左到右,必须显式排序

二值图质量不高时,连通域分析容易把汉字的多个笔画拆成多个连通域。比如“京”字的左右结构,如果笔画间有断裂,会被识别成两个独立轮廓。这时候需要先做一次闭运算,让笔画连起来,再进行轮廓提取。

把垂直投影和连通域结合使用,是这套源码里比较实用的做法:先用投影做粗分割,再用连通域对疑似黏连的超宽段做细拆。两种方法互补,能应对大多数边框粘连和笔画断裂的情况。

3.4 字符归一化:训练和推理必须共用同一套尺寸

分割出来的字符高度宽度不一致,尤其是汉字和字母、数字之间差异明显。在喂给分类器之前,必须把所有字符图归一到统一尺寸。常见选择是20×40或28×56,宽高比1:2左右,贴合字符形态。

def normalize_char(char_img, target_w=20, target_h=40): gray = cv2.cvtColor(char_img, cv2.COLOR_BGR2GRAY) # 统一缩放到目标尺寸,INTER_CUBIC 在放大时更平滑 resized = cv2.resize(gray, (target_w, target_h), interpolation=cv2.INTER_CUBIC) # 再转成0~1区间的浮点特征,方便后续分类器处理 return resized.astype(np.float32) / 255.0

这里有一个隐藏要求:训练字符识别模型时用什么尺寸,推理时也必须用相同尺寸,包括config.py里配置的字符宽高。很多人训练时用28×56,推理时却沿用默认的20×40,特征分布完全对齐不上,准确率会掉一大截,而且很难排查出来,因为代码没有报错。

还有一个细节是颜色通道。字符颜色信息对识别有一定帮助,但车牌字符图经过二值化后基本只剩轮廓结构,灰度图输入已经足够。如果后续要换成CNN,可以在灰度图基础上加一个边缘叠加层,这样既有纹理信息又不至于引入过多噪声。

4. 避坑:车牌识别毕设最常见的5个翻车点

很多同学代码逻辑上能跑通,但换一张新图就出问题。下面几条是我实际调试中反复遇到的高频问题,按“现象 → 原因 → 解决”的顺序整理,可以当排查手册看待。

4.1 现象:GUI.py启动后卡死,处理一帧要好几秒

原因:绝大多数情况不是死循环,而是识别全流程跑在了UI主线程里。部分实现对输入图片做全尺寸的HSV转换、形态学操作和轮廓查找,每一帧都在做大量冗余计算。

解决:把识别逻辑拆到独立线程,UI线程只负责显示结果;同时把输入帧先缩放到宽度不超过960再进入识别流程。实测相同代码,只加这一步缩放,处理耗时能减少将近一半。

4.2 现象:汉字识别出来永远是同一个字

原因:常见有三种。一是训练数据太少,模型学不到汉字差异;二是模型加载失败,svmchinese.dat路径错误,代码却仍然运行,没有报错;三是推理时字符归一化尺寸和训练时不一致,特征分布错位。

解决:先检查模型文件是否成功加载,可以在加载处加一段打印,输出模型的类别数和尺寸信息;再核对字符归一化尺寸是否与config.py一致;最后检查数据集中相似汉字的样本量,比如“沪”和“浙”这类结构相近的汉字,样本少的一方会被模型忽略。

4.3 现象:蓝色车身被识别成车牌,定位框框住整片车门

原因:HSV阈值过宽,尤其S饱和度上限设置太高,把和蓝色车牌相近的车身颜色也纳入了范围;加上轮廓几何校验里的填充率和宽高比约束没有同时生效,车身区域的矩形恰好落在了允许区间内。

解决:压缩HSV上界,重点调低S饱和度上限;同时给候选轮廓增加填充率判断:轮廓面积除以外接矩形面积,正常车牌在0.45到0.85之间,车身光面通常只有0.2到0.4。把这个值加入校验,能挡掉大多数车身误检。

4.4 现象:新能源绿牌和大型车黄牌识别结果一团糟

原因:这里的实现默认针对蓝底白字车牌设计。绿牌、黄牌的HSV色相范围完全不同,字符颜色也不一样,直接套用蓝色阈值当然无效。

解决:毕设需要覆盖多色车牌时,可以增加颜色分支。先识别颜色类型,输入HSV范围不同的车牌则走对应的定位路径,分割和识别环节共用。如果只做论文演示,建议测试素材集中在蓝牌,避免在颜色适配问题上消耗太多时间。

4.5 现象:换成自己采集的数据集训练后,准确率不升反降

原因:数据不均衡和特征未归一化是最常见的两个原因。例如数字“0”样本2000张,字母“I”只有40张,SVM的决策边界会被多数类带偏;特征向量的量纲不一致时,距离计算也会失真。

解决:训练前先做标准化处理,让特征零均值单位方差;再按类别做最简单的上下采样,让每类样本量相差不超过3倍。训练完成后,务必用一套完全没参与训练的数据做验证,测试集上的准确率才有参考价值。

4.6 现象:svm.dat和svmchinese.dat加载后识别结果完全不对

原因:两个模型文件的数据结构不同。svm.dat通常对应字母数字分类器,svmchinese.dat对应汉字分类器。部分实现对加载顺序有隐含要求,先识别汉字还是先识别字母数字,顺序错了后面的结果自然不对。

解决:查看main_VLPR.py的调用链,确认每次推理时使用的模型类型。汉字位只允许出现省份简称,字母数字位不允许出现汉字。可以打印每个字符位的模型输出概率分布,概率明显分散时说明模型类型和字符位不匹配。

5. 最后一步:把SVM替换成CNN,并跑一组可信的泛化验证

前面几章用的字符识别核心是SVM。如果你的毕业设计希望在“深度学习”这个方向上有更实质的体现,不需要推翻整个项目,只需要替换识别环节。定位和分割输出的都是归一化后的字符图,对CNN来说是天然输入。

5.1 用Keras替换SVM的简易实现

一个可用的CNN骨架如下:

from tensorflow import keras from tensorflow.keras import layers def build_cnn(input_shape=(28, 56, 1)): model = keras.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dense(65, activation='softmax') ]) model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) return model

代码说明:

  • 输出层65对应“31个省份汉字 + 24个字母 + 10个数字”的闭集。注意字母I和数字1、字母O和数字0建议合并为同一类别,否则标注冲突会严重拉低训练准确率
  • 输入特征(28, 56, 1)对应归一化后的单通道字符图,和前面的normalize_char输出一致
  • 替换SVM推理时,用model.predict(字符图)得到每个类别的概率向量,取argmax即得到字符下标

需要注意,网络复杂度并不需要很高。字符图只有28×56,两层卷积已经足够,堆太多层在小数据集上只会过拟合。

5.2 验证模型性能的可靠方法

数据划分建议按8:2随机切分,训练用80%,验证用20%。字符数据要按图片分组切分,避免同一张车牌的不同字符出现在训练和验证两组里,否则验证准确率虚高,答辩时容易被问穿。

实际测试时,记录三个指标:定位耗时、单字符识别耗时、整张车牌的最终准确率。至少准备三张从未出现在训练过程中的实拍图,正对、侧倾、光照较差各一张,逐张跑完并记录结果。这些数据比一张运行截图有说服力得多。

5.3 一个值得保留的调试习惯

从那以后,我每次拿到车牌识别相关源码,第一件事就是把测试图分成“训练集里出现过的”和“完全没见过的”两组,两套图各跑一遍,再决定要不要碰模型训练部分的调参。这个习惯帮我挡掉了不少自我感觉良好、实则在过拟合的假象。位置、分割、识别三个环节逐个确认到位,整个系统的可靠性才真正可控。希望这一套拆解下来的经验,能帮你在自己的毕设里少踩几个同样的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询