☰
深度学习车牌识别实战:Python与CNN在智能交通中的应用
2026/9/28 3:30:53 网站建设 项目流程

简介:基于Python与CNN构建的车牌识别工程,面向计算机视觉入门者及智能交通应用开发者,完整演示了从图像预处理、卷积网络搭建到模型训练、评估与部署的落地路径,适合快速上手深度学习图像任务。压缩包共25个文件、约29.2MB,内含jpg/png车牌样本、Python训练脚本、预训练权重、7z备份数据,以及说明文档和license文件,目录划分清晰,便于按模块复现实验。已有206人学习下载。内容覆盖数据增强、Keras/TensorFlow建模、优化器调参、准确率与召回率评估等关键环节,并对不同光照、角度和背景的车牌图片给出处理思路,适合对照源码理解字符定位与识别逻辑,也可进一步扩展至车辆检测、智慧停车等场景。资源附带的图像样本与权重文件可帮助读者跳过繁琐的数据准备,聚焦网络结构与参数调优,是一份可运行、可参考的实战案例。

1. 车牌识别不是玄学:Python+CNN 这套方案到底解决了什么

打开基于Python-CNN的车牌.zip,里面是一套完整的车牌识别工程:数据预处理、CNN模型构建、训练评估到预测脚本,链路是通的。很多拿到这类资源的同学卡在同一个问题上——照着教程把MNIST手写数字跑通了,换成真实车牌数据就频繁翻车。原因不在模型结构,而在于车牌图像的光照差异、字符间距不均、背景干扰这些细节处理。这套项目把Python生态里的图像处理、Keras/TensorFlow建模串成一条完整线,适合正在学深度学习、想做智能交通或停车场识别底座的工程师。下文按原理、构建、实战、避坑、进阶五个层次拆开讲,讲透每一步怎么落地。

2. CNN识别车牌的原理:卷积、池化与全连接层各自负责什么

2.1 卷积层:车牌字符特征从哪来

车牌识别本质上是图像分类任务——把包含车牌字符的图像区域,映射到对应的字符类别。CNN在这里做的事情和传统图像处理完全不同:传统方法靠人写Sobel算子、Canny边缘检测去手工设计特征,而卷积层是让模型从数据里自己学。一个3×3的卷积核在输入的64×64车牌灰度图上滑动,每次计算9个像素的加权和,得到一个特征图。多个卷积核并行,分别学边缘、角度、曲率、笔画粗细这些低级特征。真实车牌图像比MNIST难在字符周围有铆钉、边框和污渍,而这些噪声恰恰会激活卷积核,所以后面的池化和Dropout才那么关键。

到第二层、第三层卷积,低级的边缘特征逐渐组合成“数字7的上横线”“字母B的左侧竖线”这类结构性特征。常见做法是连续两层Conv2D后接一层MaxPooling2D,先提取特征再压缩,而不是一层卷积一层池化交替到底。连续卷积能扩大感受野,让第三层看到更大的区域,同时增加非线性表达能力。第一层卷积设32个3×3卷积核,第二层64个,第三层128个——卷积通道翻倍,因为越到高层特征越抽象,需要更多通道去容纳不同的组合方式。

