MMDeploy实战:把RTMPose模型部署到树莓派的全流程记录(含ONNX转换踩坑记录)
从实验室到边缘:RTMPose模型在树莓派上的轻量化部署实战
最近在做一个智能三角板检测的课堂项目,需要把训练好的关键点检测模型部署到树莓派上实时运行。本以为用OpenMMLab生态的MMDeploy工具链会很简单,结果从PyTorch模型转换到ONNX,再到树莓派上跑起来,整个过程踩了不少坑。特别是RTMPose这种基于SimCC结构的新模型,官方文档里有些细节没讲清楚,网上也找不到完整的树莓派部署案例。这篇文章就是把我整个部署流程记录下来,包括环境配置、模型转换、量化优化、树莓派推理全步骤,还有那些官方文档没写的坑和解决方案。如果你也在做类似的项目,想把课堂训练的模型真正落地到边缘设备,这篇实战记录应该能帮你省下不少时间。
1. 环境准备与工具链选择
在开始模型转换之前,搭建一个稳定可靠的工作环境至关重要。我选择在Ubuntu 20.04的服务器上进行模型转换和量化,主要考虑到服务器有GPU可以快速验证转换后的模型精度,同时也能处理一些计算密集型的优化操作。树莓派这边用的是Raspberry Pi 4B 8GB版本,运行64位的Raspberry Pi OS。
核心工具链的选择直接决定了后续工作的顺畅程度。经过对比测试,我最终确定了以下组合:
- MMDeploy 1.3.0:OpenMMLab官方的模型部署工具,支持多种后端,ONNX导出功能相对成熟
- PyTorch 2.0.1:训练框架,需要与MMPose版本匹配
- MMPose 1.x dev分支:包含最新的RTMPose实现
- ONNX Runtime 1.15.1:树莓派上的推理引擎
- OpenCV 4.8.0:图像预处理和后处理
注意:版本兼容性是个大坑。MMDeploy对PyTorch和ONNX的版本非常敏感,建议严格按照官方推荐的版本组合安装,不要随意升级。
安装过程看似简单,但有几个关键点容易出错。首先是MMDeploy的编译安装,需要确保所有依赖都正确安装:
# 安装MMDeploy
git clone -b main https://github.com/open-mmlab/mmdeploy.git
cd mmdeploy
git submodule update --init --recursive
pip install -e .
# 安装ONNX Runtime推理后端
pip install onnxruntime==1.15.1
# 对于树莓派,需要安装ARM64版本的ONNX Runtime
wget https://github.com/microsoft/onnxruntime/releases/download/v1.15.1/onnxruntime-linux-aarch64-1.15.1.tgz
tar -zxvf onnxruntime-linux-aarch64-1.15.1.tgz
树莓派上的环境配置更加考验耐心。由于ARM架构的限制,很多预编译包不可用,需要从源码编译。我建议先在x86服务器上完成所有模型转换和验证工作,再把最终的ONNX模型和推理脚本移植到树莓派。
2. RTMPose模型结构与转换难点
RTMPose是OpenMMLab在2023年推出的轻量级姿态估计模型,采用了SimCC(Simple Coordinate Classification)的坐标表示方法。与传统的热图方法不同,SimCC将坐标预测转化为两个独立的一维分类任务,这种设计在保持精度的同时大幅减少了计算量,特别适合边缘设备部署。
模型结构特点决定了转换时的特殊处理需求:
| 组件 | 传统热图方法 | RTMPose (SimCC) | 转换影响 |
|---|---|---|---|
| 输出表示 | 2D热图 (H×W×K) | 两个1D分布 (W×K, H×K) | 输出形状完全不同 |
| 后处理 | 找峰值+亚像素细化 | 直接取argmax | 简化了后处理逻辑 |
| 计算复杂度 | O(H×W×K) | O((H+W)×K) | 内存占用大幅降低 |
从PyTorch到ONNX的转换,最大的挑战在于动态形状的处理。RTMPose支持可变尺寸的输入,但树莓派上的ONNX Runtime对动态尺寸的支持有限。我的解决方案是固定输入尺寸,虽然损失了一些灵活性,但推理速度有显著提升。
转换命令看起来简单,但隐藏着不少细节:
from mmdeploy.apis import torch2onnx
from mmdeploy.backend.onnxruntime import ORTWrapper
# 错误的做法:直接使用默认参数
# torch2onnx('configs/rtmpose.py', 'checkpoints/rtmpose-s.pth', 'demo.jpg', 'output')
# 正确的做法:显式指定动态轴
deploy_cfg = {
'backend_config': {
'type': 'onnxruntime',
'common_config': {
'input_shape': None, # 保持动态
'dynamic_axes': {
'input': {
0: 'batch',
2: 'height',
3: 'width'
},
'output': {
0: 'batch'
}
}
}
}
}
# 实际转换时需要固定尺寸以获得最佳性能
img_shape = (256, 192) # RTMPose-S的标准输入尺寸
torch2onnx(
model_cfg='configs/rtmpose-s.py',
model_checkpoint='checkpoints/rtmpose-s.pth',
img='test_image.jpg',
work_dir='onnx_models',
device='cuda',
deploy_cfg=deploy_cfg
)
转换过程中最常见的几个错误和解决方案:
-
ONNX节点不支持:RTMPose中某些PyTorch操作在ONNX中没有直接对应
- 解决方法:更新ONNX opset版本到14或更高
- 修改模型代码,用ONNX支持的操作替换
-
动态尺寸导致推理失败:树莓派上ONNX Runtime对动态尺寸支持不完善
- 解决方法:导出时固定输入尺寸,或使用多个固定尺寸的模型
-
精度下降明显:转换后模型在验证集上mAP下降超过2%
- 解决方法:检查输入归一化方式,确保与训练时一致
- 验证时使用与训练相同的预处理流水线
3. ONNX模型优化与量化实战
模型转换成功只是第一步,要让RTMPose在树莓派上实时运行(目标>15FPS),还需要进行深入的优化。我尝试了多种优化技术,下面这张表格对比了不同优化策略的效果:
| 优化方法 | 模型大小 | 树莓派推理时间 | 精度损失 | 实现难度 |
|---|---|---|---|---|
| 原始ONNX | 18.7MB | 420ms | 基准 | 简单 |
| ONNX Runtime优化 | 18.7MB | 380ms | <0.1% | 中等 |
| 动态量化 (INT8) | 4.8MB | 210ms | 1.2% | 中等 |
| 静态量化 (INT8) | 4.8MB | 190ms | 0.8% | 困难 |
| 混合精度 (FP16) | 9.4MB | 250ms | 0.3% | 简单 |
动态量化是最容易上手的优化方法,适合快速验证:
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
# 加载原始ONNX模型
model = onnx.load('rtmpose-s.onnx')
# 动态量化 - 只量化权重,激活保持浮点
quantized_model = quantize_dynamic(
model_input=model,
model_output='rtmpose-s_quantized.onnx',
weight_type=QuantType.QInt8,
per_channel=True,
reduce_range=True
)
但动态量化的加速效果有限,我最终选择了静态量化,虽然实现复杂,但效果最好。静态量化的关键在于准备一个代表性的校准数据集:
import numpy as np
from onnxruntime.quantization import CalibrationDataReader
class RTMPoseDataReader(CalibrationDataReader):
def __init__(self, calibration_dataset):
self.dataset = calibration_dataset
self.iter = iter(self.dataset)
def get_next(self):
try:
batch = next(self.iter)
# RTMPose的输入需要特定的预处理
img = batch['img'].numpy()
img_metas = batch['img_metas'].data
return {'input': img}
except StopIteration:
return None
def rewind(self):
self.iter = iter(self.dataset)
# 准备校准数据
calibration_dataset = load_calibration_images(100) # 100张代表性图像
dr = RTMPoseDataReader(calibration_dataset)
# 执行静态量化
from onnxruntime.quantization import quantize_static, QuantType
quantize_static(
model_input='rtmpose-s.onnx',
model_output='rtmpose-s_static_quant.onnx',
calibration_data_reader=dr,
quant_format=QuantFormat.QOperator,
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8,
per_channel=True,
reduce_range=True
)
量化过程中遇到的典型问题和解决经验:
-
量化后精度损失过大(>3%)
- 原因:校准数据集不具有代表性
- 解决:从验证集中随机选择100-200张图像,覆盖各种场景
-
量化模型推理速度反而变慢
- 原因:树莓派上INT8计算没有充分优化
- 解决:确保使用最新版ONNX Runtime,开启ARM NEON优化
-
某些层量化失败
- 原因:层输出范围动态变化太大
- 解决:跳过这些层的量化,保持FP32精度
提示:量化前一定要在x86服务器上验证量化模型的精度,避免在树莓派上调试困难。建议保留原始FP32模型作为回退方案。
4. 树莓派推理环境搭建与性能调优
树莓派的环境配置与x86服务器有很大不同。首先需要为ARM64架构编译安装优化版的ONNX Runtime:
# 在树莓派上编译ONNX Runtime(需要2-3小时)
git clone --recursive -b v1.15.1 https://github.com/microsoft/onnxruntime
cd onnxruntime
./build.sh --config Release --arm64 --build_shared_lib \
--parallel --skip_tests \
--enable_pybind --build_wheel \
--use_openmp --use_armnn
pip install build/Linux/Release/dist/onnxruntime*.whl
系统层面的优化同样重要。树莓派的性能受限于散热和功耗,需要合理配置:
# 1. 启用GPU内存(至少分配128MB给GPU)
sudo raspi-config
# 选择Performance Options -> GPU Memory -> 128
# 2. 设置CPU性能模式
echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# 3. 增加交换空间(避免内存不足)
sudo dphys-swapfile swapoff
sudo nano /etc/dphys-swapfile
# 修改CONF_SWAPSIZE=2048
sudo dphys-swapfile setup
sudo dphys-swapfile swapon
# 4. 安装必要的依赖
sudo apt-get update
sudo apt-get install -y libopenblas-dev libatlas-base-dev liblapack-dev
推理代码需要针对树莓派进行特殊优化。下面是我最终使用的推理脚本核心部分:
import onnxruntime as ort
import numpy as np
import cv2
import time
class RTMPoseInference:
def __init__(self, model_path, input_size=(192, 256)):
# 创建推理会话,启用所有优化
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
sess_options.intra_op_num_threads = 4 # 使用4个CPU核心
# 对于量化模型,需要指定特定的执行提供者
providers = ['CPUExecutionProvider']
self.session = ort.InferenceSession(
model_path,
sess_options=sess_options,
providers=providers
)
self.input_size = input_size
self.input_name = self.session.get_inputs()[0].name
# 预热推理(第一次推理通常较慢)
dummy_input = np.random.randn(1, 3, *input_size).astype(np.float32)
for _ in range(3):
self.session.run(None, {self.input_name: dummy_input})
def preprocess(self, image):
"""图像预处理,针对树莓派优化"""
# 调整尺寸
h, w = image.shape[:2]
scale = min(self.input_size[1] / h, self.input_size[0] / w)
new_h, new_w = int(h * scale), int(w * scale)
resized = cv2.resize(image, (new_w, new_h))
# 填充到目标尺寸
pad_h = self.input_size[1] - new_h
pad_w = self.input_size[0] - new_w
pad_top = pad_h // 2
pad_bottom = pad_h - pad_top
pad_left = pad_w // 2
pad_right = pad_w - pad_left
padded = cv2.copyMakeBorder(
resized,
pad_top, pad_bottom,
pad_left, pad_right,
cv2.BORDER_CONSTANT,
value=(0, 0, 0)
)
# 归一化 (与训练时保持一致)
padded = padded.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
padded = (padded - mean) / std
# 调整维度顺序 HWC -> CHW -> NCHW
return np.transpose(padded, (2, 0, 1))[np.newaxis, ...]
def inference(self, image):
"""执行推理"""
input_tensor = self.preprocess(image)
start_time = time.perf_counter()
outputs = self.session.run(None, {self.input_name: input_tensor})
inference_time = (time.perf_counter() - start_time) * 1000 # 毫秒
# RTMPose输出处理
# outputs[0]: x坐标分布, outputs[1]: y坐标分布
x_preds = outputs[0][0] # [K, W]
y_preds = outputs[1][0] # [K, H]
keypoints = []
for k in range(x_preds.shape[0]):
x_idx = np.argmax(x_preds[k])
y_idx = np.argmax(y_preds[k])
# 转换为原始图像坐标
x = (x_idx / x_preds.shape[1]) * image.shape[1]
y = (y_idx / y_preds.shape[1]) * image.shape[0]
keypoints.append((x, y))
return keypoints, inference_time
性能调优的关键发现:
- 批量大小的影响:树莓派上batch_size=1时速度最快,增加批量反而降低FPS
- 线程数设置:设置
intra_op_num_threads=4(树莓派4B的CPU核心数)效果最佳 - 内存布局:使用NHWC布局在某些情况下比NCHW更快,但需要模型支持
- 预热的重要性:前几次推理速度较慢,需要预热3-5次才能达到稳定速度
经过优化后,RTMPose-S在树莓派4B上的性能表现:
- FP32模型:~380ms/帧,精度100%
- INT8量化模型:~190ms/帧,精度下降0.8%
- 目标帧率:>5FPS(满足实时性要求)
5. 实际部署中的问题排查与解决方案
在实际部署过程中,我遇到了许多预料之外的问题。这里记录下最具代表性的几个案例和解决方法。
案例一:模型推理结果完全错误
症状:量化后的模型在树莓派上运行,关键点位置完全混乱,像是随机输出。
排查过程:
- 首先在x86服务器上验证量化模型,结果正常 → 排除量化过程问题
- 在树莓派上运行原始FP32模型,结果也错误 → 问题出在树莓派环境
- 对比输入数据发现,树莓派上图像预处理时颜色通道顺序错误
根本原因:OpenCV在x86上默认读取BGR,但在某些树莓派配置下可能不同。预处理代码没有统一颜色空间转换。
解决方案:
# 统一的预处理函数
def preprocess_image(image_path, input_size):
# 明确指定颜色转换
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 明确转换到RGB
# ... 后续处理保持一致
案例二:推理速度波动大
症状:同一模型、同一输入,推理时间从150ms到500ms波动。
排查过程:
- 监控树莓派CPU频率发现动态调频
- 温度监测显示推理时温度迅速上升触发降频
- 内存使用接近上限,频繁触发交换
解决方案组合:
# 1. 固定CPU频率
sudo nano /boot/config.txt
# 添加:force_turbo=1
# 添加:boot_delay=1
# 2. 改善散热(物理方案)
# 安装散热片和风扇
# 3. 优化内存使用
# 减少不必要的后台进程
# 使用内存更小的轻量级桌面环境或纯命令行
案例三:长时间运行后内存泄漏
症状:树莓派运行几小时后内存耗尽,程序崩溃。
排查:使用memory_profiler逐行分析,发现ONNX Runtime会话没有正确释放。
解决方案:
class RTMPoseInference:
def __init__(self, model_path):
self.model_path = model_path
self.session = None
def create_session(self):
"""按需创建会话,避免内存累积"""
if self.session is not None:
del self.session
sess_options = ort.SessionOptions()
# ... 配置选项
self.session = ort.InferenceSession(
self.model_path,
sess_options=sess_options,
providers=['CPUExecutionProvider']
)
def inference_with_cleanup(self, image, cleanup_interval=100):
"""定期清理会话,防止内存泄漏"""
if not hasattr(self, 'inference_count'):
self.inference_count = 0
if self.inference_count % cleanup_interval == 0:
self.create_session()
result = self.session.run(...)
self.inference_count += 1
return result
部署检查清单是我总结的必做项目:
- [ ] 在x86环境完整验证模型精度
- [ ] 测试量化模型在验证集上的表现
- [ ] 树莓派环境依赖完整安装
- [ ] 内存和存储空间充足
- [ ] 散热方案有效,避免热节流
- [ ] 电源供应稳定(建议5V/3A)
- [ ] 设置开机自启动服务
- [ ] 实现看门狗机制,异常自动重启
- [ ] 日志系统记录运行状态
- [ ] 远程监控和更新机制
6. 工程化建议与扩展应用
将研究模型部署到实际产品中,需要考虑的远不止技术实现。经过这个项目的实践,我总结了一些工程化经验。
代码架构设计应该考虑可维护性和扩展性。我最终采用的目录结构:
rtmpose_deployment/
├── models/ # 模型文件
│ ├── rtmpose-s_fp32.onnx
│ ├── rtmpose-s_int8.onnx
│ └── model_metadata.json # 模型元数据
├── src/
│ ├── inference.py # 推理核心类
│ ├── preprocess.py # 图像预处理
│ ├── postprocess.py # 结果后处理
│ └── utils.py # 工具函数
├── configs/
│ ├── deployment.yaml # 部署配置
│ └── model_config.json # 模型配置
├── tests/ # 测试代码
├── scripts/
│ ├── deploy_to_pi.sh # 部署脚本
│ └── benchmark.py # 性能测试
└── docs/ # 文档
配置管理使用YAML文件,便于不同环境切换:
# deployment.yaml
model:
path: "./models/rtmpose-s_int8.onnx"
input_size: [192, 256] # 宽度, 高度
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
inference:
use_quantized: true
batch_size: 1
num_threads: 4
warmup_runs: 3
performance:
target_fps: 5
enable_benchmark: true
benchmark_duration: 60 # 秒
logging:
level: "INFO"
file: "./logs/inference.log"
max_size: 10485760 # 10MB
监控与维护是长期运行的关键。我实现了一个简单的健康检查系统:
import psutil
import logging
from datetime import datetime
class SystemMonitor:
def __init__(self, log_interval=60):
self.log_interval = log_interval
self.last_check = time.time()
def check_system_health(self):
current_time = time.time()
if current_time - self.last_check < self.log_interval:
return True
health_status = {
'timestamp': datetime.now().isoformat(),
'cpu_percent': psutil.cpu_percent(interval=1),
'memory_percent': psutil.virtual_memory().percent,
'temperature': self.get_cpu_temperature(),
'disk_usage': psutil.disk_usage('/').percent
}
# 记录到日志
logging.info(f"System health: {health_status}")
# 检查阈值
if health_status['temperature'] > 80:
logging.warning("CPU温度过高,可能触发降频")
return False
if health_status['memory_percent'] > 90:
logging.error("内存使用率过高")
return False
self.last_check = current_time
return True
def get_cpu_temperature(self):
"""获取树莓派CPU温度"""
try:
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
temp = float(f.read()) / 1000.0
return temp
except:
return 0.0
扩展应用场景不仅限于三角板检测。同样的部署流程可以应用于:
- 教育场景:几何教具识别、实验仪器状态监测
- 工业检测:零件装配质量检查、机械臂引导
- 体育分析:运动员动作捕捉、训练姿势纠正
- 医疗辅助:康复训练指导、手术器械跟踪
每个应用场景都需要针对性的优化。比如工业场景可能需要更高的精度而牺牲速度,体育分析则需要更高的帧率。关键是根据实际需求调整模型大小、量化策略和推理参数。
这个项目让我深刻体会到,从训练好的模型到实际可用的产品,中间还有很长的路要走。特别是在资源受限的边缘设备上,每一个细节都需要精心设计和反复调试。但当你看到自己训练的模型在树莓派上流畅运行,实时检测出关键点时,那种成就感是单纯在服务器上跑通代码无法比拟的。
更多推荐
所有评论(0)