
简介面向需要快速落地人体姿态估计的开发者与计算机视觉学习者现分享一份可直接运行的完整工程以YOLOv5为基础检测器配合HRnet高分辨率网络与SimDR回归方法对图片、视频以及摄像头实时画面中的多人关键点进行定位省去自行下载权重与调试参数的环节。压缩包共两千个文件其中绝大部分为Python脚本涵盖边界框预测、关键点回归、骨骼绘制与调用入口另有数十个C/C源文件与头文件用于底层算子加速和扩展文本配置、说明文档以及接口描述文件辅助项目阅读压缩后约八百四十一点五兆字节。已有约一千七百五十七人参与学习。工程内部按项目准备、目标检测、姿态估计、结果演示、报错分析五个模块组织介绍从克隆仓库到环境配置从导入权重到添加SPPF结构从照片、视频到实时摄像头的完整流程同时针对常见的路径配置问题、PyTorch上采样算子警告、Matplotlib后端设置及GBK编码错误给出具体修正思路使用者只需把路径改成自己的即可一键体验关键点检测全流程。1. YOLOv5HRnet为什么“检测关键点”是当前性价比最高的姿态估计方案做人体姿态估计的需求这几年越来越多健身App的动作计数、体育训练的姿态纠偏、安防场景的摔倒检测、人机交互里的手势意图判断核心都落到同一个任务上——先把画面里的人找出来再定位出他身体的关节位置。很多人第一个想到的方案是直接上端到端的姿态估计模型但实际落地时你会发现复杂场景下人的漏检率、关键点的抖动程度、以及单人推理时浪费掉的算力都会让项目陷入反复调参的泥潭。我的习惯是先做目标检测、再做关键点回归也就是标题里说的 YOLOv5HRnet 组合用 YOLOv5 出人体框把框裁剪出来再送进 HRnet 做 17 个关键点的热图回归。这套方案不是最新潮的但它是目前在工程上最稳的。YOLOv5 检测人体的成熟度不用多说HRnet 作为姿态估计的骨干网络最大的特点是全程保持高分辨率特征图不像常见的编码-解码结构那样把特征压扁再拉回来小关节和遮挡关节的定位精度明显更好。对实时性有要求的场景两阶段方案反而更容易控耗时——检测器负责稀疏计算姿态网络只处理有人的区域比单阶段全图密集预测更省。这篇文章会把原理、数据集准备、核心代码、参数设置和踩坑记录全部摊开讲适合想快速把姿态估计跑通并部署到业务的开发者。2. 选型与原理检测框和热图回归各自解决什么问题2.1 YOLOv5做人体检测选哪个体量输入分辨率怎么定YOLOv5 按网络深度和宽度分成 n/s/m/l/x 几个体量做人体检测我一般推荐从 YOLOv5s 起步。原因很直接人体是大目标占画面比例通常不小s 体量在 COCO 上的 mAP 已经够用单张 640×640 的推理在普通 GPU 上能跑到 2~3ms在 CPU 上用 OpenVINO 转换后也能到 30ms 量级。除非你的场景里人特别小——比如监控画面拍走廊人只有几十个像素高——才需要上 m 甚至 l 体量否则 s 是性价比最高的选择。输入分辨率直接影响小目标召回。YOLOv5 默认的 640×640 在大多数场景够用但如果你用 1080p 摄像头且人的占比小于画面的十分之一我建议把推理分辨率提到 960 或者 1280。代价是检测耗时近似平方增长所以先算算你的帧率预算再定。实际部署时还有个细节保持检测输入和训练输入一致否则框的坐标会偏移。YOLOv5 的推理会自动做 letterbox也就是等比缩放后补灰边这一步在姿态估计阶段同样要做后面细说。锚框方面YOLOv5 在训练时会自动从数据集里聚类出锚框人体这种宽高比比较集中的类别不需要特别调整。真正要调的是置信度阈值。人体检测我习惯把置信度设在 0.45~0.5NMS 的 IoU 阈值设在 0.45 左右——阈值设高了容易漏人设低了姿态估计阶段会被无意义的背景框拖慢。2.2 HRnet关键点回归高分辨率特征为什么更稳HRnet 的核心创新说起来很简单大多数姿态估计网络用类似沙漏的结构把图片下采样到 1/4、1/8、1/16 分辨率提取抽象特征再上采样回原分辨率预测关键点低分辨率特征图上的小误差在上采样过程会被放大。HRnet 反其道而行从输入开始就保持一份 1/4 分辨率的高分辨率特征图同时并行地往下采样出 1/8、1/16、1/32 的低分辨率分支多尺度特征之间反复融合让每个分支都能拿到其他尺度的信息。这意味着腕部、踝部这种小关节的位置信息从输入到输出都被完整保留不会经历下采样丢失再找回的过程。COCO 关键点榜单上 HRnet 一直是性价比标杆正因为这个设计。常用的 HRnet 变体有 HRNet-W32 和 HRNet-W48W 代表通道宽度W48 精度更高但参数量和显存占用明显更大。实时场景我一般选 W32配上 192×256 的输入分辨率单帧关键点推理在 GPU 上能控制在 10ms 内。关键点回归不是直接回归坐标而是预测热图。每个关键点对应一张二维高斯峰图网络输出的通道数是 17COCO 定义的人体关键点数每张热图的尺寸是输入分辨率的 1/4比如 192×256 输入对应 48×64 热图。推理时取热图上最大值的位置乘以下采样倍数得到关键点坐标。2.3 两阶段串联还是端到端联合为什么选两阶段有人会把 YOLOv5 的检测头去掉直接接 HRnet 做端到端训练让检测和关键点共享一个骨干网络。这么做的好处是省一次特征提取的耗时坏处也很明显姿态估计的精度会掉。原因不难理解目标检测的损失函数和关键点热图的损失函数尺度不同、收敛速度不同联合训练时很容易互相干扰尤其是关键点这一路本来就需要更精细的空间特征。两阶段方案的优势在于每个阶段都可以独立调优。检测漏了人你可以单独调检测的阈值和分辨率关键点偏了你可以单独换 HRnet 的变体或调整热图 sigma。而且两阶段天然支持多人场景——检测器先输出多个人体框姿态网络挨个处理就行不需要额外写多人分组逻辑。代价是每帧多了几次前向推理但实践中只要单帧人数在个位数量级GPU 上依然能保持实时。如果你的目标是移动端部署两阶段也是最稳妥的拆分方式检测和姿态可以分别做模型转换。3. 环境与数据准备COCO关键点子集到自定义标注的最小闭环3.1 环境搭建与依赖清单先说环境。YOLOv5 在 PyTorch 生态里HRnet 现在也有大量 PyTorch 实现两者可以共存于同一个工程。我建议用 conda 建一个独立的 Python 3.8 环境PyTorch 版本取 1.10 以上即可CUDA 版本按你的显卡驱动选。HRnet 仓库里通常自带简单的依赖但很多实现是研究向的代码直接跑不一定顺手我一般只用它的模型定义和权重推理逻辑自己写。安装完依赖后第一步是验证两段模型各自能跑通。用 YOLOv5 官方权重跑一张含人的图片看检测框输出从 HRnet 官方权重跑同一张图片看 17 个点的热图输出。这一步的目的是尽早发现环境层面的问题比如 torchvision 版本不匹配、权重下载路径错误、GPU 显存不足等。很多项目翻车都是死在这第一步后续所有调试都建立在“模型单测通过”的前提上。conda create -n pose python3.8 conda activate pose pip install torch1.12.0 torchvision0.13.0 --extra-index-url https://download.pytorch.org/whl/cu113 pip install ultralytics opencv-python numpy pyyaml tqdm git clone https://github.com/ultralytics/yolov5.git pip install -r yolov5/requirements.txt代码逻辑说明conda 建独立环境是防止依赖冲突的老办法尤其是 torch 和 torchvision 的搭配必须严格对应。ultralytics 的 YOLOv5 包已经足够新通过 git clone 拉取官方仓库是为了获取预训练权重和数据集配置脚本。HRnet 模型定义通常直接以 Python 文件方式引入工程不需要额外安装。3.2 数据集选择COCO val2017 起步自定义数据怎么标注第一阶段开发我强烈建议直接用 COCO 2017 的 val 集做验证不用自己标数据。COCO 的 person 类别有精确的边界框标注同时包含 17 个关键点标注天然适配两阶段方案。你需要下载 val2017 的图片和标注 JSON筛选出包含 person 的图片。总共约 5000 张足够验证流程跑一次全量验证在 GPU 上也就十几分钟。如果业务场景特殊比如只有俯视视角或者需要识别特定服装的人就必须准备自定义数据集。检测阶段你需要把标注转成 YOLO 格式即每个框一行类别ID、归一化中心x、归一化中心y、归一化宽、归一化高。姿态阶段需要把关键点标注整理成 COCO 的 JSON 格式。常见做法是用开源标注工具画出框和点再写脚本导出。这一步很多人被卡住因为标注工具的导出格式五花八门转换脚本容易出坐标错位问题。# 将 COCO person 标注转为 YOLO 检测格式 import json import os ann_file annotations/person_keypoints_val2017.json img_dir val2017 out_dir labels with open(ann_file, r) as f: data json.load(f) img_info {img[id]: img for img in data[images]} anns_by_img {} for ann in data[annotations]: if ann[category_id] ! 1: # COCO person 类别 ID 为 1 continue anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, anns in anns_by_img.items(): img img_info[img_id] w, h img[width], img[height] lines [] for ann in anns: bx, by, bw, bh ann[bbox] x_center (bx bw / 2) / w y_center (by bh / 2) / h lines.append(f0 {x_center:.6f} {y_center:.6f} {bw / w:.6f} {bh / h:.6f}) name img[file_name].replace(.jpg, .txt) with open(os.path.join(out_dir, name), w) as f: f.write(\n.join(lines))代码逻辑说明COCO 标注里 person 的 category_id 是 1先把标注按 image_id 分组再对每张图写出 YOLO 格式的标签文件。注意 bbox 的坐标是左上角 x、y 加宽高必须换算成归一化的中心点表示。这段脚本过滤掉了没有 person 的图片但保留了同一张图里的多个人框多人数据是姿态估计泛化能力的关键。3.3 关键点标签转换从 COCO JSON 到热图训练需要的数据结构HRnet 训练需要的是输入图像和对应的 17 张热图。COCO 的关键点标注是 17 个坐标点加一个可见性标志0 表示未标注1 表示标注但遮挡2 表示可见。训练时通常只计算可见关键点的损失被遮挡的点不参与反向传播否则模型会学到错误的偏差。热图的生成方式是在每个关键点的坐标位置铺一个高斯峰sigma 决定峰的宽度。sigma 设太大相邻关键点的高斯峰会重叠空间分辨能力下降sigma 设太小峰值过于尖锐训练初期误差梯度难传导。我一般取 sigma2配合 192×256 的输入分辨率热图尺寸为 48×64一个像素的偏差折合回原图大约 4 个像素精度在可接受范围。import numpy as np import cv2 def generate_heatmap(shape, keypoints, sigma2): shape: 热图尺寸 (h, w)如 (64, 48) keypoints: 17 个关键点坐标形状 (17, 3)第三位是可见性 num_joints keypoints.shape[0] heatmaps np.zeros((num_joints, shape[0], shape[1]), dtypenp.float32) for i in range(num_joints): x, y, v keypoints[i] if v 1: continue x, y int(x * shape[1]), int(y * shape[0]) # 归一化坐标映射到热图尺寸 if x 0 or y 0 or x shape[1] or y shape[0]: continue # 生成以 (y, x) 为中心的高斯分布 xx, yy np.meshgrid(np.arange(shape[1]), np.arange(shape[0])) heatmaps[i] np.exp(-((xx - x) ** 2 (yy - y) ** 2) / (2 * sigma ** 2)) return heatmaps代码逻辑说明输入的关键点坐标是归一化的需要先映射到热图分辨率。可见性为 0 的点直接跳过热图全零训练时对应位置的损失也会被掩码掉。高斯分布的归一化系数这里省略了因为后面会用 MSE 或加权的 MSE 损失常数项不影响梯度方向。sigma 的值要和热图分辨率挂钩分辨率越高 sigma 可以适当加大我常年在 2~3 之间试。3.4 训练参数学习率、批大小、数据增强的实践经验HRnet 的训练参数在官方实现里已经给了一组不错的默认值但按我的经验有几个点必须根据你的显卡调整。批大小直接决定 BN 层的稳定性HRnet 的 BN 对批大小敏感我建议至少 32显存不够就降低输入分辨率而不是减少批大小。学习率用 Adam 的话我习惯初始 1e-4每训练 30 个 epoch 衰减 0.1 倍总共训练 120~150 个 epoch。数据增强里最关键的是随机旋转和随机缩放。人体关节在旋转后的坐标需要做对应变换所以增强必须发生在坐标层面而不是只增强图像。常见做法是先把图像旋转一个随机角度再把关键点坐标做同样的旋转最后生成热图。水平翻转对关键点有影响左右对称的关键点要交换索引这个坑很容易踩——忘了换索引模型精度会掉 3~5 个点。4. 实现步骤YOLOv5检测人体框 HRnet关键点回归的落地代码4.1 检测人体框加载模型、过滤置信度、坐标映射在两阶段推理管线里第一步是拿 YOLOv5 检测出所有 person 框。这里我不用 ultralytics 的封装接口而是直接走 PyTorch 推理这样可以更精确地控制中间步骤尤其是把检测框坐标映射回原图坐标避免 letterbox 带来的偏移问题。import torch import cv2 import numpy as np model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.45 model.iou 0.45 model.classes [0] # 只检测 person 类别 def detect_persons(image): results model(image, size640) boxes results.xyxy[0].cpu().numpy() # 格式: [x1, y1, x2, y2, conf, cls] person_boxes [] for box in boxes: x1, y1, x2, y2, conf, cls box if cls ! 0: continue # 过滤掉过小的人体框比例小于原图 1/20 的一般是误检 if (x2 - x1) image.shape[1] / 20 or (y2 - y1) image.shape[0] / 20: continue person_boxes.append([int(x1), int(y1), int(x2), int(y2), conf]) return person_boxes参数说明conf0.45 意味着置信度低于这个值的一律丢弃实际用下来阈值调到 0.4~0.5 之间最稳。classes[0] 让模型只输出 person 类别省掉其他类别的计算。size640 是最常用的推理分辨率如果此前训练时用的是 1280这里必须改成一致。图中的 area 过滤是经验值监控场景下非常靠下的小人往往是误检直接跳过也能节省姿态推理的时间。4.2 人体裁剪与预处理保持宽高比的 letterbox 必须对齐HRnet 的输入是固定尺寸的 192×256直接把检测框拉伸到该尺寸会导致关节变形尤其当人体框宽高比和 192×256 不一致时骨骼比例会失真。正确做法是先等比缩放到目标尺寸附近再补灰边推理完成后把热图坐标还原时再做逆向映射。这一步的坑在于检测框往往比身体区域大一圈直接裁剪会包含较多背景关键点反而容易偏移。我一般会把检测框向四周各扩 20%保证手和脚都在框内。对于边缘处的框扩完后要限制在原图范围内不要越界。def crop_and_resize(image, box, target_h256, target_w192): x1, y1, x2, y2 box[:4] # 向外扩展 20% pad 0.2 w, h x2 - x1, y2 - y1 x1 max(0, int(x1 - pad * w)) y1 max(0, int(y1 - pad * h)) x2 min(image.shape[1], int(x2 pad * w)) y2 min(image.shape[0], int(y2 pad * h)) crop image[y1:y2, x1:x2] crop_h, crop_w crop.shape[:2] # 等比缩放 灰边补齐到 target 尺寸 scale min(target_w / crop_w, target_h / crop_h) new_w, new_h int(crop_w * scale), int(crop_h * scale) resized cv2.resize(crop, (new_w, new_h)) canvas np.full((target_h, target_w, 3), 114, dtypenp.uint8) offset_x (target_w - new_w) // 2 offset_y (target_h - new_h) // 2 canvas[offset_y:offset_y new_h, offset_x:offset_x new_w] resized return canvas, scale, offset_x, offset_y, (x1, y1)代码逻辑说明扩展比例 20% 是个经验值太小会切掉手腕太大会引入过多背景干扰。等比缩放的关键在于用 min 求出缩放系数保证图片在长宽两个方向都不超出目标尺寸然后将缩放后的图贴到灰色画布的正中央。offset 值在推理结束后做坐标还原——从热图坐标换算回原图坐标时需要先减去 offset再除以 scale最后加上裁剪框左上角的坐标。4.3 HRnet推理从模型加载到热图坐标还原模型加载部分我用 hrnet 官方仓库里定义的 PoseHighResolutionNet 结构权重文件下载后放到本地用 torch.load 加载。推理时要留意输入归一化方式官方实现用的是 ImageNet 的 mean 和 std不是简单的 0~1 归一化直接套用 opencv 的常规预处理会导致关键点定位明显偏移。import torch import torch.nn.functional as F def load_hrnet(model_path, num_joints17): from models import PoseHighResolutionNet # 假设从官方仓库导入 model PoseHighResolutionNet(num_jointsnum_joints) state torch.load(model_path, map_locationcpu) model.load_state_dict(state) model.eval() return model.cuda() def preprocess_hrnet(canvas): # 从 uint8 转为 float 并归一化到 ImageNet 分布 x canvas.copy().astype(np.float32) x x[:, :, ::-1].transpose(2, 0, 1) # BGR - RGB, HWC - CHW mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) x x / 255.0 x (x - mean[:, None, None]) / std[:, None, None] return torch.from_numpy(x).unsqueeze(0) def inference_hrnet(model, canvas): x preprocess_hrnet(canvas).cuda() with torch.no_grad(): heatmaps model(x) # 输出形状 (1, 17, 64, 48) # 取最大值位置乘以下采样倍数还原到输入分辨率 heatmap_resized F.interpolate(heatmaps, size(canvas.shape[0], canvas.shape[1]), modebilinear, align_cornersFalse) coords [] scores [] batch heatmap_resized[0].cpu().numpy() for i in range(batch.shape[0]): hm batch[i] idx np.unravel_index(np.argmax(hm), hm.shape) coords.append([idx[1], idx[0]]) # (x, y) scores.append(hm[idx]) return np.array(coords), np.array(scores)代码逻辑说明为什么不直接算热图最大值因为 64×48 的热图尺寸太小直接取整坐标会有最大 4 像素的量化误差。先用双线性插值把热图上采样到输入分辨率 256×192再做 argmax精度能提升接近 1 个像素。这种做法对实时性影响很小因为只是多了一次小尺寸的上采样操作。score 值表示该关键点的置信度当某个点被遮挡时 score 会明显偏低可以作为后续逻辑的过滤依据。4.4 坐标还原到原图一套完整的推理管线把检测、裁剪、姿态推理、坐标还原串成一条管线这就是生产环境里可以直接用的原型。坐标还原是最容易出错的环节表面上只是算个映射关系但五个参数裁剪框坐标、scale、offset、热图上采样倍率、检测框扩展中任何一个出错关键点都会偏移得莫名其妙。def full_pipeline(image, det_model, hr_model): person_boxes detect_persons(image) all_keypoints [] all_scores [] for box in person_boxes: canvas, scale, offset_x, offset_y, (bx1, by1) crop_and_resize(image, box) coords, scores inference_hrnet(hr_model, canvas) # 坐标从 canvas 还原到原图 orig_coords np.zeros_like(coords) for i, (x, y) in enumerate(coords): x (x - offset_x) / scale bx1 y (y - offset_y) / scale by1 orig_coords[i] [x, y] all_keypoints.append(orig_coords) all_scores.append(scores) return person_boxes, all_keypoints, all_scores逻辑说明每个检测框独立走一遍裁剪到还原的完整链路互不干扰。注意 offset_x 和 offset_y 是补边时的偏移量scale 是等比缩放的系数还原公式是逆操作顺序不能反。all_scores 可以用于过滤低置信度关键点——比如 score 小于 0.5 的点通常是被遮挡的在可视化时用不同颜色标记。整个管线的耗时分布大约是检测 3ms、姿态推理 8ms单个人体框、预处理和后处理约 2ms单帧在 GPU 上稳定跑到 50FPS 以上。5. 避坑从数据到部署的5个高频翻车点5.1 坑1关键点索引错位左右手神秘互换现象模型输出的关键点在可视化时左右明显交叉或者左脚的位置出现在画面上方。原因HRnet 的 COCO 关键点顺序是固定的0鼻子、1左眼、2右眼、3左耳、4右耳、5左肩、6右肩、7左肘、8右肘、9左腕、10右腕、11左髋、12右髋、13左膝、14右膝、15左踝、16右踝。但很多代码在数据预处理阶段做了水平翻转增强翻转后左肩变成了右肩如果不做索引交换网络看到的标签就是错的。解决水平翻转时生成一个索引交换列表把 [1,2] 互换、[3,4] 互换以此类推。我做了一个映射数组 flip_idx [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]在翻转增强后按这个数组重排关键点。如果你用的是别人的预训练模型注意检查他用的关键点定义是不是和自己一致不一致时也要做索引映射。5.2 坑2检测框和姿态网络输入分辨率打架现象检测阶段框得很准但关键点整体偏向裁剪框的一侧或者在画面边缘时尤其严重。原因这是我最常在别人代码里发现的问题。检测框裁剪后直接 cv2.resize 拉伸到 192×256没有做等比缩放补边导致整个人体被横向或纵向拉长HRnet 训练时没见过这种变形输出自然偏移。更隐蔽的是即使做了等比缩放坐标还原时如果用错 offset 和 scale偏移会表现得很有规律。解决严格按前文 crop_and_resize 的写法等比缩放 灰边补齐并确保还原公式的参数对应同一组运算。我在调试时会在画面上同时画检测框、裁剪框和关键点人眼一看就能发现是谁没对齐。5.3 坑3多人场景性能骤降帧率直接掉半现象单人场景 50FPS变成 5 个人瞬间跌到 15FPS业务方不接受。原因两阶段方案的推理耗时随人体框数量线性增长。每多一个人姿态网络就多一次前向推理。很多人会想到批量推理但人体框的尺寸各不相同强行凑成一个 batch 需要分别裁剪和补边反而增加了预处理开销。解决我一般把姿态推理改成批量处理——同一个画面里的多个人体框先各自 resize 到 192×256堆成 batch 后一起推理。如果 GPU 显存足够一次处理 8 个人体框的耗时约等于处理 1 个的 1.5 倍。另外可以按面积从大到小排序优先处理大目标小目标在排队过程中如果帧率不够就直接跳过这一帧的检测保证主流程不卡顿。5.4 坑4边缘处人体被检测框切掉现象人站在画面边缘时手腕或脚踝关键点始终定位不准但画面中央同样姿势的人一切正常。原因YOLOv5 的检测框在目标被画面边缘截断时预测结果往往比真实身体区域更小比如整个下半身被截掉了检测框只框住上半身。关键点检测的输入里根本没有膝盖和脚踝模型只能靠先验瞎猜score 很低但位置看起来也算合理反而难以发现。解决裁剪时向外扩展 20% 已经能缓解一部分但我还会对贴边的检测框做强制扩展——检测框中心点靠近画面边缘时把远离中心的一侧额外扩展 30%。同时结合 3D 连续性校正后处理阶段用上一帧的关键点位置约束当前帧边缘处的点如果再突然跳变可以判定为不可信并回退到上一帧的结果。5.5 坑5不同设备上结果不一致GPU和CPU分数差一个档位现象模型在 GPU 上验证精度不错部署到 CPU 推理后关键点抖动明显精度评估掉的分数远超预期。原因CUDA 的卷积计算默认启用 TF32 精度CPU 上如果用 fp32两者的数值精度有细微差异热图峰值位置可能偏移一个像素。更常见的差异来自预处理——CPU 部署时的 opencv 版本不同resize 的插值算法可能不一样补边值被写死成 255 或者 0都会影响归一化后的像素分布。解决在部署代码里显式指定插值算法cv2.resize 的 interpolation 参数固定用 cv2.INTER_LINEAR。补边值我在训练时就用了 114对应 ImageNet 的平均灰度方向推理部署也统一用 114。跨平台验证时跑同一批验证图片输出坐标逐点比对误差小于 1 个像素才算通过。6. 进阶与验证精度评估和实时性优化的三个关键操作6.1 用 OKS 评估关键点精度别只盯着肉眼效果肉眼观察关键点位置“看起来不偏”和真正达到业务精度是两回事很多翻车案例是在实际测量物理距离时才发现的。关键点评估的标准指标是 OKSObject Keypoint Similarity它考虑到不同关键点的标注难度差异——比如肩膀相对稳定权重高手腕自由度大标注方差大权重低。def compute_oks(pred_kpts, gt_kpts, gt_visible, bbox_area, sigmas): # sigmas 是 COCO 预定义的关键点标准差 squared_dist ((pred_kpts - gt_kpts) ** 2).sum(axis1) oks_per_joint np.exp(-squared_dist / (2 * (bbox_area * sigmas ** 2 1e-6))) visible_mask gt_visible 0 return (oks_per_joint * visible_mask).sum() / visible_mask.sum()参数说明bbox_area 是关键点所属的检测框面积检测框越大允许的绝对误差越大这符合人体姿态的标注直觉——远距离拍摄时一个像素的偏差对应实际距离更大。sigmas 数组 COCO 官方直接给出不需要自己调。我一般在验证集上跑一遍OKS AP以 0.5 为阈值能达到 0.8 以上说明这套两阶段方案基本合格。低于这个值优先怀疑数据预处理而不是网络结构。6.2 多线程流水线让检测和姿态推理并行实时应用的瓶颈往往不在单帧耗时而在帧间串行等待。检测完再姿态推理每一帧的耗时是所有阶段耗时之和。改成流水线之后检测线程处理第 N 帧的同时姿态线程推理第 N-1 帧的人体框总延迟不变但吞吐量提升一倍。import threading import queue class PosePipeline: def __init__(self, det_model, hr_model): self.det_model det_model self.hr_model hr_model self.frame_queue queue.Queue(maxsize3) self.result_queue queue.Queue() self.worker threading.Thread(targetself._worker, daemonTrue) self.worker.start() def _worker(self): while True: frame self.frame_queue.get() if frame is None: break boxes, kpts, scores full_pipeline(frame, self.det_model, self.hr_model) self.result_queue.put((frame, boxes, kpts, scores)) def push(self, frame): try: self.frame_queue.put_nowait(frame) except queue.Full: pass # 队列满时丢帧保证实时性优先代码逻辑说明队列长度设为 3满时丢帧而不是阻塞。这样在业务高并发时姿态处理永远以最新帧为优先不会因为积压导致延迟越来越高。丢帧对姿态估计类应用无害因为连续帧之间的动作变化本来就不大丢一帧不影响结果判断。实际部署时可以把检测和姿态各放在一个线程里中间用共享内存传递检测框进一步降低拷贝开销。6.3 导出到 TensorRT 的注意事项和最后的工程习惯GPU 部署的最后一步是把两个模型分别导出为 TensorRT 引擎。YOLOv5 的 TensorRT 导出工具链比较成熟官方就支持。HRnet 导出时我遇到过一个典型的坑模型里有 F.interpolate 上采样TensorRT 对动态分辨率支持不好导出时输入分辨率必须固定为 1×3×256×192不要在导出后用动态形状。reshape 操作也可能触发不兼容导出的引擎文件大小异常时优先看日志里对 resize 层的警告。做完一切之后我有两个常年保留的习惯。第一是每次改动任何预处理代码立刻跑一遍 OKS 评估用数字说话分数字掉了再厉害的优化都先回滚。第二是保存关键点的可视化结果到固定目录按日期归档这样隔两周回头看能直观发现模型回归还是变好比翻 git log 高效得多。这两个习惯帮我避免了不少“改了代码、效果变差、想不起来改了什么”的被动局面。姿态估计这条路踩过的坑不少从关键点索引错位到边缘检测框切肢、从插值算法不一致到队列积压卡顿每一个都花了至少半天排查。但 YOLOv5HRnet 这套组合只要你把数据预处理和对齐做好它就是能稳定上线、可拆可换、性能可预测的方案。希望这篇笔记能帮你在做人体关键点项目时少走几段弯路。本文还有配套的精品资源点击获取