首页 / 资讯中心 / 文章详情

智能停车场车牌识别计费系统:Python+HyperLPR+MySQL全流程解析

智能停车场车牌识别计费系统:Python+HyperLPR+MySQL全流程解析 ★ FEATURED ARTICLE
简介一份基于Python的智能停车场车牌识别计费系统源码包面向有一定Python基础、希望学习车牌识别与小型计费系统开发的学习者也适合课程设计或毕业设计参考。系统借助百度AI开放平台图像识别能力实现车牌识别、车辆出入场判断、收入统计柱状图展示和满员预警等核心功能描述中已写明Key配置与运行步骤可快速启动。压缩包共2003个文件约189.59MB以1777个py源码为主辅以pyc编译文件、txt与doc说明文档、html/css/js前端文件、xml/json配置文件等集齐程序源码、依赖与使用资料整体组织较清晰。已有247人学习下载。通过阅读和运行源码可掌握百度API对接、界面交互设计、数据统计可视化以及基于历史记录的预警逻辑附带说明文档方便部署调试也适合二次开发和功能扩展。1. 智能停车场车牌识别计费系统一条从识别到计费的完整闭环智能停车场车牌识别计费系统最花时间的往往不是车牌识别模型而是计费规则和异常订单处理。这套 Python 源码把摄像头抽帧、车牌识别、按时长计费、MySQL 订单存储串成了完整闭环跑通之后不是只看到一张识别结果图而是能直接生成一笔可核对金额的停车订单这是它比单纯的“车牌识别 demo”值钱的地方。它适合三类人。正在做课设或毕设的学生需要演示一套完整业务链而不是孤立的算法想快速给物业或停车场运营方搭建原型的开发者这套代码可以直接改配置接真实摄像头以及有真实计费需求、想少排一个人工收费员的运营者。源码本身不依赖重型框架识别端用 OpenCV 加 HyperLPR业务端用原生 Python数据落在 MySQL整体结构清楚改起来不费劲。下面按识别、计费、存储、避坑、上线验证这个顺序拆每个环节都给出可复现的参数和代码。2. 车牌识别模块HyperLPR 选型理由与置信度阈值调优2.1 为什么没有从零训练HyperLPR 面向国内车牌的先天优势车牌识别这个问题很多人上来就想训 YOLO但实际项目里这是性价比最低的做法。停车场场景的摄像头位置固定、角度固定车辆过闸速度也就 5 到 10 公里每小时根本不需要一个通用目标检测器去满图找车牌。真正难的是车牌字符的结构识别省份汉字、字母、数字混合加上蓝牌 7 位、新能源绿牌 8 位、黄牌单双层、警车白牌这些模板差异自己从零训练得准备几千张带标注的车牌图分类器还得单独处理汉字这个数据量和标注成本对个人开发者来说非常不划算。HyperLPR 是专门针对国内车牌训练的识别库检测和字符识别是一体的直接用就行。我一般把它和 PaddleOCR 做对比PaddleOCR 是通用 OCR确实能读出图里的文字但它不做车牌结构校验识别出“鲁 A12345”和识别出“鲁A12345”中间有没有空格、是不是车牌格式都要你自己再写一层规则去兜。HyperLPR 的输出天然带车牌框和置信度省掉了这层胶水代码。当然如果停车场是多车道、大广角、车辆斜插进画面单靠 HyperLPR 的检测会吃力这时常见做法是前面加一层 YOLO 专门定位车牌区域裁出来再喂给 HyperLPR 做字符识别。这套源码针对的是单车道或双车道闸机场景所以直接用 HyperLPR 就够了。2.2 抽帧、定位、识别一条链核心代码与结果清洗识别模块的核心代码不长但有几个顺序问题容易翻车。我拆这套源码时第一处注意到的就是 BGR 和 RGB 的转换OpenCV 读图默认是 BGRHyperLPR 内部按 RGB 处理顺序不对识别率会明显下降。import cv2 import re try: from hyperlpr3 import LPR as HyperLPR except ImportError: from hyperlpr import HyperLPR lpr HyperLPR() def recognize_plate(frame): # OpenCV 读出来的是 BGRHyperLPR 需要 RGB转一下再送识别 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results lpr(rgb) if not results: return None # 一张图中可能有多块疑似车牌取置信度最高的那条 best max(results, keylambda x: x[confidence]) plate clean_plate(best[plate]) if not plate: return None return { plate: plate, confidence: best[confidence], boxes: best[boxes], } def clean_plate(plate): # HyperLPR 偶尔会带出噪点字符先过滤再按国内车牌长度校验 plate re.sub(r[^A-Z0-9\u4e00-\u9fa5], , plate) return plate if len(plate) in (7, 8) else None逻辑说明results是列表每项包含plate、confidence、boxes三个字段plate是识别出的车牌字符串confidence是置信度boxes是车牌在画面里的坐标框。用max取置信度最高的一条防止画面里出现广告牌、反光区域被误判成车牌。clean_plate做字符白名单过滤把标点、空格、噪点字符全部去掉最后只放行长度为 7 或 8 的车牌这是国内蓝牌和绿牌的标准位数能挡掉相当一部分乱码结果。如果你装的是hyperlpr3导入名是LPR老版本则是HyperLPR代码里那个try就是兼容两个版本用的。初始化之后直接调用实例就行不需要额外加载模型文件。2.3 三个影响识别率的关键参数置信度、抽帧间隔与图像预处理识别模块里最值得调的就是下面这张表三个参数直接决定识别率上限参数默认值影响confidence 阈值0.7低于阈值直接丢弃结果调太高会漏识别调太低会出现误识别抽帧间隔0.5~1 秒间隔太短连续帧重复识别间隔太长会漏掉过闸瞬间图像预处理灰度 直方图均衡化晚上和逆光场景下这项决定识别率能不能稳住置信度阈值建议按时间段分开配置白天光照好可以设 0.75晚上整体置信度会下降设 0.6 更合理一个参数走到底在车牌识别场景里基本行不通。抽帧间隔 0.5 秒是经验值车辆过闸机时要经过减速、停车、再起步的过程0.5 秒一帧足够在车头进入画面的瞬间抓到车牌。图像预处理这块我一般会在识别前加一步尤其针对夜间场景def preprocess(frame): # 转灰度再均衡化把车牌的对比度拉起来 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)直方图均衡化对夜间车牌的反光问题非常有效车牌字符在过曝或偏暗时边缘模糊均衡化能把对比度拉开效果比调低 confidence 阈值实在得多。代价是每帧多花十几毫秒在 0.5 秒抽一帧的频率下几乎无感。提示摄像头架设角度比算法参数更影响识别率。建议高度 1.2 到 1.5 米向下倾斜 20 到 30 度正对车头避开大灯直射。这个角度不对调再多参数也是白搭。3. 计费模块时长计算、跨天费率与防重复入场3.1 先把费率规则立住小时计费、分时段与单日封顶计费模块最大的坑不是写不出函数而是费率规则没定清楚就动手写代码。停车场的计费规则看起来简单实际拆开至少有四个维度按小时还是按分钟计费、是否分白天夜间时段、是否设置免费时长、是否有单日封顶。这四个维度组合起来代码里的分支会指数级增加如果不把规则抽象成配置写出来的函数就是一堆 if 嵌套改一次费率改到怀疑人生。这套源码的做法是把费率抽成一个字典所有业务逻辑只认这个字典不认硬编码数字rules { free_minutes: 15, # 入场 15 分钟内免费 day_rate: 3.0, # 白天每小时 3 元 night_rate: 2.0, # 夜间每小时 2 元 day_start: 8, # 白天从 8 点开始 day_end: 22, # 白天到 22 点结束其余算夜间 max_daily: 30.0 # 单日封顶 30 元 }字段含义很简单free_minutes是免费时长day_rate和night_rate是分时段的小时费率day_start和day_end定义白天区间max_daily是封顶金额。把费率独立成配置的好处是停车场要调价时只改这个字典计费函数不用动后面接 MySQL 的fee_rules表也只是把这个字典的字段落库而已。3.2 分钟级结算函数免费时长、跨天分段与封顶实现计费函数的核心逻辑是先判断是否在免费时长内不在的话从进场时间开始逐分钟累加费率直到出场时间。逐分钟循环看起来笨但它的好处是天然处理跨天不需要额外判断“隔了几个晚上”因为循环里每一分钟都带着自己的日期时间。from datetime import datetime, timedelta def calculate_fee(entry_time, exit_time, rules): if exit_time entry_time: raise ValueError(exit_time must be after entry_time) stay_minutes (exit_time - entry_time).total_seconds() / 60 if stay_minutes rules[free_minutes]: return 0.0 total_fee 0.0 current entry_time while current exit_time: hour current.hour is_night not (rules[day_start] hour rules[day_end]) rate rules[night_rate] if is_night else rules[day_rate] total_fee rate / 60 # 把小时费率换算成分钟费率 current timedelta(minutes1) return round(min(total_fee, rules[max_daily]), 2)逻辑说明stay_minutes是总停车分钟数先和free_minutes比较在免费时长内直接返回 0 元。逐分钟累加时每一分钟根据current.hour判断属于白天还是夜间把对应的小时费率除以 60 得到这一分钟的金额累加进total_fee。循环结束后用min和max_daily做封顶最后保留两位小数。这里有一个规则语义要讲清楚免费时长是整体豁免不是抵掉前 15 分钟。也就是说停了 20 分钟不是只付 5 分钟的钱而是按 20 分钟全额计费。这是大多数停车场实际采用的规则如果你接的场地规则是“超出免费时长才从头计费”保持这个实现即可。逐分钟循环的性能边界也说一下一天最多 1440 次循环单笔订单计算耗时是毫秒级对一个车道闸机的吞吐量来说完全够用。如果以后要做高并发的集中计费服务可以改成按小时段批量计算——先算完整跨越的小时段再补零头分钟逻辑会复杂很多但目前这个场景不需要。3.3 防重复入场与异常订单缓存兜底加人工补录重复入场是计费模块最容易出的隐蔽问题。摄像头按 0.5 秒一帧抽帧车辆在闸机口从减速到停稳通常有好几秒同一块车牌子会被识别到多次。如果不做去重一次入场可能生成三四条入场记录出场时按哪条记时就是一笔糊涂账。from datetime import datetime from collections import OrderedDict class ParkingGate: def __init__(self): # plate - (entry_time, confidence) self._active OrderedDict() def entry(self, plate, tsNone): ts ts or datetime.now() last self._active.get(plate) # 同一辆车 3 秒内重复识别只更新时间不重复建入场记录 if last and (ts - last[0]).total_seconds() 3: self._active[plate] (ts, last[1]) return False self._active[plate] (ts, 1.0) return True def exit(self, plate, tsNone): ts ts or datetime.now() entry self._active.pop(plate, None) if entry is None: return None # 没有入场记录走人工补录流程 return entry, ts逻辑说明_active字典维护的是当前在场车辆entry方法在写入前先查一下这个车牌是否在短时间内已经入场过3 秒内的重复识别只刷新入场时间不生成新记录。exit方法取出并删除_active里对应的入记录把进出时间一起返回给计费函数。这里有个点值得注意如果直接返回None交给人工补录补录界面要能同时修改进场和出场时间否则这笔订单时长永远算不对。真实部署时如果停车场有多个岗亭、多台设备同时写数据OrderedDict要换成 Redis 哈希表key 用车牌号value 存入场时间戳跨进程共享才不会乱。4. 数据持久化MySQL 表结构与断网兜底日志4.1 三张核心表在场车辆、历史订单与费率配置数据层设计成三张表而不是一张大表原因是在场车辆和历史订单的查询模式完全不一样。运营要随时看“现在场里有几辆车、各自停了多久”这个查询如果每次都扫历史订单表数据量大了会越来越慢。把在途车辆单独成表出场时把记录挪到订单表两边都清爽。vehicles表存放当前在场车辆字段类型说明idINT AUTO_INCREMENT主键plate_noVARCHAR(16)车牌号加唯一索引entry_timeDATETIME(3)入场时间毫秒精度entry_image_pathVARCHAR(255)入场抓拍图片路径方便对账statusTINYINT0 在场1 已出场orders表存放已完结的历史订单字段类型说明idINT AUTO_INCREMENT主键plate_noVARCHAR(16)车牌号entry_timeDATETIME(3)入场时间exit_timeDATETIME(3)出场时间stay_minutesINT停车时长分钟total_feeDECIMAL(10,2)实收金额created_atTIMESTAMP订单创建时间fee_rules表就是第 3 章那个规则字典的落库版本字段一一对应不重复列了。4.2 建表 SQL 与 Python 读写封装三张表的建表 SQL 如下时间字段统一用DATETIME(3)而不是字符串这是为了避免后面算时长时到处做类型转换。我见过有人用VARCHAR存时间排序靠字典序硬撑跨天记录一多就出幺蛾子这个习惯要改。CREATE TABLE vehicles ( id INT AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(16) NOT NULL, entry_time DATETIME(3) NOT NULL, entry_image_path VARCHAR(255) DEFAULT , status TINYINT DEFAULT 0, UNIQUE KEY uk_plate (plate_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(16) NOT NULL, entry_time DATETIME(3) NOT NULL, exit_time DATETIME(3) NOT NULL, stay_minutes INT NOT NULL, total_fee DECIMAL(10,2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_plate (plate_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE fee_rules ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, free_minutes INT DEFAULT 15, day_rate DECIMAL(6,2) DEFAULT 3.00, night_rate DECIMAL(6,2) DEFAULT 2.00, day_start TINYINT DEFAULT 8, day_end TINYINT DEFAULT 22, max_daily DECIMAL(8,2) DEFAULT 30.00 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;vehicles表上的uk_plate唯一索引是有意为之的它保证同一块车牌在同一时刻只有一条在场记录从数据库层面挡住重复入场。如果出现“车辆还在场内但又被识别入场”的情况插入会报唯一键冲突程序里捕获异常并更新原有记录即可。Python 读写层用pymysql封装插入订单的写法如下import pymysql def create_conn(): return pymysql.connect( host127.0.0.1, userparking, passwordyour_password, databaseparking_system, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) def insert_order(conn, order): sql INSERT INTO orders (plate_no, entry_time, exit_time, stay_minutes, total_fee) VALUES (%s, %s, %s, %s, %s) with conn.cursor() as cur: cur.execute(sql, ( order[plate_no], order[entry_time], order[exit_time], order[stay_minutes], order[total_fee], )) conn.commit()注意这里一定要用%s参数占位不要用 f-string 拼 SQL。停车场管理系统会接外部设备属于半公开网络环境SQL 注入这种低级问题不能犯。entry_time和exit_time直接传 Python 的datetime对象pymysql会自动处理类型匹配。4.3 断网兜底本地日志缓存与恢复回放停车场岗亭的网络环境比想象中差弱电箱断电、光纤被挖断、交换机老化这些事在真实运营中都会遇到。如果 MySQL 连不上时程序直接把订单丢弃月末对账就全是窟窿。这套源码里有一个本地日志兜底方案思路是先落本地文件再尝试写库写失败也不丢数据。import json import os from datetime import datetime PENDING_FILE pending_orders.log def save_order_locally(order): order[cache_time] datetime.now().isoformat() with open(PENDING_FILE, a, encodingutf-8) as f: f.write(json.dumps(order, ensure_asciiFalse) \n) def replay_pending(conn): if not os.path.exists(PENDING_FILE): return 0 recovered 0 with open(PENDING_FILE, r, encodingutf-8) as f: lines [json.loads(line) for line in f if line.strip()] for order in lines: try: insert_order(conn, order) recovered 1 except Exception: # 单条失败不中断让其他订单继续回放 continue if recovered len(lines): os.remove(PENDING_FILE) return recovered逻辑说明save_order_locally把订单序列化成 JSON 追加到本地日志文件replay_pending在数据库恢复后逐条回放全部成功后删除日志文件。这个方案能兜住断网场景但也有一点粗糙如果回放到一半程序崩溃已经回放成功的订单会重复插入。正式落地时建议改成“读一条、写一条、标记一条”的消费模式或者给订单加唯一业务编号回放前先查重。5. 避坑指南识别率骤降、绿牌乱码与重复计费五个典型坑5.1 晚上识别率骤降对比度才是真凶现象白天识别率能到九成到了晚上直接腰斩车灯一开近景车牌一片白板HyperLPR 返回空结果或乱码。很多人第一反应是模型不行其实模型在白天和晚上用的是同一套权重问题出在图像预处理没跟上。原因夜间环境光不足摄像头自动增益会把暗部提亮同时车灯和反光车牌过曝字符边缘被亮光吃掉。识别模型在过曝图上提取不到字符特征置信度自然掉到阈值以下。解决进识别前强制做直方图均衡化把对比度拉开这一步能明显改善过曝车牌的识别。摄像头参数上开启宽动态功能或者把曝光补偿调成负值优先保住车牌区域不过曝。逆光导致整张图偏暗时再做一次 Gamma 校正。最后记得把夜间 confidence 阈值单独调低一档全局一个阈值在晚上会漏掉大量本来能救回来的识别结果。5.2 新能源绿牌识别成 7 位乱码模型对 8 位车牌的适配问题现象绿牌新能源车进场识别出来的车牌只有 7 位或者中间带一个多余的“·”导致车牌号和数据库里的月卡信息对不上计费直接失效。原因HyperLPR 的老版本模型主要围绕 7 位蓝牌训练绿牌是 8 位字符字符间距和分隔符结构和蓝牌不一样模型在切分字符时容易丢掉一位或者把分隔符当成字符输出。解决在clean_plate层做针对性清洗把“·”和噪声字符过滤掉只保留汉字、字母、数字。对长度为 8 的结果再校验末位是否符合新能源车牌的常见字符特征比如数字或 D、F 等字母。如果项目允许升级优先切到hyperlpr3的新模型绿牌支持度好了不止一档。5.3 计费时长出现负数或零时间类型不统一的后果现象订单列表里出现stay_minutes为负数或者刚出场就显示“停车 0 分钟、金额 0 元”对账时一眼就能看出数据有问题。原因十有八九是进出场时间一个来自 Python 的datetime对象另一个从数据库查出来变成了字符串或者是进场用的本地时间出场用的 UTC 时间两个时间一减直接算错。解决时间字段在入库前统一用datetime对象数据库表保持DATETIME(3)类型不要在应用层手动拼字符串。计算时长前加一个类型断言确保两边都是datetime一旦发现字符串就报错而不是静默往下走。这个坑我翻过车调了半天才发现是读库时间被转成了str。5.4 MySQL 连接隔夜就断wait_timeout 与自动重连现象程序刚启动一切正常跑了一夜之后第二天第一笔订单就报Lost connection to MySQL server during query重启又好了过一天又断。原因MySQL 默认的wait_timeout是 8 小时连接池里的长连接如果 8 小时内没有任何活动服务端会主动断开。pymysql本身不会自动重连拿到一个已经失效的连接去查询必然报错。解决每次执行 SQL 前先conn.ping(reconnectTrue)让连接池自动重建失效连接或者捕获OperationalError后重建连接对象重试一次。不要试图把 MySQL 的wait_timeout调大那是治标不治本应用层做好重连才是正解。5.5 内存持续上涨视频帧释放与队列限制现象程序跑半天内存占用涨到 1GB 以上最后被系统 OOM 杀掉日志里什么都没留下。原因抽帧循环里的frame变量处理完后没有显式释放HyperLPR 内部对识别过的图像有缓存引用加上抽帧间隔太短图像数据堆积在内存里。解决每帧处理完以后del frame把抽帧间隔从 0.2 秒调到 0.5 秒以上。如果用了多线程并发识别再加一个信号量限制同时在途的图片数量防止识别速度跟不上抽帧速度导致队列无限膨胀。6. 上线前验证批量识别率统计与可视化复核工具6.1 批量识别率统计三分钟跑出准确率上线前最重要的一步是用历史抓拍图批量跑一遍识别率。我习惯把测试图片按“真实车牌号_序号.jpg”的格式命名放到test_imgs/目录然后跑这个脚本import glob import os import cv2 from recognition import recognize_plate images glob.glob(test_imgs/*.jpg) total len(images) hit 0 errors [] for path in images: expect os.path.basename(path).split(_)[0] frame cv2.imread(path) result recognize_plate(frame) if result and result[plate] expect: hit 1 else: errors.append((path, expect, result[plate] if result else None)) print(ftotal{total}, hit{hit}, acc{hit/total:.1%}) for path, expect, actual in errors[:10]: print(fFAIL {path}: expect{expect}, actual{actual})这个脚本的意义在于它把识别率变成了一个可以量化的数字。如果测试集有 100 张图准确率 85%你就知道这个系统在正式环境大概是什么水平而不是凭感觉说“好像还行”。跑的时候注意 confidence 阈值要用正式环境的值不要为了测试好看调松那是在骗自己。6.2 可视化复核把识别框画回原图批量统计只能告诉你哪些图片识别错了看不出为什么错。我的习惯是再把识别结果画框写回原图生成一批带标注的图片按时间排序快速翻一遍灯光、角度、逆光的整体问题一眼就能看出来def draw_result(frame, result, save_path): x, y, w, h result[boxes] cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText( frame, result[plate], (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2 ) cv2.imwrite(save_path, frame)画完框以后重点看三类图片识别错误的、置信度刚好在阈值边缘的、以及夜间抓拍。如果错误集中在某个时段基本可以断定是光线问题而不是模型问题直接去调摄像头参数或者预处理策略比盲目调模型权重快得多。我从那次“上线第二天被物业打电话说计费对不上”之后养成了一个习惯不管改动多小部署前都强制走一遍批量识别脚本再模拟三组进出场订单最后人工核对库里几条计费金额三条全过才敢接真实摄像头。这套流程看着费时间实际上能挡住九成以上的低级问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站