- Mark Ren
-
-
-
在计算机视觉领域,手势识别是一项重要的人机交互技术。Google 的 MediaPipe 提供了完整的 Gesture Recognizer pipeline,通过手部检测、关键点定位、嵌入向量和手势分类四个阶段,实现端到端的实时手势识别。然而,MediaPipe 官方主要针对 PC CPU / NVIDIA GPU / Android GPU 做了优化,而在嵌入式 Rockchip RK3566/RK3588 这类 SoC 上,直接运行会非常低效。
Rockchip 提供了 RKNN Toolkit2,能够将主流深度学习框架(TFLite、ONNX、Caffe 等)的模型转换为 .rknn 格式,并在 NPU 上加速运行。这样一来,我们就能把 MediaPipe 的手势识别模型迁移到 RK3566 板子上,实现低功耗、高性能的边缘 AI 推理。
🔹 MediaPipe Gesture Recognizer 的模型组成
MediaPipe 的 gesture_recognizer.task 并不是单一模型,而是一个 Task Bundle,里面打包了多个 .tflite 模型和任务配置文件。解压后,你会发现:
- hand_landmarker.task
- hand_detector.tflite:手部检测(Palm Detection)
- hand_landmarks_detector.tflite:手部关键点(21个点)
- hand_gesture_recognizer.task
- gesture_embedder.tflite:将关键点向量化为 embedding
- canned_gesture_classifier.tflite:手势分类器(输出具体手势类别)
这四个模型构成了完整的 pipeline:
手部检测 → 关键点定位 → 特征嵌入 → 手势分类
为什么要逐个转换为 RKNN?
RKNN Toolkit2 不支持直接解析 .task 文件,因此我们必须将里面的四个 .tflite 模型分别转换为 .rknn。在推理过程中,也需要按照 MediaPipe 的顺序逐步调用这四个模型,才能还原完整的手势识别功能。
总结来说,迁移流程分为三步:
- 解包 .task → 获取 .tflite 模型
- 逐个转换 .tflite → .rknn
- 构建 pipeline → 在 RK3566 上运行完整手势识别
🔹 模型转换步骤
步骤架构流程图
--- title: "MediaPipe 到 RKNN:模型转换与推理管线" --- graph TD %% ========== 阶段 A:解包 ========== subgraph A["阶段A:解包(PC侧)"] direction TB A0["gesture_recognizer.task"] A1["解包为 4 个 TFLite 模型"] A2["hand_detector.tflite"] A3["hand_landmarks_detector.tflite"] A4["gesture_embedder.tflite"] A5["canned_gesture_classifier.tflite"] A0 --> A1 A1 --> A2 A1 --> A3 A1 --> A4 A1 --> A5 end %% ========== 阶段 B:转换 ========== subgraph B["阶段B:转换(PC侧)"] direction TB B0["配置 rknn.config(target='rk3566',quantized_dtype='w8a8')"] B1["(图像模型需 dataset.txt)"] B2["逐个执行:load_tflite → build → export_rknn"] end A2 --> B A3 --> B A4 --> B A5 --> B B --> C2["hand_detector.rknn"] B --> C3["hand_landmarks_detector.rknn"] B --> C4["gesture_embedder.rknn"] B --> C5["canned_gesture_classifier.rknn"] %% ========== 阶段 C:部署 ========== subgraph C["阶段C:部署(RK3566板端)"] direction LR D0["输入图像/视频帧"] D1["依次调用 4 个 RKNN 模型"] D2["输出手势分类结果"] D0 --> D1 --> D2 end C2 --> C C3 --> C C4 --> C C5 --> C %% ========== 样式(科技清新风) ========== classDef stageA fill:#E6F4FF,stroke:#1677FF,color:#0B3D91,stroke-width:1.5px,rounded:10px classDef stageB fill:#FFF7E6,stroke:#FAAD14,color:#7C4A03,stroke-width:1.5px,rounded:10px classDef stageC fill:#E8FFEE,stroke:#52C41A,color:#124D18,stroke-width:1.5px,rounded:10px classDef node fill:#FFFFFF,stroke:#D0D5DD,color:#111827,rounded:8px class A stageA class B stageB class C stageC class A0,A1,A2,A3,A4,A5,B0,B1,B2,C2,C3,C4,C5,D0,D1,D2 node %% 关键链路配色 linkStyle 5,6,7,8 stroke:#1677FF,stroke-width:2px linkStyle 9,10,11,12 stroke:#52C41A,stroke-width:2px
Step 1. 安装 RKNN Toolkit2
在 Windows/Linux x86_64 环境下(Mac 需虚拟机/容器),安装:
pip install rknn-toolkit2
推荐 Python 3.6–3.10。RKNN Toolkit2 v2.3.2 是常见版本。
Step 2. 准备四个 TFLite 模型
从 gesture_recognizer.task 解包得到的四个模型分别是:
- hand_detector.tflite
- hand_landmarks_detector.tflite
- gesture_embedder.tflite
- canned_gesture_classifier.tflite
Step 3. 编写转换脚本
关键点:在新版 Toolkit2 中必须先调用 rknn.config(),再调用 load_tflite()。
下面是通用转换模板:
from rknn.api import RKNN
def convert_model(tflite_file, rknn_file, is_image_model=True):
rknn = RKNN()
# 1. 配置参数
if is_image_model:
rknn.config(
mean_values=[[0, 0, 0]],
std_values=[[255, 255, 255]],
target_platform='rk3566',
quantized_dtype='w8a8'
)
else:
rknn.config(
target_platform='rk3566',
quantized_dtype='w8a8'
)
# 2. 加载模型
rknn.load_tflite(model=tflite_file)
# 3. 构建(是否量化)
if is_image_model:
# 需要准备 dataset.txt
rknn.build(do_quantization=True, dataset='dataset.txt')
else:
rknn.build(do_quantization=False)
# 4. 导出模型
rknn.export_rknn(rknn_file)
rknn.release()
# 调用示例
convert_model('hand_detector.tflite', 'hand_detector.rknn', True)
convert_model('hand_landmarks_detector.tflite', 'hand_landmarks_detector.rknn', True)
convert_model('gesture_embedder.tflite', 'gesture_embedder.rknn', False)
convert_model('canned_gesture_classifier.tflite', 'canned_gesture_classifier.rknn', False)
🔹 常见报错与解决方案
1. E config: Invalid quantized_dtype ‘asymmetric_quantized-u8’
- 原因:在 RKNN Toolkit2 v2.3.2 中不再支持 asymmetric_quantized-u8。
- 解决:改为 quantized_dtype='w8a8'(权重和激活均为 8bit,是最常用的设置)。
2. E load_tflite: Please call rknn.config first!
- 原因:调用顺序错误。
- 解决:必须先 rknn.config(),再 rknn.load_tflite()。
3. E build: Dataset file dataset.txt not found!
- 原因:量化模式需要 dataset.txt 提供校准图片,但文件不存在。
- 解决方法有两种:
- 准备一个 dataset.txt 文件(每行一个图片路径),至少几十张图片用于量化;
- 如果只是测试,可直接 do_quantization=False 跳过量化。
4. 导出后 rknn 文件为空
- 原因:build 失败(量化数据集缺失或参数错误)。
- 解决:先确认 build() 返回正常,再调用 export_rknn()。
🔹 在 RK3566 上构建完整的 Gesture Recognition Pipeline
1) 数据流回顾(RKNN 四模型串联)
原始帧 → hand_detector.rknn → 手框(box)
→ 裁剪/仿射归一化(hand ROI) → hand_landmarks_detector.rknn → 21点关键点
→ 关键点坐标标准化/展平(21×2=42) → gesture_embedder.rknn → embedding 向量
→ canned_gesture_classifier.rknn → 分类概率/手势ID
关键点:每一步的输入尺度/归一化与 MediaPipe 源逻辑一致(图像模型通常 RGB、0~1 归一化;向量模型直接传 float32)。
2) 单张图片推理示例(最短可运行路径)
说明:不同版本的 hand_detector/landmarks 模型输出格式可能不完全一致(有的给多框+置信度,有的是中心点+尺度)。本示例给出常见格式的解析模板;若与你的模型不符,请打印输出 shape 并按注释处调整。
import cv2, numpy as np
from rknn.api import RKNN
# ========== 工具函数 ==========
def to_rgb_norm(img_bgr, size):
img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, size)
img = img.astype(np.float32) / 255.0
return img
def crop_by_box(img_bgr, box_xyxy, pad=0.1):
h, w = img_bgr.shape[:2]
x1, y1, x2, y2 = box_xyxy
# 扩边一点,防止裁剪太紧
cx = (x1 + x2) / 2; cy = (y1 + y2) / 2
bw = (x2 - x1); bh = (y2 - y1)
bw *= (1 + pad); bh *= (1 + pad)
x1 = max(0, int(cx - bw/2)); x2 = min(w, int(cx + bw/2))
y1 = max(0, int(cy - bh/2)); y2 = min(h, int(cy + bh/2))
return img_bgr[y1:y2, x1:x2].copy(), (x1, y1, x2, y2)
def norm_landmarks_to_roi_xy(lm_21x2, roi_xyxy):
x1, y1, x2, y2 = roi_xyxy
rw = x2 - x1
rh = y2 - y1
# 将 ROI 内归一化坐标(0~1)还原到整图像素坐标
pts = []
for i in range(21):
x = x1 + lm_21x2[i,0] * rw
y = y1 + lm_21x2[i,1] * rh
pts.append([x, y])
return np.array(pts, dtype=np.float32)
# ========== 加载模型 ==========
det = RKNN(); det.load_rknn('hand_detector.rknn'); det.init_runtime()
lm = RKNN(); lm.load_rknn('hand_landmarks_detector.rknn'); lm.init_runtime()
emb = RKNN(); emb.load_rknn('gesture_embedder.rknn'); emb.init_runtime()
clf = RKNN(); clf.load_rknn('canned_gesture_classifier.rknn'); clf.init_runtime()
# ========== 推理一张图片 ==========
img_path = 'test.jpg'
ori = cv2.imread(img_path)
H, W = ori.shape[:2]
# 1) 手部检测(按你的输入尺寸修改)
det_in_size = (224, 224)
det_in = to_rgb_norm(ori, det_in_size)
det_in = np.expand_dims(det_in, 0) # NHWC
det_out = det.inference([det_in])
# ★★ 请打印 det_out 看真实结构,然后在此处解析 box ★★
# 假设输出是 [N, 6]: x1,y1,x2,y2,score,class;坐标为相对比例
boxes = det_out[0]
boxes = np.array(boxes).reshape(-,6) # 若维度不同请调整
boxes = boxes[boxes[:,4] > 0.5] # 置信度阈值
if len(boxes) == 0:
print('No hand detected'); exit(0)
# 取置信度最高的一个
best = boxes[np.argmax(boxes[:,4])]
x1, y1, x2, y2 = best[:4]
x1, y1, x2, y2 = int(x1*W), int(y1*H), int(x2*W), int(y2*H)
roi, roi_xyxy = crop_by_box(ori, (x1,y1,x2,y2), pad=0.15)
# 2) 关键点
lm_in_size = (224, 224) # 按你的模型实际输入
lm_in = to_rgb_norm(roi, lm_in_size)
lm_in = np.expand_dims(lm_in, 0)
lm_out = lm.inference([lm_in])
# ★★ 请打印 lm_out 看真实结构 ★★
# 常见: [1, 21, 3] 或 [1,63];x,y 为 0~1 的 ROI 归一化坐标
lm_arr = lm_out[0]
lm_arr = np.array(lm_arr).reshape(21, -1) # 取前两列为 x,y
lm_xy_roi = lm_arr[:,:2]
lm_xy = norm_landmarks_to_roi_xy(lm_xy_roi, roi_xyxy)
# 3) 嵌入向量(将 21×2 展平为 42)
vec_42 = lm_xy_roi.reshape(-1).astype(np.float32) # 若 embedder 需要已中心化/尺度归一化,请按 MP 同步处理
vec_42 = np.expand_dims(vec_42, 0)
emb_out = emb.inference([vec_42])
embedding = np.array(emb_out[0]).astype(np.float32).reshape(-1)
# 4) 分类
logits = clf.inference([np.expand_dims(embedding,0)])[0]
probs = np.array(logits).reshape(-1)
gid = int(np.argmax(probs))
print('Gesture ID:', gid, 'score:', float(probs[gid]))
# 结果可视化
for (x,y) in lm_xy.astype(int):
cv2.circle(ori, (x,y), 2, (0,255,0), -1)
cv2.rectangle(ori, (x1,y1), (x2,y2), (0,128,255), 2)
cv2.putText(ori, f'G:{gid} {probs[gid]:.2f}', (x1, max(0,y1-8)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.imwrite('result.jpg', ori)
# 释放
det.release(); lm.release(); emb.release(); clf.release()
print('Saved result.jpg')
两处必须先 print() 再对齐的点:
① det_out 的维度/含义(有的模型给中心点+宽高,需要先转为 x1y1x2y2;有的坐标不是归一比例而是输入尺度)。
② lm_out 的维度/含义(部分模型输出 63 维,或除了 xy 还有 z/visibility)。
一旦你贴出真实 shape,我可以给你替换为完全对齐版本。
3) 摄像头实时推理 Demo(OpenCV + RKNN)
该版本按单手识别编写,如需多手,遍历全部检测框即可。
import cv2, time, numpy as np
from rknn.api import RKNN
def to_rgb_norm(img, size):
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, size)
return (img.astype(np.float32) / 255.0)[None, ...] # NHWC
def main():
det = RKNN(); det.load_rknn('hand_detector.rknn'); det.init_runtime()
lm = RKNN(); lm.load_rknn('hand_landmarks_detector.rknn'); lm.init_runtime()
emb = RKNN(); emb.load_rknn('gesture_embedder.rknn'); emb.init_runtime()
clf = RKNN(); clf.load_rknn('canned_gesture_classifier.rknn'); clf.init_runtime()
cap = cv2.VideoCapture(0)
det_size = (224,224); lm_size = (224,224)
while True:
ok, frame = cap.read()
if not ok: break
H, W = frame.shape[:2]
t0 = time.time()
det_out = det.inference([to_rgb_norm(frame, det_size)])
# ★★ 解析 det_out 得到最佳手框 (x1,y1,x2,y2) — 同上一例调整 ★★
boxes = np.array(det_out[0]).reshape(-1, 6)
boxes = boxes[boxes[:,4] > 0.5]
if len(boxes) == 0:
cv2.putText(frame, 'No hand', (10,30), 0, 1, (0,0,255), 2)
cv2.imshow('RKNN Gesture', frame)
if cv2.waitKey(1) == 27: break
continue
best = boxes[np.argmax(boxes[:,4])]
x1, y1, x2, y2 = (best[0]*W, best[1]*H, best[2]*W, best[3]*H)
x1, y1, x2, y2 = map(int, [x1,y1,x2,y2])
x1 = max(0, x1); y1 = max(0, y1); x2 = min(W, x2); y2 = min(H, y2)
roi = frame[y1:y2, x1:x2].copy()
lm_out = lm.inference([to_rgb_norm(roi, lm_size)])
# ★★ 解析 lm_out 得 21×2 归一化坐标 — 同上一例调整 ★★
lm_arr = np.array(lm_out[0]).reshape(21, -1)
lm_xy_roi = lm_arr[:,:2]
# 还原到整图坐标
rw, rh = (x2-x1), (y2-y1)
lm_xy = np.zeros((21,2), np.float32)
lm_xy[:,0] = x1 + lm_xy_roi[:,0] * rw
lm_xy[:,1] = y1 + lm_xy_roi[:,1] * rh
# embed & classify
vec_42 = lm_xy_roi.reshape(1,-1).astype(np.float32)
embedding = np.array(emb.inference([vec_42])[0]).reshape(1,-1).astype(np.float32)
probs = np.array(clf.inference([embedding])[0]).reshape(-1)
gid = int(np.argmax(probs))
# draw
for (x,y) in lm_xy.astype(int):
cv2.circle(frame, (x,y), 2, (0,255,0), -1)
cv2.rectangle(frame, (x1,y1), (x2,y2), (0,128,255), 2)
fps = 1.0 / (time.time()-t0 + 1e-6)
cv2.putText(frame, f'G:{gid} p:{probs[gid]:.2f} FPS:{fps:.1f}', (10,30), 0, 1, (0,255,255), 2)
cv2.imshow('RKNN Gesture', frame)
if cv2.waitKey(1) == 27: break
cap.release(); cv2.destroyAllWindows()
det.release(); lm.release(); emb.release(); clf.release()
if __name__ == '__main__':
main()
4) 精度与性能优化清单(实战有效)
- 量化数据集:为 hand_detector 与 hand_landmarks_detector 准备场景相似的 100–300 张 RGB 图片(人手、不同肤色/光照/背景),能显著提升 int8 量化后的效果稳定性。
- 输入尺寸:严格使用与 TFLite 模型一致的输入分辨率(用 Netron 打开确认)。
- 预处理一致性:RGB 排列、0~1 归一化(mean=0, std=255)与转换阶段保持一致;不要前后不统一。
- ROI 仿射:若检测框为旋转框或模型假定正面手掌,裁剪前可做旋转对齐(optional)以提高关键点稳定度。
- 批量/流水线:摄像头实时可采用帧间跟踪降低检测频率(每 N 帧跑一次 detector,中间帧仅跑 landmarks)。
- 多手支持:遍历所有置信度>阈值的框,分手独立跑后 3 阶段(注意上限以保证实时性)。
- 线程/绑核:RK3566 上可用多线程把摄像头采集、NPU 推理、绘制解耦。
5) 快速排错对照表
现象 | 可能原因 | 快速验证 | 修复建议 |
---|---|---|---|
没有手框或框飘 | 量化漂移/阈值过高/输入预处理不一致 | 打印 det_out;用非量化模型对比 | 降阈值、修正 mean/std、增加量化集 |
关键点在错误位置 | ROI 坐标还原错/输入通道或归一化错误 | 单步可视化 ROI 与关键点 | 核对 ROI 映射、RGB、归一化 |
分类全是同一类 | embed 输入不匹配/未标准化关键点 | 打印 vec_42 范围/分布 | 对关键点中心化+尺度归一化(对齐 MP 逻辑) |
FPS 太低 | 每帧全流程、Python 单线程 | 打印每阶段耗时 | 降采样、N 帧一次检测、多线程 |
导出 rknn 失败 | dataset.txt 缺失/quantized_dtype 写法不对 | 查看 build 日志 | 准备量化集;dtype 用 w8a8 |
注:MediaPipe 的 gesture_embedder 常对关键点做平移到手掌中心、尺度归一化、镜像处理等增强;若你发现分类不稳,建议完全复刻 MP 的前处理(可在 MP 源码里找到归一化细节)。
结语
通过把 MediaPipe 的四个子模型分别转换为 RKNN,并在 RK3566 上按原有 pipeline 串联推理,你可以获得低功耗、实时的手势识别方案。迁移的关键在于:保持预处理/后处理的一致性、为视觉模型准备足量量化数据集、并根据设备资源做工程化优化(检测降频、帧间跟踪、多线程)。
这篇 Blog 的核心价值,在于为用户提供了一条从 MediaPipe 到 RKNN 的完整迁移路径,让复杂的手势识别模型可以顺利在 RK3566 这样的低成本嵌入式设备上运行。
维度 | 价值体现 |
---|---|
硬件成本 | RK3566 低价位,功耗小,不需要 GPU 或服务器 |
部署难度 | 通过 RKNN 工具链,客户只需转换模型,不用重写算法 |
功能价值 | 支持实时手势识别、关键点检测 → 可扩展到交互、安防、教育、零售等场景 |
商业价值 | 客户用低成本设备即可开发高价值应用,提升市场竞争力,缩短上市周期 |
更重要的是,它体现了“低成本硬件 + 高价值视觉识别” 的组合优势。客户无需昂贵的 GPU 或服务器,就能在功耗更低、价格更优的 SoC 平台上,实现实时的手势识别与人机交互。这种方案能显著提升 ROI,为智能家居、教育、零售、安防等行业带来实际落地价值。
典型应用介绍