☰
基于Python与OpenCV的车牌识别系统:Unet+CNN源码详解
2026/9/28 15:52:35 网站建设 项目流程

简介:面向计算机视觉方向的高校学生,这套基于Python与OpenCV的车牌识别系统源码包适合毕业设计、期末大作业或课程设计,项目包含核心识别模块、模型训练脚本和图形化交互界面,覆盖车牌定位、字符分割与识别等典型流程。压缩包共15个文件,以9个Python脚本为主干,包含Unet和CNN两个H5模型文件、3个GIF演示动图及1个MP4效果视频,整体大小26.7MB。Python脚本涵盖GUI界面、视频切帧与图像转视频等工具,可从头理解数据预处理、模型训练到结果展示的完整链条。资源已获导师认可并高分通过,部署简单,下载后稍作配置即可运行,基础偏弱也能上手实战。目前已有635人学习下载,适合作为参考项目复用或二次开发,对理解车牌识别系统的工程落地很有帮助。

1. 车牌识别系统源码:能跑的毕设,不只是能答辩

做毕设最怕的不是不会写代码,而是代码调不通、效果出不来、答辩时被老师一问就卡壳。这份基于 Python 与 OpenCV 的车牌识别系统源码,最大的价值在于它不是半成品,而是一套已经跑通、被导师认可并高分通过的完整项目。压缩包里包含了 Unet 分割模型、CNN 识别模型、训练脚本、GUI 界面、演示视频和数据集处理工具,解压后按照环境配置说明把依赖装上,就能看到识别效果。

这套系统走的不是“先检测车牌位置、再逐字符识别”的传统两步走,而是用 Unet 做语义分割定位车牌区域、用 CNN 做字符分类识别,两个模型配合起来,比单纯用 OpenCV 边缘检测再模板匹配的方式要稳得多。适合三类人:正在做图像处理方向毕设的学生、需要课设代码做参考的开发者,以及想快速实现一个能演示的识别 demo 的入门者。

2. 项目构成与数据流:先搞清楚每个文件是干什么的再动手

2.1 模型文件与主控脚本的对应关系

解压这份源码后,你会看到一堆 .py 文件和 .h5 权重文件,第一次打开容易懵。我把核心文件按功能分成三组:

第一组是模型文件,包括 cnn.h5 和 unet.h5。cnn.h5 是训练好的字符识别模型权重,它接收单张字符图像,输出对应的字符类别;unet.h5 是车牌区域分割模型权重,它接收整张车辆图像,输出车牌区域的掩码图。这两个模型文件是项目的“黑匣子”,你不用重新训练,直接加载就能用。

第二组是训练与核心逻辑脚本,包括 train.py、Unet.py、CNN.py 和 core.py。train.py 是训练入口,负责启动 Unet 和 CNN 的训练流程;Unet.py 定义了 Unet 网络结构和分割模型的训练过程;CNN.py 定义了字符识别网络结构和训练过程;core.py 是核心逻辑模块,把分割和识别串起来,对外提供 API 函数,供 GUI 调用。

第三组是界面与工具脚本,包括 GUI.py、vidGUI.py、imgGUI.py、videospilt.py 和 imgtovid.py。GUI.py 是图片版图形界面,imgGUI.py 是增强后的图片处理界面,vidGUI.py 是视频版图形界面。videospilt.py 把视频按帧拆成图片,imgtovid.py 把处理后的图片重新合成视频,这两个工具是用来制作演示素材的。

2.2 推理链路:从车辆图片到车牌字符串

整个系统的数据流可以概括为一条完整链路。以 vidGUI.py 为例,它读取视频帧后传给 core.py,Unet 分割模型把头定位到车牌区域,裁剪出来再做字符分割,最后送入 CNN 分类器逐个识别。为了把这条链路说清楚,我画一张核心调用关系的逻辑图:

视频帧/图片 -> core.py |-- Unet 分割模型 (unet.h5) --> 车牌区域掩码 -> 定位裁剪 |-- 字符分割 (轮廓查找/投影法) --> 单字符图 |-- CNN 分类模型 (cnn.h5) --> 逐字符结果 |-- 拼接字符串 -> GUI 显示

实际运行时的代码路径比这更直观,如果你只想快速验证模型效果,直接跑 vidGUI.py 加载 demo_car.mp4 就行。核心逻辑集中在 core.py 中,它做了三件事:调用 Unet 得到分割掩码,对掩码做形态学处理和轮廓提取得到车牌外接矩形,然后按列投影或者连通域分析切出字符,最后把每个字符图缩放成 CNN 输入尺寸,批量预测得到最终车牌号。

