基于OpenCV与CNN的车牌识别系统:从定位到字符识别完整实现

发布时间:2026/10/15 17:43:12
基于OpenCV与CNN的车牌识别系统:从定位到字符识别完整实现
简介面向毕业设计场景的车牌识别系统基于Python与OpenCV实现使用两个结构相同的卷积神经网络分别完成车牌定位过滤与字符识别。定位部分覆盖图像预处理、车牌轮廓提取与区域定位识别部分涵盖字符分割及车牌信息输出可作为图像处理、深度学习课程项目或毕设的完整参考。压缩包共2000个文件约459MB主要包含Python源码、预训练模型检查点checkpoint/meta/index/data、XML配置及大量JPG车牌样本模型权重文件齐全下载后可按说明直接调用无需从零训练。资源已吸引8400余人学习下载社区验证度较高工程将模型输入层设为36x128卷积层采用ReLU激活与SAME填充并配置池化与多层卷积结构方便结合代码理解CNN在车牌识别中的完整落地流程。同时附带的字符分割与车牌定位脚本能帮助快速复现实验、对比效果并迁移到类似识别任务。1. 认识车牌识别系统从 OpenCV 定位到 CNN 识别的完整闭环车牌识别这个方向网上能搜到一堆工作台演示但能闭环跑通的很少。这个基于 OpenCV 卷积神经网络的车牌识别系统把车牌定位、字符分割、CNN 分类和训练预测完整打通输入一张汽车照片输出一串车牌号字符串和置信度。我自己拿它同时处理了蓝牌、黄牌、新能源绿牌定位精度和字符识别率都比较稳。它适合两类人一类是做毕业设计或者课程项目需要一个能把每一步原理讲清楚的完整代码另一类是刚接触图像识别的开发者想看看传统图像处理和卷积神经网络在真实项目里怎么分工配合。整个项目没有什么黑匣子所有中间步骤都导出可查跑通之后二次开发的空间也大。2. 技术选型与架构设计为什么把任务拆成 OpenCV 预处理和 CNN 分类2.1 为什么不直接上端到端目标检测做车牌识别摆在你面前的有两条技术路线。一条是端到端检测识别把整张汽车图片直接送进一个目标检测网络网络输出车牌的边界框和字符内容优势是上限高一个模型搞定识别但弊端也很明显——需要大量标注数据光照、角度、遮挡稍有变化性能就会明显波动。另一条就是本项目采用的路线把任务拆成两段。车牌定位交给 OpenCV 的经典图像处理手段核心只有边缘检测、形态学运算、轮廓筛选这几招字符识别交给一个轻量级卷积神经网络。拆开的好处有几点定位车牌的视觉特征非常固定无非是矩形、边框、字符纹理、底色对比度。这些规律用传统图像处理方法就能稳定抓住而且每一步都是中间图像参数调起来非常直观不像端到端模型那样只能对着 loss 曲线猜问题。字符识别的类别是有限的——省份汉字、字母、数字加起来不到 70 类一个小型 CNN 就能扛住。CNN 对字符的形态差异有天然优势尤其是字体、粗细、清晰度不一致的情况下分类效果比传统的模板匹配好一个量级。整套管线的可解释性也强定位框画出来不对直接调 OpenCV 参数识别错了去看是哪一类样本不够。这种分步排查能力是端到端方案很难给的。2.2 目录结构按定位、分割、识别三个职责拆开plate_recognition/ ├── data/ │ ├── raw_images/ # 原始车辆图片 │ ├── crop_plates/ # 定位后裁剪出的车牌区域 │ ├── split_chars/ # 字符分割后的单字图 │ └── dataset/ # 整理成训练/验证集的字符图像 │ ├── train/ │ └── val/ ├── modules/ │ ├── plate_detector.py # 车牌定位模块 │ ├── char_segmentor.py # 字符分割模块 │ ├── cnn_model.py # CNN 模型定义、训练与保存 │ └── utils.py # 图像读写、字符映射、可视化 ├── config.py # 全局参数路径、阈值、类别数 ├── train.py # 训练入口脚本 ├── predict.py # 单图推理入口脚本 ├── label_map.json # 字符类别到数值索引的映射 └── requirements.txt # Python 依赖清单目录结构是按“定位 → 分割 → 识别”三段流水线拆的。plate_detector 只负责输出车牌坐标char_segmentor 只吃车牌区域图片输出单字符图cnn_model.py 只处理字符分类训练也在里面。三者通过 config.py 中的路径和尺寸参数解耦。中间产物都放在 data 下面crop_plates 能直接看出定位是否准确split_chars 能看出分割是否干脆。排查问题时先把中间结果导出来用看图工具打开一条条对比对着代码猜要快得多。我第一次调试的时候定位一直把车灯当车牌就靠导出 crop_plates 才发现问题是闭运算核宽度不够导致字符边缘没有连成整体。2.3 数据流原始图片到车牌字符串的六个关键阶段整条识别链路可以拆成六步。第一阶段输入预处理读取车辆原图按比例缩放、灰度化、高斯滤波去噪。缩放是为了减少计算量灰度化是为了让后续边缘检测不受颜色通道干扰。第二阶段车牌定位用 Sobel 算子在灰度图上求纵向边缘再通过形态学闭运算把字符边缘连成块最后用轮廓筛选加长宽比判断锁定车牌位置。第三阶段裁剪与校正根据定位坐标裁出车牌区域遇到倾斜车牌还需要做旋转校正或透视变换轻倾斜用旋转矩阵就够了。第四阶段字符分割对车牌区域做 Otsu 二值化再用垂直投影法统计每列白色像素数量找到字符之间的空隙按空隙切分出单字符图。第五阶段字符归一化每个字符图缩放到统一尺寸转成单通道灰度矩阵归一化到 01 区间作为 CNN 的输入张量。第六阶段CNN 分类与拼接。每个字符图送入卷积神经网络得到在各字符类别上的概率分布取最大概率为预测结果最后按顺序拼接成完整的车牌字符串。简化后的推理脚本只需执行一条命令python predict.py --image data/raw_images/plate_sample.jpg --model save_model/plate_cnn.h5输出就是京A·12345这样的字符串附带每个字符的置信度。中间每一步的数据都会在对应 data 子目录留底方便复现。3. 核心代码拆解车牌定位、字符分割与 CNN 识别的关键实现3.1 车牌定位边缘检测加形态学处理车牌定位的核心是“找矩形框”。现实中的车牌有蓝色底、黄色底、白色底但共同特征都有有边框、字符排列整齐、长宽比稳定在 2.55.5 之间。利用这些特征做边缘检测和几何过滤是经典做法。这一块的常见实现import cv2 import numpy as np def locate_plate(image): # 缩放图片限制最大宽度 800加快后续处理 scale 1.0 if image.shape[1] 800: scale 800.0 / image.shape[1] image cv2.resize(image, (800, int(image.shape[0] * scale))) # 灰度化 高斯滤波减少噪声干扰 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) # 形态学闭运算先膨胀再腐蚀让字符边缘连成整体 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel) # Sobel 纵向边缘检测车牌字符的垂直边缘最密集 sobel cv2.Sobel(closed, cv2.CV_16S, 1, 0, ksize3) abs_sobel np.absolute(sobel) sobel_8u np.uint8(abs_sobel) # 自适应阈值二值化选出边缘区域 _, binary cv2.threshold(sobel_8u, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 再做一次闭运算把相邻字符连成一个整体区域 closed_binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 提取外轮廓按面积、长宽比筛选候选车牌 contours, _ cv2.findContours(closed_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w * h 2000 or w 80 or h 25: continue ratio w / float(h) if 2.5 ratio 5.5: candidates.append((x, y, w, h)) if not candidates: return None # 候选里取面积最大的一块 candidates.sort(keylambda r: r[2] * r[3], reverseTrue) return candidates[0], image这段代码有几个关键参数值得说明。第一个是闭运算的核大小核宽 17 像素、高 5 像素对应车牌字符横向密集、纵向较窄的特点。核太窄字符连不成块核太宽会把旁边物体也连进来导致定位框变大。第二个是 Sobel 的方向选取车牌识别场景一般用纵向边缘因为字符的竖笔是主要轮廓横向边缘受车辆底盘干扰更大。第三个是 Otsu 二值化它会自动寻找阈值不用手调但前提是图像直方图是双峰分布如果光照不均导致双峰不明显可以先用 CLAHE 做对比度增强再进这步。第四个是长宽比过滤这个值能过滤掉大部分非车牌区域但也存在风险——侧面视角下车牌比例偏离 3:1 时容易被过滤掉需要配合面积阈值一起用而不是只看比例。实际跑下来最常翻车的点是只靠边缘检测时车身贴纸、车灯、进气格栅都有很强的边缘特征容易产生误检。所以很多实现会加一路颜色检测在 HSV 空间里筛出蓝色或黄色区域再和边缘检测结果取交集能有效减少误检。这个项目里预留了 HSV 颜色过滤接口config.py 里 BLUE_LOWER、BLUE_UPPER 这组参数就是干这个的。3.2 字符分割垂直投影法定位到车牌区域之后下一步把车牌上的字符切分成单个字符。最常用的手段是垂直投影法统计二值化图像每一列上的白色像素数字符所在列白像素明显多字符间隙列白像素接近零二者之间的分界就是切分点。def split_characters(plate_roi, char_width_min8): 输入裁剪得到的车牌区域图返回切分后的单字符图像列表。 gray cv2.cvtColor(plate_roi, cv2.COLOR_BGR2GRAY) # 自适应阈值二值化 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 蓝底白字车牌中字符是白色黄底黑字车牌需取反 if np.mean(binary 127) 0.5: binary 255 - binary h, w binary.shape # 统计每列的白色像素数量 col_count np.count_nonzero(binary, axis0) threshold col_count.max() * 0.2 in_char col_count threshold # 扫描连续字符区间 chars [] start None for i in range(w): if in_char[i] and start is None: start i elif not in_char[i] and start is not None: if i - start char_width_min: chars.append((start, i)) start None if start is not None: chars.append((start, w)) # 过滤过宽或过窄的候选一般是噪声或字符粘连 result [] for (x1, x2) in chars: if x2 - x1 60 or x2 - x1 char_width_min: continue result.append((binary[:, x1:x2], (x1, x2))) return result这里有个关键判断二值化之后要确认字体颜色和底色之间的关系。蓝底白字车牌字符部分是白色像素多直接统计即可黄底黑字车牌字符是黑色需要先把整张图取反否则投影统计时全反了。这是分割结果为空的最大根源。另一个关键是投影阈值的选择。用 col_count.max() 乘以 0.2 作为阈值可以让分割决策适应不同亮度的车牌。如果阈值写成固定值比如 10遇到光照较暗整张图都是低像素值时会把背景噪声误判成字符用动态阈值抗干扰能力强很多。最后一步过滤宽度单个字符的宽度在归一化前一般落在 1245 像素之间过宽大概率是两个字符粘连过窄是噪点。切割完成后的字符图会归一化并保存送入 CNN 训练。如果发现分割结果里字符个数不等于 7大概率是二值化阈值没调好或者车牌本身有倾斜导致投影重叠此时需要考虑先做透视校正再回来切字符。3.3 CNN 识别模型架构设计与输入映射字符分类是项目的核心部分。CNN 模型的输入是分割出的单字符图归一化到固定尺寸输出是字符类别的概率分布。类别映射通过 label_map.json 文件维护训练和推理时共用。import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_shape(32, 128, 1), num_classes34): model models.Sequential(namelicense_plate_char_cnn) model.add(layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activationrelu, paddingsame)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activationrelu, paddingsame)) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dense(256, activationrelu)) model.add(layers.Dropout(0.5)) model.add(layers.Dense(num_classes, activationsoftmax)) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) return model这个模型放在字符识别这种任务上完全够用原因是字符图像已经过定位和分割的预处理背景噪声很小类别之间形态差异明显不需要非常深的网络就能学到判别特征。参数上注意两点一是输入尺寸 32×128这是针对车牌字符细长比例做的约定如果换成 32×64 的横向尺寸卷积层的 padding 和池化层输出形状需要同步检查二是 dropout 放在全连接层之前设 0.5 防止过拟合训练数据量小的时候尤其有用。训练时建议用 ImageDataGenerator 做数据增强包括随机旋转 10 度、宽度偏移、缩放等把每个字符样本扩充 35 倍。数据增强能直接改善样本不足导致的识别率波动尤其是省份汉字这种类别内样本不平衡的情况。训练结束用 model.save 保存整个网络结构加权重推理时 keras.models.load_model 一行就能加载回来。4. 运行项目环境搭建、数据准备与训练推理全流程4.1 环境依赖与安装这个项目的运行依赖集中在 requirements.txt 里主要包括 OpenCV、NumPy、TensorFlow 三个核心库加上 Pillow 和 Matplotlib 用于图像读取与可视化。常见做法是创建虚拟环境再安装依赖避免污染全局 Python。python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -r requirements.txt依赖版本这里给一个参考组合opencv-python 4.8 系列、tensorflow 2.13 左右、numpy 1.24 左右。OpenCV 从 4.x 开始 findContours 的返回值从三个变成两个代码里的写法是按新版本兼容的。如果你用的是 OpenCV 3.x需要把contours, _ cv2.findContours(...)改成cnts, hierarchy cv2.findContours(...)。TensorFlow 版本的选择会影响 GPU 支持和模型保存格式。如果机器上没有 NVIDIA GPU装 CPU 版也能跑训练和推理速度会慢一些但功能完整。装了 GPU 版之后第一次训练前建议确认 GPU 可用import tensorflow as tf print(tf.config.list_physical_devices(GPU))如果输出是空列表说明驱动或 CUDA 没配好模型会自动落在 CPU 上训练时间会明显变长但不会报错。这个检查步骤建议每次换机器都做一遍省下很多在训练中才发现卡顿的时间。4.2 数据准备与目录组织训练 CNN 需要一个字符级数据集。项目里 data/dataset 目录按 train 和 val 两个子目录组织每个子目录下以字符类别命名子文件夹比如省份汉字“京”、字母“A”、数字“1”各一个文件夹每个文件夹内存放若干张该字符的图片。最省事的准备方式是先用真实图片跑一遍定位和分割把生成的分割单字图按类别手动归类进 train 和 val。固定套路是先准备 200 到 300 张不同场景下的车牌照片运行分割脚本得到上万张单字图再按字符类别装箱经过简单清洗去掉噪声样本就能得到一个初始训练集。label_map.json 是类别映射文件内容类似下面这样{ 0: 0, 1: 1, A: 10, B: 11, 京: 30, 沪: 31 }键是字符对应的映射文字值是 CNN 输出层用到的类别索引。训练和推理都只认这个映射新增类别时先在 label_map.json 里登记再改 config.py 的 num_classes模型输出层会自动适配。4.3 训练脚本与推理脚本的参数说明训练脚本的常见参数如下python train.py \ --data data/dataset \ --epochs 30 \ --batch_size 32 \ --lr 1e-3 \ --save_dir save_model参数含义逐一说明--data 指向数据集根目录脚本会自动拼接 train 和 val 子路径--epochs 设 30一般情况下训练 20 到 30 个 epoch 就能达到 95% 以上的字符识别率继续增加对最终精度提升有限--batch_size 根据显存或内存大小调整调大能加速训练但增加显存压力--lr 初始学习率 1e-3配合 Adam 优化器比较稳--save_dir 是模型和训练曲线图的保存目录脚本会在里面生成 .h5 模型文件。推理脚本使用更简单python predict.py \ --image data/raw_images/test_001.jpg \ --model save_model/plate_cnn.h5 \ --show True--image 是待识别图片路径--model 指向训练生成的模型文件--show 设为 True 会在屏幕上弹出定位框和分割结果的可视化窗口False 则纯命令行输出。输出内容是车牌字符串加每个字符的置信度比如京A·12345 0.98 0.96 0.99 ...。这样一套流程跑完系统就能从单张照片识别车牌了。第一次跑通时拿到一张阴天逆光拍的蓝牌车定位框、分割结果、识别输出一次成型成就感还是不小的。5. 避坑与常见问题排查从定位偏差到字符误识别的实战记录5.1 现象定位框总是偏向一侧框不住完整车牌输出图像上定位矩形没有包住整个车牌而是切走了半边。原因一般是形态学闭运算的核宽度不够字符边缘没有完全连成整体导致轮廓只覆盖了部分区域。解决方法是把核改大一点比如从 (17,5) 改成 (21,7)。如果改成 (21,7) 之后定位框反而变大过多说明连上了旁边的东西需要同时收紧长宽比过滤区间把阈值从 2.55.5 收成 2.84.8。调整时建议每改一个参数就导出一次 crop_plates 目录的中间图对照观察变化不要一次改太多参数学会玄学调参。5.2 现象findContours 报错提示返回值数量不对这个报错在 OpenCV 版本切换时非常常见。OpenCV 3.x 的 cv2.findContours 返回两个值OpenCV 4.x 的返回写法也有变化不同版本之间代码不兼容直接报 TypeError。解决方法是把调用改成兼容写法。判断安装的 OpenCV 主版本号或者直接固定使用 OpenCV 4.x 加contours, _ cv2.findContours(...)写法这是最省心的路径。如果必须在旧环境跑cnts, hierarchy cv2.findContours(...)也能用。装饰一下把版本号打印出来再跑代码能少走很多弯路import cv2 print(cv2.__version__)5.3 现象字符识别把“0”认成“O”、“1”认成“I”数字和字母的相似字符混淆是所有车牌识别系统都会遇到的老大难问题。原因有两个一是字符形态本身相似尤其是分割后分辨率低时特征更模糊二是训练样本里这些类别的样本分布可能不均匀某一类图片特别少模型没学够。解决路径分两步。第一步在数据准备阶段对易混淆字符做针对性采集确保两种字符的样本数量大致均衡第二步在模型后处理环节加规则约束车牌字符串第 1 位是省份汉字、第 2 位是字母、后面位次多为字母和数字混合。根据位次约束做纠错能把混淆率压下去。比如某一位根据车牌规则只可能是字母时把预测结果强制映射到字母类别。5.4 现象训练时 loss 波动大准确率徘徊不涨一种常见场景是模型训练没有收敛loss 在几个 epoch 后开始震荡准确率卡在某个值上不去换了更大的模型也没有改善。原因多半是数据增强太激进每轮喂进去的样本变化过大模型学不到稳定的字符特征。解决方法是把旋转角度从 10 度降到 5 度关掉水平翻转只保留轻微缩放和平移。车牌字符是有方向性的水平翻转会让“6”和“9”这类字符语义颠倒模型学到的特征就不可靠了。这是很多人训练字符识别时容易忽略的点数据增强不是越多越好。偶尔还会遇到另一种情况准确率很高但 loss 偏高这通常是标注错误或数据里混了噪声样本。把训练里 loss 最大的那批样本导出来人工看一眼能快速定位脏数据来源。6. 二次开发改造思路换数据集、轻量化与工程化落地6.1 用自己采集的数据集替换默认数据这个项目的数据组织和训练流程是清晰干净的。要换成自己的数据集只需要按 data/dataset 的目录约定整理train 和 val 下每个类别一个文件夹图片统一裁剪为单字符图格式不限JPG 或 PNG 均可。类别数量和之前不同时改 config.py 里的 num_classes然后检查 label_map.json 是否覆盖了所有新类别。还有一点是类别映射的索引不能出现空洞否则 CNN 输出层的索引和自己数据集的类别对不上会直接导致识别结果错乱。6.2 模型轻量化从三层卷积换成轻量特征提取网络如果要把系统部署到嵌入式设备上三层卷积网络也可以继续瘦身。常见做法是用 MobileNetV2 去除最后几层分类层只做特征提取再接一个全连接输出层整个模型参数量可以大幅下降推理速度至少提升一倍。具体操作不复杂把 cnn_model.py 中模型定义部分换成更轻的特征提取网络即可输入输出形状保持一致训练和推理脚本一行都不用改。部署到嵌入式设备之前在 PC 上先验证性能和精度的平衡点保存模型后再转成 openvino 或 tflite 格式。6.3 把识别链路封装成 HTTP 服务要接入到真实业务里把 predict.py 的识别逻辑封装成函数对外提供输入图片路径或二进制流、返回车牌字符串和置信度的接口是常规路径。用 Flask 或 FastAPI 起一个服务调用时 POST 一张图片返回 JSON 结果前后端集成和测试都会简单很多。最后分享一个我自己的习惯每次改动模型结构或数据处理逻辑我都会强制先跑一套固定的测试图片集记录改动前后的识别率变化防止“改好了这里、坏了那里”的回归问题。做图像识别项目底层的图像预处理和样本质量永远比模型结构影响更大如果识别的目标不稳定任何后处理规则都只是安慰。希望这篇拆解能帮你在车牌识别这类任务上少走一次弯路直接跑通闭环。本文还有配套的精品资源点击获取