资讯详情

Android端YOLO人体关键点检测:NCNN部署与CPU/GPU优化实战

📅 2026/11/4 8:41:05 | 华诺云谱 👁 阅读
Android端YOLO人体关键点检测:NCNN部署与CPU/GPU优化实战
1. 项目概述Android 端跑 YOLOv8-Pose / YOLO11-Pose / YOLO26-Pose 到底怎么做前段时间我把 YOLOv8-Pose、YOLO11-Pose、YOLO26-Pose 三个人体关键点检测模型逐个塞进 Android 手机CPU 和 GPU 两条后端都跑通了。这个项目的标题很直接YOLO人体关键点检测Android端上部署 YOLOv8-Pose、YOLO11-Pose、YOLO26-Pose含源码支持 CPU 和 GPU。它要解决的核心问题就一句话把姿态估计从服务器搬到用户口袋里的手机上并且不能卡成 PPT。人体关键点检测做 AI 的应该不陌生就是从图像或视频里先检测到人再把人身上的鼻子、眼睛、肩膀、手肘、手腕、胯、膝盖、脚踝这 17 个关键点标出来。听上去只是多输出几个点但从工程角度看它比普通目标检测要多处理两件事一是关键点坐标要跟人物框一一对应二是关键点预测必须在人物框的 NMS 过程中保持同步。所以模型结构、输出通道、后处理过程都不一样。这也是为什么我会单独开一个项目而不是随手拿一个 YOLO 检测模型直接传上去。先说结论这条路完全走得通。以 YOLOv8n-Pose 和 YOLO11n-Pose 这样的轻量级模型为例640x640 输入在骁龙中高端手机上CPU 推理大约 30 到 60 毫秒开启 GPU 之后能压到 15 到 30 毫秒。用来做健身计数、康复训练动作比对、体育动作分析这类实时场景帧率是够用的。至于 YOLO26-Pose目前更多是社区分支在推动结构上还在迭代但它和前两代在部署层面的逻辑高度一致只要导出格式对Android 端的接入方式几乎不用改。这套部署链路主要适合三类人第一类是想在 Android 上做实时动作识别的 App 开发者第二类是手里有 YOLO 训练经验但没做过端侧推理的算法工程师第三类是想把姿态检测集成到边缘设备、工控盒子或者 RK3588 这类板子上的嵌入式工程师。Android 只是载体NCNN 推理层、模型转换、关键点后处理这些工作放到 Linux 板卡上一样能复用。1.1 标题里三个模型到底有什么区别YOLOv8-Pose 是大家最熟悉的版本官方直接把 Pose Head 和检测头做成解耦结构一个分支输出检测框和置信度另一个分支输出 17 个关键点的坐标和可见性。它的主干用了 C2f 结构Neck 还是 PAN-FPN 那套特点是训练稳定、部署资料多、踩坑成本低。网上找 NCNN 部署案例大部分都是 YOLOv8-Pose出问题也容易搜到答案。YOLO11-Pose 是 v8 之后的产品结构上把 Backbone 和 Neck 又调整了一轮引入了 C3k2 这类轻量化模块参数更少、FLOPs 更低。单看 mAP 和 n 模型体积YOLO11n-Pose 在检测头保持不变的情况下比 YOLOv8n-Pose 更省所以我在手机端默认主力就是 YOLO11n-Pose。它导出后的输出格式和 v8 基本一致后处理代码可以同一套逻辑覆盖。YOLO26-Pose 这个名字在官方仓库里还没有一个完全统一的版本更多是社区用 YOLO26 主干拼 Pose Head 的产物。我在项目里用到的那份模型是从社区分支拉下来重新导出的结构上比 YOLO11 更“重”一点精度也略高但部署原理没有任何变化都靠导出端到端的 ONNX再转成 Android 能跑的格式。所以遇到 YOLO26-Pose 不用慌先看它导出的输出 tensor 是几通道剩下的都是老套路。1.2 这套方案能落在什么场景里很多人觉得姿态检测离普通业务很远其实这几年已经用得很多了。比如健身 App 里的“深蹲计数”和“卧推次数统计”本质是检测到人之后根据髋、膝、踝的角度判断动作是否标准体态矫正工具会看肩膀两条线和胯部两条线是否平行康复训练则需要更精确的关节角度计算肘关节、膝关节的弯曲程度。这些功能在手机上跑通之后最明显的优势是不依赖云端用户的视频帧不用上传隐私压力小也没有网络延迟。再往外走一步这套内置 NVIDIA 优化之外的方案同样可以迁移。NCNN 本身就是跨平台推理库模型文件、预处理、后处理代码都跟 Android System UI 无关。我后面把同一套 C 代码搬到 RK3588 的 Linux 环境只改了模型加载路径和摄像头取流方式推理部分基本没动。这就是为什么我在做 Android 工程时习惯把推理和业务代码拆开写而不是把所有逻辑都堆在 MainActivity 里。2. 部署方案选型NCNN 为什么是主力CPU 和 GPU 怎么切技术选型是最容易纠结的一步。我试过 ONNX Runtime、TFLite、MNN最后项目里默认用 NCNN。原因并不复杂NCNN 对 YOLO 系列的支持成熟参数模型文件是文本加二进制的组合调试方便并且在 Android 上 CPU 走 ARM NEON、GPU 走 Vulkan两套后端切换就是一个 bool。源码里如果要同时保留 CPU 和 GPU 支持NCNN 是最省事的选择之一。2.1 推理框架怎么选NCNN、MNN、TFLite 对比做 Android 端部署常见路线有四种NCNN、MNN、TFLite、ONNX Runtime Mobile。四者的差异集中体现在算子兼容性、二进制体积、GPU 后端方式还有文档数量上。框架CPU 优化GPU 后端模型格式上手难度适合场景NCNNARM NEONVulkanparam/bin中等YOLO 系列部署、嵌入式设备MNNARM NEONOpenCL / Vulkan / Metalmnn中等跨平台移动端、商城 AppTFLiteXNNPACKGPU Delegatetflite低通用分类检测、已有 TF 生态ONNX Runtime Mobile自带优化NNAPIonnx中不想转格式、原型验证我选择 NCNN还因为它的 param 文件可以直接打开看网络结构。比如模型跑不通我会先打开 .param 确认输入层名字是不是 images输出层名字是不是 output0再确认每个卷积层的 shape 是否正常。这种透明度在定位问题上非常有用。2.2 CPU 和 GPU 的切换逻辑NCNN 里切 CPU 和 GPU 非常简单核心就是 Net 的 option。ncnn::Net net; net.opt.use_vulkan_compute true; // true 走 GPUfalse 走 CPU net.opt.num_threads 4; net.opt.use_fp16_packed true; net.opt.use_fp16_storage true;但不是说把use_vulkan_compute设成 true 就万事大吉。第一次创建 Vulkan 上下文、编译 shader、上传权重都要额外耗时实测有些老机型 Vulkan 驱动还有兼容性坑。我的做法是在 App 启动时先调用一次ncnn::get_gpu_count()如果 GPU 数量为 0直接回退 CPU如果数量大于 0再尝试用 GPU 初始化模型初始化失败也回到 CPU。这样既能拿到 GPU 加速也不会让用户卡在一个黑屏页。还有一个很多人踩过的点GPU 不是一定比 CPU 快。模型很小、输入分辨率不高的时候GPU 的 dispatch 开销和显存读写耗时可能超过它省下来的计算时间特别是在 Mali GPU 和低端 Adreno 上。所以我建议真机上做一次 AB 测试别只盯着纸面理论。2.3 环境准备Android Studio、NDK 和第三方库如果你从零开始搭项目环境部分需要准备这几样Android Studio、NDK、CMake、NCNN、OpenCV。OpenCV 不是必须的我用它主要是做图像旋转、resize 和画框画骨架省去自己写像素遍历的时间。NCNN 可以直接用源码编译也可以下载预编译包。我习惯在 CMake 里直接引入 ncnnset(ncnn_DIR ${CMAKE_SOURCE_DIR}/libs/ncnn-20240820-android-vulkan/${ANDROID_ABI}/lib/cmake/ncnn) find_package(ncnn REQUIRED) target_link_libraries(pose_native ncnn ${OpenCV_LIBS} android log)ABI 只需要保留 arm64-v8a 就够了。如果是测试老旧 32 位设备可以加 armeabi-v7a但打包体积会变大。我在项目里把 x86 和 x86_64 删掉了因为模拟器上跑姿态检测没有参考价值还会让 APK 体积明显增加。3. 模型转换与关键配置从 .pt 到 .param/.bin拿到 YOLOv8-Pose 或 YOLO11-Pose 的 .pt 文件之后不能直接往 Android 里塞。PyTorch 模型不能脱离 Python 环境运行必须导出成 ONNX再转成 NCNN 认识的文件。转换链路看起来只是两条命令但中间有大量细节稍不注意就会遇到输出全零、NMS 失效甚至模型崩掉。3.1 用 Ultralytics 导出 ONNX最标准的导出命令是这样pip install ultralytics onnx onnxsim yolo export modelyolo11n-pose.pt formatonnx imgsz640 simplifyTrue opset12 batch1导出完成后会得到yolo11n-pose.onnx。这里的关键参数有两个opset12和simplifyTrue。opset 太高某些 NCNN 版本不认不做 simplify模型里会残留一些对部署无用的算子转 NCNN 时容易报 unknown layer。batch1也是必须的Android 端是单帧推理动态 batch 只会增加转换复杂度。还有一个容易忽略的问题导出时不要开 NMS。Ultralytics 的 export 参数里有nmsTrue的选项会把非极大值抑制一起打包进图里。这种做法在端到端部署时确实方便但 NCNN 对 NMS 算子的支持不算友好而且后处理写在端侧更灵活。我选择导出干净 tensor在 C 里自己写解码和 NMS输出效果可控。3.2 ONNX 转 NCNN 和常见的报错ONNX 文件转 NCNN 用官方工具onnx2ncnn yolo11n-pose.onnx yolo11n-pose.param yolo11n-pose.bin跑完之后用一段测试图片做一次 NCNN 推理确认输出数值正常然后用 ncnnoptimize 做模型瘦身ncnnoptimize yolo11n-pose.param yolo11n-pose.bin yolo11n-pose-fp16.param yolo11n-pose-fp16.bin 65536这里的65536表示开启 fp16 存储模型体积能再缩一圈在 Vulkan 后端上推理速度通常也更快。如果遇到Reshape或Slice不支持的问题优先确认 ONNX 是否做了 simplify再检查 NCNN 版本是不是太老。社区里说的“YOLO 模型转换失败NMS 报错”九成都是算子兼容问题解法是升级工具链或者把模型的导出后端从 PyTorch 换成 ONNX 官方导出。3.3 模型体积和精度怎么权衡手机端部署最忌讳无脑用大模型。同样是姿态检测YOLOv8x-Pose 的模型体积可能是 n 版的十倍以上但精度提升肉眼不容易察觉推理速度却会掉到不可用。我在这套源码里提供的是三档选择模型体积fp16 转 ncnn 后参考 CPU 耗时参考 GPU 耗时推荐场景YOLOv8n-Pose约 5~7MB35~50ms15~25ms通用实时姿势识别YOLO11n-Pose约 5MB30~45ms12~22ms默认首选YOLO26-Pose社区版约 7~10MB45~70ms20~35ms对精度要求更高的离线分析如果还想进一步压缩可以把模型转成 int8 量化再把权重存成 int8 bin体积能砍一半以上。但 int8 在 CPU 上依赖硬件支持旧手机未必走得通GPU 上也不是所有算子都支持 int8。所以我的经验是在 2025 到 2026 年的主流设备上fp16 是性价比最高的档位。4. 预处理、模型推理与关键点后处理真正决定识别效果的部分模型能不能跑出来只看工程链路通不通跑出来准不准关键在看前后处理是否严谨。我在这个项目里调试时间最长的不是模型推理而是 letterbox、坐标映射和关键点解码这三块。4.1 letterbox 前处理别踩“直接拉伸”的坑YOLO 系列模型输入是固定正方形通常是 640x640。但摄像头画面是 16:9 或 4:3直接把画面 resize 成 640x640 会把人体压变形关键点坐标也会整体偏移。正确做法是 letterbox按比例缩放后把空白区域用固定像素值填充。int target_w 640; int target_h 640; float scale std::min(640.f / img.cols, 640.f / img.rows); int resized_w std::round(img.cols * scale); int resized_h std::round(img.rows * scale); float pad_x (target_w - resized_w) / 2.f; float pad_y (target_h - resized_h) / 2.f; ncnn::Mat in ncnn::Mat::from_pixels_resize( img.data, ncnn::Mat::PIXEL_BGR, img.cols, img.rows, resized_w, resized_h); ncnn::Mat padded; ncnn::copy_make_border(in, padded, (int)pad_y, (int)(target_h - resized_h - pad_y), (int)pad_x, (int)(target_w - resized_w - pad_x), ncnn::BORDER_CONSTANT, 114.f); padded.substract_mean_normalize(mean_vals, norm_vals); ex.input(images, padded);注意这里的pad_x和pad_y一定要保留下来传给后处理否则你从输出里拿到的坐标是 letterbox 之后的坐标映射不回原图。我有一次忘记除以 scale导致所有关键点画在人物身边偏离一个身位排查了很久才发现坐标换算只在检测框里做了关键点没有一起改。4.2 推理输出到底是什么形状YOLOv8-Pose、YOLO11-Pose 的 ONNX 输出通常是1x56x8400。这个 56 维可以拆成4 个框坐标、1 个人体目标置信度、51 维关键点数据。51 是怎么来的COCO 姿态定义是 17 个关键点每个点有 x、y、可见性三个值三乘十七等于五十一。8400 是 640x640 输入下所有 grid 单元的总数。不同输入分辨率这个数会变NCNN 的 Mat 里通常表现为通道数和宽高。有的导出方式会把输出转置成1x8400x56NCNN 取出来时out.c和out.h的位置就不一样。这也是很多新手一脸懵的地方同样一份模型别人代码能用你代码全输出零。我的建议是转换后先打印一遍out.c、out.h、out.w用一张已知结果图对比确认数据排布再写解析逻辑。4.3 关键点解码和 NMS 的一端代码我项目里的后处理是典型的检测加关键点同步逻辑。检测框先按置信度筛一遍再做 NMS最后把选中框对应位置的关键点输出。核心伪代码是这样int num_anchors out.h; const float* data (const float*)out.data; for (int a 0; a num_anchors; a) { float obj_score data[a * 56 4]; if (obj_score BOX_THRESH) continue; float cx data[a * 56 0]; float cy data[a * 56 1]; float w data[a * 56 2]; float h data[a * 56 3]; // 恢复到原图坐标 float x0 (cx - w / 2 - pad_x) / scale; float y0 (cy - h / 2 - pad_y) / scale; float x1 (cx w / 2 - pad_x) / scale; float y1 (cy h / 2 - pad_y) / scale; Object obj; obj.rect Rect(x0, y0, x1 - x0, y1 - y0); obj.score obj_score; for (int k 0; k 17; k) { KeyPoint kp; kp.x (data[a * 56 5 k * 3 0] - pad_x) / scale; kp.y (data[a * 56 5 k * 3 1] - pad_y) / scale; kp.conf data[a * 56 5 k * 3 2]; obj.keypoints.push_back(kp); } proposals.push_back(obj); } ncnn::nms(proposals, picked, NMS_THRESH);画点连线的时候我会把左右肩、左右肘、左右胯、左右膝这些关节用颜色区分开骨架连接关系就用 COCO 标准定义的那组。如果人物在镜头里转身、遮挡关键点置信度会低我一般把可视化阈值设在 0.3低于这个值的点不画避免线连到奇怪的位置。5. CPU/GPU 实测优化NCNN Vulkan 与 CPU 的表现很多读者最关心的还是性能CPU 和 GPU 到底谁快能快多少在什么手机上快。我这里放一组我自己测试机上的数据不同 ROM 和驱动版本会有浮动但趋势是稳定的。5.1 几台真机的实测参考设备芯片yolo11n-pose fp16 CPUyolo11n-pose fp16 GPU (Vulkan)测试机 A骁龙 8 Gen135ms14ms测试机 B骁龙 778G55ms40ms测试机 C麒麟900045ms58ms测试机 D入门 Helio G85160ms130ms从数据能看出来高通 Adreno 对 Vulkan 的驱动优化普遍不错GPU 加速收益明显Mali 在部分设备上反而 CPU 更快大概率是驱动调度和 shader 编译策略的问题。我做了一个简单的自选逻辑前 30 帧分别用 CPU 和 GPU 各跑一遍取平均耗时谁快就用谁。虽然多花了一点启动时间但换来了不同设备上的稳定体验。还有一个容易误导新手的点Android Studio 调试模式下跑出来的耗时没有任何参考意义Debug 构建本身没有开启优化NCNN 的算子性能会大打折扣。一定要用 Release 构建或者至少用 CMake 的 Release 配置去测帧率。5.2 工程上的几个加速手段第一Extractor 要复用不要每帧net.create_extractor()。这会反复创建推理上下文CPU 后端能明显看到卡顿。第二输入图像不要总是从 Bitmap 转 Mat。CameraX 的 ImageAnalysis 给的是 YUV 数据如果能直接转成 NV21 或者灰度再送进 NCNN能省掉一次 RGB 转换开销。我项目里先用 OpenCV 把 YUV 帧转成 BGR Mat虽然多了一步但是模型输入固定要三通道兼容性最好。第三帧率不够就抽帧。实时姿态检测并不需要每帧都推理25ms 一帧的输入每隔两帧做一次检测人眼根本感觉不到延迟CPU 占用却能降一半。这个“检测频率”的配置我放在了设置页用户可以自己调。5.3 怎么判断当前设备适不适合 GPU代码里可以这么判断int gpu_count ncnn::get_gpu_count(); bool use_gpu gpu_count 0; net.opt.use_vulkan_compute use_gpu; net.load_param(mgr, yolo11n-pose-fp16.param); net.load_model_bin(mgr, yolo11n-pose-fp16.bin); if (use_gpu net.get_vulkan_device() 0) { net.opt.use_vulkan_compute false; }get_vulkan_device() 0说明 Vulkan 设备不可用这时候立刻回退 CPU 是最稳妥的。如果你在自定义 ROM 或者模拟器上跑有些设备宣称支持 Vulkan但实际驱动是坏的GPU 初始化不报错推理结果却全错。这种情况只能靠灰度测试发现所以我的源码里加了一个“自检模式”加载模型后先用一张内置测试图跑一次如果输出目标数量为 0自动切回 CPU。6. 常见问题与排查技巧实录这个项目里我踩过的坑不少大部分都能靠几个固定方向排查。整理成速查表方便直接对号入座。现象可能原因处理方式模型加载失败param/bin 路径不对或 AAssetManager 传空检查 assets 目录打印__android_log_print推理输出全为 0输入是 RGB模型导出是 BGR统一用PIXEL_BGR或导出时确认通道顺序关键点整体偏移letterbox 的 pad 和 scale 没传到后处理把 pad_x、pad_y、scale 保存到解码器画面卡顿每帧重新创建 ExtractorExtractor 只创建一次并复用GPU 黑屏或崩溃Vulkan 驱动兼容问题初始化失败时回退 CPU或在设置页关闭 GPUNMS 后多个框重叠NMS 阈值太高调到 0.45 左右配合置信度 0.25输出通道读取错位NCNN 的 Mat 转置问题打印 out.c/h/w先跑通测试图模型文件太大用了 fp32 或大模型改用 fp16换 n 或 s 版本最后再分享一个我用时间换回来的经验先把模型转换和前后处理在电脑上跑通再做 Android 移植。很多问题在电脑上用一张图片就能暴露比如坐标换算错误、channel 顺序不对、输出 tensor 形状理解错。到了 Android 端再去面对摄像头预览、内存抖动、JNI 传参这些问题时剩下的才真正是移动端特有的坑。如果你打算复现这套项目顺序建议是先拿 YOLO11n-Pose 跑通全流程再去对比 YOLOv8-Pose 和 YOLO26-Pose 的差异。先把 NCNN 的 examples 里检测部分跑起来再换 Pose 模型会顺手很多。这是我在这个项目里最大的体会。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