2.3 数据集与增强逻辑:训练数据从哪来

源码里没有附带完整训练数据集,只给了训练脚本和示例图片,这是因为训练数据涉及车牌隐私,大多数毕设项目都不会直接公开。但 Unet.py 和 CNN.py 里都内置了数据增强逻辑,包括随机旋转、平移、缩放、亮度扰动、高斯噪声等操作,这是 Python 生态里做图像识别的标准做法。

如果你有收集到的车牌图片,按这个目录结构放置就能启动训练:训练集根目录下分 images 和 masks 两个子目录,images 放原始车辆图,masks 放对应的车牌区域掩码图,命名保持一致。CNN 的数据集按字符类别分子文件夹,0-9 和 A-Z 各建一个目录,图片按字符内容归类存放。

3. Unet 车牌区域分割:把“车牌子在哪”这个问题交给语义分割

3.1 为什么选 Unet 而不是传统边缘检测

传统 OpenCV 车牌定位方法靠颜色过滤(蓝色/黄色)、边缘检测(Sobel/Canny)、形态学闭运算再找矩形轮廓,晴天顺光效果还行,一到阴天、逆光、夜间或者车身颜色和车牌接近的场景就废了。这套源码用 Unet 做分割回归,本质上是把一个工程玄学变成了一个可训练的学习问题,模型学习的是“车牌区域”这个语义概念本身。

Unet 的优势是编码器-解码器结构加跳跃连接。编码器逐层下采样提取高维语义特征,解码器逐层上采样恢复空间分辨率,跳跃连接把底层细节信息直接传给解码器,让分割边缘更精细。在车牌场景里,车牌边界清晰但有时被遮挡、部分损坏或被泥污覆盖,Unet 对这类噪声输入更鲁棒。

3.2 训练 Unet 时需要注意的参数

Unet.py 里有一些关键超参数,这些直接影响分割效果。IMAGE_SIZE 决定输入图片统一缩放的尺寸,一般设成 256 或 512,太小会让小车牌细节丢失,太大会显存吃紧。BATCH_SIZE 根据显卡显存调整,常规设 4 或 8,显存不够就减小图片尺寸。EPOCHS 设为 50 到 100,具体看训练集大小,用小数据集训练时建议设 100 配合早停。

loss 函数用的是 binary_crossentropy 加 sigmoid 激活,因为车牌分割是单类二分类问题,每个像素只需要判断“是车牌/不是车牌”。优化器选 Adam,初始学习率 1e-3,训练到一半时降低为 1e-4。如果你发现分割掩码里有很多小的噪声块,在 Unet 的输出后面加一个条件随机场后处理或者简单的中值滤波就能改善,源码里常用的是中值滤波加连通域过滤的方式。

3.3 跑通分割模型:加载权重看输出

加载 unet.h5 验证分割效果的代码非常简单,核心部分像这样:

import cv2 import numpy as np from tensorflow.keras.models import load_model # 加载训练好的 Unet 权重 model = load_model('unet.h5') # 读取车辆原图并预处理 img = cv2.imread('test_car.jpg') img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized = cv2.resize(img_rgb, (256, 256)) / 255.0 img_input = np.expand_dims(img_resized, axis=0) # 前向推理得到分割掩码 mask = model.predict(img_input)[0, :, :, 0] mask_bin = (mask > 0.5).astype(np.uint8) # 在原图上画出车牌区域 contours, _ = cv2.findContours(mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) # 还原到原图尺寸 scale_x = img.shape[1] / 256 scale_y = img.shape[0] / 256 cv2.rectangle(img, (int(x*scale_x), int(y*scale_y)), (int((x+w)*scale_x), int((y+h)*scale_y)), (0, 255, 0), 2) cv2.imshow('result', img) cv2.waitKey(0)

这段代码演示了如何把 Unet 的输出转成实际的车牌框。model.predict 返回的是一个 256×256 的浮点掩码,每个像素值在 0 到 1 之间,代表该像素属于车牌的概率。用 0.5 做阈值二值化得到掩码,再用 findContours 找到连通域,取外接矩形就得到了车牌在缩略图上的位置,最后按缩放比例映射回原图。

