17191073931

将 MediaPipe 手势识别迁移到 RKNN:从 .task 到 RK3566 实战

手把手教你将 MediaPipe 手势识别 .task 拆分为四个 TFLite,并用 RKNN Toolkit2 转换与量化,部署到 Rockchip RK3566 NPU;覆盖常见报错修复、推理管线与实时示例,快速实现低功耗高性能的边缘 AI 手势识别。


在计算机视觉领域,手势识别是一项重要的人机交互技术。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 的顺序逐步调用这四个模型,才能还原完整的手势识别功能。

总结来说,迁移流程分为三步:

  1. 解包 .task → 获取 .tflite 模型
  2. 逐个转换 .tflite → .rknn
  3. 构建 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 提供校准图片,但文件不存在。
  • 解决方法有两种
    1. 准备一个 dataset.txt 文件(每行一个图片路径),至少几十张图片用于量化;
    2. 如果只是测试,可直接 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,为智能家居、教育、零售、安防等行业带来实际落地价值。



典型应用介绍

相关技术方案

物联网平台

是否需要我们帮忙?

若是您有同样的需求或困扰,打电话给我们,我们会帮您梳理需求,定制合适的方案。

010-62386352


星野云联专家微信
星野云联专家微信

© 2025 Zedyer, Inc. All Rights Reserved.

京ICP备2021029338号-2