
简介基于Python与卷积神经网络的车牌识别完整项目资源面向计算机视觉、深度学习的入门与进阶学习者可用于智能交通、自动车辆等场景中的车牌检测与识别任务。包内聚焦CNN建模全流程涵盖数据预处理、Keras/TensorFlow模型构建、训练调参、评估指标与部署思路等核心内容。资源共25个文件以jpg图像数据集、png示例图、py模型脚本与md说明文档为主辅以权重数据与辅助配置整体容量29.2MB目录按工程模块组织便于对照学习与二次开发。目前已有206人学习下载适合希望以完整可运行项目理解图像识别实战流程的开发者。通过该资源读者能掌握从车牌图像数据准备到CNN模型训练、优化与部署的关键步骤并获得可直接扩展应用的项目结构思路是理解深度学习解决现实视觉问题的良好范例。1. Python-CNN车牌识别一个压缩包能跑通的完整链路提到车牌识别很多人第一反应是YOLO、车牌检测、OCR那一整套重型工程。但这个项目不一样解压开 License-Plate-Recognition-master 你会发现它走的是另一条路用 OpenCV 做车牌定位和字符分割再用 CNN 对分割好的字符图片做分类识别。两个阶段各司其职单张图片从输入到输出车牌号一条链路完整走通没有拆成两三个项目拼凑。这套思路特别适合两类人。一是刚接触视觉深度学习的开发者项目里模型结构简洁训练脚本、预测脚本标注清晰能一口气看到数据怎么喂给网络、损失怎么算、准确率怎么评估。二是有具体落地需求但不想为识别引擎付高额授权费的人比如停车场道闸、园区门禁的离线识别场景。虽然这个项目不算工业级系统但作为识别引擎的原型足够用了。这份资源的核心价值不在于模型有多新而是它把「采集车牌图像 → 字符分割 → CNN 分类 → 输出号码」的每个环节都用 Python 代码串了起来。你拿到的不是一个孤零零的模型文件而是一整套可以拆开改造的源码包。2. CNN 在车牌字符识别里怎么工作网络结构与数据流2.1 为什么选 CNN 而不是传统模板匹配车牌字符识别和通用 OCR 有一个显著差异车牌字符集是封闭的。国内车牌通常由省份汉字、英文字母、数字组成类别总数不超过 80 个。封闭字符集意味着分类问题可以做得非常聚焦不需要像通用 OCR 那样考虑上万种字形。但即便是封闭字符集传统模板匹配方法也顶不住拍摄角度、光照、污损带来的形变。同一张车牌在不同天气、不同角度下字符的灰度分布差异很大模板匹配的鲁棒性很差。CNN 的优势在于它自动学习特征层级。底层卷积核学习边缘、角点这类局部模式高层卷积核组合成字符的笔画结构特征。这个特征提取过程是数据驱动的不需要人工设计 HOG、LBP 这些特征描述子。在车牌识别这个场景下训练数据只要覆盖足够多的车牌字体和拍摄角度CNN 对形变的容忍度明显高于传统方法。这个项目用的正是典型的卷积-池化-全连接路线。卷积层负责提取特征图池化层压缩特征图尺寸、保留主要响应全连接层把特征图展平后映射到字符类别概率。整体结构对新手很友好不需要理解 ResNet 的残差连接或者注意力机制把基础模块串起来就能跑。2.2 识别流程中的数据流拆解整套识别链路不是一步到位的。原始图像先要经过车牌定位把车牌区域从整图中抠出来抠出来的车牌图再经过字符分割切成单个字符的小图每个字符小图分别送入 CNN 分类最后把分类结果拼成完整车牌号。数据流大致如下输入整图(640x480) → 车牌定位(OpenCV 边缘检测轮廓筛选) - 车牌区域图(约 440x140) - 灰度化/二值化 - 字符分割(连通域分析/投影法) - 字符图(约 40x80) - 缩放至 CNN 输入尺寸(32x32 或 64x64) - CNN 前向传播 - 输出字符类别各阶段输出数据的格式、尺寸必须对齐这是整个流程最容易翻车的地方。字符图缩放尺寸和 CNN 输入层尺寸不一致模型会直接报错或者给出毫无意义的预测结果。我在拆这个项目时第一步就是确认训练脚本里输入层尺寸再倒推字符分割脚本的输出尺寸确保两者匹配。2.3 数据预处理灰度、缩放、归一化一个都不能少项目里的训练数据是字符级别的小图不是整张车牌。预处理环节挂在字符分割和模型输入之间处理顺序有讲究。我一般按下面这个顺序来做def preprocess_char(img, target_size(32, 32)): # 输入是单字符灰度图可能是分割后的不规则 ROI # 先做灰度化如果原图是三通道 if len(img.shape) 3: img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化突出字符前景抑制背景噪声 _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 统一缩放到固定尺寸保持字符长宽比 h, w img.shape scale min(target_size[0] / w, target_size[1] / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(img, (nw, nh), interpolationcv2.INTER_CUBIC) # 贴到黑色画布中央避免变形 canvas np.zeros(target_size, dtypenp.uint8) x_off (target_size[0] - nw) // 2 y_off (target_size[1] - nh) // 2 canvas[y_off:y_offnh, x_off:x_offnw] resized # 归一化到 [0, 1] return canvas.astype(np.float32) / 255.0这段逻辑里有两个容易被忽略的细节。缩放到目标尺寸时我用的是等比例缩放加居中贴板而不是直接cv2.resize(img, target_size)。直接拉伸会把字符压扁尤其是汉字字符笔画结构对形变极其敏感压扁之后分类准确率会掉好几个百分点。归一化这一步把像素值从 0-255 映射到 0-1是为了配合网络输出层的激活函数让梯度传播更平稳。如果训练代码里用了categorical_crossentropy做损失对应标签必须做 one-hot 编码这个是配套动作容易漏。3. 从压缩包到第一次训练环境搭建与数据集组织3.1 环境依赖怎么装才不会翻车解压License-Plate-Recognition-master.zip之后第一步不是看代码而是先确认依赖版本能不能匹配上。这个项目的代码风格属于 TensorFlow 1.x 到 2.x 过渡时期的写法很多老项目会有tf.placeholder、tf.Session()这些 API在 TF 2.x 下直接跑会报AttributeError。我建议直接新建虚拟环境别往系统 Python 里塞依赖不然装坏了影响其他项目。# 创建 Python 3.7 虚拟环境这个版本对 TensorFlow 兼容性最稳 conda create -n plate_recog python3.7 conda activate plate_recog # 安装 TensorFlow。如果代码是 1.x 风格装 1.15.0 pip install tensorflow1.15.0 # 需要的话TensorFlow 2.x 下跑旧代码可以用兼容模式 # pip install tensorflow2.4.0 # 然后在代码里加一行 # import tensorflow.compat.v1 as tf # tf.disable_v2_behavior() # 图像处理与科学计算库 pip install opencv-python4.5.3.56 numpy1.19.5选 TensorFlow 1.15 而不是 2.x 的原因是老项目训练脚本里如果直接调用了tf.Session、tf.train.Saver在 TF 2.x 原生模式下无法运行强行用 compat 模式适配反而会增加排查成本。Python 3.7 是这两代 TensorFlow 都能覆盖的版本往上 3.8、3.9 在 TF 1.15 下会有编译兼容问题。OpenCV 版本不用追求最新4.5.x 足够稳定。3.2 数据集目录结构标签藏在文件夹名里模型训练需要大量标注数据。这个项目里字符图片的标签不是写在 JSON 或 CSV 里的而是直接利用文件夹名做标签。这是早期数据集的常用组织方式优点是直观缺点是标签和文件一一对应没有额外信息比如拍摄条件、车牌底色。典型的目录结构如下dataset/ ├── train/ │ ├── 0/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ │ └── ... │ ├── 1/ │ ├── ... │ ├── A/ │ ├── B/ │ ├── 京/ │ └── ... └── val/ ├── 0/ ├── 1/ └── ...项目自带的数据集如果不够用可以到开源的 CCPD 数据集中国城市车牌数据集里提取字符级图片补齐。需要注意训练集和验证集的划分不要按文件名随机切最好按车牌图来源划分保证同一张车牌衍生出的字符图片不会同时出现在两个集合里否则验证集指标会虚高。3.3 训练入口脚本跑起来前先改这三处网上下载的老项目解压后第一件事是找到训练入口文件。一般叫train.py或train_cnn.py。打开后先找data_dir、batch_size、epochs这几个变量大概率要改。# 训练脚本关键配置段 data_dir ./dataset/train # 改成你本地数据集的绝对路径 val_dir ./dataset/val # 验证集路径 batch_size 64 # 显卡显存不够就降到 32 epochs 50 # 首次跑可以降到 10 验证流程 lr 0.001 # 初始学习率 img_size (32, 32) # 必须和预处理脚本保持一致 num_classes len(os.listdir(data_dir)) # 自动从文件夹数量读取类别数num_classes用os.listdir自动读取可以避免手数类别数量的低级错误。img_size必须和字符分割脚本里的缩放尺寸一致否则模型输入维度不匹配。epochs第一次跑建议调小先验证数据加载和模型构建没问题再调回完整训练轮数。训练过程会输出每个 epoch 的损失和精确率留意验证集精确率是否持续上升。如果训练损失下降但验证损失升高说明过拟合了常见对策是按需调整网络层数或增加数据增强。4. 训练参数与评估方法指标怎么算、曲线怎么看4.1 CNN 的训练配方优化器、损失函数、Dropout 与模型的搭配这个项目原版模型结构是三个卷积层搭配两个全连接层每个卷积层后跟一个最大池化层全连接层之间加了 Dropout 做正则化。这种结构在字符分类任务上属于经典配方作为 baseline 完全够用。如果你改进了模型结构训练参数也需要同步调整。我习惯的适配方式如下from keras.models import Sequential from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_model(input_shape(32, 32, 1), num_classes65): model Sequential([ # 第一个卷积块8 个小卷积核提取低级边缘特征 Conv2D(8, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), MaxPooling2D((2, 2)), # 第二个卷积块16 个卷积核组合出笔画级特征 Conv2D(16, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), # 第三个卷积块32 个卷积核抽象局部结构 Conv2D(32, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), # 展平后接全连接层Dropout 控制过拟合 Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(num_classes, activationsoftmax) ]) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] ) return model这里的关键决策是卷积核数量从 8 到 16 到 32 逐层翻倍因为越靠后的层感受野越大需要更多卷积核来覆盖不同位置的模式组合。Dropout 放在最后一个全连接层前面作用是在训练时随机屏蔽一半神经元迫使网络不依赖单个路径做决策这是防止字符分类过拟合性价比最高的手段。优化器我通常用 Adam 而不是 SGDAdam 自带自适应学习率调整对新手友好不用手动做学习率退火。如果追求极致准确率可以等 Adam 训到接近收敛后换成低学习率的 SGD 再精调几个 epoch不过车牌字符集封闭、数据量有限这个收益不明显。4.2 评估指标准确率之外还要看混淆矩阵训练脚本最后会打印测试集准确率但准确率高不意味着模型能直接上线。车牌字符里有几个天然难点汉字省份简称「沪」和「苏」在低分辨率下很容易混淆数字「0」和字母「O」在很多车牌字体下几乎一样字母「B」和数字「8」在模糊时也难以区分。只看总准确率是发现不了这些问题的。评估时应该加一个混淆矩阵输出import numpy as np from sklearn.metrics import confusion_matrix, classification_report # 完成预测后 y_pred model.predict(test_generator, verbose1) y_pred_classes np.argmax(y_pred, axis1) y_true_classes test_generator.classes # 打印每个类别的精确率和召回率 print(classification_report( y_true_classes, y_pred_classes, target_namesclass_names )) # 混淆矩阵建议存成文本后续定位具体错误用 cm confusion_matrix(y_true_classes, y_pred_classes) np.savetxt(confusion_matrix.txt, cm, fmt%d)逐个类别的精确率和召回率比总准确率更说明问题。如果「沪」的召回率明显低于其他字符说明这个字符的训练样本可能太少或者字符分割阶段经常把它的左半部分切掉。车牌识别系统里字符级别的错误会直接导致整张车牌号识别失败一张车牌 7 个字符哪怕每个字符准确率 99%整牌准确率也只剩 93% 左右所以字符级别的弱点必须单独清理。4.3 字符分割质量直接影响 CNN 上限CNN 训练得再好字符分割阶段出问题整张图也白搭。分割阶段最常见的输出问题是字符粘连、字符断裂、边缘噪声残留。def segment_chars(plate_gray): # 先做二值化 binary cv2.adaptiveThreshold( plate_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 ) # 形态学开运算去除细小噪声点 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 按列投影统计字符区域位置 col_sum np.sum(binary, axis0) col_blank col_sum 0 # 找出字符列的连续区间再按宽度过滤掉噪声段 # 车牌字符宽度占比相对固定过滤条件按图像宽度比例设置 # 找到每个连通域后按 x 坐标排序 # 再根据已知字符宽度做二次筛选 return char_regionsadaptiveThreshold比固定阈值好在能适应车牌上光照不均的情况但它的参数如blockSize11和C2需要根据实际图像微调。字符分割这个环节的玄学程度不亚于模型调参后面单独写一节讲踩坑记录这里先按下不表。5. 车牌识别翻车现场五个高频踩坑与排查路径5.1 现象ModuleNotFoundError: No module named keras或 TensorFlow 报错原因压缩包里有些版本代码用from keras.models import Sequential有些用from tensorflow.keras.models import Sequential。TensorFlow 2.x 环境下keras和tensorflow.keras是两个不同的包路径混用时会互相覆盖或直接报找不到模块。解决统一改成from tensorflow.keras.models import Sequential同时把tensorflow.keras.optimizers、tensorflow.keras.layers里的所有 Keras 组件全部改成tensorflow.keras.*前缀。如果用的 TF 1.15则反过来统一用独立的keras包。两套路径混用是编译阶段最常见的报错来源。5.2 现象训练时损失震荡不下降准确率卡在几个点原因学习率设置过高lr0.01或更高时Adam 优化器容易在损失曲面边缘震荡另一种情况是输入图片没有归一化到[0,1]像素值在 0-255 范围直接进入网络导致梯度数值不稳定。解决先检查预处理阶段是否做了img / 255.0。然后用lr0.001起步观察前五个 epoch 的损失是否单调下降。如果仍然震荡把学习率降到0.0001。网络结构的全连接层单元数也可以从 128 降到 64参数量减少之后小数据集上的训练会更平稳。5.3 现象验证集准确率远低于训练集训练曲线后期分离原因典型的过拟合。字符训练集通常只有几百张到几千张图片CNN 参数量远多于样本量特征提取层会把训练集中的背景纹理、噪声模式一并记进去导致泛化能力差。解决顺序依次是第一增加 Dropout 比例从 0.5 提到 0.7第二做数据增强把字符图片随机轻微旋转±10 度、平移±2 像素、缩放±10%第三检查训练集和验证集是否来自同一张车牌是的话改为按车牌图分组划分。增强代码可以用ImageDataGenerator三行就能配置完。5.4 现象识别结果里「京」「津」「沪」等汉字老是错原因汉字字符在低分辨率下笔画密集分割时容易切掉偏旁部首或者和旁边字符粘连。CNN 接收到的输入图片本身是残缺的分类准确率自然上不去。解决优先检查字符分割脚本输出的二值图有没有完整的汉字轮廓。分割宽度阈值如果只按数字和字母的宽度范围设置会漏掉宽度较大的汉字。我会把分割后的字符图按批次保存到文件夹里肉眼扫一遍再决定改分割参数还是补训练数据。5.5 现象OpenCV 定位不到车牌整图输入直接报错原因蓝底白字的车牌在 HSV 颜色空间有明显特征但老项目里常用的定位方式是灰度图边缘检测加轮廓筛选。如果目标车牌在深色背景下边缘特征不清晰轮廓筛选会直接落空。解决改用 HSV 颜色过滤先行定位蓝色区域再在原图对应位置裁剪。把 BGR 转 HSV 后蓝色车牌对应的色相范围约在100~124饱和度 100明度 80。这个方式对蓝色车牌的命中率比纯边缘检测高很多代价是只能识别蓝色车牌对新能源绿色车牌需要另加一路判断。如果你要处理混合颜色的车牌代码里要分别定义每种颜色的 HSV 区间。6. 从静态图片到实时视频流模型推理与批处理技巧训练完模型拿到model.h5文件之后下一步不是部署上线而是先封装一个推理函数把「图片 → 车牌号」的完整链路固定下来。我习惯把加载模型、预处理、预测这组动作封装成一个函数输入路径输出字符串互不依赖方便后续接摄像头或接 HTTP 接口。import cv2 import numpy as np from tensorflow.keras.models import load_model model load_model(model.h5) def predict_plate(image_path): # 1. 读取整图 img cv2.imread(image_path) # 2. 定位车牌区域这里用 HSV 蓝色过滤 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (100, 100, 80), (124, 255, 255)) # 用 mask 找到蓝色区域外接矩形裁剪出 plate_img # 3. 车牌图灰度化、二值化后做字符分割 char_imgs segment_chars(plate_gray) # 4. 每个字符图预处理后送入 CNN result [] for char_img in char_imgs: processed preprocess_char(char_img, target_size(32, 32)) prob model.predict(processed.reshape(1, 32, 32, 1), verbose0) char np.argmax(prob) result.append(char) return .join(result)preprocess_char的处理顺序可以复用第 2 章里那套灰度化、二值化、等比例缩放、居中贴板、归一化的逻辑只要训练时和推理时使用同一套预处理模型的输入分布就能保持一致。result.append(char)里的char实际上是一个类别索引需要逆映射回字符本身。这个映射关系在class_names训练时由文件夹名生成的列表里推理脚本一定要保留这个列表文件否则预测结果就是一堆没有意义的数字。从单张图片过渡到实时视频流要注意的地方在于帧率。我的做法是每隔 N 帧抽取一帧做完整识别其余帧直接跳过。车牌识别本身不需要逐帧处理因为车辆进出道闸时车牌在视野内停留的时间足够长间隔 0.3 秒抽一帧已经能满足使用场景。Python 版本可以用cv2.VideoCapture读摄像头对抽出的帧执行predict_plate的定位部分C 版本则是把同一套逻辑翻译过去核心不变。批量处理一批车牌图片时可以加一个简单的并发优化用multiprocessing.Pool把不同图片的定位、分割、识别分配到多个进程并行执行。CNN 推理本身在 GPU 上是高吞吐的但 OpenCV 的定位、分割这些 CPU 操作占了整个链路的一半以上时间并行化之后整批图片的吞吐能明显提升。这个项目我从下载到跑通前后花了一天时间最大教训是不要上来就调网络结构。先把预处理、训练、推理这条链路原封不动地跑一遍确认基线准确率再改模型或加数据。从那以后我每次拆这类老项目都强制自己先按原作者的参数跑通一轮记录 baseline之后的所有改动才有对照意义。希望这份拆解能帮你省下一些试错的时间。本文还有配套的精品资源点击获取