1. 项目缘起与整体设计思路
1.1 为什么要在RK3566上跑手势识别
先说背景。RK3566这颗芯片在边缘计算圈子里热度一直不低,四核A55架构,主频1.8GHz,内置0.8TOPS算力的NPU,功耗控制得相当克制。很多人拿它做平板、做广告机、做工业HMI,甚至有人刷上安卓系统当桌面小电脑用。但真正让它有意思的,是那颗NPU——你不用起来,它就只是一颗普通的ARM SoC;你用起来,它就能在本地实时跑一些轻量级的视觉模型。
手势识别就是其中一个非常典型的场景。相比人脸识别,手势识别的交互感更强,隐私争议更小,而且应用面很广:智能家居的无接触控制、车载的中控手势、教育硬件的互动操作、甚至展览馆的体感交互装置。MediaPipe Hands是Google开源的一套手势关键点检测方案,精度不错,模型也足够轻量,社区里用的人很多。
但问题来了:MediaPipe官方给的模型是TFLite格式,RK3566的NPU只认RKNN格式。这中间的转换和优化,就是整个项目的核心工作量。我这次做的事情,就是把MediaPipe的手部关键点检测模型,通过RKNN Toolkit2工具链,完整地部署到RK3566上,并且做到实时推理。
1.2 整体技术路线怎么定
整个项目的技术路线可以拆成四段:模型获取与理解、格式转换与量化、板端部署与推理、性能调优与验证。
模型获取这块,MediaPipe Hands的核心是一个手掌检测模型加一个手部关键点回归模型。手掌检测负责在整张图里找到手的位置,关键点回归负责在裁剪后的小图上预测21个关键点的坐标。两个模型配合使用,才能完成完整的手势识别流程。
格式转换是重头戏。RKNN Toolkit2支持从ONNX、TensorFlow、TFLite等多种格式导入模型,但导入之后需要做量化、图优化、算子融合等一系列操作。这里有个关键决策:是直接用TFLite模型转,还是先转成ONNX再转RKNN?我实测下来,TFLite直接转RKNN的路径在MediaPipe这种包含大量自定义算子的模型上容易出问题,所以最终选择了TFLite转ONNX、ONNX再转RKNN的路线。
板端部署这块,RK3566支持Android和Linux两种系统。考虑到很多人拿RK3566当安卓桌面电脑用,我这次以Linux系统为主进行部署,但Android下的思路是相通的,只是推理框架的调用方式略有不同。
性能调优是最后一步,也是最考验经验的地方。NPU的算力虽然只有0.8TOPS,但用好了跑手势识别完全够用。关键在于量化策略、输入分辨率、线程调度这几个参数的配合。
1.3 方案选型的几个关键考量
为什么不用RKNN Toolkit1?因为Toolkit2对RK3566的支持更完善,算子覆盖率更高,而且API设计更合理。Toolkit1在RK3566上跑一些较新的模型时,经常遇到算子不支持的问题,Toolkit2在这方面改善明显。
为什么不用其他推理框架比如NCNN或者MNN?因为RK3566的NPU只有通过RKNN才能调用。如果你只用CPU推理,那NCNN确实是个好选择,但CPU推理的功耗和延迟都远不如NPU。既然板子上有NPU,没有理由不用。
为什么选择MediaPipe Hands而不是自己训练一个模型?因为MediaPipe Hands在公开数据集上训练得很充分,泛化能力好,而且21个关键点的定义已经成为事实标准,后续做手势分类、手势映射都很方便。自己从头训练一个,时间和数据成本都不划算。
注意:RKNN Toolkit2的版本选择很关键。我建议用1.5.0以上的版本,对RK3566的NPU驱动兼容性更好。低于1.4.0的版本在量化时容易出精度问题。
2. 核心细节解析与实操要点
2.1 MediaPipe Hands模型结构拆解
MediaPipe Hands实际上包含两个独立的模型。第一个是手掌检测模型(Palm Detection),输入是192x192的RGB图像,输出是手掌的边界框和7个关键点(手腕、食指根部、中指根部、无名指根部、小指根部、拇指根部、手掌中心)。第二个是手部关键点模型(Hand Landmark),输入是224x224的裁剪手掌图像,输出是21个关键点的三维坐标。
这两个模型都是基于MobileNetV2的轻量级架构,参数量很小。手掌检测模型大约1.5M参数,关键点模型大约2.5M参数。在RK3566的NPU上,两个模型加起来推理一次的时间可以控制在30ms以内,也就是30FPS左右,完全满足实时性要求。
但这里有个细节:MediaPipe的TFLite模型里包含了一些自定义算子,比如TFLite_Detection_PostProcess和TFLite_Custom_Ops。这些算子在转ONNX的时候需要特殊处理,否则会转换失败。我的做法是先用tf2onnx工具转换,遇到不支持的算子就手动替换成等效的标准算子组合。
2.2 RKNN Toolkit2环境搭建的坑
环境搭建这块,我踩了不少坑。RKNN Toolkit2官方推荐用Ubuntu 18.04或20.04,Python版本3.6到3.8。我一开始在Ubuntu 22.04上装,Python 3.10,结果各种依赖冲突。后来老老实实开了个Ubuntu 20.04的虚拟机,Python 3.8,才顺利跑通。
安装步骤大致如下:
# 创建虚拟环境 python3 -m venv rknn_env source rknn_env/bin/activate # 安装依赖 pip install numpy==1.19.5 pip install opencv-python==4.5.5.64 pip install onnx==1.10.0 pip install onnxruntime==1.10.0 pip install tensorflow==2.5.0 # 安装RKNN Toolkit2 pip install rknn-toolkit2-1.5.0-cp38-cp38-linux_x86_64.whl这里有几个版本必须锁死:numpy用1.19.5,高了低了都容易出问题;onnx用1.10.0,和RKNN Toolkit2的兼容性最好;tensorflow用2.5.0,主要是为了加载TFLite模型。
提示:如果你用的是Windows系统,建议直接上WSL2,装Ubuntu 20.04。原生Windows下RKNN Toolkit2的安装极其折腾,而且很多算子转换会出问题。
2.3 模型转换的核心参数怎么调
模型转换是整个项目最核心的环节。我先把TFLite转成ONNX,命令如下:
python -m tf2onnx.convert --tflite palm_detection.tflite --output palm_detection.onnx --opset 12这里--opset 12很关键。opset版本太低,一些算子不支持;太高,RKNN Toolkit2又解析不了。12是实测下来最稳的版本。
转成ONNX之后,用RKNN Toolkit2加载并转换:
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置模型参数 rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3566', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) # 加载ONNX模型 ret = rknn.load_onnx(model='palm_detection.onnx') if ret != 0: print('Load model failed!') exit(ret) # 构建模型 ret = rknn.build(do_quantization=True, dataset='./dataset.txt') if ret != 0: print('Build model failed!') exit(ret) # 导出RKNN模型 ret = rknn.export_rknn('./palm_detection.rknn') if ret != 0: print('Export model failed!') exit(ret)这里有几个参数需要重点解释。mean_values和std_values是归一化参数,MediaPipe的模型输入是[0,1]范围的浮点数,所以用127.5做均值和标准差,把[0,255]的像素值归一化到[-1,1]。quantized_dtype选asymmetric_quantized-8,这是RK3566 NPU支持最好的量化类型。optimization_level设为3,表示最高级别的图优化,会做算子融合和内存复用。
dataset.txt是量化校准数据集,里面每行是一张图片的路径。我用了大概200张包含各种手势的图片,覆盖不同光照、不同背景、不同手型。量化校准数据集的质量直接影响量化后的精度,这一步不能偷懒。
2.4 量化精度损失的补偿策略
量化必然带来精度损失,这是没办法的事。8位量化相比32位浮点,精度损失一般在1%到3%之间。对于手势识别来说,关键点坐标的误差在2到3个像素以内,实际使用中基本感知不到。
但如果量化数据集选得不好,精度损失可能超过10%,关键点就会明显偏移。我的经验是:量化数据集要尽可能覆盖实际应用场景。比如你的手势识别是用在客厅里控制电视,那量化数据集就应该包含客厅场景下的各种手势图片,而不是随便找一些实验室环境下的图片。
另外,RKNN Toolkit2支持混合量化。你可以把某些对精度敏感的层设为16位量化,其他层保持8位。具体做法是在生成量化配置文件后,手动修改某些层的quantized_dtype。不过混合量化会增加模型大小和推理时间,需要权衡。
我实测下来,对于MediaPipe Hands这两个模型,纯8位量化已经足够,不需要混合量化。关键点坐标的平均误差在1.5个像素左右,完全可用。
3. 实操过程与核心环节实现
3.1 完整转换流程的逐步拆解
先把手掌检测模型和关键点模型分别转换。手掌检测模型的转换脚本如下:
import numpy as np from rknn.api import RKNN def convert_palm_detection(): rknn = RKNN(verbose=True) rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3566', quantized_dtype='asymmetric_quantized-8', optimization_level=3, quantized_algorithm='normal', quantized_method='channel' ) ret = rknn.load_onnx(model='./palm_detection.onnx') if ret != 0: print('Load palm detection model failed!') return ret ret = rknn.build(do_quantization=True, dataset='./palm_dataset.txt') if ret != 0: print('Build palm detection model failed!') return ret ret = rknn.export_rknn('./palm_detection.rknn') if ret != 0: print('Export palm detection model failed!') return ret rknn.release() return 0 if __name__ == '__main__': convert_palm_detection()关键点模型的转换脚本类似,只是输入尺寸和数据集路径不同:
def convert_hand_landmark(): rknn = RKNN(verbose=True) rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3566', quantized_dtype='asymmetric_quantized-8', optimization_level=3, quantized_algorithm='normal', quantized_method='channel' ) ret = rknn.load_onnx(model='./hand_landmark.onnx') if ret != 0: print('Load hand landmark model failed!') return ret ret = rknn.build(do_quantization=True, dataset='./landmark_dataset.txt') if ret != 0: print('Build hand landmark model failed!') return ret ret = rknn.export_rknn('./hand_landmark.rknn') if ret != 0: print('Export hand landmark model failed!') return ret rknn.release() return 0这里quantized_algorithm选normal,表示用标准的KL散度量化算法。quantized_method选channel,表示按通道量化,精度比按层量化更好。
3.2 板端推理代码的编写要点
板端推理我用的是Python,因为RKNN Toolkit2提供了Python的运行时接口,开发效率高。如果你追求极致性能,可以用C++接口,但开发周期会长很多。
先初始化RKNN运行时:
from rknnlite.api import RKNNLite import cv2 import numpy as np class HandDetector: def __init__(self, palm_model_path, landmark_model_path): self.palm_rknn = RKNNLite() ret = self.palm_rknn.load_rknn(palm_model_path) if ret != 0: raise RuntimeError('Load palm model failed') ret = self.palm_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0) if ret != 0: raise RuntimeError('Init palm runtime failed') self.landmark_rknn = RKNNLite() ret = self.landmark_rknn.load_rknn(landmark_model_path) if ret != 0: raise RuntimeError('Load landmark model failed') ret = self.landmark_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0) if ret != 0: raise RuntimeError('Init landmark runtime failed') def detect(self, image): # 手掌检测 palm_input = cv2.resize(image, (192, 192)) palm_input = palm_input.astype(np.float32) palm_outputs = self.palm_rknn.inference(inputs=[palm_input]) # 解析手掌检测结果 boxes, scores = self._parse_palm_output(palm_outputs) if len(boxes) == 0: return None # 取置信度最高的手掌 best_idx = np.argmax(scores) box = boxes[best_idx] # 裁剪手掌区域 cropped = self._crop_hand(image, box) landmark_input = cv2.resize(cropped, (224, 224)) landmark_input = landmark_input.astype(np.float32) landmark_outputs = self.landmark_rknn.inference(inputs=[landmark_input]) # 解析关键点 landmarks = self._parse_landmark_output(landmark_outputs) return landmarks这里core_mask参数指定用哪个NPU核心。RK3566只有一个NPU核心,所以用NPU_CORE_0就行。如果你用的是RK3588这种多核NPU,可以指定不同的核心来并行推理。
3.3 输入预处理的细节处理
MediaPipe的模型对输入预处理有特定要求。手掌检测模型的输入是192x192,但并不是简单地把原图缩放到192x192。MediaPipe的做法是保持长宽比,把短边缩放到192,长边按比例缩放,然后居中裁剪或者填充。
我一开始没注意这个细节,直接暴力缩放,结果手掌检测的精度下降了不少。后来改成保持长宽比的缩放方式,精度就上来了。
def preprocess_palm(image, target_size=192): h, w = image.shape[:2] scale = target_size / max(h, w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(image, (new_w, new_h)) # 创建目标尺寸的画布 canvas = np.zeros((target_size, target_size, 3), dtype=np.uint8) # 居中放置 y_offset = (target_size - new_h) // 2 x_offset = (target_size - new_w) // 2 canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized return canvas, scale, x_offset, y_offset关键点模型的输入是224x224,预处理方式类似,但裁剪区域是根据手掌检测的结果来的。MediaPipe的手掌检测输出的是7个关键点,需要根据这7个点计算一个旋转后的矩形区域,然后从原图中裁剪出来。
3.4 后处理与关键点映射
手掌检测的输出是一个[1, 896, 18]的张量,896是锚框数量,18是每个锚框的预测值(4个边界框偏移、1个置信度、7个关键点偏移、6个其他值)。解析的时候需要先做锚框解码,再用非极大值抑制(NMS)过滤重叠框。
def _parse_palm_output(self, outputs): # outputs[0] shape: [1, 896, 18] raw = outputs[0][0] # 锚框配置 anchors = self._generate_anchors() boxes = [] scores = [] for i in range(len(anchors)): confidence = raw[i][4] if confidence < 0.5: continue # 解码边界框 cx = raw[i][0] / 192.0 * self.input_width + anchors[i][0] cy = raw[i][1] / 192.0 * self.input_height + anchors[i][1] w = raw[i][2] / 192.0 * self.input_width h = raw[i][3] / 192.0 * self.input_height x1 = cx - w / 2 y1 = cy - h / 2 x2 = cx + w / 2 y2 = cy + h / 2 boxes.append([x1, y1, x2, y2]) scores.append(confidence) # NMS indices = cv2.dnn.NMSBoxes(boxes, scores, 0.5, 0.3) return [boxes[i] for i in indices], [scores[i] for i in indices]关键点模型的输出是[1, 63],63是21个关键点乘以3(x, y, z)。解析的时候需要把归一化坐标映射回原图尺寸。
def _parse_landmark_output(self, outputs): raw = outputs[0][0] # shape: [63] landmarks = [] for i in range(21): x = raw[i * 3] y = raw[i * 3 + 1] z = raw[i * 3 + 2] landmarks.append([x, y, z]) return landmarks这里得到的坐标是相对于224x224裁剪图的归一化坐标,需要根据裁剪时的变换矩阵映射回原图。
4. 常见问题与排查技巧实录
4.1 模型转换阶段的典型报错
报错一:Unsupported operator: TFLite_Detection_PostProcess
这是最常见的问题。MediaPipe的TFLite模型里包含TFLite自定义算子,ONNX和RKNN都不认识。解决办法是在转ONNX之前,先用tf2onnx的--custom-ops参数指定自定义算子的映射关系,或者手动修改模型结构,把后处理部分剥离出来,在板端用Python实现。
我选择的是后者:把TFLite模型的后处理层去掉,只保留特征提取部分,转成ONNX后再转RKNN。后处理逻辑在板端用NumPy实现。这样做的好处是模型更干净,转换成功率更高。
报错二:Quantization failed: dataset is empty
这个报错说明量化数据集路径不对,或者数据集文件格式有问题。dataset.txt里每行应该是一个图片的绝对路径,不能有空格或特殊字符。另外,图片格式必须是模型支持的格式,一般是JPEG或PNG。
报错三:Build model failed: out of memory
转换大模型时,如果虚拟机内存不够,会报这个错。RKNN Toolkit2在量化时需要把整个模型加载到内存里,加上中间激活值,内存占用可能是模型大小的10倍以上。建议虚拟机至少分配8GB内存,16GB更稳妥。
4.2 板端推理的常见异常
异常一:推理结果全是零或NaN
这种情况一般是输入数据格式不对。RKNN的输入要求是NHWC格式,数据类型是uint8或float32。如果你传的是BGR格式的OpenCV图像,需要先转成RGB。另外,输入图像的尺寸必须和模型定义的尺寸完全一致,不能多一维或少一维。
异常二:推理速度远低于预期
如果推理一帧要几百毫秒,那肯定是不正常的。先检查init_runtime的时候有没有指定core_mask,不指定的话可能跑在CPU上。另外,检查模型有没有成功量化,如果量化失败,模型会以浮点模式运行,速度会慢很多。
异常三:关键点抖动严重
关键点抖动一般是两个原因:一是量化精度不够,二是后处理没有做平滑。量化精度的问题可以通过增加量化数据集来改善。后处理平滑可以用指数移动平均(EMA)来滤波:
class LandmarkSmoother: def __init__(self, alpha=0.5): self.alpha = alpha self.prev = None def smooth(self, landmarks): if self.prev is None: self.prev = landmarks return landmarks smoothed = [] for i in range(len(landmarks)): x = self.alpha * landmarks[i][0] + (1 - self.alpha) * self.prev[i][0] y = self.alpha * landmarks[i][1] + (1 - self.alpha) * self.prev[i][1] z = self.alpha * landmarks[i][2] + (1 - self.alpha) * self.prev[i][2] smoothed.append([x, y, z]) self.prev = smoothed return smoothedalpha取0.5到0.7之间比较合适,太小了会有延迟,太大了平滑效果不明显。
4.3 性能调优的实战经验
经验一:双模型串行 vs 并行
手掌检测和关键点检测是串行关系,必须先检测到手,才能预测关键点。但如果你要处理多帧,可以用双线程:一个线程做手掌检测,一个线程做关键点检测,通过队列传递数据。这样能把NPU的利用率提上去。
经验二:输入分辨率的取舍
手掌检测的输入是192x192,关键点检测的输入是224x224。我试过把关键点检测的输入降到128x128,推理速度提升了40%,但关键点精度下降明显,尤其是手指尖的坐标偏差很大。所以不建议降分辨率,192和224已经是精度和速度的平衡点了。
经验三:NPU频率调节
RK3566的NPU频率是可以调的。默认频率是800MHz,你可以通过echo命令写到/sys/class/devfreq/下的对应节点来调节。不过不建议超频,散热跟不上容易降频,反而更慢。保持默认频率就行。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型转换报错Unsupported operator | TFLite自定义算子 | 查看报错日志中的算子名 | 剥离后处理层,板端实现 |
| 量化失败dataset is empty | 数据集路径错误 | 检查dataset.txt内容 | 使用绝对路径,确保图片可读 |
| 推理结果全零 | 输入格式错误 | 检查输入shape和dtype | 转RGB,确保NHWC格式 |
| 推理速度慢 | 未指定NPU核心 | 检查init_runtime参数 | 指定core_mask=NPU_CORE_0 |
| 关键点抖动 | 量化精度不足 | 对比浮点模型输出 | 增加量化数据集,加EMA平滑 |
| 内存不足 | 虚拟机内存小 | 查看系统内存占用 | 分配至少8GB内存 |
提示:如果你在Android系统下部署,RKNN的运行时库是
librknnrt.so,需要放到/vendor/lib64/目录下,并且确保SELinux权限配置正确。Android下的调试比Linux麻烦一些,建议先在Linux下跑通再移植到Android。
5. 实际效果与扩展方向
5.1 实测性能数据
在RK3566上,Linux系统,CPU频率1.8GHz,NPU频率800MHz,实测数据如下:
| 指标 | 数值 |
|---|---|
| 手掌检测推理时间 | 12ms |
| 关键点检测推理时间 | 15ms |
| 后处理时间 | 5ms |
| 单帧总耗时 | 32ms |
| 帧率 | 约31FPS |
| CPU占用 | 约15% |
| NPU占用 | 约60% |
| 内存占用 | 约120MB |
这个性能跑实时手势识别完全够用。31FPS意味着每帧延迟大约32毫秒,人手动作的响应基本感觉不到延迟。
5.2 手势分类的扩展思路
拿到21个关键点之后,可以做手势分类。最简单的方法是基于规则:比如判断食指和中指是否伸直、拇指和食指指尖的距离等。复杂一点的方法是用一个小的全连接网络,输入21个关键点的坐标,输出手势类别。
我试过用规则做几个基本手势:握拳、张开、点赞、OK、剪刀手。准确率在90%以上,误识别主要发生在手指部分遮挡的时候。如果要做更精细的手势,建议用SVM或者小型的MLP分类器。
5.3 多手检测的处理
MediaPipe Hands默认只检测一只手。如果要检测多只手,需要在手掌检测阶段保留多个候选框,然后对每个候选框分别做关键点检测。RK3566的NPU算力有限,同时跑两只手的话帧率会降到15FPS左右,但还能接受。
多手检测的关键是NMS的阈值要调好。阈值太高,两只手靠得近的时候会漏检;阈值太低,同一只手会被检测出多个框。我一般把NMS的IoU阈值设在0.3到0.5之间。
5.4 模型更新的注意事项
MediaPipe的模型版本更新比较频繁,新版本可能会改变输入输出格式。如果你要升级模型,记得重新走一遍转换流程,并且验证板端推理的结果是否一致。我遇到过新版本模型输出维度变了,后处理代码没跟着改,结果关键点全错位的情况。
另外,RKNN Toolkit2的版本也要和板端NPU驱动版本匹配。Toolkit2的版本高于驱动版本时,导出的RKNN模型可能无法在板端加载。建议Toolkit2和驱动版本保持一致,或者Toolkit2版本略低于驱动版本。
注意:RK3566的NPU驱动版本可以通过
cat /sys/kernel/debug/rknpu/version查看。如果这个命令报错,说明NPU驱动没加载成功,需要检查内核配置和设备树。
5.5 实际部署中的散热问题
RK3566的NPU满载运行时发热量不小。我实测连续跑手势识别30分钟,芯片表面温度能到60度左右。如果散热不好,NPU会降频,帧率会从31FPS掉到20FPS以下。建议加一个小的散热片,或者用金属外壳辅助散热。
如果是在封闭的塑料外壳里,建议加一个微型风扇,或者把NPU频率限制在600MHz,牺牲一点性能换稳定性。长时间运行的设备,稳定性比峰值性能更重要。
5.6 从手势识别到手势交互
手势识别只是第一步,真正有价值的是手势交互。比如用手势控制PPT翻页、控制音乐播放、控制智能家居设备。这部分需要把关键点映射成具体的控制指令。
我的做法是定义一个手势字典,每个手势对应一个指令。然后做一个状态机,当手势持续稳定一定时间(比如0.5秒)后,才触发指令。这样可以避免误触发。
状态机的实现很简单:
class GestureStateMachine: def __init__(self, stable_frames=15): self.stable_frames = stable_frames self.current_gesture = None self.counter = 0 def update(self, gesture): if gesture == self.current_gesture: self.counter += 1 else: self.current_gesture = gesture self.counter = 1 if self.counter == self.stable_frames: return gesture return Nonestable_frames设为15,按30FPS算就是0.5秒。这个时间窗口可以根据实际体验调整,太短了容易误触发,太长了感觉迟钝。
5.7 后续可以扩展的方向
手势识别跑通之后,可以往几个方向扩展。一是加手势轨迹识别,比如画圈、画叉、滑动等动态手势。二是加双手协同识别,比如双手缩放、旋转。三是和语音识别结合,做多模态交互。
硬件方面,可以加一个摄像头模组,做成一体化的手势识别模块。RK3566支持MIPI CSI摄像头,可以直接接OV5640或者IMX219,省去USB摄像头的麻烦。不过MIPI摄像头的驱动配置比USB复杂,需要改设备树。
软件方面,可以把推理框架封装成C++的SDK,提供更简洁的API。这样上层应用开发就不用关心RKNN的细节了。我目前是用Python做的原型,后续如果要做产品化,肯定会转C++。
最后分享一个小技巧:如果你在调试的时候发现关键点位置总是偏,先检查一下输入图像的宽高比。MediaPipe的模型对宽高比很敏感,如果输入图像被拉伸变形,关键点就会偏。保持原始宽高比,用填充的方式缩放到目标尺寸,精度会好很多。这个坑我踩过,调了两天才发现是宽高比的问题。