需要注意的一个细节是,load_model 加载 unet.h5 时会同时恢复模型结构和权重,所以 Unet.py 里定义的网络结构必须和你训练时的结构一致,否则会报结构不匹配错误。如果遇到这种问题,不要自己重新定义模型,直接新建一个和 Unet.py 相同的网络文件里实例化网络再 load_weights,这样更稳。

4. CNN 字符识别:车牌定位之后,逐个字符读出来

4.1 识别流程:字符分割 + 单字符分类

拿到了车牌区域后,接下来要把车牌字符串识别出来。中文车牌第一位是省份汉字(如“京”“粤”“苏”),第二位是发牌机关字母,后面是字母数字组合。传统思路是先用 OpenCV 做字符分割,再逐字符送入 CNN 分类器。字符分割做不好的话,后续识别率再高也没用。

国产蓝牌是单行 7 个字符,字符间距相对均匀,用轮廓查找加宽高比过滤就能切出来。绿牌新能源是 8 个字符但字间距不均匀,用等宽切分就不太靠谱。典型做法是先用形态学操作把车牌图像二值化,再按字符宽度做列投影分割。

列投影法的核心思想是统计每列像素值的总和,字符区域列和值高,间隙区域列和值低,波峰波谷自然把字符分开。如果字符粘连,可以用分水岭算法细化分割,或者直接训练一个端到端的序列识别模型,但源码里用的是 CNN 单字符分类器,说明它处理的是切分后的单字符图。

4.2 CNN 网络结构与训练细节

CNN.py 里的网络是一个标准的轻量卷积网络,适合小规模数据集训练。以训练好的 cnn.h5 为例,它接收的输入是单通道灰度图,尺寸一般是 32×32 或者 64×64,输出类别数是车牌字符类别总数(31 个省份汉字 + 24 个字母 + 10 个数字,字母 I 和 O 通常去掉)。

网络结构大致是三层卷积加池化,再接两个全连接层,最后用 softmax 输出概率分布。训练时的关键参数:学习率用 0.001,优化器用 Adam,batch size 设 32 或 64,epoch 数量视数据量而定,一般 30 到 50 轮就能收敛。训练结束后保存权重为 cnn.h5,用 model.save() 保存即可。

训练脚本的核心代码框架如下:

import tensorflow as tf from tensorflow.keras import layers, models from tensorflow.keras.preprocessing.image import ImageDataGenerator # 定义轻量 CNN 结构 model = models.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 1)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) # 数据增强 + 归一化 datagen = ImageDataGenerator( rotation_range=10, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.1, rescale=1.0/255.0 ) # 编译并训练 model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) train_generator = datagen.flow_from_directory( 'dataset/train/', target_size=(32, 32), color_mode='grayscale', batch_size=32, class_mode='categorical' ) model.fit(train_generator, epochs=30, validation_split=0.1) model.save('cnn.h5')

这段代码演示了几个关键点。ImageDataGenerator 是训练数据增强的核心,它在每个 epoch 训练时自动对图片做随机旋转、平移、缩放等变换,等于免费扩充了数据集,对车牌上的字符这类纹理简单的图像来说,增强后的模型泛化能力明显更好。Dropout(0.5) 是防止过拟合的标准手段,因为字符类别多但每类的训练样本可能只有几百张,不加 Dropout 训练集准确率能到 99%,但测试集可能只有 85%。

flow_from_directory 要求数据集目录按类别分子文件夹,文件夹名就是类别标签。你训练的类别顺序最终会写进 model 文件里,后续加载 cnn.h5 做推理时必须在训练脚本里打印过 class_indices 字典,或者从 model 的配置中读取,这一步最容易出错。

4.3 从 cnn.h5 到完整车牌识别结果

加载 cnn.h5 识别单张字符图的代码比较直接:

from tensorflow.keras.models import load_model import numpy as np import cv2 # 加载字符识别模型 model = load_model('cnn.h5') # 读取单字符图,预处理成 32x32 灰度图 char_img = cv2.imread('char.jpg', cv2.IMREAD_GRAYSCALE) char_img = cv2.resize(char_img, (32, 32)) char_img = char_img / 255.0 # 预测类别 prob = model.predict(char_img.reshape(1, 32, 32, 1))[0] pred_class = np.argmax(prob) # 从 class_indices 映射回字符 labels = list(train_generator.class_indices.keys()) # 需要在训练时保存 print('识别结果:', labels[pred_class], '置信度:', prob[pred_class])

