简介:基于Python的智能停车场车牌识别计费系统毕业设计项目,面向计算机相关专业学生,可用于毕业设计、课程设计或期末大作业的完整参考。系统以CenterNet目标检测模型定位车牌区域,结合最优CNN模型完成字符识别,再借助Pygame模块搭建图形化计费界面,覆盖车辆出入场、车牌识别、计时收费等核心环节,技术路线完整,代码结构清晰。压缩包共含2000个文件,大小约124.69MB,其中999个py文件构成系统主代码,另有数据文件、配置脚本、说明文档及多种辅助资源,便于直接运行、阅读和二次开发。项目已获老师指导并通过验收,作为毕业设计评分较高,整体实现完整,适合人工智能方向实战入门,也能根据不同停车场需求调整参数或界面。目前已有533人学习下载,对希望快速搭建智能停车系统的读者具有较高参考价值。
1. 智能停车场车牌识别计费系统:CenterNet 检测 + CNN 字符识别 + Pygame 界面的完整可跑方案
每年毕设季,停车场车牌识别都是热门选题,但绝大多数网上源码要么只有检测没有计费,要么界面和识别是两套割裂的代码,跑通一个环节,另一个环节又报错。这份基于 Python 的智能停车场车牌识别计费系统,好就好在把「检测、识别、计费、界面」四个环节串成了完整闭环:车辆入场时 CenterNet 负责从视频帧里把车牌位置框出来,CNN 模型负责把车牌图片转成字符串,Pygame 界面做上下班记录和费用结算,全部数据文件一并打包。它不是那种只有几个 py 文件凑数的演示项目,而是能直接作为毕业设计交付、也够格当课程设计或期末大作业的完整工程。
它适合两类人:一类是正在做毕设、需要一套能讲清楚原理又能演示的完整系统;另一类是想快速上手目标检测 + OCR 落地流程的开发者,想知道 CenterNet 在实际项目中怎么组织数据、怎么训练、怎么和 UI 联动。项目里附带的全部数据文件意味着你不用满网找数据集,解压后就能开始复现。
2. CenterNet 车牌检测:为什么选 anchor-free 结构,以及推理时的关键处理
2.1 车牌检测的选型逻辑:从 YOLO 到 CenterNet 的取舍
做车牌检测,最常见的方案是 YOLO 系列和 SSD 这类 anchor-based 模型,但这份源码选的是 CenterNet。原因在于车牌检测任务有它的特殊性:车牌是一个强矩形、小尺寸、高对比度的目标,它不像行人检测那样需要区分复杂的姿态和遮挡。CenterNet 把目标检测建模成「预测中心点 + 回归宽高」的问题,不需要预先设计 anchor 的尺度和长宽比,这对车牌这种尺寸相对固定的目标反而更省事。
另一个实际考量是部署环境。毕设场景里跑检测的机器大概率是普通笔记本或台式机,没有 GPU 或者只有低端 GPU。CenterNet 的骨干网络如果用 ResNet-18 或 Hourglass-52,在 CPU 上也能跑到可用的帧率。源码里选的骨干网络偏向轻量,推理时只做一次前向,不用像 anchor-based 方法那样做 NMS 的后处理,减少了大量排序和去重计算。
2.2 项目里的推理流程是怎么组织的
进入项目后,检测相关的代码主要在 detector 这一类里。下面是核心的推理流程,我根据源码里的调用关系做了简化整理:
import cv2 import numpy as np def detect_plate(model, image): # 输入图像预处理:LetterBox 保持宽高比后 resize 到 512x512 img, ratio, dw, dh = letterbox(image, new_shape=(512, 512)) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR -> RGB, HWC -> CHW img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 模型前向推理,输出是 (batch, 4, 128, 128) 的张量 # 4 个通道依次是 heatmap、宽、高、中心点偏移量 outputs = model.predict(img) # 在 heatmap 上取局部极大值作为候选中心点 heatmap = outputs[0, 0] scores = get_topk_scores(heatmap, k=5) boxes = [] for score, (cx, cy) in scores: w = outputs[1, cy, cx] * 4 h = outputs[2, cy, cx] * 4 # 把中心点坐标映射回原图尺寸 x1 = int((cx * 4 - dw - w / 2) / ratio) y1 = int((cy * 4 - dh - h / 2) / ratio) x2 = int((cx * 4 - dw + w / 2) / ratio) y2 = int((cy * 4 - dh + h / 2) / ratio) boxes.append((x1, y1, x2, y2, float(score))) return boxes这段代码里有几个地方值得说一下。letterbox这一步很关键,它保证输入图像在 resize 到 512x512 时不被拉伸变形,因为车牌字符一旦被横向拉伸,后续 CNN 识别立刻掉点。模型输出的热力图尺寸是 128x128,对应原图 512x512 的 4 倍下采样,所以宽高和偏移量在映射回原图坐标时要记得乘 4。
另外,源码里取的 top-5 候选框,不是只取最高分的一个。实际场景中画面里可能同时出现多辆车,或者一辆车前后有两个车牌(例如大型车),top-5 的设定能兼顾多目标情况。如果你在复现时发现漏检,可以把这个值调大到 10,代价是后续识别环节多几次前向。
2.3 检测结果的可视化与调试方式
为了方便调试,项目里在检测环节直接接了一段可视化代码,把检测框画到原始帧上,同时把裁剪出来的车牌区域单独存成图片,供下一步字符识别使用:
def crop_plate_region(frame, box): x1, y1, x2, y2, score = box # 向外扩 10% 边界,避免字符被切到 pad_x = int((x2 - x1) * 0.1) pad_y = int((y2 - y1) * 0.1) x1 = max(0, x1 - pad_x) y1 = max(0, y1 - pad_y) x2 = min(frame.shape[1], x2 + pad_x) y2 = min(frame.shape[0], y2 + pad_y) plate_img = frame[y1:y2, x1:x2] return plate_img调试时建议把 score 打印出来或者直接画在框上,正常情况下车牌检测框的置信度应该在 0.6 以上。如果普遍低于这个值,优先检查训练数据的标注质量,而不是急着调模型结构。车牌区域外扩 10% 这个操作,是避免检测框稍微偏小时切掉首尾字符,这在后续字符识别阶段能减少不少错误。
3. 车牌字符识别:从 CNN 模型训练到推理的完整链路
3.1 数据集与字符集设计
车牌识别本质是一个细粒度的图像分类问题,但它的类别集合不是固定的英文字母,而是「省份汉字 + 字母 + 数字」的混合字符集。项目中训练数据文件里的字符集包含常见的 31 个省份汉字(京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新),加上 24 个字母(I 和 O 通常被剔除,因为和 1、0 太像),加上 10 个数字。
数据组织方式可以参考下面的目录结构:
data/ ├── train/ │ ├── 京/ │ │ ├── 京A_0001.jpg │ │ ├── 京A_0002.jpg │ │ └── ... │ ├── A/ │ │ ├── A_0001.jpg │ │ └── ... │ └── 0/ │ ├── 0_0001.jpg │ └── ... └── val/ ├── 京/ └── ...这种按类别分目录的组织方式,训练时直接用torchvision.datasets.ImageFolder就能加载,不需要额外写数据加载逻辑。每个字符图片已经由项目里的数据预处理脚本统一裁剪成 32x32 的灰度图,推理时也保持同样的尺寸。
3.2 训练脚本的关键参数与调优经验
项目里 CNN 模型的核心是一个精简的卷积网络,训练脚本里最关键的是数据增强策略。车牌字符和自然场景图片不一样,它的形变模式非常固定:轻度旋转、水平拉伸、亮度变化,但很少出现垂直翻转。源码里的增强策略针对这一点做了定制:
train_transform = A.Compose([ A.RandomRotate(limit=10, p=0.5), # 旋转不超过 10 度 A.RandomBrightnessContrast(p=0.3), # 亮度对比度扰动 A.HorizontalScale(p=0.3), # 水平方向轻微拉伸 A.GaussNoise(var_limit=(5.0, 15.0), p=0.2), A.ToGray(p=1.0), # 统一转灰度,和推理保持一致 ])注意这里没有用A.VerticalFlip,因为车牌字符倒过来就完全失去语义了。训练时如果把垂直翻转加进去,训练集 loss 能降下来,但验证集精度会明显下滑,这是典型的增强策略和任务语义冲突。ToGray(p=1.0)这一步很重要,它保证了训练和推理时输入分布完全一致,很多复现失败的情况就是训练用彩色图、推理用灰度图,或者反过来。
训练几个实用的参数设置:batch size 用 64,初始学习率 0.001,每 20 个 epoch 衰减为原来的 0.1 倍,优化器用 Adam。如果你的机器显存不够,batch size 降到 32 也可以,但学习率要同步降到 0.0005,否则收敛速度会变慢。
3.3 推理时的一个容易被忽略的细节
字符识别推理时,项目里对检测框裁剪出来的车牌图做了二次处理,先做水平投影和垂直投影切分,把车牌区域分割成单个字符,再逐个送入 CNN。字符切分的质量直接决定识别准确率,这里有个常见的坑:
def segment_characters(plate_img): gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 车牌背景是深色,字符是浅色,需要取反 _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 找轮廓后按 x 坐标排序,得到的轮廓序列就是从左到右的字符 contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=lambda c: cv2.boundingRect(c)[0]) return contours车牌识别的底色有蓝色、黄色、白色、绿色(新能源),不同底色做二值化时结果差异很大。cv2.THRESH_BINARY_INV是针对标准蓝色车牌的设置,因为蓝底车牌取反后字符变白、背景变黑。如果你在复现时拿到的测试图是黄底黑字(大型车)或白底黑字(教练车),要把THRESH_BINARY_INV改成THRESH_BINARY,否则切分出来的字符全都是反的。更稳妥的做法是先通过车牌底色判断类型,再决定二值化方向。
3.4 CNN 模型选型:为什么不用更深的网络
项目里称用的是「最优 CNN 模型」,实际是指通过对比实验选出的一个适中深度的卷积网络。我查看了源码里模型配置文件,特征提取部分大致是:Conv 层提取 32 通道特征,经过两次 Conv + ReLU + MaxPool 的组合压缩到 128 通道,再接两个全连接层输出到 65 个类别。整体参数量很小,单次推理在 CPU 上也就几毫秒。
这个设计是有道理的。车牌字符类别只有 65 个左右,且字符形态高度规整,不需要用 ResNet-50 甚至更深的结构去拟合。模型过深反而容易在训练集上过拟合,因为字符图片的样本量通常不大。如果你自己复现时想提升精度,优先加数据量而不是加深网络,把训练集每个类别的样本数做到 500 张以上,比换任何骨干网络都有效。
4. Pygame 计费界面与业务逻辑:把识别结果变成可用数据
4.1 系统整体流程与模块划分
整个系统可以拆成三个相对独立的模块:检测识别服务、数据库与计费逻辑、Pygame 界面。检测识别服务是一个独立的类,对外提供detect_and_recognize(frame)接口,输入一帧图像,返回车牌号和置信度;数据库模块负责车辆进出记录的存储和费用计算;Pygame 界面负责展示和交互。
业务逻辑上的核心是计费规则。源码里的计费逻辑不复杂,但边界条件处理得比较细致:
def calculate_fee(entry_time, exit_time): # 实际使用分钟数向上取整 minutes = (exit_time - entry_time).total_seconds() / 60 hours = math.ceil(minutes / 60) if hours <= 1: return 5 # 首小时收费 5 元 elif hours <= 12: return 5 + (hours - 1) * 2 # 超出部分每小时 2 元 else: return 20 # 24 小时内封顶 20 元计费的几个参数可以根据你的项目需求调整:白天时段和夜间时段可以设置不同费率,跨天停车需要按天拆分计算,免费停车时长(如 15 分钟内免费)也建议加进去。源码里这一段逻辑写得比较死,但结构清楚,改起来很容易。
4.2 Pygame 界面的刷新机制与线程处理
Pygame 做界面和 OpenCV 做视频流处理最大的冲突在于事件循环。Pygame 的主循环需要以固定帧率刷新界面并处理事件,而视频流读取和模型推理是耗时的操作,如果把两者放在同一个线程里,界面会明显卡顿。项目源码里用了线程分离来处理这个问题:
import threading import queue frame_queue = queue.Queue(maxsize=2) result_queue = queue.Queue() def capture_thread(camera_id): cap = cv2.VideoCapture(camera_id) while True: ret, frame = cap.read() if not ret: continue if frame_queue.full(): frame_queue.get() # 丢弃旧帧,保持实时性 frame_queue.put(frame) def inference_thread(): while True: frame = frame_queue.get() plate = detect_and_recognize(frame) result_queue.put((frame, plate))界面主线程里做两件事:pygame.event.get()处理按键和鼠标事件,result_queue.get_nowait()获取识别结果更新显示。这里要注意两个细节:frame_queue 的 maxsize 设置为 2 而不是更大,是因为视频流处理对实时性要求高,旧帧堆积只会增加延迟,丢了反而更好;模型推理所在的 inference_thread 要设置成 daemon 线程,否则关闭窗口时程序不会正常退出。
4.3 数据持久化:从 SQLite 到可视化报表
项目里的车辆进出记录全部存在本地 SQLite 数据库中,表结构比较简单:
CREATE TABLE parking_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate_number TEXT NOT NULL, entry_time TEXT NOT NULL, exit_time TEXT, fee REAL DEFAULT 0 );这个表结构覆盖了最基本的进出场记录需求。如果你想让毕设更有亮点,可以在现有基础上增加一个 monthly 表记录月卡车辆信息,再增加一个 daily_report 视图统计每日营收。数据库文件放在 data 目录下,程序启动时如果检测到不存在会自动创建,这部分逻辑源码里已经处理好。
5. 避坑指南:模型、数据、工程三块最常见的踩坑记录
5.1 检测漏框:天气一差就识别不到车牌
现象:正常光照下车牌检测没问题,但一到傍晚逆光、雨雾天气,检测框直接消失,偶尔还会把车灯当车牌框出来。
原因:CenterNet 对中心点响应比较敏感,训练数据里如果没有足够的低光照和雨雾样本,模型学到的特征偏向「高对比度、清晰边缘」的车牌。逆光时车牌区域动态范围大,边缘变模糊,中心点热力图峰值低于阈值就漏了。
解决:收集或生成多样化的训练样本。常见的做法是给已有数据集做亮度扰动和模糊增强,cv2.addWeighted调低亮度、cv2.GaussianBlur模拟雨雾,扩充训练集后重新微调。推理侧把热力图阈值从默认的 0.4 降到 0.3,能在一定程度上挽回漏检,代价是误检率略微上升。
5.2 字符识别把 8 认成 B、0 认成 O
现象:模型在验证集上准确率 98%,但真实场景下形近字错误频出,8 和 B、0 和 O、2 和 Z 经常混。
原因:车牌字符集本身就有意剔除了 I、O,但形近字问题在字符级别依然存在。训练数据里如果某个字符的样本量偏少,模型就会偏向把模糊样本归类到样本量更大的类别。
解决:针对形近字做数据增强。把 8 的样本做轻微旋转、缩放、腐蚀膨胀后加入训练集,让模型学习到 8 的「两个圈大小接近」这个关键判别特征;把 B 的样本做水平拉伸增强,强化「竖线和两个半圆」的结构差异。另外,推理时对低置信度的字符增加一个规则兜底,比如 B 和 8 难区分时参考车牌第二位是否合法(第二位是字母)。
5.3 Pygame 界面打开后卡死或黑屏
现象:程序启动后窗口能显示,但几秒后界面变成「未响应」,CPU 占用率却接近 100%。
原因:主线程被模型推理阻塞,Pygame 的事件循环得不到执行,系统判定窗口无响应。另一种可能是pygame.display.flip()和视频帧绘制不在同一线程,画面刷新频率跟不上。
解决:确认推理逻辑全部移动到了独立线程,主线程只做事件处理和界面绘制。同时把视频帧从 queue 取出来后先缩放成适合界面显示的大小再blit,避免不必要的全尺寸图像拷贝。
5.4 解压后文件缺失或模型加载失败
现象:从网盘下载的压缩包解压时报 CRC 校验错误,强行解压后模型文件缺失,程序运行到加载模型时直接报错退出。
原因:资源包经过打包和传输后文件损坏,或者压缩包本身做了伪加密处理,常规解压工具无法完整还原文件。
解决:优先用 7-Zip 或 Bandizip 这类支持 ZIP 格式修复的工具。伪加密的 zip 文件,观察压缩包文件头中是否标记了加密位,用 7-Zip 打开后如果能看到文件名但要求密码,用十六进制编辑器清除文件头里的加密标记位后即可正常解压。
5.5 程序运行报错:ModuleNotFoundError: No module named 'torch'
现象:环境配置完运行主程序,直接提示缺少 PyTorch 或其他依赖包。
原因:项目源码里有 requirements.txt 文件,但实际部署时 Python 版本和依赖版本不匹配。比如代码里用了较新的 torchvision API,而 pip 默认安装的版本偏旧,或者 Python 3.6 环境下安装了最新版 PyTorch 直接不支持。
解决:强烈建议用 conda 创建独立环境,不要直接装在系统 Python 里。先用conda create -n parking python=3.8建环境,再根据 requirements.txt 逐个安装。安装 PyTorch 时注意从官网选择对应 CUDA 版本的命令,CPU 版统一用pip install torch==1.10.0+cpu torchvision==0.11.0+cpu -f指定 CPU 版本,这样能避免装错。装完跑通后再部署,比每次报错再补装要省时间。
6. 进阶玩法:把 CenterNet 换成 YOLOv5 的迁移路径与踩坑点
如果你不想只用现成的检测模型,想做模型替换实验来给毕设加分,一个值得尝试的方向是把 CenterNet 换成 YOLOv5s。YOLOv5 的生态更成熟,预训练权重容易找,训练脚本和可视化工具也齐全,替换后检测精度通常会有提升,尤其是小目标车牌场景。但迁移不是把权重换掉那么简单,有几个点必须处理干净。
第一,标注数据格式要改。CenterNet 阶段的数据标注是中心点坐标加宽高,YOLOv5 需要的是归一化的中心点坐标和宽高,存在 data 目录里的 txt 文件里。写脚本批量转换时注意类别编号要和数据集配置保持一致,这里不要用 CenterNet 的类别索引,因为 YOLOv5 是从 0 开始计数的,一旦错位所有标注全部错乱。
第二,输入尺寸和 Anchor 要调。YOLOv5s 默认输入 640x640,anchor 是按 COCO 数据集聚类出来的,直接用在车牌照尺寸上效果一般。常见做法是先用python train.py --data your_data.yaml --img 640训练,训练前脚本会自动对标注做 anchor 聚类。如果你想手动设置,可以把anchors改成两组,分别覆盖近距离大车牌和远距离小车牌,聚类工具直接调用utils/autoanchor.py就能算。
第三,NMS 阈值要校准。YOLOv5 默认的conf_thres=0.25, iou_thres=0.45在行人检测上没问题,但车牌场景密集度低,可以把conf_thres提高到 0.4,减少误检成车灯、车身装饰的情况。我在本地跑的时候发现,iou_thres调高到 0.5 对多辆车并排的场景更友好,不会把相邻两辆车的车牌合并成一个框,这个参数各项目差异大,建议自己用验证集跑一遍再定。
最后提一个常见的翻车点:YOLOv5 的检测框和 CenterNet 一样需要做坐标映射,但它的输出是归一化坐标,要乘回原始图像的宽高。如果你在做界面联动时发现检测框和实际车牌位置对不上,先检查是不是在letterbox之后忘了把坐标映射回原图。当初我迁移的时候,就是在这个细节上折腾了一晚上。
从那以后我每次拿到别人的源码,都强制先跑通一遍再谈优化。模型替换这类实验固然能给毕设添彩,但前提是你对原有系统的行为足够了解,不然改了检测模型,后续的字符切分、计费逻辑全要跟着调。希望这份拆解能帮你把项目跑顺,也让你在答辩时能把每个环节的「为什么」都讲清楚。
本文还有配套的精品资源,点击获取