☰
pytorch-openpose 从原理到实战:身体与手部姿态估计完整指南
2026/10/11 21:15:20 网站建设 项目流程

简介:pytorch-openpose提供了一套基于PyTorch的OpenPose实现,覆盖身体与手部姿态估计,并将原始Caffe模型成功转换,适合希望摆脱Caffe依赖、在PyTorch中进行人体关键点检测研究与二次开发的开发者。压缩包共32个文件、约19.29MB,主要包含9个Python脚本、3个Jupyter Notebook、多张样例图片与GIF效果预览、依赖清单以及模型输出尺寸配置JSON等;Python脚本覆盖模型加载、数据处理、手部关键点检测与可视化等环节,Notebook则便于交互式调试和算法讲解。通过摄像头和视频演示脚本可直接完成实时推理,核心源码中身体与手部检测模块拆分清晰,结合样例图片和GIF可直观看到人体骨骼点、手部关键点的检测效果。项目利用身体姿态估计结果生成手部包围框,该思路还可延伸至人脸关键点检测,为后续功能扩展打下基础。资源包目录结构清晰,模型配置与核心源码分离,便于按需查阅与二次开发。已有3757人学习下载,适合正在入门或复现姿态估计项目的Python开发者。

1. 先搞清楚 pytorch-openpose 解决什么问题:当 Caffe 版 OpenPose 跑不起来时

做姿态估计的开发者几乎都遇到过同一个尴尬:论文里看好的 OpenPose 开源版本,一查依赖是 Caffe,编译环境跟现在系统一堆冲突,光装依赖就能耗掉一下午。pytorch-openpose 就是在这个背景下被需要的——它把 OpenPose 的网络结构和推理链路用 PyTorch 重新实现,身体关键点和手部关键点两套模型都有,模型文件也直接从原版转换成了 .pth,推理代码短到几十行。很多人担心 PyTorch 版是“削弱版”,但实测下来,只要权重转换对齐、预处理参数一致,关键点输出与原版几乎无差别,而部署和维护体验要好得多。这篇笔记适合要做手势交互、运动姿态分析、动作教学这类业务,却不想沾老 Caffe 环境的工程师。后面从原理讲到跑通,再把手部模型和那些容易翻车的细节一起说清楚。

2. 看懂身体与手部的两套网络:PAF 原理与模型结构拆解

2.1 PAF 部分亲和场:为什么关键点检测加连线才是 OpenPose 的护城河

普通的关键点检测网络输出是一张热图(heatmap),每个通道代表一个关键点,峰值位置就是坐标。但 OpenPose 除了热图,还多输出一个分支叫 PAF(Part Affinity Fields,部分亲和场)。PAF 的通道数比热图多一倍,每个肢体连接占用两个通道,分别表示该连接在 x、y 方向上的单位向量场。意思是,图上每一个像素位置,网络不仅要告诉你“这里有没有关键点”,还要告诉你“如果这里有条肢体,它应该朝哪个方向延伸”。

做多人匹配时,PAF 的价值就体现出来了。假设检测出三个左肩点和三个右肘点,到底哪两个是一对?常见做法是在候选的两个关键点之间连一条线段,然后沿着这条线段对 PAF 做积分,计算积分向量与线段方向的点积。如果路径穿过的区域和肢体方向高度一致,得分就高,再结合匈牙利算法或贪心匹配,把得分最高的点对配对成同一个人。这套机制让 OpenPose 不需要先做行人检测框,再在框里做单人的姿态估计,而是在整张图上同时推理,适合人多的场景。

下面是 Top-Down 方法和 OpenPose 这类 Part-Based 方法的直观对比:

对比项Top-Down(先检测人再回归关键点)OpenPose 的 Part-Based
是否依赖行人检测框强依赖,检测框漏人则直接失败不依赖,全图直接输出
多人重叠/遮挡时检测框相互覆盖,容易丢人对遮挡相对鲁棒,靠匹配拆解
单图延迟人越多推理次数越多一次前向固定开销
实现复杂度需要两个模型串联单模型多输出分支

