简介:这份资源是经过导师审核的Python车牌识别系统毕设项目源码,评审分达95分以上,面向计算机相关专业学生与从业者,可用于毕业设计、期末课程设计或大作业参考。项目融合深度学习与OpenCV技术,实现车牌检测与字符识别,运行GUI.py即可启动完整系统,适合希望掌握图像处理与深度学习落地应用的中级学习者。压缩包共47个文件,约28.81MB,包含6个py核心脚本(如主程序、图像识别、数学处理与配置模块)、21张jpg与15张png测试图片、2个7z字符集压缩包、2个dat模型文件及gitignore等,覆盖从训练数据到推理代码的完整链路。目前已有297人学习下载。读者可获得一套可直接运行的毕设级方案,理解车牌定位、字符分割与SVM识别等关键环节,并借助内置测试图片快速验证效果,为二次开发或论文撰写提供扎实基础。
1. 车牌识别系统:从一张糊图到结构化文本,中间到底卡在哪
地下车库出口,摄像头拍到的车牌往往只有 90 像素宽,还带着运动模糊和顶灯反光。很多人第一次做车牌识别系统,会以为难点在「识别」这一步,实际上真正让项目翻车的是前面的定位和字符分割——车牌都没框准,后面 CNN 再强也是白搭。这套基于深度学习加 OpenCV 的 Python 车牌识别系统,核心链路是「检测车牌位置 → 校正倾斜 → 分割字符 → 深度学习分类」,最终把一张图变成「京A12345」这样的结构化文本。它适合两类人:一类是拿它当 Python 毕业设计、需要完整可跑源码和清晰模块划分的学生;另一类是想把车牌识别接进停车场、门禁、称重系统的工程师,关心的是准确率边界和部署成本。下面按我实际搭过一遍的顺序,把选型、代码、参数和踩过的坑讲清楚。
2. 技术选型:为什么是 OpenCV 做前处理、CNN 做识别
2.1 传统图像处理与深度学习的边界在哪
车牌识别拆开看是四个子问题:车牌检测、倾斜校正、字符分割、字符识别。前三个用 OpenCV 的形态学、轮廓、透视变换就能覆盖大部分场景,第四个才是深度学习的主场。原因是字符识别面对的是 65 类左右的分类问题(省份简称、字母、数字),类别多、字形相近(0 和 O、8 和 B),传统模板匹配在光照变化下掉点严重,而 CNN 对这类细粒度分类天然占优。
我一般把 OpenCV 定位成「把图变干净」的角色:灰度化、高斯模糊、Sobel 边缘、闭运算把车牌连成一块白色矩形,再用轮廓筛选出候选区域。这一步不追求 100% 召回,追求的是把候选框压到个位数,给后面的 CNN 减负。深度学习部分用一个小型 CNN 就够,不需要上 ResNet——字符图只有 20×20 左右,参数量大了反而过拟合。
提示:如果你的场景是固定机位、光照稳定,纯 OpenCV 模板匹配也能到 85% 左右;一旦有夜间、逆光、大角度,就必须上 CNN,这是分水岭。
2.2 环境搭建:Python、OpenCV、深度学习框架的版本对齐
环境是新手第一个翻车点。ModuleNotFoundError: No module named 'opencv'和python 下载 cv2这类搜索量一直很高,本质是包名和导入名不一致。OpenCV 的 pip 包叫opencv-python,导入时却是import cv2,很多人卡在这。
# 建议用虚拟环境,避免和系统 Python 打架 python -m venv lpr_env # Windows 激活 lpr_env\Scripts\activate # Linux / macOS 激活 source lpr_env/bin/activate # 安装核心依赖,版本对齐很关键 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install tensorflow==2.13.0 # 或 pytorch,二选一 pip install pillow scikit-learn逻辑说明:opencv-python提供图像处理,numpy是 OpenCV 的底层数组依赖,深度学习框架二选一即可。参数上,OpenCV 4.x 和 3.x 的 API 有差异,比如cv2.findContours在 3.x 返回三个值、4.x 返回两个值,网上老教程直接抄会报ValueError: too many values to unpack。TensorFlow 2.13 对应 Python 3.8~3.11,Python 3.12 装不上,这是硬约束。
import cv2 import numpy as np print(cv2.__version__) # 确认 4.x img = cv2.imread("car.jpg") print(img.shape) # (h, w, 3),None 说明路径错了如果img是None,九成是路径含中文或反斜杠转义问题,用cv2.imdecode(np.fromfile(path, dtype=np.uint8), -1)读中文路径更稳。
2.3 数据集从哪来、怎么标注
毕业设计常见做法是找开源车牌数据集,再自己补拍。标注分两块:车牌位置用矩形框(VOC 的 XML 或 YOLO 的 txt),字符识别用「字符图 + 标签」的文件夹结构。我一般把字符图统一缩放到 20×20 灰度,按类别建文件夹,省得写额外的标签文件。
| 数据用途 | 格式 | 数量建议 | 备注 |
|---|---|---|---|
| 车牌检测 | JPG + XML | 500~1000 张 | 含夜间、倾斜样本 |
| 字符识别 | 20×20 灰度 PNG | 每类 200 张以上 | 65 类,共约 1.3 万张 |
| 验证集 | 同上 | 总数的 15% | 不要和训练集同源 |
数据量不够时,用旋转 ±10 度、亮度 ±30%、加高斯噪声做增强,比盲目堆网络深度有效得多。
3. 车牌定位与字符分割:OpenCV 前处理的完整代码链路
3.1 从原图到候选车牌框的六步处理
这一步的目标是把彩色原图变成几个候选矩形。核心思路是利用车牌区域「边缘密集、颜色偏蓝或绿」的特性。
import cv2 import numpy as np def locate_plate(img): # 1. 高斯模糊,压掉噪点 blur = cv2.GaussianBlur(img, (5, 5), 0) # 2. 转灰度 gray = cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY) # 3. Sobel 提取垂直边缘,车牌字符垂直边缘密集 sobel = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3) # 4. 二值化,阈值可调 _, binary = cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 5. 闭运算把字符连成整块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 6. 找轮廓并按长宽比筛选 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for c in contours: x, y, w, h = cv2.boundingRect(c) ratio = w / float(h) if 2.0 < ratio < 5.5 and w > 80: candidates.append((x, y, w, h)) return candidates逻辑说明:Sobel 的(1, 0)表示只算 x 方向导数,因为车牌字符是竖直排列的,垂直边缘最丰富。闭运算的核(17, 5)是宽扁形,专门用来横向连接字符。参数上,长宽比 2.0~5.5 覆盖了蓝牌和新能源绿牌,w > 80过滤掉小噪点。如果候选框太多,把闭运算核调大;如果漏检,把阈值改成手动低值。
3.2 倾斜校正:透视变换的四个角点怎么定
拍到的车牌往往有 5~15 度倾斜,不校正会直接影响字符分割。常见做法是用cv2.minAreaRect拿到最小外接矩形,再取四个角点做透视变换。
def correct_plate(img, rect): # rect 是 minAreaRect 返回的 ((cx,cy),(w,h),angle) box = cv2.boxPoints(rect) box = np.int0(box) # 按 x 坐标排序,分出左右两组 box = sorted(box, key=lambda p: p[0]) left = sorted(box[:2], key=lambda p: p[1]) right = sorted(box[2:], key=lambda p: p[1]) src = np.array([left[0], right[0], right[1], left[1]], dtype=np.float32) # 目标矩形,宽高按实际车牌比例 440:140 dst = np.array([[0, 0], [440, 0], [440, 140], [0, 140]], dtype=np.float32) M = cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(img, M, (440, 140))逻辑说明:boxPoints拿到四个角点后,按 x 排序分成左右两组,再各自按 y 排序,得到「左上、右上、右下、左下」的顺序,这是getPerspectiveTransform要求的对应关系。目标尺寸 440×140 是标准蓝牌比例,统一尺寸后字符分割的参数才好固定。角度超过 20 度时,minAreaRect的角点顺序可能错乱,需要额外判断,这是常见翻车点。
3.3 字符分割:垂直投影法的阈值怎么定
校正后的车牌是 440×140,字符之间有明显空隙,用垂直投影就能切开。
def split_chars(plate_gray): # 二值化,字符为白 _, binary = cv2.threshold(plate_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:每列白色像素数 projection = np.sum(binary, axis=0) / 255 # 找字符区间 chars = [] in_char = False start = 0 for i, v in enumerate(projection): if v > 2 and not in_char: in_char = True start = i elif v <= 2 and in_char: in_char = False if i - start > 8: # 过滤太窄的噪点 chars.append(binary[:, start:i]) return chars逻辑说明:projection是每列的白色像素计数,字符所在列数值高,空隙列接近 0。阈值v > 2是经验值,太小会把噪点当字符,太大会把细笔画(比如「1」)切掉。i - start > 8过滤掉宽度不足 8 像素的干扰。分割后一般得到 7 个字符(新能源 8 个),如果数量不对,先回去检查二值化是否干净。
注意:中文省份简称(如「京」「沪」)笔画复杂,二值化后容易粘连,建议对第一个字符单独放宽宽度阈值。
4. 深度学习字符识别:CNN 模型结构与训练参数
4.1 为什么用小型 CNN 而不是直接上预训练大模型
字符图只有 20×20 灰度,信息量有限。用 ResNet50 这种大模型,参数量几千万,训练集才一万多张,必然过拟合,验证集准确率上不去。我一般用三层卷积加两层全连接,参数量控制在 50 万以内,训练快、部署轻,CPU 上单张推理 5 毫秒以内。
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(num_classes=65): model = models.Sequential([ # 输入 20x20 灰度 layers.Input(shape=(20, 20, 1)), layers.Conv2D(32, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), # 关键:防过拟合 layers.Dense(num_classes, activation='softmax') ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss='categorical_crossentropy', metrics=['accuracy']) return model逻辑说明:三层卷积的感受野足够覆盖 20×20 的字符结构,padding='same'保证边缘信息不丢。Dropout(0.5)放在全连接前,是防过拟合最有效的一招。优化器用 Adam,学习率 1e-3 是起点,训练 20 轮后如果 loss 不降,降到 1e-4。类别数 65 对应 31 个省份简称加 24 个字母数字(去掉易混的 I、O)。
4.2 训练参数与数据增强的实操配置
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen = ImageDataGenerator( rescale=1./255, rotation_range=8, # 小角度旋转 width_shift_range=0.1, height_shift_range=0.1, brightness_range=[0.7, 1.3], validation_split=0.15 ) train_data = train_gen.flow_from_directory( 'chars_dataset', target_size=(20, 20), color_mode='grayscale', batch_size=64, class_mode='categorical', subset='training' ) model = build_cnn() history = model.fit(train_data, epochs=30, callbacks=[tf.keras.callbacks.EarlyStopping(patience=5)]) model.save('plate_char_cnn.h5')逻辑说明:rotation_range=8模拟拍摄倾斜,brightness_range模拟昼夜光照差,这两个增强对车牌场景最有效。batch_size=64在显存 4G 以上都跑得动,太小会导致 loss 震荡。EarlyStopping(patience=5)是后悔药,验证集 5 轮不提升就停,避免白跑。训练完保存.h5,推理时直接load_model。
| 参数 | 推荐值 | 调整方向 |
|---|---|---|
| 学习率 | 1e-3 | loss 震荡降到 1e-4 |
| batch_size | 64 | 显存不足降到 32 |
| epochs | 30 | 配合 EarlyStopping |
| Dropout | 0.5 | 过拟合加重可到 0.6 |
4.3 推理阶段:把分割字符喂给模型
def recognize(chars, model, idx_to_char): result = "" for ch in chars: # 缩放到 20x20,归一化 ch = cv2.resize(ch, (20, 20)) ch = ch.astype('float32') / 255.0 ch = ch.reshape(1, 20, 20, 1) pred = model.predict(ch, verbose=0) result += idx_to_char[np.argmax(pred)] return result逻辑说明:idx_to_char是训练时class_indices的反向映射,必须和训练顺序一致,否则识别结果全乱。verbose=0关掉每张图的进度输出,批量推理时快很多。如果某个字符置信度低于 0.6,可以标记为可疑,实际系统里会触发二次拍摄。
5. 避坑与排查:车牌识别最常见的 5 个翻车现场
5.1 现象:识别结果全是同一个字符
原因:idx_to_char映射和训练时的class_indices顺序不一致,或者推理时输入没归一化(训练用了rescale=1./255,推理忘了除 255)。解决:训练完立刻把train_data.class_indices存成 json,推理时加载同一份,归一化写成函数复用。
5.2 现象:白天准、晚上全错
原因:训练集几乎全是白天样本,模型没见过夜间低对比度图。解决:数据增强里加brightness_range=[0.5, 1.5],再补拍 200 张夜间样本。如果还不行,在定位前加一步 CLAHE 自适应直方图均衡,cv2.createCLAHE(clipLimit=2.0).apply(gray)。
5.3 现象:字符分割多切或少切
原因:二值化阈值不合适,或者车牌校正后仍有残留边框。解决:分割前先用cv2.morphologyEx做一次开运算去掉细边框,投影阈值从v > 2调到v > 3试。中文省份字单独处理,宽度阈值放宽到 12。
5.4 现象:OpenCV 报too many values to unpack
原因:网上教程基于 OpenCV 3.x,findContours返回三个值,你装的是 4.x 只返回两个。解决:改成contours, _ = cv2.findContours(...),或者统一锁版本opencv-python==4.8.1.78。
5.5 现象:模型训练准确率 99%,实际用起来一塌糊涂
原因:训练集和验证集同源,字符图都是从同一批车牌裁的,分布太像。解决:验证集必须来自不同时间、不同机位的车牌,哪怕只有 100 张,也比同源 1000 张有参考价值。这是血泪经验,别省这一步。
6. 进阶技巧:把准确率从 90% 推到 96% 的三个动作
第一个动作是「检测和识别解耦调优」。很多人把定位和识别串成一个端到端模型,改一处影响全局,排查困难。我习惯保持 OpenCV 定位 + CNN 识别的两段式,定位召回率调到 98% 以上(宁可多框几个候选),识别阶段再用置信度过滤。这样每段可以独立测,定位不行就调形态学参数,识别不行就补数据,互不干扰。
第二个动作是「字符级置信度回退」。实际系统里,7 个字符全对的概率是各字符准确率的乘积,单字 98% 的话整牌只有 87%。做法是:对置信度低于 0.7 的字符,用相似字符集做二次判断,比如「0/O」「8/B」「5/S」建一个混淆表,结合车牌规则(第二位通常是字母)做约束。这一步能把整牌准确率拉高 3~5 个百分点。
第三个动作是「用真实业务数据做增量训练」。上线后把识别错误的样本自动存下来,人工标一遍,每周增量训练一次。我一般设一个阈值:同一字符连续错 20 次以上,就把它加进训练集。这个习惯坚持两个月,模型对本地车牌的适应度会明显好于通用模型。
| 优化动作 | 预期提升 | 成本 |
|---|---|---|
| 检测识别解耦 | 2~3% | 低,改架构 |
| 置信度回退 | 3~5% | 中,需混淆表 |
| 增量训练 | 5%+ | 高,需标注流程 |
最后说个习惯:每次改完参数,别只看总准确率,把混淆矩阵打出来,看错在哪些字符对上。我踩过最深的坑就是盯着 95% 的总数沾沾自喜,结果发现「京」和「津」混淆率 30%,换个城市直接崩。把错误拆到字符级,才知道下一步该补什么数据。希望帮到你。
本文还有配套的精品资源,点击获取