简介一套基于Python与CNN模型实现的停车场智能车牌识别系统源码包以CenterNet做目标检测以最优CNN模型完成车牌字符识别并通过Pygame模块构建交互式界面适合计算机视觉初学者、Python开发者以及需要完成期末大作业或毕业设计的学生参考。代码完整、可直接运行覆盖车牌定位、字符识别、结果展示等核心环节能够帮助读者快速搭建一套可演示的智能识别流程。压缩包共收录2000个文件以999个py程序文件为主体同时包含345个h头文件、253个java文件、123个md文档、82个m文件以及若干sh脚本与文本说明共约124.68MB。md文档贡献技术笔记与思路说明sh脚本辅助环境配置与批处理各类源码文件则展示跨语言工程的组织方式。资源已有475人学习使用适合具备一定Python与深度学习基础、想研究目标检测与字符识别联动方案的开发者。解压后可根据模块化目录依次查看模型训练、推理检测、界面交互等部分获得完整可复用的车牌识别工程样例。1. 车牌识别不是玄学一套基于Python和CNN的停车场系统长什么样停车场道闸前司机扫码、按键、等抬杆这几秒钟里真正干活的就是车牌识别模块。这套源码干的正是这件事用Python把CenterNet目标检测、CNN卷积神经网络识别、Pygame交互界面串成一套完整可跑的停车场智能车牌识别系统。先说实话它不是什么工业级产品但作为课程设计、期末大作业、毕业设计的前身或者你想把“目标检测字符识别界面”整条链路跑通一次这套资源是非常合适的起步骨架。它解决的核心问题很具体车辆进场时自动抓拍并识别车牌出场时按停车时长计费全程有图形界面可操作。适合正在学Python和CNN、手里缺一个能跑通全流程项目的同学也适合想快速搭一套demo做演示的从业者。2. CenterNet做车牌定位为什么弃用YOLO数据怎么造2.1 为什么是CenterNet没有anchor的参数自由做车牌识别第一步不是识别字符而是先把车牌这块区域从整张画面里“框”出来。目标检测的常见选择是YOLO系列和Faster R-CNN但这个源码场景里CenterNet反而更顺手。车牌目标有个明显特征它在画面里的尺寸相对集中宽高比基本稳定国内车牌约为440×140宽高比在3.1左右加上拍摄角度变化后画面里大致在2.5到3.5之间。这类目标的检测难点不在尺寸多样化而在背景复杂、反光和模糊。YOLO在做这类目标时需要先确定anchor框的尺寸调anchor是一套单独的玄学常用k-means聚类出一组先验框框数量的多少和尺寸分布直接决定训练收敛速度。Faster R-CNN虽然检测精度高但RPN层加上region proposal的处理链路偏重显存占用和训练时间对课程设计场景不太友好。CenterNet的思路是把目标看作一个中心点模型输出的是一张heatmap热力图峰值位置就是目标中心再通过两个回归分支输出目标的宽高和中心偏移。这样一来没有anchor、没有候选框、没有NMS常年的调参烦恼网络结构也简洁很多。对于车牌这种中心点很明确的目标CenterNet的简洁模型结构在计算资源有限的条件下往往比庞大检测框架更容易训练出可用的结果。从工程角度理解CenterNet可以抓三个关键词中心点、尺寸、偏移。中心点确定车牌在哪里尺寸确定车牌多大偏移修正中心点落在热力图低分辨率网格时产生的量化误差。理解到这层再看源码里的数据标注和损失函数就很容易对上。2.2 数据准备VOC转CenterNet与DataloaderCenterNet的标注不像VOC那样用[x1,y1,x2,y2]四个角点存而是要把标注转成中心点坐标加宽高。常见做法是先把角点坐标转成中心点和宽高再做归一化避免不同分辨率图片混用后尺度失调。我一般会把标注文件组织成一行一个JSON对象的形式方便脚本读取import json import cv2 import numpy as np from torch.utils.data import Dataset class PlateCenterNetDataset(Dataset): def __init__(self, json_list, input_size512): super().__init__() self.samples [] self.input_size input_size # json_list里是每张图对应的标注路径 for line in open(json_list, r, encodingutf-8): item json.loads(line.strip()) self.samples.append(item) def __len__(self): return len(self.samples) def __getitem__(self, idx): item self.samples[idx] img cv2.imread(item[image]) h, w img.shape[:2] # 标注存的是 [x1, y1, x2, y2] 四个角点 x1, y1, x2, y2 item[box] cx ((x1 x2) / 2.0) / w cy ((y1 y2) / 2.0) / h bw (x2 - x1) / w bh (y2 - y1) / h # 这里按项目要求缩放到统一尺寸 img cv2.resize(img, (self.input_size, self.input_size)) img img.astype(np.float32) / 255.0 # 返回画布、中心点和宽高类别固定为1只有车牌一类 return img.transpose(2, 0, 1), np.array([cx, cy, bw, bh], dtypenp.float32)这份代码逻辑上做了三件事把角点标注换算成归一化中心坐标和宽高把图片缩放到CNN输入尺寸最后以(c, h, w)通道顺序返回。训练时的标注需要严格使用归一化值否则混合分辨率批次下模型会很困惑。实际训练时CenterNet还需要在模型内部把中心点坐标映射回输入尺寸、再映射回原始图片尺寸映射两次的关系可以简单理解为模型输出的热力图尺寸是输入尺寸的1/4中心点坐标乘以4之后再按原宽高比还原。车牌数据集往往不大几百张图也能训练出一个可用的detector关键点是把“回车牌框”的质量做扎实。2.3 训练参数与损失函数中心点回归的四个超参CenterNet对车牌这类单目标检测的损失函数很简单就是热力图损失加上回归损失。热力图损失采用focal loss的变体回归分支用L1 Loss。这里的关键是focal loss的α和β两个超参数α控制难易样本的平衡β控制中心点周围负样本的惩罚强度一般α取2、β取4比较常见但这个源码场景里车牌目标大小稳定可以把β适当调大到5或6让中心点周围不要出现过多误报。回归分支的细节也直接决定定位精度。对于每个真实目标模型回归的是中心点偏移量和目标宽高。宽高以输入尺寸为基准因为热力图分辨率是输入的1/4如果直接回归热力图维度上的宽高小目标会损失严重。车牌虽然不算小目标但这个细节能明显提升边缘场景的定位精度。为了方便调参我把训练时比较关键的超参整理成参数表跑脚本前先对照检查参数推荐值说明输入尺寸512×512更大更准但显存占用高CPU训练建议降到384αfocal loss2控制难易样本权重一般不用动β中心惩罚系数4~6车牌周围误报多时可以适当调大回归Loss权重0.1~0.2回归分支占整体loss的比例太低会框不准训练轮数50~100车牌数据集小早停比硬跑完有用优化器Adam学习率1e-3起30轮后降到1e-4特别提醒一点CenterNet训练时的正样本不是只有中心点一个像素而是在目标中心按高斯分布铺一个半径区域半径大小和目标尺寸相关。很多课程设计源码里直接用单点标注结果训练出来的检测框总是偏半个车身其实就是高斯半径这块处理得不对。跑这套代码时第一步先把中心点的高斯分布逻辑找出来看看半天内基本能判断它的实现是否完整。3. 字符识别落到CNN网络结构、参数与训练策略车牌定位完成后下一阶段是把框出来的车牌图像转换为字符串。这个过程分为两步一是字符切分把车牌区域按字符位置切出单个字符二是用CNN模型对每个字符做分类。整套系统的最终识别准确率很大程度取决于字符切分这个前置环节做得是否干净。3.1 字符切分垂直投影法与过滤规则车牌字符排列有规律可循第一位是省份简称汉字第二位是发牌机关代码字母后面是数字和字母混合。字符间距相对均匀但受铆钉、灰尘和边框影响直接按固定宽度切很容易出错。我在这类项目中的常规做法是先做垂直投影统计每列像素的分布再按连续非零区域切出候选字符块import cv2 import numpy as np def split_plate_chars(gray_plate): # 输入是已经裁好的车牌灰度图 plate cv2.adaptiveThreshold( gray_plate, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, -4) # 垂直投影统计每一列中非零像素的数量 col_sum np.sum(plate 0, axis0) # 找到连续非零的列区间 is_zero col_sum 3 chars [] start None for i, flag in enumerate(is_zero): if not flag and start is None: start i elif flag and start is not None: chars.append((start, i)) start None # 过滤掉过窄的过程片段 chars [c for c in chars if 8 (c[1] - c[0]) 80] return plate, chars这段代码里有几个细节值得注意。自适应阈值选择的邻域大小是15窗口太小会把笔画内部掏空太大会让字符和背景都糊成一团15对约140像素宽的车牌是经验值。垂直投影判断阈值用的是“小于3个像素即视为空白列”这样能容忍掉一部分噪点。过滤条件的下界8像素是为了去除铆钉和边框残留上界80像素是为了避免把两个字符粘连的块切成一个。切分做完每个候选块还需要统一尺寸才能交给CNN分类。我一般将每个字符块等比缩放到28×28或者32×32再做边缘补零不要直接拉伸变形以免字符比例失实。因为车牌第二个字符是字母、后几位可能是数字也可能是字母字符块比例差异不大等比缩放后补零在多数情况下是最稳妥的选择。3.2 CNN模型结构两层卷积加一个全连接够不够字符识别本质上是个分类任务。字符集合是31个省份汉字简称、24个字母去掉I和O避免混淆加10个数字总共约65类。类别数不算多一个精简的CNN结构就足够完全不必上ResNet这种重型网络。骨架一般按“卷积-池化-卷积-池化-全连接”的顺序来搭import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Linear(32 * 7 * 7, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))输入是28×28的灰度图经过两次卷积加池化后特征图是7×7×32展平后进入全连接层。第一层卷积用16个通道第二层用32个通道这个参数量级在CPU上推理单字符分类也就几毫秒不会成为系统瓶颈。这个网络结构还有几个可调的替代方案。如果你发现省简称汉字和字母持续混淆第一层通道数从16加到32通常能带来若干点的提升代价是训练时间稍微变长。如果字符切分得过细导致笔画被截断可以把卷积核从3×3换成5×5感受野变大后能容纳更多上下文信息。全连接层的Dropout放在0.3左右比较合适太低容易过拟合训练集里那些清晰车牌的字体太高则会让验证集损失提前停滞。3.3 训练策略数据增强与类别不平衡字符CNN的分类性能对训练数据质量高度敏感。真实停车场抓拍的车牌图像中数字和字母出现的频率远高于省简称汉字而且“京”“沪”“粤”等常见简称本就集中这会造成类别不均衡。解决手段有两个一是对样本数量少的类别做在线随机增强二是用类别权重给交叉熵损失加权。操作上可以在数据加载时按类别统计样本数把低频类别的权重放大到高频类别的1.5到2倍。数据增强我在车牌场景下有固定组合随机亮度扰动-30到30灰度、高斯模糊kernel在3到5、旋转±10度、轻微水平缩放。这里要提醒的是不要加水平翻转因为车牌内容本身不能镜像对称翻转后“粤A”会变成“A粤”的乱序形态模型会被错误样本带偏。垂直方向的小幅位移可以保留模拟抓拍角度带来的上下偏移。训练参数上一般是batch size取32Adam优化器从学习率1e-3开始每8个epoch按0.5衰减验证集准确率连续4个epoch不再上升就提前停止。车牌字符数据集通常每个类别准备几百张就能训练出可用的分类模型这里的关键是切分样本要按实际车牌的切分结果来产生不要用干净字体截图喂养模型然后指望它处理现场噪声。4. Pygame界面与停车流程从入场抓拍到结算离场4.1 Pygame界面布局与事件调度识别模型只是内核系统要能看还要能操作这就轮到Pygame上场。Pygame是一个很轻量的界面库做课程设计和期末大作业的交互界面非常合适不需要像Qt那样处理复杂的信号槽机制。这套系统的界面采用了模拟摄像头的布局左侧是视频画面区域右侧是入场和出场按钮下方是当前车辆信息和计费结果展示区。界面编码的关键不在画布局而在事件循环里怎么处理耗时操作。Pygame的主循环是实时的如果直接在其中同步执行模型推理一帧推理耗时几百毫秒界面就会卡成幻灯片表现为窗口“假死”。常见做法是让推理在单独线程执行主线程只负责刷新画面和响应按钮事件推理结果通过队列或共享变量回传。如果项目代码里没有现成的线程封装你也需要自己补一个否则跑起来体验会比较差。界面刷新频率一般控制在30帧画面区域直接显示OpenCV读到的视频帧把BGR转成RGB再转成Surface即可。识别结果叠加在画面上方用Pygame的font模块绘制文本默认字体显示汉字可能没问题但显示车牌字符串时要留意字体文件是否覆盖了省份简称字符。4.2 停车计费流程一次进出场的完整状态流转识别只是入口系统真正的业务是计费。一次完整的停车流程包含两个事件车辆入场时识别车牌并写入停车记录车辆出场时识别车牌并匹配入场记录、计算费用、结清离场。为了避免一辆车在抓拍区域内被连续识别多次导致重复入场我通常在代码层面加防重逻辑识别内容相同且与当前最后一条记录间隔小于阈值的识别结果会直接丢弃。停车记录的存储用SQLite足够单文件、不用装服务、写入速度也够用。表结构大概长这样字段类型说明plate_noTEXT车牌号主键之一entry_timeTIMESTAMP入场时间exit_timeTIMESTAMP出场时间空表示在场内feeREAL应收费用image_pathTEXT入场抓拍图路径出场计费是核心用exit_time减entry_time得到停车时长按小时计费的话通常每小时收费5元超过半小时按一小时计算。车厂常见的封顶或免费时长规则也要用单独的配置参数列出来方便设置。计费完成后把出场时间写入记录车辆离开后清空识别上下文的临时状态。这个源码在纯演示模式下可能也会将计费规则做成硬编码跑通流程后替换成你自己的计费规则即可。4.3 车辆信息去重与缓存同一辆车别被识别十次停车场场景中车辆进闸是一个慢动作车在道闸前停留时摄像头是持续输出的如果每帧都触发一次完整的识别和入库流程几秒内同一个车牌会产生大量重复记录。去重逻辑我习惯放在识别事件之后、数据库操作之前维护一个最近识别记录列表内容包括车牌号码、识别时间戳和识别次数。只有当当前时间与上一次识别该车牌的时间差超过设定窗口比如10秒才把本次结果当作新事件处理。import time class PlateEventFilter: def __init__(self, cooldown_seconds10): self.cooldown cooldown_seconds self.last_seen {} def should_trigger(self, plate_no, nowNone): now now or time.time() last self.last_seen.get(plate_no, 0) if now - last self.cooldown: return False # 时间窗口内不触发新事件 self.last_seen[plate_no] now return True这个过滤器的好处是逻辑独立不耦合具体业务进场和出场两处都能复用。cooldown参数可以根据停车场实际车流行驶速度调整闸机前车流速度快就设短到5秒速度慢就设长到15秒。另外存放在last_seen里的记录还需要做定期清理否则车辆数量多起来之后内存占用会持续增长出场时清掉对应车牌、超过一定时间没有新识别的记录就强制过期。5. 避坑清单这套源码从下载到跑通的五条实测记录5.1 解压后看到upb.c、message.c这类C文件以为下错资源很多下载者解压源码包后直接看到upb.c、message.c、encode_decode.c这一批文件第一反应是“这不是Python项目”差点把压缩包删除。这类文件其实是protobuf的C语言运行时源码常见于某些机器学习依赖库的打包缓存或者在仓库初始化时被一并提交了。它们确实存在于压缩包内但并不是项目主代码。实话说看到这类文件时你先不要下结论。正确做法是先把目录结构整体列出来找models/、utils/、src/这些典型工程目录再搜索入口脚本通常是一个main.py或者app.py。如果这份资源的打包方式本身足够规范入口脚本会带清晰的运行说明如果入口脚本也找不到那就先放下它因为这已经不是“能不能跑”的问题而是资源完整度的问题。最好的检验方式是直接看入口脚本是否能在当前Python环境下无错误启动。5.2 CenterNet第一张推理图花了几十秒窗口完全卡死刚跑通时最打击信心的现象是模型加载完成后第一张推理用了几十秒期间Pygame窗口一直转圈看起来像死机。这通常是三个因素叠加导致的一是机器没装GPU版PyTorch是纯CPU推理二是CenterNet的输入尺寸设在512甚至更大卷积计算量成倍增加三是模型加载后没有预热第一次前向推理要触发各种初始化逻辑。解决思路分几条路走但最实用的组合是缩小输入尺寸加限定推理频率。输入尺寸从512降到384推理耗时一般能省下三分之一再把识别频率限制到每N帧处理一次而不是逐帧处理用户体验会有一个非常明显的提升。CPU推理场景下把识别放在独立线程并让界面保持60帧刷新也是一种必要的工程习惯哪怕识别慢界面也不至于看起来像死机。5.3 省份汉字被识别成字母或数字分类器总是“聪明过头”字符识别模型输出结果中车牌第一个汉字经常被错误分类成字母或数字。究其原因这类项目的字符类别表实现非常容易出现失误要么类别索引起始位置出错把“京”排到了数字索引区域要么训练样本中汉字类别数量太少模型对汉字特征欠拟合还有一种非常隐蔽的情况字符切分把“京”的左右结构切成了两块汉字已经不是一个完整字符。排查规律很直接先整理一份类别索引表确认输出层的65个类别与字符串映射严格对应再检查字符切分结果把每个字符块保存成图片逐张看一遍确认汉字没有被截断。训练数据层面汉字类别再少也要保证每个简称有几百张样本否则分类器会惯性把汉字往高频类别上靠。这类问题一旦定位到环节解决起来很快难的是在集成测试时才暴露出来往回追线索复杂。5.4 训练loss掉到0.3附近就再也不降验证集却在震荡定位和字符分类两个模型都可能遇到这个现象比较常见于字符分类。loss不降验证集震荡十有八九是正样本冗余和类别不均衡混杂的问题。车牌数据集里一个来源批次往往带有相似的照明条件模型会学到亮度统计量而不是字符结构本身表现为在验证集上偶尔大幅掉点。处理核心围绕数据多样性展开把相似样本做聚类抽样而不是全部放进训练集对低频类别如省简称做专门的过采样学习率从固定值改为余弦退火或ReduceLROnPlateau动态衰减都是这个阶段常用的调整手段。如果数据量在千张级别以下不要寄希望于加网络层数把现有结构训充分才是正路。5.5 检测框总是整体偏下或偏右半个车牌的位置CenterNet对中心点的回归极为敏感有一个高频错误是检测框整体偏移却能稳定框住车牌一部分。这基本指向两种情况训练时高斯半径的构造不对中心点周围正样本区域过宽或过窄或者回归分支的偏移没有覆盖原始分辨率归一化换算出了问题。解决步骤我一般固定走这套先画训练样本的真实标注和模型输出到同一张图上观察偏移方向如果偏移方向恒定检查模型输出的中心点坐标是否乘了正确的缩放因子如果偏移随着目标大小变化优先检查高斯半径的计算实现。CenterNet这类中心点模型未对齐的缩放因子会带来系统性偏移这个坑一旦踩中任何数据增强都救不回来。6. 最后一公里把识别帧率从两秒优化到两百毫秒模型跑通只是第一步停车场实景里摄像头是持续输出的如果识别一次要两秒系统就失去了可用性。我当时在这套资源上做的最有价值的一件事是把推理从“逐帧串行”改成“按需计算”配合降分辨率把单次识别耗时降到了两百毫秒量级。核心思路是在识别任务前端加一个节流器只在画面变化足够明显或者车辆进入触发区域时执行推理触发区域以外的时间帧直接放行显示。简易实现可以这样写import time class InferenceThrottle: def __init__(self, interval0.3, min_blur_thresh20): self.interval interval self.min_blur_thresh min_blur_thresh self.last_infer_time 0 def should_infer(self, frame): # 距离上一次推理时间不足间隔直接跳过 if time.time() - self.last_infer_time self.interval: return False # 连续帧之间的平均像素变化小于阈值说明画面静止也跳过 # 这里的diff简化处理实际可用absdiff计算区域均值 if diff self.min_blur_thresh: return False self.last_infer_time time.time() return True时间间隔和画面变化阈值这一层优化几乎能过滤掉一大半无效计算。配合把CenterNet输入尺寸从512降到384再把模型导出为ONNX用ONNX Runtime推理CPU上通常也能有两到三倍收益两百毫秒是完全可以达到的。如果推理部署环境有GPU这个方案还能直接起飞处理实时视频流的抓拍。从那以后我每次拿到新资源都强制自己先做一遍“最小复现路径”——用最少的数据跑通入口、看清识别事件触发逻辑再讨论性能和部署。这套车牌识别系统的CenterNet、CNN字符分类、Pygame界面三块链路如果是清晰的那么把它改成适合你自己停车场场景的版本也就是一两天的事。希望帮到你。本文还有配套的精品资源点击获取