树莓派4B车牌识别系统部署实战:从模型训练到实时推理

树莓派4B车牌识别系统部署实战:从模型训练到实时推理 简介基于树莓派与PyTorch深度学习框架的车牌检测识别完整项目面向毕业设计、课程设计、工程实训及竞赛场景适合具备一定Python基础、希望上手嵌入式AI开发的初学者。方案整合YOLOv5完成车牌定位LPRNet与STNet完成字符识别兼顾检测精度与识别速度并附有树莓派环境配置与深度学习环境搭建说明降低入门门槛。压缩包共117个文件以Python源码、YAML/yml配置、Dockerfile、模型权重pth及Markdown说明为主另有Shell脚本、UI文件与教程IPYNB等结构与用途清晰便于按模块阅读和二次开发。资源包7.64MB轻量易下载。目前已有271人学习使用适合作为项目复现或功能扩展的基础。除完整源码与工程文件外还提供模型权重、部署配置与项目说明拿到后可直接在树莓派上烧录运行若硬件基础薄弱也可按引脚定义用面包板与杜邦线搭建替代电路快速复现同款系统。资料经严格测试运行流程完整适合项目开发与学习练手。1. 树莓派上车牌检测识别系统要解决的三个问题车牌识别在服务器上跑通并不难难的是把它压进树莓派 4B 这样一块算力有限、内存紧张、散热靠被动片的板子里还能在连续视频帧中稳定框出车牌并读出字符。这个项目的价值恰好就在这训练放在 PC 或云平台部署落在树莓派上用 TorchScript 把检测与识别两个深度学习模型压缩到几十 MB以 640×480 的输入在树莓派 CPU 上换取 5~10 FPS 的实时推理足以支撑毕设、课设、竞赛演示甚至小范围实训。这套链路要解决三个核心问题硬件与摄像头取流怎么搭、检测与识别模型怎么选型与串联、部署后帧率上不去时先调哪个参数。下面按这个顺序展开中间涉及的命令和代码都可以直接抄走改参数。2. 树莓派硬件与外设选型摄像头、屏幕驱动与供电2.1 为什么车牌识别项目优先选树莓派 4B 而不是 5 或 Jetson做车牌识别这类深度学习边缘部署项目树莓派 4B 是目前资料最全、生态最稳的选择。树莓派 5 的 CPU 性能确实更强但它对电源功率、散热片和操作系统版本都有新要求部分 GPIO 库的兼容方式也变了课设阶段踩这些坑的性价比不高。Jetson Nano 算力更高但官方生态逐步收缩且系统镜像和工具链的维护对公司用户不友好。下表给出三个常见载体的取舍平台CPU/算力特点内存适合场景树莓派 4B4 核 ARMv8CPU 推理为主2/4/8 GB毕设课设、边缘视频流识别树莓派 5性能约为 4B 的 2 倍发热和供电敏感4/8 GB对帧率有更高要求的实训项目Jetson Nano带 128 核 Maxwell GPU4 GB需要 CUDA 加速的竞赛项目我一般建议选 4B 8GB 版本。车牌检测模型只有几十 MB4GB 内存跑起来也不吃力但 8GB 版本在同时开摄像头、PyTorch、OpenCV 和屏幕显示时更从容。树莓派 4B 的 4 核 CPU 在 320×320 输入下单帧检测推理时间大约在 80~200ms 之间这个水平配合跳帧策略已经能做出流畅的演示效果。2.2 OV5647 摄像头模组的接入CSI 接口与驱动确认OV5647 是树莓派官方 Camera Module v1 使用的传感器型号也是这套系统里最常见的取流设备。CSI 接口走的是专用排线比 USB 摄像头占用 CPU 少而且 OpenCV 可以通过 V4L2 直接读取/dev/video0不需要额外装驱动。新版本 Raspberry Pi OS 使用 libcamera 框架raspistill已经废弃所以配置命令按新框架来写。# 进入交互配置在 Interface Options 中启用 Camera 相关选项 sudo raspi-config # 确认摄像头被系统识别出现 supported1 detected1 即为正常 vcgencmd get_camera # 列出当前可用的相机设备 libcamera-hello --list-cameras # 拍摄一张测试图验证取流链路 libcamera-still -o test.jpg --width 1280 --height 720vcgencmd get_camera返回detected1说明 CSI 排线连接正常排线插反时这里直接显示detected0。确认设备节点存在后OpenCV 侧用 V4L2 后端取流import cv2 cap cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)这里的CAP_V4L2强制走 V4L2 接口避免 OpenCV 默认的 GStreamer 后端在树莓派上出现延迟或花屏。640×480 是成本和精度的平衡点分辨率再高检测前还是要缩放到 320×320徒增解码耗时。2.3 树莓派上安装 PyTorch 与 OpenCV 的完整流程树莓派安装深度学习环境核心原则是不要源码编译直接用官方预编译 wheel。源码编译 PyTorch 在树莓派上需要几个小时而且内存不够时还会被编译器杀死。常见做法是先用 venv 建一个独立环境避免系统 Python 被污染。# 创建虚拟环境避免污染系统 Python python3 -m venv ~/plate-env source ~/plate-env/bin/activate # 科学计算依赖树莓派 CPU 推理离不开 BLAS sudo apt update sudo apt install -y libatlas-base-dev libopenblas-dev # 安装 PyTorch CPU 版官方 ARM wheel32 位和 64 位不通用 pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cpu # OpenCV 使用无 GUI 版本识别系统不需要 qt 后端 pip3 install opencv-python-headless numpy装完以后检查一下编译目标和线程支持python3 -c import torch, cv2; print(torch.__version__, torch.backends.mkldnn.is_available(), cv2.__version__)mkldnn输出为 True 说明 CPU 推理可以走 oneDNN 加速路径这对树莓派这种没有独立 GPU 的设备非常关键。另一个常见做法是用pip install opencv-python-headless而不是opencv-python因为后者会带上 Qt 和 GTK 依赖在树莓派上容易和桌面环境起冲突。下载慢时把 pip 源切到国内镜像即可armv7l 和 aarch64 的 wheel 是两套文件烧录时尽量选 64 位系统内存使用效率更高。2.4 PWM 波输出与供电两个容易被忽略的外设坑车牌识别系统往往不止一个摄像头还可能要接补光灯、舵机云台或者 LCD 屏。树莓派 4B 的 GPIO18物理第 12 脚可以输出 PWM 波驱动舵机或者调节补光灯亮度。驱动全彩屏这类设备时建议优先考虑 ILI9341 这类 SPI 接口的屏幕通过设备树直接配置# 编辑 /boot/firmware/config.txt新系统或 /boot/config.txt旧系统 dtoverlayili9341:rotate90供电问题比 PWM 更容易踩。树莓派 4B 官方建议 5V/3A 电源但如果同时带摄像头、屏幕和外置舵机3A 很可能不够。舵机启动瞬间电流能达到 1A 以上会让板载 5V 电压跌落出现“红灯常亮但绿灯不亮”的假死现象。排查顺序是先断开所有外设只留 SD 卡看能否启动再逐个接回外设确认是哪一个造成供电不足。如果外设较多用带独立供电的扩展板或者舵机专用电源不要和树莓派共用一路电源。PWM 输出的具体操作放到第 5 章验证环节再展开。3. 车牌检测与车牌识别的两阶段深度学习模型构建3.1 两阶段方案为什么比端到端更适合树莓派基于视觉检测的深度学习模型构建车牌识别有两条路线一条是“检测 字符识别”两阶段另一条是用 LPRNet 这类端到端模型直接输出车牌字符。在实践中端到端模型的优势是前向推理只有一次但缺点是车牌区域定位和字符解码耦合在一起CPU 上跑 CTC 解码并不比两阶段快多少而且误检和误读混在一起课设答辩时很难解释清楚。两阶段方案把任务拆成“车牌检测”和“字符识别”两个独立模型各自可以单独调参数、单独优化也方便换数据集重训。检测模型的输出是一个或几个车牌框识别模型对每个框内的字符做分类。项目里常见做法是先用检测模型找到区域再用一个轻量 CNN 对裁剪图逐字符分类。这样每部分模型都足够小树莓派 CPU 才能扛得住。3.2 检测模型选型YOLOv5n 与 YOLOv8n 的取舍车牌检测模型要的是在边缘设备上跑得快。YOLOv5n 和 YOLOv8n 都是 nano 级别参数量在 3M 左右导出为 TorchScript 后文件约 6~10MB非常适合树莓派。YOLOv5n 的优点是导出工具链稳定网上教程多YOLOv8n 的优点是训练代码更现代后处理接口统一。两个模型在树莓派 CPU 上的推理耗时差距不大选哪个取决于你手头数据集格式更配哪套工具。训练在 PC 上进行常见做法是拿车牌公开数据集 CCPD 或者自采数据先转成 YOLO 格式标注再用官方预训练权重做微调。# YOLOv5 训练命令PC 上执行 python train.py --data plate.yaml --weights yolov5n.pt --img 320 --epochs 100 --batch 32 # 导出为 TorchScript供树莓派加载 python export.py --weights runs/train/exp/weights/best.pt --img 320 --include torchscript --optimize# YOLOv8 训练导出方式 yolo train dataplate.yaml modelyolov8n.pt imgsz320 epochs100 batch32 yolo export modelbest.pt formattorchscript imgsz320--img 320控制训练输入尺寸导出时保持一致推理时模型拿到的就是 320×320 的图。--optimize会对 TorchScript 做算子融合树莓派上推理能省 10% 左右时间。导出后得到的.torchscript文件直接拷贝到树莓派即可不用在树莓派上装 ultralytics 完整依赖。3.3 识别模型轻量 CNN 分类器与 65 类车牌字符集车牌字符识别的输入是检测框裁剪出来的小图。常见车牌字符集包含汉字省份简称、24 个大写字母去掉 I 和 O和 10 个数字共约 65 类。网络结构不需要复杂3 层卷积加 2 层全连接就够用。输入统一为 24×64 的灰度图保持和训练一致。import torch.nn as nn class CharNet(nn.Module): def __init__(self, n_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3), nn.ReLU(), nn.MaxPool2d(2), # 24x64 - 11x31 nn.Conv2d(32, 64, 3), nn.ReLU(), nn.MaxPool2d(2), # 4x14 nn.Conv2d(64, 128, 3), nn.ReLU(), # 2x12 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 2 * 12, 256), nn.ReLU(), nn.Linear(256, n_classes), ) def forward(self, x): return self.classifier(self.features(x))MaxPool2d(2)在树莓派 CPU 上是廉价操作真正耗时的是全连接层矩阵乘法所以256这个中间维度不要加太大。训练数据用检测模型对道路视频跑一遍把高置信度车牌框截下来再做旋转、模糊、亮度扰动增强。推理时保持输入尺寸不变输出 65 个类别的概率分布取argmax得到字符索引。3.4 在 PyTorch 里把检测和识别串成一条流水线两个模型都导出为 TorchScript 后在树莓派上串起来的核心逻辑如下。检测模型输出格式按 YOLOv8 单类配置处理即(1, 5, 8400)的向量5 表示中心点坐标、宽高和目标置信度。import cv2 import torch import numpy as np def decode_nms(x, conf0.4, iou_thr0.45, orig_w640, orig_h480): # x: (5, 8400)坐标基于 320x320 输入需要还原到原图尺寸 x x.T.cpu().numpy() x x[x[:, 4] conf] if len(x) 0: return [] boxes np.zeros((len(x), 5)) boxes[:, 0] (x[:, 0] - x[:, 2] / 2) / 320 * orig_w # x1 boxes[:, 1] (x[:, 1] - x[:, 3] / 2) / 320 * orig_h # y1 boxes[:, 2] (x[:, 0] x[:, 2] / 2) / 320 * orig_w # x2 boxes[:, 3] (x[:, 1] x[:, 3] / 2) / 320 * orig_h # y2 boxes[:, 4] x[:, 4] boxes boxes[np.argsort(-boxes[:, 4])] # 按置信度降序 keep [] while len(boxes): keep.append(boxes[0]) iou compute_iou(boxes[0], boxes[1:]) boxes boxes[1:][iou iou_thr] return keep def compute_iou(a, b): if len(b) 0: return np.array([], dtypenp.float32) x1 np.maximum(a[0], b[:, 0]); y1 np.maximum(a[1], b[:, 1]) x2 np.minimum(a[2], b[:, 2]); y2 np.minimum(a[3], b[:, 3]) inter np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1]) return inter / (area_a area_b - inter 1e-6)这段代码里conf0.4是置信度阈值低于这个值的框直接丢弃iou_thr0.45是 NMS 去重阈值两个框的交并比超过 0.45 就只保留置信度高的那个。坐标归一是最容易出错的地方模型在 320×320 上预测但原图是 640×480必须按各自维度缩放回去否则画框会偏。完整闭环里“检测 每框识别”的顺序不能改识别模型不接受原图只接受裁剪并缩放到 24×64 的小图。4. 树莓派上的模型部署从 TorchScript 到帧率优化4.1 部署的三种方式TorchScript、ONNX 与 C 后端的边界模型训练好以后部署到树莓派有几种选择各自的边界要拎清部署方式是否适合树莓派理由TorchScript 直接加载首选与训练代码一致调试成本最低ONNX Runtime可选方便做量化但要额外维护算子兼容C libtorch项目冲刺去掉 Python 开销但交叉编译复杂我一般直接选 TorchScript。ONNX Runtime 在树莓派 ARM 上有官方 wheel量化支持更好但车牌检测模型里的某些算子导出成 ONNX 后需要版本回退无形中多一层排错。libtorch C 部署能压榨不少性能但代价是 cmake 和依赖管理适合竞赛最后冲刺而非课程设计。TorchScript 加载后第一件事是设置线程数和开启推理优化import torch torch.set_num_threads(4) model torch.jit.load(plate_det.torchscript, map_locationcpu) model torch.jit.optimize_for_inference(model) model.eval()torch.set_num_threads(4)匹配树莓派 4B 的四核。注意不要设成 8超线程在 ARM 上不存在设大了反而因为线程切换拖慢速度。optimize_for_inference会做算子融合比如把卷积和 ReLU 合并推理耗时可再降 5%~10%。PyTorch 的动态量化对全连接层有效但对卷积加速有限车牌检测模型里卷积占大头量化预期的收益不要报太高。4.2 帧率上不去的三个瓶颈解码、推理与 Python 开销树莓派上跑视频流帧率上不去通常不是模型推理单独造成的而是整条链路里三个位置轮流卡。第一个是摄像头解码640×480 的 H264 流在树莓派 CPU 上软解每帧要 10~20ms这还没算 OpenCV 从 V4L2 拷贝数据的时间。第二个是检测推理320×320 输入在 CPU 上单帧大约 100ms。第三个是 Python 侧的后处理和字符识别如果一帧里检测出多个车牌每个框再做一次透视矫正和识别耗时直接翻倍。针对这三个瓶颈常见的优化手段是跳帧、降分辨率、限识别数量。代码层实现如下frame_id 0 while True: ok, frame cap.read() frame_id 1 if not ok: break # 检测只跑奇数帧偶数帧沿用上一帧结果FPS 翻倍 if frame_id % 2 1: last_det run_detection(frame) draw_boxes(frame, last_det[:4]) # 每帧最多画 4 个框跳帧策略对车辆静止或低速场景非常有效车牌位置不会在两帧之间突变。识别每帧最多只做 4 个框高置信度优先后续帧如果同一个位置还是高置信度就不重复跑识别。这样从“检测 100ms 识别 N×25ms”变成“每隔一帧检测一次识别恒定为 4×25ms”实际观感比堆硬件更直接。4.3 必调的三个参数输入尺寸、置信度阈值与 NMS IoU树莓派上部署车牌识别最值得动的参数就三个。输入尺寸是性价比最高的一档320×320 比 416×416 快约 40%代价是远距离小车牌可能漏检。置信度阈值默认 0.25 对边缘设备偏低0.35~0.5 之间更合适低了误检框多后处理白白浪费 CPU。NMS IoU 阈值保持 0.45 通常不用改它只影响相邻框去重对单车牌场景影响不大。# 用 50 帧测试不同阈值下的耗时与输出框数量 import time for conf in [0.25, 0.35, 0.5]: t0 time.time(); total_boxes 0 for _ in range(50): frame get_frame() boxes run_detection(frame, confconf) total_boxes len(boxes) print(fconf{conf}, avg{ (time.time()-t0)/50*1000:.0f}ms, boxes/frame{total_boxes/50:.2f})从输出里能明显看到阈值 0.25 时每帧平均框数可能到 1.5升到 0.5 后降到 1.0后者虽然漏检风险略增但识别模块的无效计算少了。项目里把 conf 设为全局配置晚间光线差时降到 0.35白天强光下用 0.45比改模型重训快得多。4.4 树莓派上部署时的高频报警与处置部署阶段报错和系统故障集中在这几个现象上OpenCV 打开摄像头报cannot open camera by index通常是上一次进程没释放设备用ps aux | grep python找到残留进程杀掉vcgencmd get_camera返回 detected0基本是排线松动或者插反了重插后要重启红灯常亮但绿灯不亮优先怀疑 SD 卡引导损坏或电源电流不足先用fsck检查 SD 卡再换电源测试。还有一个常见问题是 pip 装完 torch 后 import 报缺 libgomp装libgomp1就能解决。5. 用本地视频回环验证车牌识别链路并扩展舵机跟随5.1 用录好的视频代替摄像头离线跑通全流程实际调试时不要一上来就接摄像头摄像头环境不可控复现问题很费劲。更稳的做法是先用手机或相机录一段 10~20 秒的停车视频转成 mp4 后放到树莓派上用视频文件作为输入跑通检测和识别全链路。这样每次跑结果一致也能在答辩时用同一条视频演示。# 树莓派上跑离线回环输出带标注的视频 python3 run_demo.py --input test.mp4 --output result.mp4 --det plate_det.torchscript --rec char_cls.torchscriptrun_demo.py的核心就是用cv2.VideoCapture(test.mp4)替代摄像头其他处理逻辑完全不变。离线回环跑通后再把test.mp4换成0接摄像头问题范围就缩小到取流和编码这一层。5.2 三个硬指标检测耗时、字符准确率与误检率验证环节要量化不能只靠肉眼。最实用的三个指标是单帧检测耗时、车牌字符串准确率、误检框数量。检测耗时用前面的计时脚本跑 100 帧取平均值记录在项目报告里作为性能证据。字符串准确率需要准备一份带标注的测试集对每个车牌框的真实车牌和识别结果做逐字符对比correct total 0 for frame, gt_text in test_set: pred_text run_pipeline(frame) total 1 if pred_text gt_text: correct 1 print(fplate_acc{correct/total:.2%})误检率统计的是检测模型输出的框里有多少框内根本没有车牌。停车场场景常见误检来自车灯、反光地面和树叶阴影误检率高时优先调高置信度阈值而不是换模型。5.3 一个能直接落地的扩展用 PWM 舵机云台让摄像头跟随车牌识别系统跑通后加一个舵机云台跟随能让演示效果上一个档次。树莓派 4B 的 GPIO18 支持硬件 PWM用 pigpio 库可以直接输出稳定脉宽。当车牌中心偏离画面中心时按偏差比例调整舵机角度让车牌始终保持在画面中央。import pigpio pi pigpio.pi() pi.set_mode(18, pigpio.OUTPUT) pi.set_servo_pulsewidth(18, 1500) # 中位 # 假设框 cx 是车牌中心 xFRAME_CX 是画面中心 x error cx - FRAME_CX pulse 1500 int(error * 1.2) # 比例系数 1.2先粗调再细调 pulse max(1100, min(1900, pulse)) # 限幅避免超出舵机行程 pi.set_servo_pulsewidth(18, pulse)set_servo_pulsewidth的参数单位是微秒1500μs 对应舵机中位1100μs 和 1900μs 是常见舵机的限位。比例系数 1.2 需要按实际画面宽度和舵机转角标定系数太大云台会抖太小跟随迟钝。给偏差区间设置 20 像素的死区中心误差在这个范围内不触发转动能有效避免 PWM 输出频率和摄像头帧率耦合造成的抖动。增益、偏置和死区这三个值按舵机型号调好后存成 config.json整个跟随逻辑就是一组比例运算。本文还有配套的精品资源点击获取