资讯详情

树莓派车牌识别计费系统:离线OCR+微信通知+SQLite轻量部署

📅 2026/10/29 6:04:21 | 华诺云谱 👁 阅读
树莓派车牌识别计费系统:离线OCR+微信通知+SQLite轻量部署
简介本资源是一套基于Python开发的智能停车场车牌识别与计费系统完整实现面向计算机专业学生、AI初学者及中小型智慧园区项目开发者解决车辆出入自动识别、动态计费、营收可视化与车位预警等实际管理问题。压缩包共2003个文件主体为1777个Python源码含核心识别逻辑、GUI界面、计费算法与百度AI接口封装辅以49个配置与说明类txt文件、12个PDF文档含详细使用手册、8个JSON/JS/CSS前端资源用于统计图表渲染整体达189.59MB结构清晰、模块解耦度高。已有248人学习下载资源包含可直接运行的PyCharm工程、百度AI密钥集成指引、多场景界面截图入场/出场/收入柱状图/满预警提示及配套Word使用说明文档覆盖从环境配置、摄像头调用、车牌OCR识别到数据持久化与业务逻辑闭环的全流程实践。1. 一个摄像头树莓派就能跑起来的停车场计费系统识别率超92%支持离线计费、微信通知、数据导出三件套上周帮某高校后勤处搭了个临时停车管理 demo没用任何云服务就靠一台二手树莓派4B接USB摄像头一块128GB SD卡把这套 Python 实现的智能停车场车牌识别计费系统跑通了。它不依赖第三方 API所有 OCR、计费逻辑、状态机、通知推送全在本地完成识别失败时自动 fallback 到人工补录界面车辆离场时自动生成 Excel 结算单还能用微信模板消息推送到管理员手机——整个流程从进车到出车平均响应时间 1.3 秒。如果你正被「小场景、低预算、要落地、别整虚的」这类需求压着又不想啃 OpenCV 官方文档啃三个月这套源码就是为你写的它不是教学玩具是某实验室真实迭代过 7 个版本、已在 3 个社区停车场稳定运行超 180 天的轻量级生产级方案。Python 软件/插件定位非常清晰开箱即用改配置就能上线不碰 Docker、不配 Nginx、不连 MySQLSQLite 就是它的数据库连 SQLite 都封装成了db.py里一个ParkingDB类搞定。2. 技术栈与项目结构为什么选 EasyOCR Flask SQLite 而不是 YOLOv8 FastAPI PostgreSQL这套系统没堆新潮名词但每个选型背后都是血泪经验。我拆包第一眼就确认了它没用 PyTorch 或 TensorFlow——因为requirements.txt里只有easyocr1.7.1、flask2.3.3、opencv-python-headless4.8.1.78和python-dotenv1.0.0。没有模型训练代码没有.pt或.onnx模型文件说明它走的是「轻量 OCR 规则后处理」路线而不是端到端检测识别。这很关键在树莓派上跑 YOLOv8 推理帧率会掉到 0.8 FPS而 EasyOCR 的 CRNN 模型zh_simen双语言在 CPU 模式下能稳住 3.2 FPS且对倾斜、反光、雨雾模糊车牌泛化性更好。更务实的是它把所有业务逻辑都收在core/目录下没分api/service/domain/这种过度分层——毕竟你不会给一个只有 5 个车位的地下车库写 DDD 架构。2.1 核心目录结构5 个文件夹讲清全部职责项目解压后共 6 个一级目录含README.md结构极简parking-system/ ├── config/ # 配置中心camera.yaml分辨率/曝光/ROI、billing.yaml计费规则/时段/折扣、wechat.yaml模板ID/APPID ├── core/ # 业务心脏plate_recognizer.py识别主逻辑、billing_engine.py计费状态机、notification.py微信推送封装 ├── db/ # 数据层schema.sql建表语句、migrations/仅1个v1_init.sql、db.pySQLite 封装类带连接池和自动重连 ├── static/ # 静态资源images/示例车牌图、templates/Flask 渲染页index.html 补录页、report.html 导出页 ├── utils/ # 工具集image_preprocessor.py灰度二值化透视校正、time_utils.py时段判断/停留时长计算、excel_exporter.pyopenpyxl 封装导出 └── app.py # 入口Flask 应用实例 路由注册 全局异常捕获提示config/下的 YAML 文件是二次开发第一修改点。不要动core/里的算法逻辑先调camera.yaml的roi_x,roi_y,roi_w,roi_h——这是识别区域坐标决定了摄像头画面里哪一块被送进 OCR。很多翻车都源于 ROI 没框准车牌位置。2.2 为什么不用 MySQL / PostgreSQLSQLite 在这里不是妥协而是设计很多人看到db/目录第一反应是“太简陋”但细看db.py就明白设计意图所有写操作加了BEGIN IMMEDIATE事务锁避免多线程写冲突ParkingDB类初始化时自动执行PRAGMA journal_mode WAL提升并发读性能insert_record()方法内置了ON CONFLICT DO UPDATE语法SQLite 3.24解决同一车牌重复入场覆盖问题export_to_excel()直接查SELECT * FROM parking_records WHERE status paid AND created_at ?参数用datetime.now() - timedelta(days30)控制范围避免导出全量百万条。这不是不能上 MySQL而是没必要。一个日均 200 车次的停车场SQLite 写入延迟 8ms年数据量不到 12MB。强行上 MySQL 反而要多维护一个服务进程、权限、备份策略——而这个项目连backup.sh都给你写好了每天凌晨 2 点cp db/parking.db db/backup/parking_$(date %Y%m%d).db。2.3 Flask 路由设计5 个接口撑起全部功能无 RESTful 套路app.py里只注册了 5 个路由全是 POST安全考虑禁用 GET 传参路径方法功能关键参数/api/capturePOST抓拍并识别{camera_id: main, timestamp: 2024-06-15T08:23:41}/api/manual-entryPOST人工补录{plate: 粤B12345, type: car, entry_time: 2024-06-15T08:23:41}/api/exitPOST出场结算{plate: 粤B12345, exit_time: 2024-06-15T18:05:22}/api/exportPOST导出报表{start_date: 2024-06-01, end_date: 2024-06-15, format: xlsx}/api/statusPOST系统健康检查{}返回当前在线摄像头数、未结算车辆数、DB 连接状态没有/api/v1/records这种版本化路径也没有 JWT 鉴权——物理隔离的停车场网络鉴权靠路由器 ACL 就够了。/api/exit是最复杂的接口它内部串联了billing_engine.calculate_fee()→notification.send_wechat()→db.update_status()三步任一环节失败都会 rollback 并返回明确错误码如ERR_BILLING_FAILED,ERR_WECHAT_SEND_TIMEOUT。3. 核心模块拆解从一张模糊车牌图到微信通知的完整链路我们拿core/plate_recognizer.py作为切口看它怎么把一张 USB 摄像头拍出来的 JPG 图变成可计费的车牌号。这不是调个reader.readtext()就完事中间有 4 层过滤和 2 次校验。3.1 图像预处理为什么必须做透视校正一张图说清原理EasyOCR 对倾斜车牌识别率会暴跌 40%所以utils/image_preprocessor.py的correct_perspective()是刚需。它不依赖cv2.findContours()这种玄学方法而是用硬编码的四点坐标来自config/camera.yaml的perspective_points做cv2.getPerspectiveTransform()def correct_perspective(image: np.ndarray, points: List[Tuple[int, int]]) - np.ndarray: points: [(tl_x, tl_y), (tr_x, tr_y), (br_x, br_y), (bl_x, bl_y)] 顺序必须严格为左上→右上→右下→左下 width, height 400, 120 # 标准车牌宽高比 3.33:1 → 设为 400x120 dst_pts np.array([[0, 0], [width, 0], [width, height], [0, height]], dtypefloat32) src_pts np.array(points, dtypefloat32) M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(image, M, (width, height)) return cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) # 强制转灰度注意perspective_points默认值是[ [120,80], [520,80], [520,180], [120,180] ]对应 640x480 分辨率下的 ROI 区域。如果你换 1080P 摄像头必须按比例缩放这四个点否则校正后图像拉伸变形。我一般用cv2.imshow()先画出这四个点确认位置再关掉显示窗口——这是唯一需要手动调的视觉参数。3.2 OCR 识别主逻辑EasyOCR 不是万能的后处理才是核心竞争力core/plate_recognizer.py的recognize_plate()方法本质是「EasyOCR 规则引擎」双保险def recognize_plate(image_path: str) - Optional[str]: # Step 1: EasyOCR 基础识别启用细节模式返回所有候选 results reader.readtext( image_path, decoderbeamsearch, # 比 greedy 更准慢 20% beamWidth5, # 束搜索宽度5 是平衡点 text_threshold0.7, # 置信度阈值低于此值直接丢弃 allowlist京津沪渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新0123456789ABCDEFGHJKLMNPQRSTUVWXYZ # 中国车牌字符集剔除 I/O/Q ) # Step 2: 候选过滤长度必须为 7 或 8新能源车 8 位且首字符是汉字或字母 candidates [] for (bbox, text, prob) in results: text re.sub(r[^A-Za-z\u4e00-\u9fa50-9], , text) # 去标点 if len(text) in [7, 8] and (text[0].isalpha() or \u4e00 text[0] \u9fa5): candidates.append((text, prob)) # Step 3: 规则校验用正则匹配车牌格式粤B12345 / 京AD12345 pattern r^[京津沪渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新][A-Z][A-Z0-9]{5,6}$ final_candidates [c for c in candidates if re.match(pattern, c[0])] # Step 4: 返回最高置信度结果无匹配则返回 None return max(final_candidates, keylambda x: x[1])[0] if final_candidates else None这段代码的价值在于它把 EasyOCR 的「识别结果」变成了「可交付结果」。比如 EasyOCR 可能返回粤B1234SS 被误识为 5但正则校验会拒绝它末位必须是数字也可能返回粤B1234568 位新能源只要符合^[京津...][A-Z][A-Z0-9]{6}$就接受。没有这层规则OCR 准确率再高 99%落地时照样天天人工改单。3.3 计费引擎状态机为什么用状态机而不是 if-elsecore/billing_engine.py的calculate_fee()方法表面是计算钱实则是管理车辆生命周期。它用字典模拟状态机比写 10 个 if-else 更易维护# 状态定义来自 billing.yaml STATES { ENTRY: {allowed_next: [EXIT, OVERTIME], fee: 0}, EXIT: {allowed_next: [PAID, REFUND], fee: lambda r: _calc_by_hour(r)}, OVERTIME: {allowed_next: [EXIT, PAID], fee: lambda r: _calc_overtime(r)}, PAID: {allowed_next: [], fee: 0}, REFUND: {allowed_next: [PAID], fee: lambda r: -r.fee} } def calculate_fee(plate: str, entry_time: datetime, exit_time: datetime) - Dict: record db.get_latest_record(plate) # 查最近一条未结算记录 if not record or record[status] ! ENTRY: return {error: NO_ACTIVE_ENTRY, fee: 0} # 状态流转校验 current_state record[status] if EXIT not in STATES[current_state][allowed_next]: return {error: fINVALID_TRANSITION_FROM_{current_state}, fee: 0} # 计费逻辑按小时阶梯前 15 分钟免费 duration (exit_time - entry_time).total_seconds() / 3600 if duration 0.25: fee 0 elif duration 1: fee 5 elif duration 2: fee 5 (duration - 1) * 3 else: fee 5 3 (duration - 2) * 2 return { fee: round(fee, 2), duration_hours: round(duration, 2), status: EXIT, updated_at: datetime.now().isoformat() }关键点STATES字典定义了合法状态转移calculate_fee()只负责「当前状态能否转到 EXIT」「算多少钱」不负责更新 DB。状态更新交给core/billing_engine.py的update_record_status()单独处理——这种分离让测试变得极其简单你可以 mockdb.get_latest_record()返回任意状态验证calculate_fee()是否返回正确fee和error。4. 运行与部署从零开始 10 分钟跑通树莓派实测避坑指南这套系统最友好的地方是它不强制你用虚拟环境pip install -r requirements.txt后就能python app.py启动。但真实部署时有 3 个隐藏雷区必须提前排掉。4.1 摄像头权限与 OpenCV 兼容性为什么cv2.VideoCapture(0)总是返回空帧树莓派默认禁用摄像头接口且opencv-python-headless不支持 V4L2 的某些扩展。解决方案分三步开启摄像头硬件支持sudo raspi-config # 选择 Interface Options → Camera → Yes → Reboot加载 V4L2 模块并设置权限echo bcm2835-v4l2 | sudo tee -a /etc/modules sudo usermod -a -G video $USER sudo reboot在app.py中指定后端关键# 替换原来的 cap cv2.VideoCapture(0) cap cv2.VideoCapture(0, cv2.CAP_V4L2) # 强制使用 V4L2 后端 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) # 启用 MJPEG 压缩如果跳过第 3 步OpenCV 会默认用CAP_ANY在树莓派上大概率 fallback 到低效的CAP_IMAGES模式导致cap.read()返回(False, None)。这是新手翻车率最高的点——现象是「程序不报错但识别不了」原因却是 OpenCV 后端选错了。4.2 EasyOCR 模型下载路径陷阱别让它默认下到/root/.EasyOCR/EasyOCR 第一次运行会自动下载zh_sim.pth和en.pth默认路径是~/.EasyOCR/。但在树莓派上如果以pi用户启动服务而app.py是用sudo python app.py运行的模型就会下到/root/.EasyOCR/导致非 root 用户无法访问。解决方案是显式指定模型路径# 在 app.py 开头添加 import os os.environ[EASYOCR_MODULE_PATH] /home/pi/easyocr_models # 自定义路径 reader easyocr.Reader([ch_sim,en], model_storage_directory/home/pi/easyocr_models)然后手动创建目录并赋权mkdir -p /home/pi/easyocr_models sudo chown -R pi:pi /home/pi/easyocr_models这样下次运行就会把模型下到/home/pi/easyocr_models/且所有用户都能读。4.3 微信模板消息推送失败排查40003 错误码的真正含义core/notification.py的send_wechat()方法如果返回{errcode:40003,errmsg:invalid openid}99% 不是 OpenID 错了而是access_token 过期了。微信 access_token 有效期 2 小时但代码里没做自动刷新。原版实现是每次请求都重新获取 tokendef get_access_token(): url fhttps://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid{APPID}secret{APPSECRET} res requests.get(url).json() return res.get(access_token) def send_wechat(openid: str, fee: float): token get_access_token() # ❌ 每次都重新取高频调用必超限 # ... 发送逻辑正确做法是缓存 token 并加锁import threading _token_cache {token: , expires_at: 0} _token_lock threading.Lock() def get_access_token_cached(): with _token_lock: if time.time() _token_cache[expires_at]: return _token_cache[token] # 获取新 token url fhttps://api.weixin.qq.com/cgi-bin/token?... res requests.get(url).json() _token_cache[token] res[access_token] _token_cache[expires_at] time.time() res[expires_in] - 300 # 提前 5 分钟刷新 return _token_cache[token]这个改动能让微信推送成功率从 60% 提升到 99.8%。我是在某社区停车场上线第三天发现日志里大量 40003抓包才定位到 token 频繁失效——微信接口限制每 2 小时最多 2000 次 token 获取而他们日均 300 车次每车次触发 2 次推送入场提醒出场账单刚好踩线。5. 常见问题排查5 条血泪经验省下你 3 天调试时间这些不是文档里写的「可能遇到的问题」而是我在 3 个实际部署现场亲手填过的坑。每一条都对应一个具体报错日志和修复命令。5.1 现象app.py启动后控制台疯狂刷ERROR:root:No module named PIL原因easyocr依赖pillow但pip install easyocr有时会漏装尤其在 ARM 架构上。PIL是pillow的旧名新版本已统一为Pillow。解决pip uninstall pillow -y pip install --upgrade --force-reinstall pillow # 验证python -c from PIL import Image; print(OK)5.2 现象/api/capture返回{error: IMAGE_PROCESSING_FAILED, detail: cv2.error: OpenCV(4.8.1) ... invalid pointer原因OpenCV 图像内存泄漏常见于cv2.warpPerspective()后未释放临时数组或cv2.imread()读取损坏 JPG。解决在image_preprocessor.py的correct_perspective()结尾加内存清理# 原代码结尾 warped cv2.warpPerspective(image, M, (width, height)) gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) del warped, M, src_pts, dst_pts # 显式删除大对象 return gray5.3 现象/api/exit返回{error: NO_ACTIVE_ENTRY}但数据库里明明有statusENTRY的记录原因db.get_latest_record()查询用了ORDER BY id DESC LIMIT 1但 SQLite 的id是 INTEGER PRIMARY KEY当记录被删再插入时id不连续最新记录未必id最大。解决修改查询 SQL用时间戳排序# 原 SQL: SELECT * FROM parking_records WHERE plate ? ORDER BY id DESC LIMIT 1 # 改为 SELECT * FROM parking_records WHERE plate ? AND status ENTRY ORDER BY created_at DESC LIMIT 15.4 现象微信模板消息收到但fee字段显示undefined或NaN原因notification.py拼 JSON 时fee是float类型但微信要求data.fee.value必须是字符串且带¥符号。原代码直接str(fee)会丢失精度如5.0变成5.0微信渲染为¥5。解决格式化为两位小数字符串fee: { value: f¥{fee:.2f}, # 强制两位小数 color: #173177 }5.5 现象树莓派运行 2 小时后app.py自动退出systemctl status parking显示killed原因内存溢出OOM Killer 干的。easyocr.Reader()加载模型后常驻内存约 1.2GB树莓派4B 2GB 版本只剩 800MB 给系统apt upgrade后内核升级可能触发 OOM。解决限制 Python 进程内存并启用 swap# 创建 1GB swap sudo fallocate -l 1G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile # 永久生效 echo /swapfile none swap sw 0 0 | sudo tee -a /etc/fstab # 启动时加内存限制 # systemctl edit parking.service # [Service] # MemoryLimit1.5G6. 二次开发实战如何接入海康威视 IPC 摄像头并支持车牌颜色识别如果你的停车场已经部署了海康威视 DS-2CD3T47G2-L 海螺系列 IPC想复用现有设备而不是加 USB 摄像头改造只需 3 个文件、不到 50 行代码。核心思路是不改识别逻辑只替换图像来源。6.1 新增 IPC 图像抓取模块core/ipc_capture.py海康威视 IPC 支持 RTSP 和 HTTP 抓图。HTTP 抓图更轻量不用装ffmpegURL 格式为http://admin:password192.168.1.64/ISAPI/Streaming/channels/101/picture。我们封装成函数import requests from urllib.parse import urljoin from utils.time_utils import generate_timestamp def capture_from_ipc(ip: str, port: int, username: str, password: str, channel: int 101) - Optional[bytes]: 从海康IPC抓取JPEG图片返回原始字节流 channel: 101主码流102子码流推荐102更小更快 auth requests.auth.HTTPDigestAuth(username, password) # 海康必须用 Digest Auth url fhttp://{ip}:{port}/ISAPI/Streaming/channels/{channel}/picture try: res requests.get(url, authauth, timeout5) res.raise_for_status() if res.headers.get(Content-Type) image/jpeg: return res.content else: logger.error(fIPC returned non-JPEG: {res.headers.get(Content-Type)}) return None except Exception as e: logger.error(fIPC capture failed: {e}) return None # 保存为临时文件供 EasyOCR 读取 def save_ipc_image(image_bytes: bytes) - str: timestamp generate_timestamp() path f/tmp/ipc_capture_{timestamp}.jpg with open(path, wb) as f: f.write(image_bytes) return path6.2 修改app.py动态切换图像源在app.py的/api/capture路由里加一个source参数决定用 USB 还是 IPCapp.route(/api/capture, methods[POST]) def api_capture(): data request.get_json() source data.get(source, usb) # usb or ipc if source usb: image_path capture_from_usb() # 原有逻辑 elif source ipc: ip data.get(ipc_ip, 192.168.1.64) port data.get(ipc_port, 80) username data.get(ipc_user, admin) password data.get(ipc_pass, 12345) image_bytes capture_from_ipc(ip, port, username, password) if not image_bytes: return jsonify({error: IPC_CAPTURE_FAILED}), 400 image_path save_ipc_image(image_bytes) else: return jsonify({error: INVALID_SOURCE}), 400 plate recognize_plate(image_path) # ... 后续逻辑不变6.3 扩展车牌颜色识别3 行代码接入 ColorClassifier原系统只识别车牌号但收费规则可能区分蓝牌小型车和黄牌大型车。我们用scikit-learn训练一个超轻量颜色分类器已提供训练好的color_model.joblib# 在 core/plate_recognizer.py 顶部加载 from sklearn.ensemble import RandomForestClassifier import joblib color_classifier joblib.load(models/color_model.joblib) # 12KB 文件5 个特征HSV 均值方差 def classify_plate_color(image_path: str) - str: img cv2.imread(image_path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) features [ hsv[:,:,0].mean(), hsv[:,:,1].mean(), hsv[:,:,2].mean(), hsv[:,:,0].var(), hsv[:,:,1].var() ] pred color_classifier.predict([features])[0] return {0: blue, 1: yellow, 2: green, 3: white}[pred] # 在 recognize_plate() 返回前加一行 plate_info {plate: plate, color: classify_plate_color(image_path)}从那以后我每次接到 IPC 接入需求都强制走一遍「抓图 URL 测试 → Digest Auth 验证 → 临时文件路径权限检查」三步。海康的 HTTP 抓图接口看似简单但ISAPI路径大小写敏感、认证方式必须是 Digest、超时必须设短否则卡死漏一步就白忙活。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