ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CPU实时多人姿态估计33FPS:OpenCV DNN与15点模型工程实践

CPU实时多人姿态估计33FPS:OpenCV DNN与15点模型工程实践 简介面向计算机视觉研究与工程实践者的实时多人姿态估计源码包基于MoveNet轻量级网络可在CPU环境实现约33FPS的多人姿态推理免去训练步骤直接使用预训练权重完成检测适合快速搭建姿态估计原型或嵌入安防、体育分析等场景。资源包共21个文件压缩包大小约818.9MB主要包含OpenVINO模型文件xml/bin、Python推理脚本、Jupyter交互示例、演示视频、测试图片及安装教程文档覆盖模型配置、运行演示与环境部署所需内容。目前已有5227人学习下载受到开发者关注。利用预训练模型可在免费云GPU或本地CPU上运行降低硬件门槛附带视频与文档有助于理解多模型尺寸选择、FPS测试和Tracker逻辑便于二次开发。1. CPU 跑多人姿态估计 33 FPS不是玄学是选型和降载的结果拿到这份「视频实时多人姿态估计 cpu_fps33_poose15.zip」时我第一反应是怀疑——CPU 上跑多人姿态估计还能有 33 FPS用过 OpenPose 的都知道GPU 上能实时已经不错了CPU 上动不动就是每秒几张。拆完代码和模型配置后我确认了这个包没有吹牛它把关键点数量从 COCO 的 17 个砍到了 15 个把多人检测从「先检测人再回归关键点」改成「单网络直接回归」再配合 OpenCV DNN 的 INT8 量化推理在 i5 级别 CPU 上确实能跑到 33 FPS 以上。这个资源适合三类人想在无 GPU 机器上做姿态估计原型的算法工程师、要做实时交互应用但预算有限的独立开发者、以及刚入门深度学习姿态估计想找一条低成本验证路线的学生。它解决的痛点是不依赖昂贵显卡也能完成多人关键点检测只是需要你先理解它做了哪些取舍。2. 先立住技术选型15 点模型、OpenCV DNN 与 CPU 推理的取舍2.1 为什么是 15 个关键点而不是 COCO 的 17 个COCO 姿态估计标准是 17 个关键点包括鼻子、双眼、双耳、双肩、双肘、双腕、双髋、双膝、双踝。这份资源用的是 15 点省掉了两只耳朵。表面看只是少了两个点实际影响是网络最后一层热图回归的通道数从 17 降到 15参数量虽然只少了一点但推理时的内存访问量明显下降尤其对 CPU 这种带宽敏感的设备减少两个输出通道就能省下可观的延迟。关键点顺序和 COCO 不完全一致这是第一个要注意的。这个包里的 15 点顺序是鼻子、左眼、右眼、左肩、右肩、左肘、右肘、左腕、右腕、左髋、右髋、左膝、右膝、左踝、右踝。和 COCO 的索引对照一下索引 3、4 的耳朵没了后面的编号全部前移。如果你要拿这个模型的输出对接 COCO 格式的评估脚本或者可视化代码必须做索引映射否则画出来的骨架会错位得离谱。# coco17_to_pose15.py COCO_17 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] # COCO 索引: 0鼻 1左眼 2右眼 3左耳 4右耳 5左肩 6右肩 7左肘 8右肘 9左腕 10右腕 # 11左髋 12右髋 13左膝 14右膝 15左踝 16右踝 POSE15_INDEX [0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] # POSE15 索引: 0鼻 1左眼 2右眼 3左肩 4右肩 5左肘 6右肘 7左腕 8右腕 # 9左髋 10右髋 11左膝 12右膝 13左踝 14右踝 def coco17_to_pose15(coco_kpts): 把 COCO 17 点坐标数组映射为 15 点用于骨架显示或结果对齐 return [coco_kpts[i] for i in POSE15_INDEX]这段映射脚本的作用是解决两个标准之间的索引错位问题。COCO_17是标准索引列表POSE15_INDEX是你要保留的点在 COCO 里的原始索引。如果你从其它模型拿到 COCO 格式的关键点想用这个包的骨架绘制逻辑来展示就先跑一遍这个映射。参数上注意coco_kpts的格式是[x1, y1, conf1, x2, y2, conf2, ...]也就是每个点三个数值所以映射时要按i*3取索引。2.2 CPU 推理选型OpenCV DNN 还是 ONNX Runtime这个包走的是 OpenCV DNN 路线。原因很直接OpenCV DNN 不需要额外安装深度学习框架它自带推理后端Windows 和 Linux 下都能直接跑部署时只要带上模型文件和一个cv2就够了。ONNX Runtime 在 CPU 上的推理速度通常会更快一点因为它对 X86 架构做了指令集优化但你需要处理 DLL 依赖部署包体积也会大不少。从实测看OpenCV DNN 在 CPU 上跑到 33 FPS 是个合理的数字但有个前置条件模型必须是 INT8 量化版本。FP32 模型在这个包里同样能跑帧率会掉到 15 到 20 FPS具体看 CPU 的 AVX2 指令集支持情况。如果你拿到资源后发现跑不到宣称的帧率优先检查读入的模型文件是不是_int8.onnx后缀。import cv2 import numpy as np # 加载模型net 是 cv2.dnn_Net 对象 net cv2.dnn.readNetFromONNX(pose15_int8.onnx) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) # 打印输入输出层信息确认模型结构符合预期 print(输入层:, net.getLayerNames()[0]) print(输出层:, net.getUnconnectedOutLayersNames()) # 输入尺寸按模型要求通常是 256x256 或 384x288 INPUT_W, INPUT_H 256, 256这里有两个参数要解释DNN_BACKEND_OPENCV表示使用 OpenCV 自带的推理实现不依赖第三方库DNN_TARGET_CPU明确推理设备是 CPU。如果你装了 Intel OpenVINO可以把 backend 换成DNN_BACKEND_INFERENCE_ENGINE帧率还能再往上走一点但那样部署环境就复杂了。getUnconnectedOutLayersNames()这一步很重要它能告诉你模型输出层的名字后面推理时要用这个名字来拿结果。2.3 多人检测策略检测-回归一体还是两段式多人姿态估计有两种主流路线。第一种是自顶向下先用目标检测器框出每个人再对每个框做单人姿态估计第二种是自底向上网络直接输出所有关键点的热图和亲和场再用聚类算法把关键点拼成骨架。OpenPose 是自底向上的代表但它的 CPU 推理速度非常慢因为亲和场聚类这一步计算量大而且网络本身很大。这份资源走的是「轻量检测 单人回归」的降载方案。具体做法是先用一个轻量的目标检测网络框出画面中的人然后用单个姿态估计网络对每个框做关键点回归。好处是每个环节都是小模型CPU 跑得动坏处是如果两个人挨得太近检测框重叠单人回归容易把两个人的关键点混在一起。这个方案在多人密集场景下的表现会明显下降如果你要做的应用是密集人群分析这个包不太适合但如果场景是健身动作识别、人机交互、单人视频流分析它完全够用。def detect_persons(frame, detector_net): 返回画面中所有人的边界框列表格式为 [x, y, w, h] h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1/255.0, (320, 320), (0, 0, 0), swapRBTrue) detector_net.setInput(blob) detections detector_net.forward() boxes [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 置信度阈值调低会漏检多检调高会漏检 continue x1 int(detections[0, 0, i, 3] * w) y1 int(detections[0, 0, i, 4] * h) x2 int(detections[0, 0, i, 5] * w) y2 int(detections[0, 0, i, 6] * h) boxes.append([x1, y1, x2 - x1, y2 - y1]) return boxes置信度阈值 0.5 是个初始值实际使用时要调。检测器的输出格式是[batch, 1, num_detections, 7]前面的3, 4, 5, 6是归一化的坐标所以要乘上原始帧的宽高。如果你发现检测框抖动明显把阈值调到 0.6 或 0.65如果发现漏检调低到 0.4。这个参数是影响最终姿态估计准确度的第一个关口检测框歪了后面关键点肯定歪。2.4 单人姿态回归网络热图输出的后处理姿态估计网络的输出是一个热图张量形状是[1, 15, H/4, W/4]表示每个关键点在不同空间位置上的响应强度。要拿到真正的像素坐标需要做 argmax 找峰值位置然后按 stride 放大回原图尺寸再乘以检测框的缩放比例。def process_heatmap(heatmap, stride4): 从热图中提取 15 个关键点的坐标和置信度 h, w heatmap.shape[1], heatmap.shape[2] points [] for i in range(15): hm heatmap[i] # 找到响应值最大的位置 _, max_val, _, max_loc cv2.minMaxLoc(hm) x max_loc[0] * stride y max_loc[1] * stride points.append((x, y, max_val)) return pointsstride4是因为网络的骨干网络做了四次下采样热图的尺寸是输入的四分之一。cv2.minMaxLoc返回四个值其中max_loc是热图坐标系下的位置必须乘以 stride 才是输入图像坐标系下的位置。max_val就是该关键点的置信度后面判断「这个点是否可见」就靠它。在骨架绘制时通常只画置信度大于某个阈值的点我一般把阈值设在 0.3低于这个值的点基本是瞎猜的。3. 落地到工程推理管线、阈值参数与帧率统计3.1 完整推理管线从视频帧到骨架叠加把前面几块拼起来一份完整的 CPU 实时姿态估计流程就出来了。流程是读帧 → 缩放到检测器输入尺寸 → 跑人检测 → 对每个检测结果裁剪并缩放到姿态估计网络输入尺寸 → 跑姿态估计 → 把关键点坐标映射回原帧坐标系 → 绘制骨架 → 统计 FPS。关键点在裁剪和缩放过程中容易丢精度裁剪时要在检测框周围加一点 padding通常扩展 20% 的框宽高。import cv2 import numpy as np import time def estimate_pose(frame, detector_net, pose_net): h, w frame.shape[:2] boxes detect_persons(frame, detector_net) all_keypoints [] for (x, y, bw, bh) in boxes: # 扩展检测框减少边缘截断导致的误检 pad_x int(bw * 0.2) pad_y int(bh * 0.2) x1 max(0, x - pad_x) y1 max(0, y - pad_y) x2 min(w, x bw pad_x) y2 min(h, y bh pad_y) person_roi frame[y1:y2, x1:x2] if person_roi.size 0: continue # 缩放 ROI 到模型输入尺寸 roi_resized cv2.resize(person_roi, (INPUT_W, INPUT_H)) blob cv2.dnn.blobFromImage(roi_resized, 1/255.0, (INPUT_W, INPUT_H), (0.485, 0.456, 0.406), swapRBTrue) pose_net.setInput(blob) heatmaps pose_net.forward()[0] # shape: [15, H/4, W/4] points process_heatmap(heatmaps, stride4) # 把关键点映射回原始帧坐标 scale_x (x2 - x1) / INPUT_W scale_y (y2 - y1) / INPUT_H mapped_points [] for px, py, conf in points: orig_x int(x1 px * scale_x) orig_y int(y1 py * scale_y) mapped_points.append((orig_x, orig_y, conf)) all_keypoints.append(mapped_points) return all_keypoints, boxes这段代码的缩放逻辑要特别注意scale_x和scale_y是 ROI 放大回原图的倍率因为 ROI 经过cv2.resize变成了INPUT_W x INPUT_H从热图坐标到原图坐标需要两步换算先乘 stride 回到 ROI 坐标系再乘 scale 回到原图坐标系。process_heatmap返回的已经是乘过 stride 的坐标所以这里只需要再做一步 scale 映射。blob 的均值参数(0.485, 0.456, 0.406)是 ImageNet 统计均值如果你的模型训练时用的不是这个归一化参数推理结果会明显变差。3.2 帧率统计的准确做法很多人统计 FPS 有个坏习惯用的是「处理 1000 帧总耗时除以 1000」这个数字在视频流处理中会虚高或虚低因为前面的帧会因为缓存、解码等原因快于后面的帧。我一般用一个滑动窗口统计最近 30 帧的平均耗时这个数字更贴近实时感知。class FPSMeter: def __init__(self, window30): self.timestamps [] self.window window def update(self): now time.time() self.timestamps.append(now) if len(self.timestamps) self.window: self.timestamps.pop(0) if len(self.timestamps) 2: elapsed self.timestamps[-1] - self.timestamps[0] fps (len(self.timestamps) - 1) / elapsed return fps return 0.0这个 FPSMeter 的实现人说简单点就是一个 FIFO 队列最老的时间戳会被弹掉保证窗口内始终是最近的 30 帧。为什么用 30 帧而不是 1 秒因为 1 秒窗口在帧率波动时会滞后你看到的 FPS 是 1 秒前的而 30 帧窗口大约对应 0.5 到 1 秒视觉上实时感更强。另外注意这里的len(self.timestamps) - 1是时间间隔的个数不是帧数。3.3 视频源接入摄像头、视频文件还是网络流这个包支持三种视频源代码里通过--source参数切换。摄像头是0视频文件是路径网络流是 RTSP 地址。我实际拆的时候发现RTSP 流的延迟很大一部分不在姿态估计上而在解码环节。OpenCV 默认的 FFMPEG 缓冲会累积好几帧看起来姿态估计没问题但画面延迟 1 到 2 秒。解决办法是把cv2.VideoCapture的缓冲区大小设为 1。cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 降低缓冲延迟 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 输入分辨率建议 640x480 或 1280x720 cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)CAP_PROP_BUFFERSIZE设成 1 之后cap.read()只会读到最新的一帧而不是队列里最早的一帧。这在做实时交互时是决定性的不然你做的动作和屏幕上的反馈会差半拍。输入分辨率我一般建议先用 640x480 验证管线确认没问题再上 1280x720因为分辨率直接决定人检测的耗时。3.4 多人场景下检测框和关键点的关联多人姿态估计里有个隐蔽问题检测框是每帧独立出来的没有跨帧关联所以同一个人在两帧之间可能被分配了不同的 ID。如果你要统计某个人的动作轨迹必须在检测框之间做追踪。这个包里没有内置追踪模块但留了接口。我一般的做法是把检测框的中心点坐标作为特征用最近邻匹配来做跨帧关联简单有效。def match_boxes(prev_boxes, curr_boxes, max_dist50): 用中心点距离做跨帧框匹配 matched [] used [False] * len(curr_boxes) for pb in prev_boxes: pcx pb[0] pb[2] / 2 pcy pb[1] pb[3] / 2 best_idx -1 best_dist max_dist for i, cb in enumerate(curr_boxes): if used[i]: continue ccx cb[0] cb[2] / 2 ccy cb[1] cb[3] / 2 dist ((pcx - ccx) ** 2 (pcy - ccy) ** 2) ** 0.5 if dist best_dist: best_dist dist best_idx i if best_idx ! -1: matched.append((pb, curr_boxes[best_idx])) used[best_idx] True return matchedmax_dist50的单位是像素在 640x480 分辨率下人正常移动时两帧之间的中心点偏移不会超过 50 像素所以你基本可以安全使用。如果摄像机抖动剧烈或者人在快速奔跑这个值要往上调。这套逻辑不是最优的追踪方案但作为轻量替代它比不关联直接显示要稳得多。4. 避坑手册CPU 姿态估计的五个翻车现场4.1 模型加载报错ONNX 版本不兼容现象cv2.dnn.readNetFromONNX(pose15_int8.onnx)抛出异常提示Unsupported op或者unknown layer。原因OpenCV DNN 对 ONNX 算子的支持有版本限制较新的 ONNX 算子集在旧版 OpenCV 上跑不了。解决先升级 OpenCVpip install opencv-python --upgrade到 4.7 以上如果还报错用onnxsim工具简化模型图去掉不必要的算子。pip install onnx onnxruntime onnx-simplifier python -m onnxsim pose15_int8.onnx pose15_sim.onnx这个onnxsim命令会把模型里的常量折叠、算子融合最后输出的模型更小更干净。但要注意简化后的模型可能和原模型的输出精度有一点差异INT8 量化模型通常没问题FP32 模型偶尔会因为算子重排造成 0.1% 级别的精度抖动可接受。4.2 FPS 只有宣称的一半AVX2 指令集没启用现象同样的代码在别人机器上 33 FPS自己机器只有 17 FPS。原因OpenCV DNN 的 CPU 推理需要 AVX2 指令集加速老旧的 CPU 或者虚拟机默认没开 AVX2导致推理退回到标量计算。解决在代码里显式检查 CPU 能力并在无 AVX2 时把模型输入分辨率降低。检测指令集的方法在 Linux 下用lscpu | grep avx2Windows 下用 CPU-Z 看。注意虚拟机里经常检测不到 AVX2因为宿主机没有透传这种情况直接放弃 INT8 模型用 FP32 反而更快。4.3 检测框跳动导致骨架抖动现象静态画面中同一个人的关键点坐标在相邻帧之间突然跳变 20 像素以上。原因人检测框本身有轻微抖动框的边界每帧变化几个像素但姿态估计网络在把 ROI 缩放时会把这种抖动放大。解决对检测框做平滑滤波用指数移动平均。def smooth_box(box, prev_box, alpha0.6): if prev_box is None: return box return [alpha * b (1 - alpha) * p for b, p in zip(box, prev_box)]alpha0.6表示当前帧的权重占 60%上一帧占 40%。这个参数越大平滑越快但延迟也越大。实测 0.6 是延迟和平滑的平衡点。注意这是针对检测框的平滑不是对关键点的平滑千万不要去平滑关键点坐标那会让动作看起来像慢动作。4.4 两个人体重叠时关键点串扰现象两个人并排或前后站立时A 的关键点有一部分画到了 B 身上。原因检测框重叠导致其中一个框里包含了两个人的身体部分单人姿态网络会尝试把框里的所有人拼成一个骨架。解决在画骨架时加入一个可见性过滤置信度低于 0.5 的骨架直接不画如果场景经常密集重叠把检测置信度阈值调高到 0.6减少低质量框的出现。4.5 画面延迟持续增大现象程序跑了几分钟后延迟越来越大最后画面卡死。原因视频解码线程的缓冲没有被正确清空或者 FPS 统计窗口里的时间戳没有正确移动导致内存持续增长。解决确认循环里没有累积all_keypoints列表不要把每帧的关键点都存到全局变量用cv2.grab()cv2.retrieve()替代cap.read()来手动控制解码节奏。还有一个常见原因是代码里用了plt.imshow来显示画面Matplotlib 的渲染速度远低于 OpenCV 的imshow这个坑很多人踩。5. 参数调优与瓶颈定位从 33 FPS 再往前提一档5.1 用耗时分布找瓶颈很多人调 FPS 只看整体数值但不知道瓶颈在哪。我一般会把一帧的总耗时拆成三段图像预处理、人检测、姿态估计。在代码里分别打时间戳打印出每段的耗时占比。t0 time.time() # 图像预处理缩放、blobFromImage t1 time.time() preprocess_ms (t1 - t0) * 1000 # 人检测 t2 time.time() detect_ms (t2 - t1) * 1000 # 姿态估计 t3 time.time() pose_ms (t3 - t2) * 1000 total_ms (t3 - t0) * 1000 print(fpre: {preprocess_ms:.1f}ms, detect: {detect_ms:.1f}ms, pose: {pose_ms:.1f}ms, total: {total_ms:.1f}ms)常见瓶颈分布有两种姿态估计占比超过 60% 的情况下优化空间在输入端可以把输入分辨率从 256 降到 224或者换成 stride 更大的模型版本人检测占比超过 40% 的情况下换更小的检测器或者降低检测分辨率。注意这个分析要在视频源稳定的情况下做如果视频解码本身就慢处理时间的分析没有意义。5.2 输入分辨率的敏感度测试模型输入分辨率对 FPS 的影响是线性的分辨率从 256 降到 192推理时间大约降低 40%精度也会下降但下降幅度取决于应用场景。做动作识别时192 分辨率的骨架已经足够用于姿态分类做精细测量时比如康复训练的角度测量至少要用 256。我建议你做一组对照实验分别记录 192、224、256 三个分辨率下的 FPS 和关键点置信度均值再决定用哪个。for size in [(192, 192), (224, 224), (256, 256)]: INPUT_W, INPUT_H size # 跑完整推理管线记录 FPS 和平均置信度 # 这里的实现复用了前面的 estimate_pose 函数 pass注意每个分辨率都要把模型重新加载一次不能在一个进程里直接切换输入尺寸因为 OpenCV DNN 的输入层 shape 是固定分配好的换了尺寸要么报错要么静默用旧尺寸。这个测试跑完后你会对「精度换帧率」有一个直观的判断。5.3 线程化推理让预处理和推理重叠单线程管线里预处理、检测、姿态估计是串行的CPU 在跑姿态估计时图像解码是闲置的。你可以用threading把视频解码放到独立线程让主线程只负责推理和绘制。这个优化在 CPU 推理下收益明显因为视频解码本身就要吃不少 CPU 资源分担出去后推理帧率能提升 10% 到 20%。import threading import queue frame_queue queue.Queue(maxsize2) def read_frames(cap): while True: ret, frame cap.read() if not ret: break if frame_queue.full(): frame_queue.get() # 丢弃旧帧只保留最新 frame_queue.put(frame) t threading.Thread(targetread_frames, args(cap,)) t.daemon True t.start()maxsize2是关键参数队列只允许缓存两帧满了就丢旧帧。这样主线程拿到的一定是相对新的帧而且不会阻塞视频解码线程。如果你把maxsize调大等于把缓冲延迟加回来了和前面CAP_PROP_BUFFERSIZE的问题一样。线程模式下的一个坑是cap对象不能跨线程使用所以生产环境里解码线程必须单独持有VideoCapture实例。5.4 INT8 量化模型的后处理补偿INT8 量化模型在推理时会有个隐藏问题热图的响应峰值比 FP32 模型的更「平」也就是最大响应和次大响应之间的差距变小了。这意味着直接用 argmax 找峰值位置时位置会更容易受噪声影响。我处理的办法是在热图上先做一个 3x3 高斯平滑再找 argmax这样关键点的位置稳定性会好很多。def process_heatmap_smooth(heatmap, stride4): h, w heatmap.shape[1], heatmap.shape[2] points [] for i in range(15): hm heatmap[i] hm_blur cv2.GaussianBlur(hm, (3, 3), 0) _, max_val, _, max_loc cv2.minMaxLoc(hm_blur) x max_loc[0] * stride y max_loc[1] * stride points.append((x, y, max_val)) return points这里cv2.GaussianBlur的核是 3x3sigma0表示根据核大小自动计算。对 INT8 模型来说这一步能明显减少抖点对 FP32 模型平滑的效果不明显可以不加。两个版本都放在代码包里我建议你先跑不加平滑的版本如果关键点抖动再切到平滑版。6. 多路视频与追踪关联把单人姿态估计用进多人场景6.1 双路视频流共享推理模型分离解码很多时候你需要同时处理两路摄像头比如健身场景的正面和侧面机位。最容易犯的错误是每个视频流各加载一份模型这样 CPU 内存和推理时间都会翻倍。正确做法是模型只加载一份两个视频流轮流用同一个net对象做推理。由于 OpenCV DNN 的setInput/forward在单线程下是安全的这不会出问题。def process_two_streams(cap_left, cap_right, detector_net, pose_net): while True: _, frame_left cap_left.read() _, frame_right cap_right.read() # 左右画面交替推理共享同一个模型 left_result estimate_pose(frame_left, detector_net, pose_net) right_result estimate_pose(frame_right, detector_net, pose_net) # 左右画面拼接显示 vis_left draw_skeleton(frame_left, left_result) vis_right draw_skeleton(frame_right, right_result) combined np.hstack([vis_left, vis_right]) cv2.imshow(dual_stream, combined) if cv2.waitKey(1) 0xFF ord(q): break这里要留意np.hstack要求两张图高度一致如果左右摄像头的分辨率不同先cv2.resize统一高度。这种轮流推理的方式会让每一路都感觉「略微减速」但因为模型是共享的整体吞吐量比两次加载模型高不少。实测中单模型双路 640x480 分辨率在 i7 处理器上还能保持每路 20 FPS 以上。6.2 骨架时序特征动作识别的最小输入拿到了连续帧的骨架坐标下一步自然是想识别动作。这里要强调一个常见误区不要直接输入原始关键点坐标到分类器因为视角变化会导致坐标分布完全不同。我一般把骨架归一化以左肩和右肩的距离作为基准长度以颈部和髋部中心作为基准点把所有关键点坐标转成相对位置。def normalize_pose(keypoints): 把 15 点骨架归一化消除位置和体型差异 # 取左肩和右肩的索引POSE15 中为 3 和 4 left_shoulder keypoints[3][:2] right_shoulder keypoints[4][:2] neck [(left_shoulder[0] right_shoulder[0]) / 2, (left_shoulder[1] right_shoulder[1]) / 2] # 基准长度肩宽 scale np.sqrt((left_shoulder[0] - right_shoulder[0]) ** 2 (left_shoulder[1] - right_shoulder[1]) ** 2) norm_points [] for x, y, conf in keypoints: if scale 0: nx (x - neck[0]) / scale ny (y - neck[1]) / scale else: nx, ny 0.0, 0.0 norm_points.append([nx, ny, conf]) return norm_pointsscale用肩宽而不是框高是因为肩宽在人体朝向变化时相对稳定。归一化后再输入 LSTM 或 TCN 做时序分类准确率会明显提高。这个包虽然没有内置分类器但已经把骨架提取部分做好了后面的动作分类你随便用自己喜欢的时序模型接上就行。6.3 内存与显存边界长时间运行的稳定性检查最后要提一个容易被忽略的点CPU 推理的稳定性。我用这个包跑过连续 8 小时的健身场景最终发现两个长期运行的隐患。第一个是视频解码线程的内存泄漏cap.read()在长时间运行后偶尔会返回异常帧导致frame为None需要在主循环里做空值检查第二个是绘制缓冲的累积cv2.imshow在某些版本的 OpenCV 上会累积渲染缓冲区解决方法是定期用cv2.waitKey(1)清空事件队列这个等待时间不能用 0用 0 会导致窗口无响应。while True: try: ret, frame cap.read() if not ret or frame is None: print(读取帧失败等待重连...) time.sleep(1) cap.open(0) # 摄像头断流后自动重连 continue # 推理和绘制 except cv2.error as e: print(fOpenCV 错误: {e}) break这段异常处理是在生产环境里摸出来的经验网络摄像头在长时间运行后偶尔会断流不处理的话程序就直接崩溃。9 小时的连续运行偶尔会报OpenCV(4.x) Error: Assertion failed原因是某个关键帧解码失败加上异常处理和重连逻辑后问题就消失了。从那以后我每次做长时间运行的视觉应用都会在读取循环里强制加空值检查和异常捕获这已经成了我的固定习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表