如果你业务上对延迟不敏感但追求精确坐标,Top-Down 通常会更好;如果你要的是视频流里多人的实时关键点,OpenPose 这条路线更合适。pytorch-openpose 保留了这套 PAF 机制,所以模型文件里有热图分支也有 PAF 分支,后处理如果只取热图峰值,等于把一半能力浪费了。

2.2 身体 18 点与手部 21 点:两套输出头的模型结构差异

pytorch-openpose 的身体模型输出 18 个关键点通道,对应 COCO 风格的 18 点定义:鼻子、双眼、双耳、双肩、双肘、双腕、双胯、双膝、双踝。PAF 分支输出 19 条肢体连接,每条连接占 2 个通道,所以是 38 通道。手部模型则输出 21 个关键点:手腕 1 个,其余 5 个手指每指 4 个关节,手部连接定义成 20 条,PAF 分支输出 40 通道。

这两个任务为什么不能放进同一个网络?关键点数量差异只是表面原因,真正麻烦的是尺度差异。身体关键点的像素范围可能有几百个像素,手指关节在常见相机距离下只有十几二十像素,同一个骨干网络很难同时兼顾大尺度和小尺度特征。常见做法是训练两个独立模型,先用身体模型检测手腕位置,再按手腕位置把手部区域裁剪出来送进手部模型,这就是标题里“包括手和身体姿势估计”的实际含义:两条推理链路,两套权重,而不是一个模型输出两种点。

结构上,身体模型通常复用 VGG19 前若干层卷积做特征提取,之后分出两个分支,每个分支做多阶段卷积。每个阶段都会输出一次中间结果,训练时对每个阶段都加损失,推理时一般只取最后一个阶段的输出。手部模型的骨干更浅、输入尺寸更小,因为手部区域细节密度高,过深的网络反而容易过拟合。拿到模型的 .pth 文件后,先看 state_dict 里有没有两组不同规模参数,基本就能确认这两套网络的分工。

2.3 pytorch-openpose 的模块组成与权重迁移思路

pytorch-openpose 这类社区实现,核心代码通常分为三个部分:网络定义模块、推理工具模块、模型权重文件。网络定义里写着两个类,一个 BodyNet、一个 HandNet;推理工具负责图像预处理、热图峰值提取和 PAF 解析;权重文件对应原版 Caffe 模型转换出来。很多人下载代码后发现不会用,是因为没搞明白这三部分的边界,把网络定义和推理后处理混在一起猜。

如果你拿到的不是现成的 .pth,而是原始 Caffe 的 caffemodel,就需要自己做权重迁移。Caffe 的卷积层权重维度是[out, in, kh, kw],PyTorch 的 Conv2d 权重也是同样的维度,直接赋值矩阵本身没问题,麻烦的是层名映射。Caffe 里叫conv1_1的层,在 PyTorch 网络定义里可能叫features.0.weight,所以转换的核心工作是把 Caffe 层名逐个映射到 PyTorch 参数名。下面是一段简化后的键名重映射逻辑:

