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

Python智能停车场车牌识别计费系统:离线OCR与SQLite状态机实践

Python智能停车场车牌识别计费系统:离线OCR与SQLite状态机实践 ★ FEATURED ARTICLE
简介面向学习Python图像识别与停车场业务逻辑的开发者这份智能停车场车牌识别计费系统提供可运行的完整源码与配套说明重点解决车牌识别、进出场判定和费用统计等场景的实现问题适合具备Python基础、希望理解AI接口调用与桌面应用整合的中级学习者。压缩包共2003个文件约189.59MB以1777个py源码为主体辅以pyc缓存、txt配置与说明、h/c底层依赖、xml配置文件以及pdf/doc使用文档目录层级较完整便于按模块阅读与调试。已有247人学习下载。读者可从中获得百度AI图片识别Key接入示例、车辆入场与出场识别流程、按进出记录汇总并用柱状图展示收入、依据历史数据生成一周车位紧张预警等实现思路同时可通过说明文档快速理清项目结构与运行所需配置适合做课程设计或二次开发参考。1. 从出口那段三秒视频说起这套 Python 智能停车场车牌识别计费系统补的是哪一环地库出口最堵的时刻往往不是车多而是前车在道闸前反复倒车——识别没通过收费员低头手动输车牌。这套 Python 实现的智能停车场车牌识别计费系统处理的就是这三到五秒取帧、定位车牌、识别字符、查入场记录、按费率算钱、写流水、给出开闸信号。它不是调一次云端接口就完事而是把识别、状态机、计费和存储用 Python 串成一条能离线跑的链路SQLite 存数据不依赖外部服务随包带源码和使用说明。适合做毕设的学生、要给小型车场做轻量管理软件的外包同学以及想把车牌识别当模块接进自家门禁系统的开发者日均上万车次、需要多进多出云端对账的商业场子不适合那是集群和容灾的活这份代码是单机量级。下面按“跑起来”的顺序拆。2. 拆开源码骨架取帧、识别、状态机、计费四条链路怎么串拿到一个带界面的识别项目最容易犯的错是从界面代码开始读。界面只是显示器真正决定这套东西能不能用的是中间那条流水线一帧图像进来经过定位、识别、校验、投票变成一个可信的车牌字符串这个字符串再交给状态机判断是入场还是出场出场时才触发计费。四段耦合度必须低否则你换一个摄像头或者换一套费率整份代码就得跟着重写。2.1 目录结构与四个模块的职责边界先看随包源码的目录划分这套结构基本是按“输入—识别—业务—存储”切的改动时心里有数文件/目录职责你实际改它的频率main.py主循环取帧 → 识别 → 校验 → 投票 → 计费 → 落库 → 开闸低config.yaml视频源、阈值、费率、省份白名单、路径高core/capture.py摄像头/视频文件/图片目录的统一封装低core/detector.py车牌定位输出候选框中core/recognizer.py字符识别输出文本和置信度中core/validator.py正则 白名单校验中core/voter.py多帧投票压制单帧误识别中billing/calculator.py计费核心纯函数高storage/dao.pySQLite 读写、抓拍图落盘低ui/dashboard.py简易看板、手动放行、异常队列中四条链路的输入输出是明确契约的detector.detect(frame) - List[box]recognizer.recognize(frame, box) - (text, score)voter.push(text, score) - Optional[str]calculator.calc_fee(enter, leave, rate) - Decimal。这四个签名不变你后面换检测算法、换识别后端、换计费规则都只是替换实现不动主循环。我在别的项目上吃过亏——检测和识别写在一个函数里后来想换 OCR 引擎等于重写。2.2 识别后端选型为什么不是直接接一个大模型接口先说结论这套代码走的是“传统图像处理定位 轻量 OCR 识别”的本地路线不是调用在线识别服务。原因有三个都很现实。第一是延迟和可用性。道闸场景要求从触发到开闸在两秒内完成云端往返加排队高峰期抖一下就是出口堵死地库很多位置信号本来就差断网时整套系统不能瘫。第二是成本模型按次计费的接口在车流量大的场子会变成一条持续支出的账本地跑只烧电。第三是可调试性识别错了你要能回看是哪一帧、哪个候选框、二值化阈值多少云端接口只回你一个字符串是个黑匣子。代价也要讲清楚在固定机位、车牌干净、光照正常的前提下这套方案够用遇到夜间逆光、泥牌、倾斜超过 30 度、车牌被拖车钩挡一半准确率会明显掉。合格的工程做法是双通道——本地识别为主力置信度低于阈值的结果连抓拍图一起落到data/exceptions/由看板上的人工复核队列兜底而不是硬扛。2.3 数据模型状态机靠表约束不靠 if-else业务层最容易出问题的地方是“同一辆车重复入场”。很多实现是在 Python 里查一次再插入中间隔着几十毫秒多帧连续触发就会插出两条入场记录出场时匹配到两条费用直接算错。这套源码的做法是把约束下沉到数据库CREATE TABLE IF NOT EXISTS parking_session ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate TEXT NOT NULL, enter_at TEXT NOT NULL, -- ISO8601 带时区跨时区/夏令时不出错 leave_at TEXT, fee_cents INTEGER, -- 金额存分的整数不用 REAL status TEXT NOT NULL DEFAULT IN, -- IN / OUT / EXCEPTION snapshot TEXT -- 入场抓拍图相对路径 ); CREATE INDEX IF NOT EXISTS idx_plate_status ON parking_session(plate, status); -- 关键同一车牌同时只允许存在一条未出场的记录 CREATE UNIQUE INDEX IF NOT EXISTS uq_open_session ON parking_session(plate) WHERE status IN;逻辑说明最后那条是 SQLite 的部分唯一索引只对statusIN的行生效。也就是说同一车牌可以有多条历史OUT记录但IN状态的记录全局只能有一条。这样即使主循环因为多线程或者连续帧重复触发插入第二条入场记录时数据库直接抛IntegrityError业务层捕获后当作幂等处理即可不需要在 Python 里加锁做去重。参数说明enter_at用 ISO8601 字符串而不是时间戳整数是为了人工查库时能直接读懂同时保留时区信息fee_cents用整数分是硬规定REAL存金额在累加时会出现0.30000000000000004这种结果对账时能被财务追着问三天。索引idx_plate_status支撑的是出场时那句“按车牌查最近一条未出场记录”数据量上万条以后没有它出场查询会从毫秒级掉到几百毫秒。3. 把环境跑起来依赖、配置项、三种输入源这一章是照着做就能出画面的部分。使用说明里给了完整步骤但有几个地方它写得比较简略我按实际踩过的顺序补全。3.1 解释器与依赖为什么必须开虚拟环境python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt python -c import cv2, numpy, yaml; print(cv2, cv2.__version__)逻辑说明第一行建隔离环境第二行激活第三行按清单装依赖第四行做一次导入自检——这一步比直接跑main.py有用因为绝大多数启动失败都发生在导入阶段缺 DLL、numpy 版本和 cv2 不匹配提前暴露比在主循环里报错好定位。参数说明requirements.txt里的版本是钉死的别顺手pip install -U升到最新大版本。OpenCV 的 Python 轮子里带了 GUI 依赖在部分 Linux 发行版上会和系统自带的库打架升级后典型症状是import cv2直接段错误。另外opencv-python和opencv-contrib-python只能装一个两个都装会互相覆盖cv2.data.haarcascades路径可能指向错的那份。如果随包用的是 Tesseract 后端还要单独装主程序和中文语言包并在配置里把tesseract_cmd指到可执行文件的绝对路径。提示Windows 上装完 Tesseract 如果不加进 PATH识别会报tesseract is not installed而不是返回空字符串别被这个错误信息带偏去查代码。3.2 config.yaml 里真正需要动的几个参数配置文件是这个项目里改动频率最高的文件下面这几个参数决定系统行为其余基本不用碰参数典型值改动后的影响camera.source0或视频路径决定输入源见 3.3camera.width/height1280/720分辨率越高定位越准但单帧耗时线性上升detect.min_area_ratio0.005候选框最小面积占全图比例调大漏检调小误检detect.max_angle25允许的车牌倾斜角超过直接丢弃候选框recognizer.min_score0.6单帧置信度门槛低于此值不进投票缓冲voter.window8投票窗口帧数越大越稳但确认延迟越高billing.free_minutes15免费时长小于等于它的直接返回 0 元billing.unit_minutes60计费单位不足一个单位向上取整billing.unit_price5.0每单位价格元billing.daily_cap40.0单日封顶跨天按天数累加plate.province_whitelist省份简称字符集校验第一位的合法字符按实际投放地区裁剪参数说明detect.min_area_ratio是最值得先调的一个。机位固定的话车牌在画面里的像素面积基本是个固定区间把它卡在这个区间内能砍掉一大半背景误检。voter.window和min_hits是一对窗口 8 帧、命中 4 帧意味着大约 0.3~0.5 秒确认一辆车车速快的车道要相应调小否则车已经过杆了结果还没确认。3.3 三种输入源摄像头、视频回放、图片目录# core/capture.py节选 import cv2, glob, os def iter_frames(source, width1280, height720): source 支持摄像头索引(int)、视频文件路径、图片目录路径。 if isinstance(source, int): cap cv2.VideoCapture(source) # 分辨率必须在 read() 之前设置部分摄像头会忽略后设的宽高 cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 缓冲压到 1避免读到几帧前的旧画面 for _ in range(5): # 丢掉前 5 帧自动曝光还没稳定 cap.grab() try: while True: ok, frame cap.read() if not ok: break yield frame finally: cap.release() elif os.path.isdir(str(source)): for p in sorted(glob.glob(os.path.join(str(source), *.jpg))): yield cv2.imread(p) # 图片目录模式只识别不计费 else: cap cv2.VideoCapture(str(source)) # 视频文件模式可当回放源反复调参 try: while True: ok, frame cap.read() if not ok: break yield frame finally: cap.release()逻辑说明把三种源统一成一个生成器主循环只认for frame in iter_frames(...)换源不改业务代码。摄像头分支里cap.grab()空读五帧这一步是血泪经验——很多 USB 摄像头刚打开时前几帧是纯黑或花屏直接送进检测器会得到一堆莫名其妙的候选框然后你就开始怀疑算法有问题。参数说明CAP_PROP_BUFFERSIZE设 1 是为了降低实时延迟默认缓冲会积压七八帧等你识别到的时候车早过去了。图片目录分支排在文件分支之前判断因为目录路径用VideoCapture打开不会报错只会一直返回失败排查起来很费劲。三种源的启动方式对应三条命令python main.py --source 0 # 本机摄像头实时计费 python main.py --source samples/gate.mp4 # 视频回放用来调阈值 python main.py --source samples/plates/ # 图片目录只跑识别不出账单4. 识别准确率怎么调预处理、规则校验、多帧投票三道闸识别模块给出来的永远是一个字符串加一个分数它自己不知道对错。把错误结果挡在计费之前靠的是三道独立的闸门任何一道都能拦下来。4.1 预处理把“找车牌”变成“找符合宽高比的矩形”# core/detector.py节选 import cv2, numpy as np def preprocess(frame, target_w720): h, w frame.shape[:2] if w target_w: frame cv2.resize(frame, (target_w, int(h * target_w / w)), interpolationcv2.INTER_AREA) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 双边滤波去噪但保边比高斯模糊更能保住字符笔画 gray cv2.bilateralFilter(gray, 9, 75, 75) grad cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize3) # 取水平梯度车牌字符是竖向边缘 _, th cv2.threshold(grad, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 横向闭运算把离散字符连成一个整块核宽度与车牌宽高比(约3:1)相关 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel) return frame, closed逻辑说明车牌在图像里的显著特征是“密集的竖向边缘 3:1 的横向矩形”所以先降采样控制耗时再取 Sobel 水平梯度突出竖边OTSU 自适应二值化避免固定阈值在夜间失效最后用宽扁的闭运算核把字符连成块。后面接findContours按面积比和宽高比筛候选框就得到定位结果。参数说明target_w720是在单帧耗时和定位精度之间取的折中1080p 原图直接跑形态学单帧要一百多毫秒降到 720 宽后能压到 30 毫秒上下。kernel的 17×5 不是随便写的宽度方向要大于字符间距、小于车牌整体宽度高度方向要略大于笔画厚度换成 9×3 会出现字符断块换成 25×9 会把相邻两个车牌连成一块。max_angle建议留在 25 左右放宽到 45 会引入大量栅栏、广告牌的斜矩形误检。4.2 规则校验正则管结构白名单管第一位# core/validator.py import re, unicodedata def normalize(text: str) - str: if not text: return text unicodedata.normalize(NFKC, text) # 全角字符转半角 text re.sub(r[\s·.\-_:], , text) # 去掉点、横线等分隔噪声 return text.upper() def build_pattern(province_whitelist: str) - re.Pattern: # 结构省份简称 发牌机关字母 5位序号新能源6位 尾位可能为汉字 return re.compile( rf^[{province_whitelist}] rf[A-HJ-NP-Z] rf[A-HJ-NP-Z0-9]{{4,5}} rf[A-HJ-NP-Z0-9挂学警港澳]$ ) def is_valid(plate: str, pattern: re.Pattern) - bool: return bool(plate) and pattern.match(plate) is not None逻辑说明normalize只做字符层面的清洗不动语义。这里有个反直觉的点——不要对结果整体做O→0、I→1的替换。车牌的第三位及以后才是数字和字母混排第二位必须是字母且不含 I、O无脑全局替换会把合法的字母位改成数字反而把正确结果判成非法。真要做纠错得按位次加规则表。参数说明省份白名单放在config.yaml的plate.province_whitelist里按实际投放地区裁剪只留本地会出现的简称能显著降低第一位误识别的通过率。正则里的[A-HJ-NP-Z]显式排除了 I 和 O这是车牌编码规则决定的不是笔误。新能源车牌比普通车牌多一位所以序号位写成{4,5}兼容两种。注意校验失败的结果不要直接丢它是调参的重要数据。落盘时把原图、候选框坐标、OCR 原始输出一起存下来后面按车牌第一位、光照、倾斜角分组统计能快速定位是哪一环掉的。4.3 多帧投票单帧不可信时序才可信# core/voter.py from collections import deque, Counter class PlateVoter: def __init__(self, window8, min_hits4): self.buf deque(maxlenwindow) self.min_hits min_hits self._confirmed None def push(self, plate, score): 每帧调用一次返回确认的车牌未达阈值返回 None。 if not plate or score 0.6: # 低置信度不进缓冲避免污染投票 return None self.buf.append(plate) if self._confirmed: # 已确认的车辆不重复触发 return None counter Counter(self.buf) plate, hits counter.most_common(1)[0] if hits self.min_hits: self._confirmed plate return plate return None def reset(self): self.buf.clear() self._confirmed None逻辑说明同一个车牌在同一视角下连续多帧的识别结果正确值会反复出现错误值大多是随机噪声。用固定长度队列加Counter取众数等于用时间换准确率。_confirmed标志位配合上一章的部分唯一索引形成双重保险投票层保证“只确认一次”数据库层保证“即使确认两次也插不进去”。参数说明window8, min_hits4意味着需要过半帧命中才确认。车以 20km/h 通过 6 米宽的识别区大约 1 秒25fps 下能拿到 20 多帧8 帧窗口足够如果是电动自行车或行人混行的出入口可以降到window5, min_hits3减少等待。车辆离开识别区后要调reset()否则下一辆车会继承上一辆的缓冲。5. 避坑清单五条踩过一次就忘不掉的记录5.1 摄像头能打开但读到纯黑帧现象cv2.VideoCapture(0)返回TrueisOpened()也是True但读出来的画面全黑或者满屏绿色噪点检测器一个候选框都出不来。原因两类。一是摄像头刚上电自动曝光和自动白平衡还在收敛前几帧本来就是废帧二是分辨率设置时机不对部分 UVC 摄像头在read()之后才响应CAP_PROP_FRAME_WIDTH导致实际出图被裁切或拉伸。解决把分辨率设置放在所有read()之前打开后先grab()空读 5 帧再开始取图如果还是黑显式指定后端Linux 下用cv2.CAP_V4L2、Windows 下用cv2.CAP_DSHOW默认的自动选择有时会挑到一个不稳定的后端。另外确认没有别的程序包括你上一次没退干净的调试进程占着这个设备。5.2 识别结果中文全变问号或直接缺失现象英文数字能出来省份简称位置永远缺字符或者变成乱码日志里是一串问号。原因OCR 后端没有装中文语言包或者指定的语言参数还是默认的英文也有一种情况是二值化做反了——本该是黑底白字结果输出了白底黑字字符识别校验全部失败。解决先确认语言包目录里存在中文模型文件再把配置里的语言参数改成中英混合二值化反了的话在预处理末尾判断白色像素占比超过 60% 就整体取反。这个问题在换了新场地、换了补光灯之后最容易复现因为反光会改变背景亮度分布。5.3 账单里出现 0.30000000000000004 这种金额现象计费结果写进库里是浮点数几笔累加后导出报表总额和逐笔相加对不上差几分钱。原因用float做金额运算。0.1 0.2在二进制浮点里就不等于0.3累加越多误差越大如果数据库列还是REAL类型误差会固化下来。解决计费函数内部统一用Decimal并且从字符串构造Decimal(str(price))不是Decimal(5.0)落库时转成整数分展示层再除 100。数据库那一列必须是INTEGER不允许REAL。这条改起来只涉及一个函数和一个字段但越早改越省事。5.4 同一辆车产生多条入场记录现象出场时按车牌查未出场记录返回两条以上取第一条算出来的费用是错的有时候直接算成免费。原因主循环是多帧触发的车辆在识别区停留两秒就会连续确认如果业务层是“先查再插”两次查询之间的间隙足够插进第二条。加线程之后更严重。解决用 2.3 里的部分唯一索引让数据库兜底业务层捕获IntegrityError当作幂等成功处理。同时在投票层用_confirmed标志位做第一次拦截。两层都上别只靠一层——只靠代码判断某天有人加了个多进程就破防了。5.5 跑几个小时后报 database is locked现象白天测试正常晚上车流一多日志里开始刷sqlite3.OperationalError: database is locked写流水失败但识别还在跑数据就丢了。原因SQLite 默认的写锁是库级独占主循环、界面刷新、抓拍图落盘如果各开一个连接同时写就会互相等锁再加上默认没有开 WAL 模式读操作也会阻塞写。解决所有写操作收敛到一个独立的写线程用队列传递任务其它模块只投递不直连数据库连接建立后立刻执行开启 WAL 的 PRAGMA并设置合理的 busy timeout。抓拍图写文件不要放在事务里先落盘拿到路径再入库否则一次慢 IO 就把整张表锁住。6. 上线前先跑一周影子模式不抬杆只记账新装的识别系统最忌讳直接切生产。我的习惯是先跑影子模式道闸不接识别照跑每辆车都正常入场出场记账但闸机由人工控制。一周之后把系统账单和人工收费记录逐条比对差异率低于 1% 再切自动。这一步能暴露的问题远比你在实验室里调阈值多——比如某个时段太阳斜射进出口车牌过曝识别率从 95% 掉到 60%这种只有真实光照才会出现。配合影子模式把随包的视频回放用起来做回归。把一周内识别异常的时间段截成短视频按下面的方式批量跑输出每段的识别率# 批量回归对 clips/ 下的每个视频跑一遍识别统计确认率 for f in clips/*.mp4; do python main.py --source $f --dry-run --report reports/$(basename $f .mp4).json done--dry-run的作用是只识别不计费不写库--report输出每个视频的帧数、候选框数、校验通过数、投票确认数四个数字。这四个数字的比值能把问题定位得很干净候选框少说明定位参数或光照有问题校验通过率低说明 OCR 分辨率不够确认数远小于校验通过数说明投票窗口对当前车速来说太长了。调参的时候按这张表对症下药别一把梭观察到的现象优先改的参数调整方向候选框数量远小于实际车数detect.min_area_ratio调小放宽面积下限候选框一堆但都不在车上detect.max_angle/min_area_ratio收紧角度、提高面积下限单帧字符错一两位输入分辨率 /recognizer.min_score提高分辨率分数门槛先不降长时间不确认voter.window/min_hits窗口降到 5命中降到 3同一辆车确认两次voter的reset()调用时机车辆离开判定区后立即重置还有一个容易忽略的点出场匹配不要只按车牌查。真实现场会遇到车牌被泥糊住、识别成近似值的情况出场查不到入场记录。这类车不能直接放行也不能死锁正确做法是落到异常队列由看板弹出抓拍图人工选择“按最近入场记录补录”或“按临时车计费”。我一般会把异常队列的响应时间也做成指标超过 30 秒没处理就告警——出口堵不堵最后就卡在这几十秒上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站