这里有一个容易踩坑的点:OpenCV 读图默认是 BGR 顺序,但模型训练时用的是灰度图,所以 IMREAD_GRAYSCALE 读进来之后不需要做颜色空间转换。另外,车牌字符是白底黑字还是黑底白字会影响识别率,如果训练数据全是黑底白字,实际识别时发现白底黑字(比如蓝底白字车牌反色后)效果变差,需要在预处理时做二值化并保证亮暗方向和训练一致。

5. 避坑与常见问题:从下载到跑通,这些坑我替你踩过了

5.1 环境配置阶段:TensorFlow 版本不匹配导致 h5 加载失败

现象:运行 GUI.py 时提示无法加载 unet.h5,报错信息大概是 “AttributeError: 'str' object has no attribute 'decode'” 或 “Unknown layer”。

原因:h5 文件是用某个特定版本的 TensorFlow 保存的,而当前环境装的是不一样的大版本。Keras 2.x 和 Keras 3.x 之间的兼容性问题尤其突出,h5 里记录的网络结构格式不兼容。

解决:在 requirements.txt 中查看项目锁定的 TensorFlow 版本,严格安装对应版本,建议用 conda 创建独立环境,例如 conda create -n plate python=3.8,然后 pip install tensorflow==2.4.0 或安装跟权重文件匹配的版本。如果实在查不到版本,优先尝试 TensorFlow 2.4 到 2.8 之间的版本,这是大部分国产毕设项目保存模型最常用的版本区间。

5.2 处理速度阶段:视频识别掉帧严重

现象:vidGUI.py 处理 demo_car.mp4 时,画面非常卡顿,CPU 占用率接近 100%,帧率不到 1 FPS。

原因:两张模型都是串行计算,Unet 分割(输入 256×256)和 CNN 识别(32×32 输入)在 CPU 上推理本来就慢,再加上每帧都做完整的预处理和后处理,速度自然上不去。

解决:一是用 GPU 版 TensorFlow 加速推理,把模型加载后运行在 GPU 上,速度能提升 10 到 20 倍;二是调整帧处理策略,不每帧都做识别,而是每 5 帧或每 10 帧做一次识别,中间帧直接沿用上一次的结果,这在视频监控类应用中是常见做法;三是把输入视频帧先缩放再送进 Unet,例如原视频是 1080p,先缩放到 512 或 640 宽,减少推理计算量。

5.3 字符分割阶段:蓝色车牌字符连在一起

现象:定位到车牌区域后,做列投影分割,发现第二个和第三个字符粘连,投影图只有一个宽波峰,无法切分。

原因:光线不佳或车牌倾斜导致字符投影连在一起。这里说的“车牌倾斜”,是指相机拍摄角度不水平时字符投影会重叠。还有一个常见诱因是二值化阈值选得不对,导致字符笔画变粗。

解决:先做倾斜校正,用霍夫变换检测车牌区域的直线角度,再进行旋转矫正。然后调整二值化方法,在 OpenCV 里换成 adaptiveThreshold 自适应阈值,核大小设为 15 或 21,比固定阈值更抗光照变化。最后在投影分割时加入先验知识,比如蓝牌有 7 个字符,如果检测到的投影峰远小于 7,就对粘连峰按宽度均分。这块经验来自实践,实际调试时你会在“调参”这件事上花不少时间。

5.4 中文识别阶段:省份汉字总是识别错

现象:其他字符识别准确率 90% 以上,但省份汉字(京、津、沪、粤等)识别错误率偏高,经常把“京”识别成“就”或者“示”。

原因:训练数据里中文字符样本太少,汉字结构复杂且相似字符多。很多毕设数据集里,汉字部分只有几百张图片,而字母数字部分有几千张,数据不平衡加上低分辨率(32×32)下细节丢失,这是本质原因。至于具体把“京”错成哪个字,和训练集的采样分布有关。

解决:数据增强时对汉字类别额外加随机弹性形变,模拟真实拍摄中车牌扭曲的情况;把输入分辨率提高。这类问题的解决思路是给中文类别更多的样本变换。另一个技巧是字符分割时确保每个字符图居中且边缘留白一致,汉字的识别非常依赖归一化质量,位置偏移几个像素准确率就会明显下降。

5.5 编译运行阶段:No module named 'cv2' 或 No module named 'tensorflow'

现象:下载解压后直接运行 GUI.py,报错 ModuleNotFoundError。

原因:当前 Python 环境缺少项目依赖的第三方库,OpenCV、NumPy、TensorFlow 都可能没装。这个错是最常见也最容易解决的,但很多新手会卡在这里。