import torch def rename_caffe_state_dict(caffe_dict): mapping = { "conv1_1_w": "features.0.weight", "conv1_1_b": "features.0.bias", "conv2_1_w": "features.3.weight", "conv2_1_b": "features.3.bias", # 实际转换时这里是几十行层名映射表 } new_state = {} for k, v in caffe_dict.items(): if k in mapping: new_state[mapping[k]] = torch.from_numpy(v) else: # 没匹配上的层要单独处理,不能直接忽略 print(f"unmapped: {k}") return new_state

这段代码的核心逻辑是查表替换。实际转换时真正耗时间的不是写映射,而是逐层核对每个层在两边网络定义里的 stride、padding、dilation 是否一致。有一个参数容易忽略:Caffe 的填充是显式写在 prototxt 里的,PyTorch 的 padding 参数若不写就默认是 0,一旦两边差一个像素,前面几层看不出来,到输出热图时关键点位置整体偏移,而且这个偏移是累积的。转换完建议用同一张测试图,分别跑原版 Caffe 和 PyTorch 模型,对比最后一个 stage 输出张量的均值差,如果最大误差超过千分之一,基本就是中间某个卷积层参数对齐出问题了。

pytorch-openpose 的发布页通常会把转换这一步做好,你拿到的是可以直接加载的 .pth,所以第 3 章不再纠结转换脚本,而是专注于怎么把模型文件放对位置、跑通推理。

3. 环境与权重:把 pytorch-openpose 的模型文件备齐并跑通身体推理

3.1 环境选型:我一般锁这组版本

跑 pytorch-openpose 不需要追最新版本,反而建议锁一组稳定组合。Python 用 3.8 到 3.10 都行,PyTorch 用 1.13 及以上都能加载这套模型;OpenCV 用 4.x 系列,主要用它的图像读写和 resize;numpy 版本跟 PyTorch 配套就好,不要手动装太高版本,否则可能碰到二进制不兼容的警告。下面是我常用的依赖组合:

依赖推荐版本作用
Python3.8~3.10避免过新版本对部分库的兼容问题
PyTorch1.13~2.x加载 .pth、执行前向推理
opencv-python4.5~4.10图像读取、缩放、画点画线
numpy跟随 PyTorch 版本张量转数组、峰值索引计算

不建议用太老的 PyTorch,比如 1.0、1.1 那批,因为当时的张量 API 和现在差异较大,部分网络定义代码可能调用torch.nn.functional.upsample这类接口,旧版本的行为与新版interpolate不一致,图像尺寸对不上后处理就全错了。有同事在这上面踩过坑,升到 1.13 后问题消失。操作系统方面 Windows 和 Linux 都能跑,但生产环境我建议 Linux,CPU 推理时线程调度更可控,也不会出现 Windows 上 OpenCV 读取中文路径返回空图的问题。

3.2 权重文件两个,别只下一个

pytorch-openpose 的权重文件通常有两个:身体模型权重和手部模型权重。如果你只下身体模型,前面身体推理一切正常,到手部环节才发现模型加载失败,回头再找文件又是一通折腾,这类问题在项目讨论区里很常见。更隐蔽的情况是文件结构不对——有人把 PAF 相关的中间张量文件当成了模型权重,加载时直接报尺寸不匹配。

文件放哪里也有讲究。常见做法是在项目目录下建一个models文件夹,两个 .pth 文件放进去,和网络定义模块平级。有些版本的代码会在启动时自动从固定路径读取,路径写死成models/body_pose_model.pth,你随便改目录名就会在torch.load那一步报文件不存在。建议先保持默认路径跑通一遍,再按需求改配置。

如果你拿到的不是 .pth 而是 Caffe 模型,需要先完成一次权重转换。合理路径是:先确认手上是 caffemodel 还是已经转换好的 PyTorch 权重,用下面这段检查代码快速确认:

import torch state = torch.load("body_pose_model.pth", map_location="cpu") # 只看键名结构,不关心具体数值 keys = list(state.keys()) print(len(keys)) print(keys[:5])

加载脚本的注意点是map_location="cpu"。如果你是在 CPU 机器上检查 GPU 训练出的权重,不加这个参数会在设备不匹配时报错,加上它就能先看键名结构再决定后续处理。另外,新版本 PyTorch 默认把权重保存为安全格式,旧项目里某些通过torch.load直接执行 pickle 的写法,换成新版 PyTorch 后可能报allow_pickle相关错误,解法是给torch.load显式加参数即可。

3.3 跑通人体姿态推理的最小脚本

下面这个脚本是完整可运行的,网络定义部分用项目自带的 BodyModel 类,这里重点展示预处理、推理、热图峰值提取和坐标还原这条链路:

import cv2 import numpy as np import torch from openpose_network import get_body_model # 项目里的网络定义 BODY_INPUT_SIZE = 368 # 身体模型输入尺寸 STRIDE = 8 # 网络下采样倍数,热图尺寸=输入/8 THRESHOLD = 0.3 # 关键点响应阈值 # 1. 加载模型并切到 eval 模式 model = get_body_model() model.load_state_dict(torch.load("models/body_pose_model.pth", map_location="cpu")) model.eval() # 2. 读取图像并预处理:resize + 归一化到 [-0.5, 0.5] img = cv2.imread("test.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized = cv2.resize(img_rgb, (BODY_INPUT_SIZE, BODY_INPUT_SIZE)) img_norm = img_resized.astype(np.float32) / 255.0 - 0.5 img_tensor = torch.from_numpy(img_norm.transpose(2, 0, 1)).unsqueeze(0) # 3. 前向推理:outputs[0] 是 PAF,outputs[1] 是热图 with torch.no_grad(): paf, heatmap = model(img_tensor) heatmap = heatmap[0].cpu().numpy() # 形状 [18, 46, 46] # 4. 逐关键点找峰值并还原到原图坐标 scale_x = img.shape[1] / BODY_INPUT_SIZE scale_y = img.shape[0] / BODY_INPUT_SIZE keypoints = [] for ch in range(heatmap.shape[0]): hm = heatmap[ch] _, max_val, _, max_loc = cv2.minMaxLoc(hm) if max_val < THRESHOLD: keypoints.append(None) continue x = int(max_loc[0] * STRIDE * scale_x) y = int(max_loc[1] * STRIDE * scale_y) keypoints.append((x, y, float(max_val))) # 5. 可视化:画点 for kp in keypoints: if kp is not None: cv2.circle(img, (kp[0], kp[1]), 4, (0, 255, 0), -1) cv2.imwrite("body_result.jpg", img)

脚本里三个关键参数要理解:BODY_INPUT_SIZE=368是模型训练时的输入分辨率,大部分转换过来的权重都对齐这个尺寸,随意改成 320 或 416 会影响热图感受野和关键点精度;STRIDE=8是网络下采样倍数,368 输入对应 46×46 的热图,峰值在热图上的坐标乘以 8 才能回到 368 分辨率下的坐标,再用scale_x、scale_y还原到原始图像尺寸;THRESHOLD=0.3是峰值响应阈值,画面里人比较小时热图峰值会整体偏低,调低阈值会导致随机位置出现假点,调高则可能漏检。

跑通这一步后,你就有了身体 18 个关键点的坐标。下面要做手部估计,需要先用身体模型输出的手腕点做区域裁剪,这是第 4 章的内容。

4. 手部 21 点推理:区域裁剪、关键点还原与坐标对齐

4.1 先跑身体模型,再用手腕坐标裁剪 ROI

手部估计和身体估计不是并行跑的,而是串行。常见的实现顺序是:先跑一遍身体模型,拿到左腕和右腕两个关键点坐标,然后以手腕为中心,切出一块包含整只手的矩形区域,放大后送进手部模型。原因在前面说过,手部区域在整张图里占比太小,直接全图推理手部网络,特征图里手指之间只差一两个像素,输出热图根本区分不开。

裁剪区域怎么定?我一般以手腕点为圆心,取手部区域边长为“手腕到指尖距离”的两倍左右,但这个距离在身体模型里没有直接输出,所以常见做法是用一个固定外扩比例,例如以手腕为中心向外扩 1.8 到 2.0 倍的手腕检测框宽度。假如你的身体模型还输出了肘部坐标,可以用手腕到肘部的距离估算手掌长度,这样更稳。下面这段裁剪代码可以直接接在身体推理脚本后面:

def crop_hand_roi(img, wrist_point, crop_size=256): h, w = img.shape[:2] cx, cy = wrist_point # 以手腕为中心裁剪一个正方形区域,边长是 crop_size half = crop_size // 2 x1 = max(0, cx - half) y1 = max(0, cy - half) x2 = min(w, cx + half) y2 = min(h, cy + half) roi = img[y1:y2, x1:x2] return roi, (x1, y1)

裁剪时注意两个边界问题。第一,手腕点如果位于图像边缘,裁剪框会被图像边界截断,导致手部区域被切掉一部分,此时应该给裁剪框一个外扩补偿,比如允许 ROI 超出图像边界一定像素并用零填充,而不是强行走 clip。第二,裁剪框太正好会把手腕附近的手掌边缘切掉,我习惯在算出坐标后再乘以 1.2 的扩展系数,宁多勿少。ROI 拿到后,还需要 resize 到手部模型的输入尺寸,常见是 256×256,具体以你手上权重训练时用的尺寸为准,可以从手部网络的 forward 断点看它接收的 tensor 尺寸。

4.2 手部模型推理与 21 点坐标还原

手部模型的推理过程和身体模型几乎一样,区别在于输入不再是全图,而是裁剪出的 ROI,输出热图尺寸也比身体模型小。下面这段代码完成 ROI 推理和坐标还原:

import cv2 import numpy as np import torch from openpose_network import get_hand_model HAND_INPUT_SIZE = 256 STRIDE = 8 THRESHOLD = 0.3 hand_model = get_hand_model() hand_model.load_state_dict(torch.load("models/hand_pose_model.pth", map_location="cpu")) hand_model.eval() def estimate_hand(hand_model, roi, offset_x, offset_y): # 预处理 ROI,归一化方式与身体模型保持一致 roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) roi_resized = cv2.resize(roi_rgb, (HAND_INPUT_SIZE, HAND_INPUT_SIZE)) roi_norm = roi_resized.astype(np.float32) / 255.0 - 0.5 roi_tensor = torch.from_numpy(roi_norm.transpose(2, 0, 1)).unsqueeze(0) with torch.no_grad(): _, heatmap = hand_model(roi_tensor) heatmap = heatmap[0].cpu().numpy() # 形状 [21, 32, 32] scale_x = roi.shape[1] / HAND_INPUT_SIZE scale_y = roi.shape[0] / HAND_INPUT_SIZE points = [] for ch in range(heatmap.shape[0]): hm = heatmap[ch] _, max_val, _, max_loc = cv2.minMaxLoc(hm) if max_val < THRESHOLD: points.append(None) continue x = int(max_loc[0] * STRIDE * scale_x) + offset_x y = int(max_loc[1] * STRIDE * scale_y) + offset_y points.append((x, y, float(max_val))) return points

关键点在offset_x、offset_y上。ROI 内部的坐标只是相对坐标,必须加上裁剪框左上角在原图中的位置,才能回到原图坐标系。很多人画出来的手部关键点整体跑到图外,就是因为忘了加偏移。另外,手部关键点里第 0 个点对应手腕,第 1 到第 4 个点对应拇指的 4 个关节,第 5 到第 8 个点对应食指,以此类推。如果你想按真实手指结构连线,直接用固定的索引表跳连就行。

后处理有个细节值得注意:热图峰值提取用cv2.minMaxLoc拿全局最大点,但这只适用于单个手部区域。如果 ROI 里同时出现两只手,或者手部区域有强干扰物,某个通道可能出现多个相近峰值,这时应该把minMaxLoc换成 3×3 邻域极大值筛选,取响应最大且彼此距离大于一定像素的若干个峰值。这个改动对单手场景没有影响,但能避免双手交叉时关键点跳来跳去。

4.3 手部关键点左右手区分与常见失败形态

左右手不需要额外分类。身体模型输出左腕和右腕两个独立关键点通道,你按 4.1 的方式分别裁剪左腕和右腕两个 ROI,各自推理就能自然分出左右手。有个坑是镜像问题:如果你出于某些原因对输入图像做了水平翻转,身体模型的左右定义也会跟着翻转,左腕通道对应的实际是画面右侧的人体左腕,坐标和后处理都不用改,但业务层如果要把结果映射到“物理左手”,这里需要做一次左右交换。

手部估计最常见的失败形态是手指“粘在一起”,也就是相邻手指的关键点几乎重叠。原因通常是 ROI 分辨率不够,手指在 256×256 里已经被压缩到一两个像素。解决方式不是换更大的模型,而是在裁剪时用更高分辨率保留 ROI,例如把 256 的 ROI 放大到 368,再送进手部模型,配合 STRIDE=8 仍然有效。这套改法不会增加模型计算量太多,但对指尖分离度改善明显。

5. 避坑清单:五个真实场景下的报错排查与效果修复

5.1 现象:代码运行时报No module named 'caffe.contrib'

这是个一眼懵的报错。pytorch-openpose 按理是纯 PyTorch 项目,但网上流传的代码版本里偶尔残留了 Caffe 时代的片段,页面上复制下来的脚本里还带着import caffe或from caffe import layers。还有一个来源是某开发者为了调试顺手从旧项目里复制了工具函数,结果把 Caffe 也带了进来。

原因就是代码版本混杂。解决方法是全文搜索import caffe、caffe.这类关键字,把无关引用删掉。如果删除后发现报错变成缺少某个函数定义,说明网络定义模块里还有残留依赖,回到发布页下载未修改的network.py替换即可。这里补一句:不要为了让脚本跑通去装 Caffe,只为跑一个推理脚本去搭一套老环境,属于典型的投入产出倒挂。

5.2 现象:模型加载正常但推理输出全部接近零或 NaN

模型加载没有报错,图像预处理也没有异常,但热图输出全部是接近 0 的小数值,或者某些通道出现 NaN。这个坑在社区里很常见,九成出在预处理归一化不一致上。原版训练时用的是(image / 256) - 0.5,而你自己写成了(image / 255) - 0.5,二者只差千分之四,但经过多层卷积累积,热图响应会被整体压制到阈值以下,视觉上就是“模型失效”。

解决方法是先别改代码,而是确认你手上权重对应的预处理方式。我一般会在网络的定义文件里搜一下是否有transform、normalize相关注释,或者直接在 GitHub 项目的 README 里找示例代码,以它为准。不想猜的话,就做一个对照实验:把两种归一化都跑一遍,看哪边热图峰值更接近 1,哪边就是对的。

5.3 现象:手部关键点全落在 ROI 边界上,排成一条线

手部推理结果画出来,21 个点不是分布在各手指上,而是全部挤在裁剪框的某一条边附近。这个现象几乎可以断定是手腕定位偏移或者 ROI 裁剪太紧。手腕点本身偏了几个像素,放大到手部 ROI 后这个偏差被放大好几倍,手实际上被切掉一大半,剩余部分的关键点都在图像边缘。

解决方法是两步:第一,确认身体模型输出的手腕点是否准确,可以在全图上先把手腕点画出来,人工核对;第二,把裁剪框外扩,我一般以手腕为中心、取裁剪尺寸的 1.5 到 2 倍作为实际 ROI 边长,再重新推理。注意外扩后 ROI 面积变大,手在 ROI 中的占比变小,如果手部模型输入是固定 256,适当调高 ROI 提取分辨率,别让手占得太小。

5.4 现象:多人场景下骨骼连线乱配,跨人连成骨架

单个人没问题,两个人或三个人站一起时,输出的骨头把 A 的肩膀连到了 B 的胳膊肘,整个骨架是错的。这是因为 PAF 解析阶段的连线置信度阈值设太低,错误连线也能通过积分打分。OpenPose 的贪心匹配本质是找每个点对之间“局部最优”,阈值低时会放过大量低质量连接,导致同一个关键点被多次使用,甚至跨人连接。

解决办法是把 PAF 的连线阈值从默认的 0.5 往上调,常见有效区间在 0.7 到 0.9 之间。同时加上一个硬约束:每个关键点在同一轮匹配里最多只能被使用一次,已经被某条肢体占用的点不能参与第二次匹配。这两个条件同时生效后,基本能消除跨人连错问题。代价是个别遮挡严重的关节会直接从骨架中断开,这比连错更好处理,因为可以靠前后帧插值修复。

5.5 现象:CPU 推理一张图要十几秒,完全没法用

pytorch-openpose 默认输入是 368×368,身体模型有两个分支多阶段卷积,计算量本来就大。如果在没有 GPU 的环境里直接跑原版网络,单张图十几秒是常态,这还没算手部模型的两次推理。很多人在这一步就放弃了,但其实大部分业务根本不需要用 GPU,优化一下就能实时。

优化路径有两步。第一步,把输入从 368 降到 256 或 224,关键点精度会下降一些,但多数交互场景足够用;第二步,用torch.jit.trace把模型导出成 TorchScript,去掉 Python 层调度开销。下面是一段导出代码:

import torch from openpose_network import get_body_model model = get_body_model() model.load_state_dict(torch.load("models/body_pose_model.pth", map_location="cpu")) model.eval() example = torch.randn(1, 3, 368, 368) traced_model = torch.jit.trace(model, example) traced_model.save("body_pose_script.pt")

导出的 TorchScript 模型可以用torch.jit.load直接加载,推理速度通常能提升 20% 到 40%,且可以交给torch.compile进一步优化。如果场景还要压,可以考虑转 ONNX 后用推理框架做动态尺寸输入和半精度推理,但那是另一套部署链路了,不在本篇展开。

6. 把 PAF 热图接到自定义后处理:业务化落地的一点点经验

前面的链路已经能输出身体 18 点和手部 21 点了,但实际项目里很少直接消费这些点坐标——业务侧要么要做动作分类,要么要做手势指令映射,要么需要把关键点归一化后送给前端渲染。内置的绘图函数只能在调试时用,真正业务化时,我习惯自己做一层封装,让输入一张图片,输出一个结构化字典。

封装的关键是把身体模型和手部模型的调用放在同一个类里,内部按顺序跑,对外只暴露一个方法:

class PosePipeline: def __init__(self, body_model, hand_model): self.body = body_model self.hand = hand_model def __call__(self, img): body_points = estimate_body(self.body, img) hand_left = estimate_hand_cropped(self.hand, img, body_points[9]) hand_right = estimate_hand_cropped(self.hand, img, body_points[10]) return { "body": body_points, "left_hand": hand_left, "right_hand": hand_right, }

这里body_points[9]和body_points[10]对应身体模型输出索引里的左腕和右腕,具体索引要看你的权重训练定义的 COCO 顺序。业务层拿到这个字典后,再把坐标统一除以图像宽高得到 0 到 1 之间的归一化坐标,这样前端渲染时不管屏幕尺寸是多少都不会错位。有一类常见问题是前端拿到归一化坐标后画面仍然错位,多半是后端返回的是 ROI 坐标系坐标,忘了加裁剪框偏移,或者坐标系方向是从左上角还是左下角起算没对齐。

我做过一个手势识别 Demo,第一版直接用了内置的画图函数截图给前端用,结果前端要的是每个手指关节点坐标,不得已又改了一版,把热图峰值和后处理坐标全部导出成 JSON,前后端联调才顺畅。这个教训后来固化成习惯:任何模型类项目,第一版交付就输出结构化关键点坐标,而不是只交付可视化结果。毕竟坐标是数据,可视化只是数据的呈现方式。代码写到这里,身体和手部两条推理链路从原理到落地,再到排查和封装,已经是一条完整可复现的路径了,希望对你能有实际帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询