from tensorflow.keras.layers import Conv2D, MaxPooling2D, Input from tensorflow.keras.models import Sequential model = Sequential([ Input(shape=(64, 64, 3)), Conv2D(32, (3, 3), activation='relu', padding='same'), # 64x64 -> 64x64 Conv2D(32, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), # 64x64 -> 32x32 Conv2D(64, (3, 3), activation='relu', padding='same'), Conv2D(64, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), # 32x32 -> 16x16 Conv2D(128, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), # 16x16 -> 8x8 ])

这段代码前三行定义了一个不带分类头的特征提取器,最后输出的特征图是8×8×128。第一层卷积的input_shape必须和预处理后的图像尺寸一致——灰度图就把最后的3改成1,保留RGB就用3。这里选padding='same'是为了让每个卷积块输出尺寸和输入保持一致,方便你数清楚每一层特征图的宽高变化;如果不用padding,64×64的输入经过两层3×3卷积后会缩到60×60,后面池化层算出来的尺寸就不是整数了。

激活函数选ReLU而不是sigmoid,因为ReLU在正区间梯度恒为1,深层网络不容易梯度消失,训练速度快。车牌字符笔画密集,用5×5的大卷积核反而容易把相邻笔画的细节混在一起,所以这里全部用3×3。连续的3×3卷积感受野等效于一个大卷积核,但参数量更少,中间还多一次非线性变换,这是现在搭建CNN最主流的做法。

2.2 池化层:降维的同时保留什么

池化层存在的理由有两个。第一是计算量:一个64×64×128的特征图有52万多个值,直接拉平做全连接,参数量会爆炸。每经过一次2×2最大池化,宽高各减半,特征图缩小到原来的四分之一,到第三层池化结束时只剩下8×8×128,这个尺寸接全连接层才合理。第二是平移不变性:车牌在摄像头里往左挪两个像素,字符语义不该变,最大池化取窗口内最大值,把微小位移钝化掉。

from tensorflow.keras.layers import MaxPooling2D # 2x2 最大池化,默认 strides 等于 pool_size,窗口不重叠 model.add(MaxPooling2D(pool_size=(2, 2))) model.add(MaxPooling2D(pool_size=(2, 2), strides=1)) # 少用,strides=1 会保留更多位置信息

第一行是标准用法,窗口2×2、步长2,输出尺寸减半。第二行把strides改成1,窗口每次只挪一个像素,输出尺寸几乎不变,参数量和计算量都上去了。车牌识别场景我基本只用第一种。另一个容易踩的坑是池化层放在连续卷积之后还是之前——先做两层卷积再池化,能让模型在压缩前先看到更丰富的局部组合,这是当前实践里更稳的顺序。

为什么不用平均池化?最大池化保留的是窗口内最显著的特征响应,对应字符边缘的强激活;平均池化会把背景噪声的平均值也带进来,对光照不均的车牌不友好。整张图做粗粒度分类时平均池化或许合适,但车牌这种需要精细笔画的场景,用平均池化大概率损失精度。这点在不少开源代码里能看到反例,替换成MaxPooling之后准确率能回升两三个点。

池化层的输出尺寸有个硬约束:最后一层池化输出的宽高最好是偶数且不要小于4。8×8、6×6、4×4都行,如果是7×7这种奇数,后面Flatten之后做Dense虽然也能跑,但特征图的空间结构已经不太好对齐了。我在设计模型时一般先手算一遍尺寸,再写代码,省得训练到一半爆出维度不匹配的报错。

2.3 全连接层与输出层:从特征图到车牌号码

卷积和池化把输入图像压缩成特征图的集合,最终要输出字符类别,这一步由全连接层完成。Flatten把最后一个池化层的三维特征图拉成一维向量,经过一两个Dense层做非线性组合,最后接Softmax输出每个类别的概率。字符类别数必须和你的数据集一一对应,比如省份简称、字母、数字各占多少类,直接决定最后一个Dense的神经元个数。

from tensorflow.keras.layers import Flatten, Dense, Dropout model.add(Flatten()) model.add(Dropout(0.5)) model.add(Dense(128, activation='relu')) model.add(Dense(num_classes, activation='softmax'))

num_classes是字符类别总数,这个值要在训练前确认好,不能拍脑袋。做单字符识别时,字符集通常包括31个省份汉字、24个字母(去掉I和O)、10个数字,加起来65类;如果你只需要识别数字,那就只有10类。常见做法是分三个独立分类器分别识别省份、字母和数字,也有的项目把所有可能字符合并成一个类别集合,用一个大分类器输出。这套项目里如果只做单字符识别,那么num_classes就是字符集大小;如果做端到端识别,就要改用CTC损失,那完全是另一套结构。

全连接层神经元数量128、256、512都有人用,差别没有想象中大。输入分辨率只有64×64时,128个神经元足够,再往上增益极小,只拖慢训练。Dropout放在Flatten之后、Dense之前是防过拟合性价比最高的位置,训练时每次随机丢弃一半神经元,让网络不依赖某一条特定路径;放在卷积层之间反而会破坏特征的空间连续性,除非用SpatialDropout2D。如果你训练时发现val_loss和train_loss差距拉大,第一步就是把Dropout从0.3往0.5调。

硬要抠规则的话,最后一个池化层输出的特征图宽高最好在4×4到8×8之间。太小意味着特征被压缩过头,字符细节丢光了;太大则Flatten后的向量过长,全连接层参数量失控。输入64×64经过三个2×2池化后变成8×8,正好落在区间里。如果输入是128×128,建议多加一层卷积池化,或者把第三个池化窗口改成(3,3),让输出落在目标区间内。

3. 从数据到模型:车牌识别项目的完整构建流程

3.1 数据集准备:目录结构怎么摆

拿到这套资源,第一步不是急着打开训练脚本,而是先把数据集目录理清楚。Keras的ImageDataGenerator.flow_from_directory要求数据按类别分文件夹存放,常见的单字符车牌数据集结构是这样:根目录下分train、val、test三个大文件夹,每个大文件夹里再按字符类别建子文件夹,子文件夹名就是标签。如果目录结构摆错了,flow_from_directory要么报错,要么把每个子文件夹当作一个类别,类别数对不上模型输出层。

import os import random import shutil source_dir = 'data/raw/all_chars' target_base = 'data' ratio = {'train': 0.7, 'val': 0.15, 'test': 0.15} for cls in os.listdir(source_dir): cls_path = os.path.join(source_dir, cls) if not os.path.isdir(cls_path): continue files = [f for f in os.listdir(cls_path) if f.endswith(('.jpg', '.png'))] random.shuffle(files) n_total = len(files) n_train = int(n_total * ratio['train']) n_val = int(n_total * ratio['val']) for split, indices in [ ('train', range(n_train)), ('val', range(n_train, n_train + n_val)), ('test', range(n_train + n_val, n_total)) ]: out_dir = os.path.join(target_base, split, cls) os.makedirs(out_dir, exist_ok=True) for i in indices: shutil.copy2( os.path.join(cls_path, files[i]), os.path.join(out_dir, files[i]) )

这段脚本的核心逻辑是遍历每个字符类别文件夹,随机打乱文件顺序,再按7:1.5:1.5切分。用shutil.copy2拷贝而不是move,是为了保留原始文件——万一某个类别分坏了,还能从raw目录重新切。random.shuffle那一步别省略,很多采集车牌的摄像头会连续拍同一辆车的多帧,如果不打乱,训练集和验证集可能各自只包含某几辆车的全部照片,模型就会学成“认车不认字”。

划分完成后必须做一次数量校验。打印每个类别在三个集合里的文件数,如果某个类别只有不到10张图,这个类别基本没法学,后面训练时它的准确率会拖垮整体指标。常见做法是把数量太少的类别做轻度增强,或者干脆删掉这个类,在真实场景里把识别结果映射到“未知”处理。另外,验证集和测试集之间不能有重复文件,如果切分脚本有bug导致同名字文件同时落进两个集合,评估结果会虚高,等部署到摄像头前就露馅。

3.2 图像预处理四步:灰度化、归一化、缩放与增强

车牌图像的预处理,核心是四件事:灰度化、归一化、缩放到统一尺寸、数据增强。灰度化的理由是车牌识别的核心信息是字符轮廓,颜色反而是干扰项。如果你的识别任务包含新能源绿牌和蓝牌的类型区分,那就不能灰度化,得保留RGB三个通道,让模型自己决定要不要用颜色特征。

from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rescale=1.0 / 255.0, # 像素归一化到 [0, 1] rotation_range=10, # 随机旋转 ±10 度 width_shift_range=0.1, # 水平平移 10% height_shift_range=0.1, # 垂直平移 10% zoom_range=0.1, # 随机缩放 10% brightness_range=[0.8, 1.2] # 亮度随机调 ±20% ) train_generator = datagen.flow_from_directory( 'data/train', target_size=(64, 64), batch_size=32, class_mode='categorical' )

rescale=1.0/255.0是必须的,把像素从0-255压到0-1区间。神经网络用梯度下降训练,数值范围太大会让损失函数的梯度震荡,收敛速度肉眼可见地变慢。target_size必须和模型的input_shape严格一致——这里设64×64,模型第一层卷积的input_shape就得是(64, 64, 3)或(64, 64, 1),对不上会在训练时报维度错误。

rotation_range设10度,因为真实摄像头里车牌最多歪个五六度,设太大会让模型学到奇怪的角度。brightness_range用0.8到1.2对应实际场景里的曝光波动。数据增强的幅度要跟真实部署场景对齐,不是越狠越好——过度旋转会让字符形变到失真,模型反而学不到真实的笔画结构。如果训练集和测试集都来自同一批正常角度图片,增强参数可以小一点;如果计划部署到多个停车场,角度和亮度范围就放宽一些。

3.3 模型训练:损失函数、优化器与早停策略

车牌字符识别是个多分类任务,损失函数基本锁定categorical_crossentropy。如果你的标签是整数而不是one-hot编码,就要用sparse_categorical_crossentropy,这两个很容易搞混——用错了,loss在训练一开始就会很奇怪,要么不降,要么直接NaN。

from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model.compile( optimizer=Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'] ) early_stop = EarlyStopping( monitor='val_loss', patience=8, restore_best_weights=True ) checkpoint = ModelCheckpoint( filepath='best_model.h5', monitor='val_accuracy', save_best_only=True ) history = model.fit( train_generator, steps_per_epoch=train_generator.n // 32, epochs=50, validation_data=val_generator, validation_steps=val_generator.n // 32, callbacks=[early_stop, checkpoint] )

优化器我默认用Adam,初始学习率0.001。Adam的好处是自适应调整每个参数的学习率,省去手动设计学习率衰减的麻烦。但要注意,Adam不等于万能——如果loss在某个值附近震荡降不下去,大概率是学习率偏高,把它降到0.0001再试,往往比换优化器更有效。

EarlyStopping监控val_loss,连续8个epoch没下降就提前停。patience设8而不是3或5,是因为车牌数据量小,验证集loss本身会有波动,太小的patience会在刚到局部低谷时误判为收敛,白白打断训练。restore_best_weights=True保证训练结束后模型权重回滚到验证集最好的那个状态,相当于吃了后悔药。ModelCheckpoint在val_accuracy最好时额外保存一份权重文件,配合EarlyStopping双保险,即使断电丢失了内存状态,也能从磁盘恢复。

steps_per_epoch为什么要用train_generator.n除以batch_size?因为生成器是按批次产出数据的,不指定步数它会无限迭代下去。batch_size这个参数,显存够就设32或64,太小的batch会让梯度更新方向噪音大,训练曲线抖得厉害。如果报显存不足,把batch_size降到16,同时把图片从64×64降到48×48,是最直接的缓解手段。

4. 实战代码解析:代码包结构、训练脚本与评估指标

4.1 代码包结构与运行路线

这套资源解压后是个标准的深度学习工程目录,我建议按下面的顺序去读代码,而不是从头到尾扫一遍。先看train.py把整体流程跑通,再回头看utils.py里的预处理细节,最后改predict.py做推理验证。

路径作用上手优先级
data/字符图片数据集,按类别分文件夹直接替换成自己的数据
train.py训练主脚本,负责生成器、模型构建和fit必看
predict.py单张图片预测脚本跑通后改
utils.py预处理、数据加载、绘图工具函数按需改
requirements.txttensorflow、opencv-python、numpy等依赖先装

运行前先装依赖,用pip install -r requirements.txt。如果机器上已经装了新版TensorFlow,要注意requirements里指定的版本和现有环境冲突的情况——常见做法是新建一个虚拟环境专门跑这个项目,避免把系统环境搞乱。我第一次跑这类开源项目时,就是在base环境里硬装,结果把别项目的TensorFlow版本覆盖了,浪费一个下午。

4.2 训练脚本的核心逻辑与参数怎么改

train.py的逻辑主线很清晰:解析命令行参数、加载生成器、构建模型、编译、训练。常见的写法是开头放一个argparse,把batch_size、epochs、learning_rate这些超参数暴露成命令行参数,这样调参不用反复改源码。

import argparse from tensorflow.keras.optimizers import Adam from models.cnn import build_cnn from utils.data_loader import create_generators parser = argparse.ArgumentParser(description='Train license plate CNN') parser.add_argument('--data_dir', default='data', help='dataset root') parser.add_argument('--epochs', type=int, default=50) parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--lr', type=float, default=0.001) args = parser.parse_args() train_gen, val_gen = create_generators( args.data_dir, target_size=(64, 64), batch_size=args.batch_size ) model = build_cnn(num_classes=train_gen.num_classes) model.compile( optimizer=Adam(learning_rate=args.lr), loss='categorical_crossentropy', metrics=['accuracy'] )

这段代码最值得借鉴的地方是从train_gen.num_classes自动读取类别数,而不是在模型里硬编码。替换数据集时,只要目录结构合规,num_classes自动跟着变,少改一处就少一处出错的机会。target_size这里写64×64,如果后面想用128×128做更高精度的训练,直接改这一行,但要确认build_cnn里的input_shape也跟着改,两处不一致时Keras会报错。

命令行传参的方式是python train.py --epochs 30 --lr 0.0001,每次实验留一行命令记录,方便对比不同参数的效果。我在实际调参时会用一个简单的文本文件记录每次实验的命令、最终准确率和备注,比在聊天记录里翻历史靠谱得多。

4.3 评估指标:准确率之外还要看什么

训练结束后,用测试集做一次最终评估是必须的。测试集从头到尾没参与过训练和早停判断,它的指标才接近真实部署表现。但只看一个accuracy会掩盖很多问题,车牌识别场景尤其要关注哪些类被搞混。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np test_generator.reset() y_true = [] y_pred = [] for _ in range(test_generator.n // test_generator.batch_size): images, labels = next(test_generator) preds = model.predict(images, verbose=0) y_true.extend(np.argmax(labels, axis=1)) y_pred.extend(np.argmax(preds, axis=1)) print(classification_report( y_true, y_pred, target_names=list(test_generator.class_indices.keys()) ))

classification_report会列出每个类别的精确率、召回率和F1分数。车牌识别里最典型的错误是“0”和“O”、“1”和“I”互相混淆,这两个类别的精确率会明显低于其他类。如果出现这种情况,单纯加大训练数据不一定有效,更实际的做法是在后处理阶段做规则约束:车牌首位不可能是字母O和I,中段数字位不可能是字母,利用这些先验信息把模型输出修正掉。

看混淆矩阵的时候,如果某个类被大量错分成另一个类,先别急着调模型——回去翻那个类别的原始图片,大概率是标注错了,或者两个类在视觉上确实长得像。比如汉字“京”和“津”在低分辨率下就容易混淆,这种问题靠增强数据解决不了,只能提高输入分辨率,或者专门收集这两个字的样本做难例挖掘。

5. 避坑指南:车牌识别项目中最容易翻车的四个环节

5.1 数据标注不一致:训练曲线震荡却查不出原因

现象:训练时loss曲线反复震荡,val_loss到某个值就不再下降,训练集和验证集准确率始终差十几个百分点。换模型结构、调学习率都没用。

原因:数据标注有误,某个字符的文件夹里混入了其他字符的图片。这种情况在网上下载的车牌数据集里很常见,特别是“0”和“O”、“1”和“I”这种视觉相近的字符,人工标注时很容易手滑。

解决:用训练好的模型对全量数据做一次预测,把置信度低但预测类别和标签不一致的样本抽出来人工复查,剔除或修正标签。另一个办法是统计每个类别图片的平均像素分布,离群明显的图片单独拿出来看,通常是标注错误或者混入了背景噪声图。从那以后我每次拿到新数据集,都会先跑一轮快速训练,用预测结果辅助清洗数据,再开始正式训练。

5.2 过拟合:训练集98%验证集只有72%

现象:训练准确率一路上升到98%,验证集准确率卡在72%附近,而且随着epoch增加,差距越拉越大。验证集loss在某个点之后开始反弹。

原因:模型容量过剩,训练数据量不够。一个三层卷积模型就有几十万参数,车牌字符数据集通常只有几千张图片,模型把训练集的特征细节背下来了,却没有学到可泛化的字符结构。

解决:按顺序做三件事——把Dropout从0.3调到0.5;在ImageDataGenerator里把rotation_range、width_shift_range、brightness_range等增强参数打开;如果还压不住,把第三层卷积的128通道降到64。做完这三步再看训练曲线,如果差距收窄但验证集准确率仍然上不去,就要回头检查训练集和验证集的分布是否一致,比如验证集全是白天照片、训练集全是傍晚照片,这种分布差异是模型救不回来的。

5.3 光照与角度:暗光场景下识别率骤降

现象:白天训练好的模型,拿到地下车库或者傍晚测试,准确率从95%掉到70%以下,而且错的都是同一批字符。

原因:训练数据的光照分布太窄,模型只在“正常亮度”区间内学到了特征。摄像头装在停车场出入口时,车牌区域经常处于逆光状态,字符和底板的对比度极低。

解决:在ImageDataGenerator里加brightness_range=[0.7, 1.3],直接模拟暗光场景。如果数据里完全没有暗光样本,光靠亮度扰动不够,还需要物理上补充一些真实暗光照片,或者对预处理做直方图均衡化——把车牌区域的灰度分布拉开,字符轮廓会更清晰。注意直方图均衡化要在缩放之前做,顺序反了效果会差很多。

5.4 字符粘连:单字符模型读不出整块车牌

现象:单字符识别准确率很高,但把一整块车牌切成单个字符后,某几个字符连在一起,送到模型里输出一串乱码。

原因:字符分割环节出了问题。车牌定位模块找到了车牌区域,但二值化阈值选得不好,或者字符间距本身不均匀,导致两个字符被切成一个连通域。

解决:检查分割逻辑里的二值化阈值。先对灰度图做自适应阈值处理,再用水平投影找到字符边界——统计每一行像素的黑色像素数量,字符区域的投影会形成明显的峰谷,谷底就是切分点。如果摄像头安装角度大导致字符倾斜严重,显式分割这条路会越走越窄,最稳妥的办法是换成端到端方案:CNN提取特征后接RNN和CTC损失,直接输出车牌字符串,完全不依赖显式分割。这套资源如果只是单字符识别版本,遇到多字符场景时,就按这个思路去升级。

6. 进阶技巧:数据增强、迁移学习与实时识别的落地细节

6.1 数据增强参数怎么调才不伤精度

基础的数据增强只做旋转、平移、缩放、亮度,够用但难出彩。想把车牌识别准确率再往上提,可以增加shear_range(剪切变换)来模拟摄像头斜拍的车牌形变,再用channel_shift_range模拟不同色温下的偏色。增强幅度要控制:剪切超过0.2,字符形状会畸变到无法辨认;brightness_range超过0.5,字符和底板的对比度会反转,模型反而学到错误特征。

datagen = ImageDataGenerator( rescale=1.0 / 255.0, rotation_range=15, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.15, zoom_range=0.15, brightness_range=[0.7, 1.3] )

我自己的经验是,增强参数的上下限要和部署场景对齐。停车场摄像头固定机位、固定角度,增强幅度可以小;移动巡检设备拍到的车牌什么角度都有,增强幅度就要大。如果增强后验证集准确率不升反降,先检查是不是增强过度,把rotation_range从15改回10,往往就恢复了。

6.2 迁移学习:用MobileNetV2做特征提取

车牌字符数据集通常几千张,从零训练一个CNN容易过拟合。迁移学习是把ImageNet上预训练好的模型拿来,冻结前面的卷积层,只训练最后几层分类头。ImageNet类别和车牌字符差异很大,但模型底层学到的边缘、纹理特征是可复用的。

from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.models import Model from tensorflow.keras.layers import GlobalAveragePooling2D, Dense base_model = MobileNetV2( input_shape=(128, 128, 3), include_top=False, weights='imagenet' ) base_model.trainable = False # 冻结主干 x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(128, activation='relu')(x) x = Dense(num_classes, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=x)

MobileNetV2要求输入至少是32×32,建议用128×128。weights='imagenet'会从云端下载预训练权重,第一次跑要先联网。base_model.trainable=False冻结主干,只训练顶层,速度很快;等顶层收敛后,再把主干后几层解冻做fine-tune,学习率降到0.0001以下,准确率还能再涨一点。这套资源里如果自带训练脚本和模型结构,把build_cnn函数里的模型替换成上面的结构,再调一下输入尺寸就行了。

6.3 实时视频流识别的小技巧

部署到摄像头时,逐帧跑推理会非常慢。我的习惯是跳帧处理——每5帧取1帧做识别,同时对提取到的车牌区域单独维护一个结果队列,连续3帧识别结果一致才输出,避免单帧抖动导致误报。

import cv2 import numpy as np cap = cv2.VideoCapture(0) frame_count = 0 result_queue = [] while True: ret, frame = cap.read() frame_count += 1 if frame_count % 5 != 0: continue # 假设已经通过定位模块拿到车牌区域 roi roi = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) roi = cv2.resize(roi, (128, 128)) roi = roi.astype('float32') / 255.0 roi = np.expand_dims(roi, axis=-1) # (128, 128) -> (128, 128, 1) roi = np.expand_dims(roi, axis=0) # 增加 batch 维度 pred = model.predict(roi, verbose=0) result_queue.append(np.argmax(pred)) if len(result_queue) == 3: if result_queue[0] == result_queue[1] == result_queue[2]: print(f'识别结果: {result_queue[0]}') result_queue.pop(0)

这段代码里,跳帧用frame_count % 5控制,平均每秒只做4次推理。灰度图在通道维度上是1,把reshape后的数组再expand_dims两次,凑成模型要求的(1, 128, 128, 1)四维输入。如果模型是RGB训练的就去掉cvtColor,直接resize原图。多帧一致性判断模仿了实际工程里的时序投票,比单帧输出稳定得多,尤其适合车辆高速通过闸机的场景。

从那以后我每次训练车牌识别模型,都强制自己先花半小时检查数据集的分布和标注质量,再谈模型结构。数据干净了,一个普通的CNN就能跑到90%以上;数据一团糟,换再花哨的网络也白搭。希望帮到你。

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

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

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

立即咨询