解决:推荐用 Anaconda 创建虚拟环境,然后用 pip 批量安装依赖。一个典型的安装命令序列是:

conda create -n plate python=3.8 -y conda activate plate pip install opencv-python opencv-contrib-python pip install numpy pip install tensorflow==2.4.0 pip install matplotlib

安装完成后用 python -c "import cv2, tensorflow" 验证导入无报错,再运行 GUI.py。需要特别注意 opencv-python 和 opencv-contrib-python 不要同时安装,它们会互相覆盖,可能导致某些功能异常。

6. 进阶用法与验证:把毕设从“能跑”做到“能讲清楚”

6.1 用 eval 脚本量化模型效果

答辩时老师最常问的问题是“准确率多少”。光靠肉眼看不准,我一般会给项目加一个批量评测脚本,对测试集图片跑完整流程,统计字符级和车牌级准确率。核心逻辑是读取标注文件、跑推理、比对结果,然后打印混淆矩阵。

import os import cv2 from core import recognize_plate # 假设每张测试图片对应一个真值文件,如 img_001.jpg.txt test_dir = 'test_images/' total = 0 correct_chars = 0 correct_plates = 0 for fname in os.listdir(test_dir): if not fname.endswith('.jpg'): continue img_path = os.path.join(test_dir, fname) label_path = img_path + '.txt' if not os.path.exists(label_path): continue with open(label_path, 'r', encoding='utf-8') as f: true_label = f.read().strip() # recognize_plate 是你 core.py 里封装的识别函数 pred_label = recognize_plate(img_path) total += 1 if pred_label == true_label: correct_plates += 1 # 字符级准确率:统计每字符是否一致 for c1, c2 in zip(pred_label, true_label): if c1 == c2: correct_chars += 1 print(f'车牌级准确率: {correct_plates / total:.2%}') print(f'字符级准确率: {correct_chars / (total * len(true_label)):.2%}')

这段脚本的意义在于,它把“效果好不好”这件事量化成了两个数字,答辩时拿出来非常有说服力。车牌级准确率考察的是端到端效果,字符级准确率考察的是各个环节累计误差。如果汉字容易错,字符级准确率会明显低于车牌级,这可以作为继续改进的指引。

6.2 把图片识别扩展成视频实时识别

如果你拿到的版本只有图片识别功能,想扩展成视频版,不要从零写。用 VideoCapture 读取视频帧,每帧丢给识别函数,最后用 VideoWriter 把标注结果写回视频文件就行。核心结构是这样的:

import cv2 from core import recognize_plate cap = cv2.VideoCapture('demo_car.mp4') fps = int(cap.get(cv2.CAP_PROP_FPS)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter('output.avi', cv2.VideoWriter_fourcc(*'XVID'), fps, (width, height)) while True: ret, frame = cap.read() if not ret: break # 每隔几帧做一次识别,降低计算压力 plate_text, box = recognize_plate(frame) if box is not None: x, y, w, h = box cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, plate_text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) writer.write(frame) cv2.imshow('plate', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() writer.release() cv2.destroyAllWindows()

这里的关键参数是 VideoWriter_fourcc 编码格式,XVID 兼容性最好,MP4V 适合输出 mp4 文件。如果输出视频打不开,多半是编码器问题或者宽高和输入不一致。每帧都调用识别函数会让速度非常慢,你在实际使用时可以改成每 3 帧识别一次,中间两帧沿用上一次的车牌框和文字,画面看起来几乎无感,速度却能翻倍。这也是我当时处理视频时最常用的一招。

6.3 答辩前的验证清单与演示技巧

拿到这份源码后,建议按下面这个顺序做一遍完整验证,确保演示时不翻车:检查 unet.h5 和 cnn.h5 能否正常加载,用 python -c 验证模型结构;用图片测试整条识别链路,先用 demo_car.mp4 里的某一帧静态图测,速度快;再用视频测试整体流畅度,记录平均帧率;测试不同光线条件下图片,观察车牌定位失败情况;看 Unet 输出掩码质量,评估是否需要调整分割阈值。

演示时先跑视频,再展示 GUI 界面的图片功能,最后切到训练脚本展示损失曲线和准确率曲线。这套顺序能覆盖“分割、识别、界面、训练”四个层次。我从那以后做任何毕设项目都会强制走一遍“先加载权重、再挨个模块调用、最后端到端验证”的流程,顺序反了就会莫名浪费时间在排查环境问题上。希望帮到你。

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

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

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

立即咨询