
简介这套基于 Python OpenCV 开发的国内车牌识别系统源码适合正在完成毕业设计、课程设计或期末大作业的计算机视觉方向学习者。项目集成 Tkinter 图形界面下载到本地后运行主程序即可体验完整的车牌检测、字符分割与识别流程无需自行搭建复杂环境可直接作为毕设项目使用。资源包共 16 个文件以 Python 源码为主同时包含训练好的 CNN 与 UNet 模型权重文件、演示视频、动态效果图和使用说明文档。源码中既有图像处理核心模块也有视频转图片、图片转视频等辅助工具脚本整体结构清晰方便对照学习与二次开发压缩包整体大小约 26.7MB从拆解视频帧到模型预测各环节代码均有覆盖。目前已有 1085 人学习下载对于想要快速上手车牌识别项目、理解 OpenCV 与深度学习模型配合使用的同学这份源码能提供完整的参考框架和可直接运行的实验环境。1. 国内车牌识别系统Python OpenCV 双模型拿到就能跑的毕设方案做课程设计或毕设时最怕的不是模型精度不够而是代码能跑但讲不清原理答辩时一问就翻车。这套基于 Python OpenCV 的国内车牌识别系统和网上那些只贴一个 YOLO 检测脚本的“半成品”不一样它的完整链路是Unet 做车牌定位 → CNN 做字符分类 → Tkinter 做 GUI 界面从图像输入到结果展示一条龙闭环。你下载到本地后装好依赖直接运行 GUI.py就能看到完整的交互界面支持单张图片识别和视频流识别自带测试视频和已训练好的模型文件不需要自己再训练就能出结果。这套方案比较适合正在做毕设、课程设计或期末大作业的深度学习 / 计算机视觉方向同学既能直接用也适合你拆开代码研究“定位 识别”每一步具体是怎么串起来的。2. 项目结构与技术选型先把文件清单读懂再决定从哪个文件下手2.1 文件构成六个核心文件分别负责什么拿到压缩包后别急着双击运行先把根目录下的文件过一遍。这个项目的文件命名很直白基本一眼能看出各自的作用。解释一下我拆包后整理的逻辑文件/目录类型职责GUI.pyPython 脚本主程序入口Tkinter 图形界面图片与视频识别的人机交互层imgGUI.pyPython 脚本处理静态图片识别流程核心识别逻辑在这里被调用vidGUI.pyPython 脚本处理视频流识别流程从视频中逐帧提取并识别车牌core.pyPython 脚本识别核心流程管线把定位、分割、识别串起来CNN.pyPython 脚本定义字符分类模型结构用于区分汉字、字母、数字Unet.pyPython 脚本定义 Unet 分割模型结构用于从复杂背景中分离车牌区域train.pyPython 脚本模型训练入口含数据预处理、训练参数配置cnn.h5 / unet.h5模型权重已训练好的 Keras 模型文件分别对应字符识别与车牌定位demo_car.mp4测试视频实拍车辆视频用于验证视频流识别效果这个结构有一个很好的拆解逻辑GUI 层是纯展示core.py 是编排层CNN.py 和 Unet.py 是模型定义层train.py 是训练层。如果你的目标是做毕设优先看 core.py 和 GUI.py如果你的目标是理解算法优先看 CNN.py 和 Unet.py如果你要换数据集重新训练从 train.py 开始。2.2 为什么用 Unet 做定位而不是 YOLO 或传统形态学很多同学看到车牌识别第一反应是 YOLO 系列做目标检测。这个思路没错但 YOLO 的检测框是矩形框且需要较大的标注成本。传统形态学方法颜色阈值 轮廓提取也有不少教程但对光照和背景的要求很苛刻稍微有点阴影或者车牌轻微倾斜就废了。这个项目选择 Unet 做分割式定位是把“找车牌”的问题转成“逐像素分类”的问题每个像素点被预测为“属于车牌”或“不属于车牌”最终得到一个 mask。这样做的直接好处有两个一是能应对车牌在画面中偏小、倾斜、被部分遮挡的场景因为像素级分类比框级回归精度更高二是 Unet 的输出天然是二值图配合 OpenCV 的轮廓查找可以很稳定地提取四边形区域。之后再做倾斜校正交给字符分割和识别模块。从代码结构看Unet.py 里定义了标准的 U 型结构下采样路径提取语义特征上采样路径恢复空间分辨率。我贴一下 Unet 的核心结构你自己看模型文件时能对上from tensorflow.keras.layers import Conv2D, MaxPooling2D, UpSampling2D, concatenate from tensorflow.keras.models import Model from tensorflow.keras.layers import Input # 输入是灰度图归一化后的 512x512x1 def unet(input_size(512, 512, 1)): inputs Input(input_size) # 下采样路径连续两次卷积 ReLU然后池化下采样 conv1 Conv2D(64, 3, activationrelu, paddingsame)(inputs) conv1 Conv2D(64, 3, activationrelu, paddingsame)(conv1) pool1 MaxPooling2D(pool_size(2, 2))(conv1) conv2 Conv2D(128, 3, activationrelu, paddingsame)(pool1) conv2 Conv2D(128, 3, activationrelu, paddingsame)(conv2) pool2 MaxPooling2D(pool_size(2, 2))(conv2) conv3 Conv2D(256, 3, activationrelu, paddingsame)(pool2) conv3 Conv2D(256, 3, activationrelu, paddingsame)(conv3) pool3 MaxPooling2D(pool_size(2, 2))(conv3) # 底部最深层特征提取 conv4 Conv2D(512, 3, activationrelu, paddingsame)(pool3) conv4 Conv2D(512, 3, activationrelu, paddingsame)(conv4) # 上采样路径与下采样对应通过拼接融合浅层与深层特征 up5 UpSampling2D(size(2, 2))(conv4) up5 concatenate([up5, conv3], axis-1) conv5 Conv2D(256, 3, activationrelu, paddingsame)(up5) conv5 Conv2D(256, 3, activationrelu, paddingsame)(conv5) up6 UpSampling2D(size(2, 2))(conv5) up6 concatenate([up6, conv2], axis-1) conv6 Conv2D(128, 3, activationrelu, paddingsame)(up6) conv6 Conv2D(128, 3, activationrelu, paddingsame)(conv6) up7 UpSampling2D(size(2, 2))(conv6) up7 concatenate([up7, conv1], axis-1) conv7 Conv2D(64, 3, activationrelu, paddingsame)(up7) conv7 Conv2D(64, 3, activationrelu, paddingsame)(conv7) # 输出层二分类sigmoid 输出 0~1 的置信度图 outputs Conv2D(1, 1, activationsigmoid)(conv7) return Model(inputs, outputs)这里有个关键点模型输入用了 512x512 灰度图而不是彩色图。原因在于 Unet 定位放的是“纹理与形状先验”——车牌区域的边缘梯度、字符排列特征在灰度空间里足够明显转灰度还能压缩计算量。训练时输入尺寸是固定的但推理时 core.py 里会先做 resize 再还原坐标这个细节后面在避坑部分会单独说。2.3 train.py 训练参数解读怎么看懂别人的训练配置train.py 是很多人忽略但值得细读的文件。它能告诉你模型的训练数据长什么样、输入尺寸是多少、训练了多少轮。我把训练部分的关键参数整理一下# train.py 核心训练参数 EPOCHS 50 # 训练轮数 BATCH_SIZE 4 # 批大小 IMG_W, IMG_H 512, 512 # 输入图像尺寸 TRAIN_RATIO 0.8 # 训练/验证集划分比例 LEARNING_RATE 1e-4 # 学习率逐行解释批大小设成 4 是因为 Unet 输入尺寸大、显存压力大如果你自己的显卡只有 4GB 显存训练时 BATCH_SIZE 甚至要降到 2TRAIN_RATIO 用 0.8 是常规做法但要注意数据是否做了随机增强旋转、平移、亮度扰动train.py 里如果没写数据增强这往往是后续模型泛化能力不足的主要原因学习率用 1e-4 配合 Adam 优化器属于稳妥选择不建议改成 1e-3Conv 层密集的模型用大步长很容易震荡。提示用 cnn.h5 和 unet.h5 做推理前注意检查 Keras 的版本。h5 权重文件在 TensorFlow 2.x 和 1.x 下加载方式略有差异下文避坑章节会给出具体解决办法。3. 从图片到车牌号core.py 识别全流程拆解每个环节怎么接3.1 一张图进一个车牌号出管线逻辑core.py 是整个系统的串联核心。理解它你就理解了整套系统是怎么把“定位、分割、识别”三步拼起来的。我按执行顺序把流程拆开读图 → 缩放到模型输入尺寸 → Unet 推理得到车牌 mask → mask 二值化 → 轮廓查找 → 获取车牌最小外接矩形 → 透视变换校正 → 字符分割 → 单字符送入 CNN 分类 → 拼接结果。代码里的核心逻辑我简化贴出你对照自己的项目看import cv2 import numpy as np from Unet import unet from CNN import cnn_model # 加载两个模型unet 定位车牌区域cnn 识别字符 unet_model unet() unet_model.load_weights(unet.h5) cnn cnn_model() cnn.load_weights(cnn.h5) def recognize_plate(img_path): # 步骤1缩放至统一尺寸保证模型输入稳定 img cv2.imread(img_path) h, w img.shape[:2] resized cv2.resize(img, (512, 512)) gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) input_blob gray.astype(float32) / 255.0 input_blob input_blob.reshape(1, 512, 512, 1) # 步骤2Unet 输出 mask二值化提取前景 mask unet_model.predict(input_blob)[0, :, :, 0] mask (mask 0.5).astype(uint8) # 步骤3调整回原图尺寸找到车牌轮廓 mask_original cv2.resize(mask, (w, h)) contours, _ cv2.findContours(mask_original, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取面积最大的轮廓视为车牌区域 plate_contour max(contours, keycv2.contourArea) rect cv2.minAreaRect(plate_contour) # 带角度信息的最小外接矩形 # 步骤4透视校正转成水平车牌图 plate_img four_point_transform(img, cv2.boxPoints(rect)) # 步骤5字符分割灰度 二值化 轮廓筛选 chars split_chars(plate_img) # 步骤6每个字符过 CNN 分类拼接字符串 plate_number .join([predict_char(cnn, c) for c in chars]) return plate_number识别流程里值得注意的参数有三个mask 阈值取 0.5 是默认选择实际运行时如果 Unet 输出置信度普遍偏低比如阴影干扰严重可以下调到 0.4多试几次找到最优平衡点这个属于经验调参轮廓查找用 RETR_EXTERNAL 是为了只取最外层边框避免字符内部孔洞被识别成独立轮廓取面积最大轮廓的假设是车牌在画面中占比最大如果视频里同时出现多个车辆这个逻辑需要改成取所有面积超过阈值的轮廓。3.2 CNN.py 的字符分类七个字符是怎么被逐一识别的CNN 部分解决的是“定”到车牌后怎么把每个字符读出来。国内车牌结构是第一个位置是汉字省份简称第二个是字母发牌机关后面五个是字母数字混合。这套模型使用的 CNN 结构不算复杂但胜在稳定。看代码能发现它使用了几层卷积加全连接的结构核心是每个字符先缩放到固定尺寸再进网络输出一个类别概率。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def cnn_model(num_classes65): # 65 类对应31个省份汉字 24个字母 10个数字部分字母不用 model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shape(32, 32, 1)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activationrelu), Flatten(), Dropout(0.5), Dense(256, activationrelu), Dense(num_classes, activationsoftmax) ]) return model这个 CNN 把输入固定为 32x32 单通道灰度图三组卷积提取特征后接全连接层。最后按 softmax 维度是 65每个类对应一个字符。特别提醒注意 Dropout(0.5)有了它即使你是用自己采样的少量车牌字符数据训练也不容易过拟合这层千万别去掉。字符分割是整条链路里最依赖“经验参数”的地方。核心做法是按列投影先把车牌区域转灰度、二值化然后计算每列的像素和连续有像素的列段就是一个字符的边界。由于字符间有固定间隙这个方法的准确率在正常拍摄角度下很高。代码片段如下def split_chars(plate_img): # 预处理灰度化 自适应阈值二值化 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影统计每列白色像素数量 col_sum np.sum(binary, axis0) # 找到连续的列段字符区域宽度小于阈值的视为噪声 char_cols [] in_char False start 0 for i, val in enumerate(col_sum): if val 0 and not in_char: start i in_char True elif val 0 and in_char: char_cols.append((start, i)) in_char False # 根据字符间距判断实际字符边界过滤掉过窄的干扰段 # 国内车牌通常为 7 个字符取宽度最大的前 7 个列段 ...二值化用了 Otsu 自动阈值好处是不同光照下不用手动调。4. GUI 与视频识别Tkinter 界面怎么把模型包成毕设可演示的作品4.1 主界面运行流程GUI.py 都干了什么很多算法模型跑通了但作业一交就显得单薄因为缺一个可视化的界面。这套源码用 Tkinter 做了桌面 GUI直接运行 GUI.py 就能弹窗操作。Tkinter 是 Python 内置的 GUI 库优势是不用额外装包、打包成 exe 也方便适合毕设演示场景。界面逻辑大致是这样左边是图片预览区右边是识别结果展示区底部一排功能按钮。图片识别按钮绑定 imgGUI.py 里的处理函数视频识别按钮绑定 vidGUI.py。你选一张本地图片程序会展示标注了车牌位置的图像并在文本框输出识别出的车牌号。核心交互代码大致是import tkinter as tk from tkinter import filedialog from imgGUI import process_image class PlateApp: def __init__(self): self.root tk.Tk() self.root.title(国内车牌识别系统) self.root.geometry(900x600) # 按钮事件绑定点击后调用图片识别流程 self.btn_open tk.Button(self.root, text选择图片, commandself.open_image) self.btn_open.pack() def open_image(self): path filedialog.askopenfilename(filetypes[(Images, *.jpg *.png)]) if path: result process_image(path) # 调用核心识别流程 self.show_result(result)注意 process_image 返回的应该是一个字典或元组包含识别结果字符串和标注后的图像数组。这里建议你自己运行一遍后根据返回值的结构微调展示逻辑。如果你想把界面做得更像“产品”可以在左侧加一个车牌区域裁剪图的预览这个改动很小但答辩演示时的效果差距很大。4.2 视频识别按帧处理的性能取舍vidGUI.py 处理的是视频文件demo_car.mp4 就是配套测试素材原理不复杂就是 OpenCV 逐帧读取每一帧都走一遍完整识别流程。这里最大的现实问题就是速度单帧识别需要两次模型推理Unet CNN在纯 CPU 环境下 1 秒只能处理 1-2 帧所以你看到的视频识别其实是抽帧识别而不是实时识别。实际运行时我发现视频识别里有一个必要的优化操作——跳帧处理。如果每帧都做全流程视频播放会卡成幻灯片。项目采用了隔几帧推理一次的策略代价是车牌在画面中的位置必须相对稳定车辆快速掠过时容易漏检。我的建议是如果你要拿视频识别做演示提前把车速放慢或者把跳帧间隔调低比如每 3 帧推理一次效果会好很多。这个参数一般在 vidGUI.py 中对应一个帧计数变量改动代价很小。界面部分还有一个演示上的小技巧视频识别时把识别到的车牌号和当前帧时间戳同时显示在界面上配合一个文本框做识别历史列表看起来会专业很多这个基本不用改核心代码靠 Tkinter 的 Text 组件追加文本就能实现。5. 避坑与排查环境配置、模型加载、识别失败的十个常见问题5.1 依赖与环境的坑OpenCV、Keras、TensorFlow 版本冲突先说最可能劝退新手的坑h5 文件加载失败。现象运行 GUI.py 后立刻报错No such file or directory: unet.h5或者AttributeError: str object has no attribute decode。原因分析第一种是路径问题你大概率没把运行目录切换到项目根目录代码里写的相对路径找不到权重文件第二种是 h5py 版本不兼容 Keras 2.x典型的“旧权重配新库”问题。解决办法路径问题用os.chdir()强制切到项目所在目录或者在 GUI.py 顶部把模型路径改成绝对路径。h5py 版本问题如果你用的是 Python 3.8 或以上建一个虚拟环境后固定安装h5py2.10.0、tensorflow2.4.0、opencv-python4.5.5.64这套组合我实测下来能顺畅加载 h5 权重不会出现 decode 报错。第二个高频坑是 OpenCV 的轮廓函数在不同版本中返回值数量不一致。OpenCV 4.x 里cv2.findContours返回两个值OpenCV 3.x 返回三个值。如果你装了 OpenCV 3.4.1代码里contours, _ cv2.findContours(...)必然报错。解决思路是写一行兼容代码# 兼容 OpenCV 3.x 与 4.x 的不同返回值 result cv2.findContours(mask_original, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(result) 2: contours result[0] else: contours result[1]这个坑很隐蔽因为只有旧版 OpenCV 用户才会遇到但高校机房环境里很常见。5.2 识别效果类问题定位不准、字符错乱定位框漂移是 Unet 推理常见的现象。表现是识别出的车牌区域明显偏离真实位置不是整体偏上就是偏下。原因大概率是原图缩放到 512x512 后被拉伸变形Unet 对严重形变敏感坐标还原时产生系统性偏移。解决办法是等比例缩放填充而不是直接 resize把原图缩放到宽边 512短边 pad 成灰色推理后再从 mask 裁掉 padding 区域这一条经验对任何分割模型都通用。字符识别错乱分两种首汉字识别错和数字字母混淆。首汉字错基本是训练数据中省份汉字样本不均衡导致的cnn.h5 里有些生僻省份的样本少识别效果就差。数字字母混淆则要看分割环节如果分割出的单个字符宽高比严重失调CNN 输入时被压变形L、I、1 这类相似字符就容易混。解决方向是给分割模块加一个字符宽高比校验比如正常字符的宽高比在 0.3 到 0.7 之间超出这个区间的列段丢弃或合并代码改动不超过十行但对精度提升很明显。还有一个小坑容易被忽略输入 GUI.py 的测试图片不能是手机拍的高分辨率大图。如果图片宽度超过 2000 像素直接缩放 512 会导致小车牌区域太小Unet 很难分割出来。我一般会先做一次长边裁剪操作把图像按比例压缩到 1000 像素以内再进模型识别成功率会明显上升。另一个值得记录的现象白牌车新能源绿牌实际是渐变绿但某些地区临时牌照是全白底识别效果差。原因是训练数据里绿牌和蓝牌为主白牌底色导致二值化阈值计算失败。解决思路是在字符分割前加一步颜色判断计算车牌区域的 RGB 均值如果接近白色就对二值化做反向操作并调小阈值。5.3 GUI 卡死与线程问题、打包问题GUI 界面在做视频识别时整个窗口无响应这是典型的 Tkinter 主线程阻塞问题。原因视频识别循环在 Tkinter 的主线程里执行前端的update()被密集的推理计算阻塞界面自然假死。解决方法把视频识别放进threading.Thread推理完成后通过队列回传结果。但要注意回调里不要直接修改 Tkinter 控件变量否则可能引发线程安全问题。我通常的做法是用root.after()在主线程里定时从队列取值刷新界面。还有一个打包问题如果你用 PyInstaller 打包 GUI.py 成 exe运行时提示找不到 h5 模型文件。这几乎是必踩的坑。原因很简单PyInstaller 默认不会把 h5 权重和 mp4 测试视频一并打入。解决的办法是编辑.spec文件在datas参数中显式添加资源文件路径例如# 打包命令注意把项目根目录下的资源文件包含进去 a.datas [(unet.h5, unet.h5, DATA), (cnn.h5, cnn.h5, DATA)] pyinstaller -F -w GUI.py --add-data unet.h5;. --add-data cnn.h5;.Windows 下用分号分隔Linux/macOS 用冒号。打包完成后务必在非项目目录下测试一次 exe确认能独立找到模型文件再提交。6. 进阶技巧把模型的省份识别能力和边缘场景拉高一个档拿到这套源码如果你不只是想交作业而是想让它成为你简历上一个有说服力的项目我建议花半天时间做三件事替换省份字符分类、扩充测试集、给 GUI 加批量识别功能。第一件事是把 cnn.h5 替换成你本省数据增强后的版本。打开 train.py找到字符数据集加载部分。自己拍 2000 张车牌字符图片按省份分类存放做随机旋转、对比度扰动# 数据增强任意角度和亮度扰动 from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rotation_range5, width_shift_range0.05, height_shift_range0.05, brightness_range(0.8, 1.2), zoom_range0.1 ) # 用增强器重新训练 CNN而不是直接加载原 h5 继续推理 train_generator datagen.flow_from_directory(dataset/chars, target_size(32, 32), batch_size32, class_modecategorical) # cnn_model.fit(train_generator, epochs20, validation_dataval_generator)增强后的模型对新场景的适应性会明显更好而且这段代码在论文的“实验与改进”部分可以直接写成一节。第二件事是造一个边界测试集。我一般会拿 20 张不同环境拍的车牌图含夜间、雨天、轻微倾斜、部分遮挡四种情况跑一遍系统并记录失败案例。这一步的意义不在调参而在于你知道这套系统的能力边界在哪。实测下来夜间场景受制于二值化质量故障率偏高倾斜超过 30 度时透视校正有概率把字符拉歪。知道这些边界后答辩时面对老师的追问可以给出合理的技术解释而不是含糊带过。第三件事是给 GUI 加上批量图片识别并导出 CSV 结果。改动量不大遍历文件夹里的所有图片逐张调用 process_image把识别到的车牌号写入 csv。这个功能看似简单但它直接把你的系统从“演示工具”变成了“可用的小工具”也让你的代码量有了实质性的增长。核心逻辑二十行不到import csv, os def batch_recognize(folder_path, output_csv): with open(output_csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([filename, plate_number]) for name in os.listdir(folder_path): if name.lower().endswith((.png, .jpg, .jpeg)): result process_image(os.path.join(folder_path, name)) writer.writerow([name, result])做这三件事加起来的代码量不超过一百行但每个改动都对应一个可以写进毕业设计里的真实实验。从那以后我每次拿到别人的开源项目都会强制走一遍这个流程先跑通看效果再换自己的数据验证泛化能力最后加一个批量处理的工具函数补完这三点项目才真正变成了你能讲明白的东西。希望这套车牌识别源码也能帮你少踩几个版本和环境上的坑顺利跑通、做完、讲清楚。本文还有配套的精品资源点击获取