说白了,这个项目就是一个“人体关键点实时检测”的三端Demo,分别用Python、Android、C++跑同一套2D Pose推理流程。我这次直接以OpenCV DNN为统一推理引擎,把OpenPose单人模型跑通三个平台,省去切换框架的麻烦,代码风格也高度一致。不管你是刚入门想做点视觉项目,还是想把手头Python原型迁移到手机或边缘设备,这篇文章都能给你一条不用绕远路的技术路线。
1. 项目全貌:2D Pose到底在解决什么问题
1.1 人体关键点检测能落地到哪些场景
2D人体关键点检测的任务,简单说就是给一张图或一段视频里的每个人,标出鼻子、脖子、肩膀、手肘、手腕、胯、膝盖、脚踝这些关节在图像上的二维坐标。它输出的不是3D空间位置,而是“在画面里的像素位置”,所以叫2D Pose。
这个能力在真实项目里用处非常大。健身App要判断你深蹲做没做标准,本质上就是在算髋关节、膝关节、踝关节之间的夹角;安防监控想识别跌倒、异常徘徊,关键点序列比纯目标框信息丰富得多;手势控制、AR特效、体感游戏,更是在关键点坐标上做实时交互。我见过不少团队一开始想上3D姿态估计,后来发现业务只需要“关节角度”“肢体对齐”这种2D信息,反而用2D Pose更轻、更快、更稳定。
这个Demo适合三类人:一是想快速验证人体姿态算法效果的开发者;二是需要把原型从Python迁到移动端或嵌入式端的工程师;三是准备用关键点坐标做上层应用(比如动作打分、姿态告警)的初学者。你不需要懂太深的深度学习原理,只要会基本的Python或Java或C++,能跟着流程把代码跑起来,就够用了。
1.2 技术路线对比:为什么选择 OpenCV DNN + OpenPose
人体关键点检测现在主流方案有OpenPose、HRNet、Lite-HRNet、MoveNet、BlazePose这些,推理框架也有TensorFlow Lite、ONNX Runtime、PyTorch、OpenCV DNN等一堆选择。我一开始也纠结过,最后定了“OpenCV DNN + OpenPose Caffe模型”作为三端统一方案,原因很现实。
先看一张对比表:
| 方案 | 跨平台统一度 | 部署复杂度 | CPU实时性 | 移动端友好度 | 依赖体积 |
|---|---|---|---|---|---|
| PyTorch | 差,移动端麻烦 | 高 | 中 | 差 | 大 |
| TensorFlow Lite | 较好 | 中 | 高 | 好 | 小 |
| ONNX Runtime | 较好 | 中 | 高 | 好 | 较小 |
| OpenCV DNN + OpenPose | 极好 | 低 | 中 | 中 | 小 |
重点说一下为什么OpenCV DNN最契合多端Demo场景。OpenCV在Python、Android、C++三个平台都有官方支持,DNN模块的函数接口几乎一致,readNetFromCaffe、blobFromImage、setInput、forward这四个函数在三个平台写法一模一样。这就意味着你只要把Python版调试通,后面C++版和Android版基本都是“翻译代码”的工作量,不需要重新设计数据流,也不用为每个平台单独引入一套推理SDK。
我用的OpenPose单人Caffe模型也是经过大量项目验证的老牌模型,模型文件体积适中,CPU上就能跑,关键点定义清晰,后处理逻辑容易讲明白。相比MoveNet这种需要TFLite专用后处理的模型,OpenPose的通用性更强,换到其他框架也更容易。
如果你想追求更高实时性,后面可以把模型换成Lite-HRNet或MoveNet,推理引擎也可以换成ONNX Runtime或TFLite,但整套“输入预处理-推理-热图后处理-坐标映射”的流程是通用的,本文的代码骨架照样能复用。
1.3 模型参数与关键点定义
我用的是OpenPose COCO单人模型,输入端要求一张368x368的RGB图像(实际是BGR,OpenCV默认通道顺序,后面会说清楚)。推理输出是一个四维张量,格式大致是[1, num_keypoints, 46, 46],其中46由输入分辨率除以8得到(368 / 8 = 46),num_keypoints对应关键点数量。我用的模型输出18个关键点,不含背景通道,不同版本可能略有差异,跑起来之后先打印输出shape确认一下就好。
COCO 18点的关键点索引如下:
| 索引 | 部位 | 索引 | 部位 |
|---|---|---|---|
| 0 | Nose 鼻子 | 9 | RKnee 右膝 |
| 1 | Neck 脖子 | 10 | RAnkle 右脚踝 |
| 2 | RShoulder 右肩 | 11 | LHip 左胯 |
| 3 | RElbow 右手肘 | 12 | LKnee 左膝 |
| 4 | RWrist 右手腕 | 13 | LAnkle 左脚踝 |
| 5 | LShoulder 左肩 | 14 | REye 右眼 |
| 6 | LElbow 左手肘 | 15 | LEye 左眼 |
| 7 | LWrist 左手腕 | 16 | REar 右耳 |
| 8 | RHip 右胯 | 17 | LEar 左耳 |
这里容易踩个小坑:OpenPose的COCO定义和官方COCO 17点定义相比,多了Neck(脖子),且其它索引不完全一致。所以后处理画骨骼线时,务必按我这里列的索引关系来做,别直接套用其他模型的手写连接数组。
2. 三平台环境准备:一场大型排雷现场
2.1 模型文件准备与工程目录
不管是哪个平台,都要先准备两个模型文件:pose_deploy_linevec.prototxt和pose_iter_440000.caffemodel。前者是模型结构描述,后者是训练好的权重。这两个文件在OpenCV官方示例仓库和OpenPose原仓库都能找到,下载后我都统一放在models/目录下。
工程目录我建议这样组织:
pose-demo/ ├── models/ │ ├── pose_deploy_linevec.prototxt │ └── pose_iter_440000.caffemodel ├── python_demo/ ├── android_demo/ └── cpp_demo/这样三端共用同一份模型,换模型文件只改一个地方,不会出现三端模型版本不一致的问题。模型文件别放中文路径,之前在Windows上踩过坑,OpenCV底层读文件在某些场景对中文路径支持不稳定,白折腾很久。
2.2 Python环境搭建与常见安装报错
Python版建议直接用Python 3.8到3.11之间的版本,别追最新版本,否则一些依赖编译包可能还没来得及出wheel包。安装依赖就两行:
pip install opencv-python pip install numpy如果网速比较慢,可以加国内镜像源安装:
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple安装过程中有个报错非常常见,就是error: Microsoft Visual C++ 14.0 is required。这个错其实不是OpenCV本身导致的,而是某些依赖包在Windows上没有预编译的wheel,pip尝试从源码编译,结果缺了MSVC编译工具链。解决方法是去微软官网下载Microsoft C++ Build Tools,安装时勾选“使用C++的桌面开发”组件。装完重新打开终端再pip install一次一般就好了。
装完检查一下opencv版本:
python -c "import cv2; print(cv2.__version__)"能看到版本号,比如4.8.0,环境就算就绪。我还会顺带打印cv2.__file__看一下路径,防止多Python环境冲突导致import到旧版本。
2.3 Android Studio 环境准备
Android端我用的Android Studio,版本没有特别要求,新一点的稳定版即可。首次启动会提示下载Android SDK,按向导装就行。如果下载慢,可以在Settings -> Appearance & Behavior -> System Settings -> Android SDK里配置镜像源。
OpenCV在Android上接入方式有几种:一种是下载OpenCV Android SDK,把openCVLibrary作为Module导入工程;另一种是通过Maven依赖直接引入:
implementation 'org.opencv:opencv:4.8.0'我个人建议用Maven依赖,省去一堆手动导入配置。不过要注意,Maven中央仓库的OpenCV Android包需要对Android版本做兼容适配,最新版本一般没问题,如果你用的较老则自行调整版本。
工程里需要在MainActivity的静态代码块里初始化OpenCV:
static { if (!OpenCVLoader.initLocal()) { OpenCVLoader.initDebug(); } }这段代码很多人容易漏掉,不初始化就调用Dnn.readNetFromCaffe会直接抛UnsatisfiedLinkError,属于高频踩坑点。
2.4 C++ 编译链准备
C++版我是在Windows上用Visual Studio 2019编译的,OpenCV用的是官方Windows预编译包,版本4.8.0。如果你习惯vscode,也可以把vscode配置成C/C++开发环境,但重点是OpenCV库路径和依赖库要配置正确。
最省心的是用CMake组织工程,CMakeLists.txt里核心配置如下:
cmake_minimum_required(VERSION 3.16) project(PoseDemo) set(CMAKE_CXX_STANDARD 14) find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) add_executable(pose_demo main.cpp) target_link_libraries(pose_demo ${OpenCV_LIBS})用CMake的好处是它会自动处理OpenCV的Debug/Release库切换,不用手工去VS里逐个配置包含目录和库目录。如果你是用vscode手写配置,核心就是三个地方:includePath指定OpenCV头文件路径、path指定启动程序时用的dll目录、linkArgs指定要链接的lib文件。配置过程比较琐碎,但每个平台装OpenCV的教程都很多,对着来就行。
3. Python版Demo:先跑通,再优化
3.1 核心代码:加载模型与推理
Python版是整个项目的主干,先把逻辑跑通,后面两个平台照着翻译就行。完整推理代码不长,核心就几行:
import cv2 import numpy as np PROTO = "../models/pose_deploy_linevec.prototxt" MODEL = "../models/pose_iter_440000.caffemodel" INPUT_SIZE = 368 CONFIDENCE_THRESHOLD = 0.2 net = cv2.dnn.readNetFromCaffe(PROTO, MODEL) def infer(frame): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1.0 / 255.0, (INPUT_SIZE, INPUT_SIZE), (0, 0, 0), swapRB=False, crop=False) net.setInput(blob) output = net.forward() return output, w, hblobFromImage这个函数需要解释一下,它负责把原图做归一化、缩放、减均值,并转换成NCHW这种网络输入格式。第一个参数1.0 / 255.0是缩放因子,把像素从0~255缩放到0~1;(368, 368)是模型输入尺寸;(0, 0, 0)是均值,这里设0就是不做减均值操作;swapRB=False表示不交换通道。
这里有个常见误区:OpenCV读图默认是BGR通道顺序,而很多模型训练时用的是RGB顺序。OpenPose这个Caffe模型在训练时接受的是BGR输入,所以swapRB要设False。如果你换成其他模型,一定要先确认模型训练时用的通道顺序,否则关键点检测结果会整个“偏色”,效果非常差。
net.forward()返回的就是模型输出,按前面说的,shape一般是[1, 18, 46, 46],但保险起见建议print一下确认:
print(output.shape)如果输出的通道数不是18,先检查模型下载是否正确,别急着改代码。
3.2 关键点后处理与骨架绘制
模型输出的46x46小热图(heatmap)里,每个位置对应一个置信度分数。要还原成图像上的关键点坐标,标准做法是找到每个通道的峰值位置,再把46x46的网格坐标映射回原图尺寸。
完整后处理和绘制代码如下:
KEYPOINT_PAIRS = [ (0, 1), (1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (8, 9), (9, 10), (1, 11), (11, 12), (12, 13), (0, 14), (14, 16), (0, 15), (15, 17), (8, 11) ] def draw_keypoints(frame, output, orig_w, orig_h): points = [] n_points = output.shape[1] for i in range(n_points): heatmap = output[0, i, :, :] _, conf, _, point = cv2.minMaxLoc(heatmap) if conf > CONFIDENCE_THRESHOLD: x = int(point[0] * orig_w / output.shape[3]) y = int(point[1] * orig_h / output.shape[2]) points.append((x, y)) cv2.circle(frame, (x, y), 4, (0, 255, 255), -1) else: points.append(None) for pair in KEYPOINT_PAIRS: a, b = pair if points[a] and points[b]: cv2.line(frame, points[a], points[b], (0, 255, 0), 2) return frame用cv2.minMaxLoc找热图峰值是最直接的后处理方式,它一次性返回最大值、最大值置信度和最大值坐标。坐标映射公式是x = point_x * orig_w / 46,因为模型输出的热图尺寸是46x46,而原图是更大的分辨率,等比例放大就能得到最终关键点在原图上的位置。
画骨骼线时我特别强调连接关系,OpenPose的COCO 18点连接关系里,脖子(1)到臀部(8和11)、脖子到肩膀(2和5)是核心,画错会显得骨架特别奇怪。画线之前一定要判空,置信度低于阈值的点不参与连线,否则会出现大量乱飞的线条。
3.3 摄像头实时检测与FPS统计
实时检测就是把上面的推理逻辑塞进VideoCapture循环里:
cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break tick = cv2.getTickCount() output, orig_w, orig_h = infer(frame) frame = draw_keypoints(frame, output, orig_w, orig_h) fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick) cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) cv2.imshow("2D Pose Demo", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()用cv2.getTickCount和getTickFrequency计算FPS是比较常见的做法,比用time.time()更精确。注意infer函数里我每次都对frame.shape重新取宽高,不要在外面只取一次,因为不同相机的分辨率可能变化。
实测下来,在普通笔记本CPU上,OpenPose单人模型368x368输入大概在100到300毫秒一帧,也就是3到10 FPS左右,属于“能实时但不够流畅”的水平。如果觉得卡,优先缩小输入尺寸,比如把INPUT_SIZE改成320或256,帧率能提升不少,精度损失对一般应用场景来说可以接受。
3.4 高频报错与调试心得
Python端跑通过程中,我遇到最多的是这三个问题。
第一个是readNetFromCaffe报错或直接崩掉。大概率是模型文件路径写错,或者模型文件下载不完整。检查路径时建议用os.path.exists先确认文件存在,别用相对路径跨目录跑。
第二个是输出shape对不上。很多人下载了多人的OpenPose模型(pose_deploy_linevec和pose_iter_440000其实是单人模型,多人模型文件是pose_deploy_linevec.prototxt的变体),或者用了其他关键点数的模型,导致后处理循环的通道数不对。所以一定要printoutput.shape,然后动态设置n_points = output.shape[1]。
第三个是画面颜色不对。如果检测出来的人体姿势很怪,先怀疑swapRB参数。我习惯先用一张包含完整人体的测试图片跑一遍,把关键点直接画出来看效果,确认无误再接摄像头,这样排查问题更快。
4. Android版Demo:把模型塞进手机里
4.1 工程架构与OpenCV集成
Android版我建议先跑通“静态图片推理”,再上相机预览。因为手机端的链路比Python长很多,涉及相机配置、图像旋转、OpenCV初始化、Assets模型拷贝等环节,一步到位容易出问题还不好定位。
工程结构大致分三层:
MainActivity ├── OpenCV初始化 ├── CameraX预览(ImageAnalysis) └── 推理与绘制(Dnn + Canvas)OpenCV接入按照前面2.3节配置好之后,首先把模型文件放到assets/models/目录。注意Android不能直接访问Assets路径,需要先把文件拷贝到应用私有目录,再传给readNetFromCaffe,这是Android端最容易漏掉的一步。
拷贝核心代码:
private File copyAssetFile(String assetPath) throws IOException { File outFile = new File(getCacheDir(), assetPath); if (!outFile.exists()) { outFile.getParentFile().mkdirs(); InputStream is = getAssets().open(assetPath); FileOutputStream fos = new FileOutputStream(outFile); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.close(); is.close(); } return outFile; }4.2 相机预览与图像输入
相机部分我用的是CameraX,ImageAnalysis的回调里拿到ImageProxy,转成OpenCV的Mat。这个过程中最磨人的是旋转问题。手机传感器拿到的图像是旋转过的,如果不处理,检测出的关键点方向会不对。
我封了一个转换函数:
private Mat imageProxyToMat(ImageProxy imageProxy) { Image image = imageProxy.getImage(); if (image == null) return null; Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); int ySize = yBuffer.remaining(); int uSize = uBuffer.remaining(); int vSize = vBuffer.remaining(); Mat yuvMat = new Mat(image.getHeight() + image.getHeight() / 2, image.getWidth(), CvType.CV_8UC1); yuvMat.put(0, 0, new byte[ySize + uSize + vSize]); Mat rgbMat = new Mat(); Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2RGB_NV21); return rgbMat; }这段代码在Android上很典型,YUV到RGB的转换、Mat内存分配、ImageProxy生命周期,每一步都可能出问题。我的建议是先用OpenCV的示例代码跑通CameraX预览,再接推理,别在转换环节卡太久。
4.3 模型推理与关键点叠加绘制
推理核心代码和Python版几乎一样,只是换成了Java API:
Net net = Dnn.readNetFromCaffe(protoPath, caffePath); Mat blob = Dnn.blobFromImage(rgbMat, 1.0 / 255.0, new Size(368, 368), new Scalar(0, 0, 0), false, false); net.setInput(blob); Mat output = net.forward();输出拿到手之后,后处理逻辑跟Python版一致,遍历output的通道,找每个热图的峰值坐标,再映射回原图尺寸。
关键是叠加绘制。我用的是Custom View的onDraw,把相机预览帧显示在底层,然后在上面用Paint画圆和画线。坐标映射要注意:检测结果基于的是Mat的图像坐标系,而Canvas绘制的是View坐标系,如果View尺寸和图像尺寸不一致,需要做好缩放换算。我直接用Matrix做统一映射,避免手写一堆比例计算:
canvas.drawBitmap(bitmap, matrix, null); for (Point p : points) { canvas.drawCircle(p.x * scaleX, p.y * scaleY, 8f, paint); }更稳妥的做法是把绘制逻辑放在SurfaceView的同一个线程里,避免UI线程卡顿。
4.4 真机运行与权限、URI文件访问
真机调试时,别忘了在AndroidManifest.xml里声明相机权限:
<uses-permission android:name="android.permission.CAMERA" />Android 6.0以上需要在运行时动态申请权限,这部分用requestPermissions处理即可。
还有一个容易折腾人的点:如果你后续想扩展功能,比如从相册选视频做离线检测,会很容易碰到content://开头的Uri。这种Uri并不是普通的文件路径,不能用File直接访问,正确做法是先用ContentResolver打开输入流,把文件拷贝到应用缓存目录,再用OpenCV读取。我早期在这里绕了很久,一直以为路径拼错了,其实是没搞明白content://和file://的区别。
4.5 性能优化方向
手机端OpenPose模型跑368x368输入,中端手机上大概2到5 FPS,效果一般。想流畅跑实时,我建议做三件事。
第一件事是把推理放到独立线程,不要占用UI线程。用HandlerThread或协程都行,CameraX的ImageAnalysis回调本身就在后台线程,但后续绘制要切回主线程。
第二件事是输入尺寸降到256或224,帧率能明显提升。精度会有损失,但关键点大致形状还是准的,适合做实时预览场景。
第三件事是换更轻量的模型。如果想长期做移动端,MoveNet或BlazePose是更好的选择,它们的模型结构天生为移动端设计,TFLite部署也更方便。换模型的代价是后处理逻辑要跟着改,但整体数据流不用变。
5. C++版Demo:底层部署与性能摸底
5.1 工程配置与依赖
C++版本质上就是把Python版的逻辑用C++重新写一遍。依赖就两个:OpenCV的core、imgproc、dnn、highgui这些都是自带的。
CMakeLists核心配置前面写过,这里再补充一个关键点:如果你用Visual Studio直接编译,注意运行时要保证OpenCV的dll在环境变量PATH里,否则程序一启动就报“找不到opencv_world480.dll”。我建议直接把opencv\build\x64\vc15\bin加进系统PATH,省得每次手动拷dll。
工程里项目文件结构:
cpp_demo/ ├── CMakeLists.txt └── main.cpp5.2 推理核心代码段
C++推理代码和Python极像,这也是当初选OpenCV DNN的原因之一:
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main() { string proto = "../models/pose_deploy_linevec.prototxt"; string model = "../models/pose_iter_440000.caffemodel"; Net net = readNetFromCaffe(proto, model); VideoCapture cap(0); if (!cap.isOpened()) { cerr << "Cannot open camera" << endl; return -1; } Mat frame; while (true) { cap >> frame; if (frame.empty()) break; Mat blob = blobFromImage(frame, 1.0 / 255.0, Size(368, 368), Scalar(0, 0, 0), false, false); net.setInput(blob); Mat output = net.forward(); // 后处理与绘制省略,逻辑同Python版 imshow("2D Pose Demo", frame); if (waitKey(1) == 'q') break; } return 0; }blobFromImage的参数跟Python版完全一致,swapRB=false同样适用于OpenPose Caffe模型。如果你后续换模型,这里也要同步检查通道顺序。
5.3 坐标后处理与绘制
C++后处理有个点需要注意:net.forward()返回的Mat是4维的,不能直接用at<float>遍历全部维度,要先取出二维平面:
int nPoints = output.size[1]; int h = output.size[2]; int w = output.size[3]; for (int i = 0; i < nPoints; i++) { Mat heatmap(h, w, CV_32F, output.ptr<float>(0, i)); double conf; Point maxLoc; minMaxLoc(heatmap, nullptr, &conf, nullptr, &maxLoc); if (conf > 0.2) { int x = maxLoc.x * frame.cols / w; int y = maxLoc.y * frame.rows / h; circle(frame, Point(x, y), 4, Scalar(0, 255, 255), -1); } }这里用了output.ptr<float>(0, i)拿到第i个通道的指针,再用Mat包成二维矩阵,避免手工处理四维索引。minMaxLoc返回的坐标是热图坐标,映射回原图时用热图宽高而非模型输入分辨率,这个细节挺重要,不能搞混。
关于画线和指针,C++里如果要维护一个关键点数组,直接声明vector<Point> points(nPoints, Point(-1, -1)),别用裸指针去管理动态内存。我见过不少初学者用Point* points = new Point[18],然后忘记delete,跑久就崩。用vector自动管理内存,省心又安全。
5.4 三种平台性能对比与调优
我在同一台笔记本上用同一模型测了Python版和C++版的推理耗时,做个对比参考:
| 平台 | 设备 | 输入尺寸 | 单帧推理耗时 | 预估FPS |
|---|---|---|---|---|
| Python | 笔记本CPU | 368x368 | 150-300ms | 3-7 |
| C++ | 笔记本CPU | 368x368 | 80-150ms | 7-12 |
| Android | 中端手机CPU | 368x368 | 250-500ms | 2-5 |
| Android | 中端手机CPU | 256x256 | 100-200ms | 5-10 |
C++比Python快一倍左右,这很正常,Python在数据预处理、张量搬运上有额外开销,深度学习计算本身大部分都调到底层优化库。
C++端想继续提速,有几个方向:第一,如果CPU支持AVX2指令集,OpenCV DNN会在编译时自动优化,但Windows预编译包不一定开了全指令集;第二,换推理后端,OpenCV DNN支持OpenVINO,如果你的设备是Intel平台,开启后速度提升非常明显;第三,减小输入尺寸或者跳帧处理,比如每两帧推理一次,简单粗暴而且效果直接。
6. 常见问题排查与避坑速查表
6.1 Python侧高频问题
| 现象 | 原因 | 解决办法 |
|---|---|---|
ModuleNotFoundError: No module named 'cv2' | 未安装或装错环境 | pip install opencv-python,确认python环境 |
安装依赖报Microsoft Visual C++ 14.0 is required | 依赖包源码编译缺少MSVC | 安装Microsoft C++ Build Tools |
| 推理结果全是错的或骨架乱飞 | 模型通道顺序/输入尺寸不匹配 | 打印output.shape,检查swapRB参数 |
cv2.VideoCapture(0)黑屏 | 摄像头被占用或权限问题 | 关闭其他占用摄像头的程序,检查系统权限 |
再说一个经验之谈:调试模型时,先找一张单人、光线好的全身照做静态测试,不要一上来就接摄像头,否则画面里有走动的人,你会分不清到底是算法问题还是画面问题。
6.2 Android侧高频问题
OpenCV初始化失败是最常见的问题之一。现象是加载OpenCV库时抛UnsatisfiedLinkError,或者OpenCVLoader.initLocal()返回false。解决办法是确认Maven依赖已正确引入,并且静态初始化块确实执行了。还有稳妥做法:初始化时打印日志,确认so库加载路径。
相机预览方向不对。手机竖屏状态下,如果不对图像做旋转,检测出的姿势会横躺着。处理方式是在ImageAnalysis里根据imageProxy.getImageInfo().getRotationDegrees()对Mat做旋转,旋转后再送入模型。
模型加载失败。如果Assets里的模型没拷贝成功,readNetFromCaffe可能不会立刻报错,但forward时才会崩。我在拷贝后都会检查文件长度是否大于0,再开始推理。
6.3 C++侧高频问题
C++编译通过但运行找不到dll,这个最简单,把OpenCV的bin目录加到PATH,或者直接把dll放到exe同目录。
net.forward()返回空Mat,多半是模型加载时没报错但实际读取失败。建议在forward之前打印net.empty(),如果是true,检查模型路径和文件权限。
C++内存管理问题在长时间运行时特别明显。关键点数组、Mat对象如果在一个大循环里反复创建,内存会持续上涨。解决办法是循环体外创建对象,循环内复用;Mat赋值时用clone()或copyTo()显式拷贝,避免浅拷贝导致共享数据被意外修改。
6.4 三平台通用避坑
坐标映射是三个平台都容易出错的点。模型输出坐标是在46x46热图空间,映射回原图必须除以热图宽高,而不是模型输入尺寸368。这两个数值不一样,但很多人会搞混,导致关键点位置明显偏右偏上。
颜色通道顺序要统一。只要是在OpenCV生态里,blobFromImage的输入都是BGR顺序,后处理画图也都是BGR。一旦模型换过,要回到模型官方文档确认训练时的通道顺序,别盲目沿用swapRB=false的配置。
置信度阈值不要设得太低。我习惯设0.2,太低会出现大量虚点,画出的骨架像蜘蛛网;太高又会丢点,导致骨骼线断断续续。这个值可以根据实际场景微调,遮挡场景可以适当降到0.1,正规场景保持0.2到0.3都行。
最后,再把模型文件替换一下也顺带可以说清:如果你不想用OpenPose模型,换成MobileNet版的姿态模型或者MoveNet,只要模型输入是图像、输出是热图或关键点坐标,我前面讲的后处理流程稍微调整一下就能复用。重点还是先把整条链路跑通,再谈模型和性能优化。我自己做这类项目最大的体感是:跨平台部署最怕思路不统一,只要数据流设计好、模型文件统一,平台迁移真的只是换一层壳而已。