☰
轻量级人体姿态估计落地:PyTorch+ONNX实现仰卧起坐实时计数
2026/10/11 8:43:53 网站建设 项目流程

简介:针对无GPU环境下的实时动作识别与健身计数需求,这套仰卧起坐计数应用开发项目从人体姿态估计出发,系统梳理数据采集、标注、训练到部署的完整链路,适合希望掌握轻量化模型落地的深度学习开发者和健身应用方向的工程人员参考学习。压缩包共423个文件,包含348张jpg姿态标注与训练图像、46个py实现代码、Qt标注工具的ui界面、onnx与pth权重文件、json配置及docx说明文档,整包仅35.6MB,结构紧凑便于快速检索。该资源发布后已有60人浏览学习。核心价值在于将PyTorch轻量化网络设计、Heatmap姿态热图输出、标注工具开发与模型导出部署等关键技术完整串联,既能直接在CPU环境复现仰卧起坐实时计数,也可将数据标注与训练流程迁移至其他健身动作识别场景。附赠文档对网络结构、标注工具使用及数据集构成做了补充说明,为后续二次开发提供了清晰起点。

1. 轻量级仰卧起坐计数:不依赖GPU的人体姿态估计落地项目

做健身计数类应用,最头疼的不是模型精度,而是「跑不起来」。手机和教室电脑大多没有NVIDIA显卡,你辛辛苦苦训练的姿态估计模型在GPU上跑得飞快,换到纯CPU机器上就变成幻灯片。这个项目就是冲着这个痛点来的:用PyTorch实现一套轻量级人体姿态估计网络,在无GPU环境下完成从数据采集、标注、训练到部署的全流程,最终跑通仰卧起坐的实时计数。整套资源包含PyTorch轻量化网络设计、Qt标注工具、训练脚本和推理部署代码,适合正在做毕业设计、健身App原型、体测自动化的工程师直接复现。我拆完这个包之后最深的感受是:它没有把精力耗在堆模型宽度上,而是把关键点检测、时序计数、CPU加速这些环节都落到了实处。

2. 技术选型与无GPU环境搭建:PyTorch CPU版和ONNX Runtime的组合

2.1 为什么选PyTorch + 轻量级网络而不是现成姿态估计库

做人体姿态估计,OpenPose和MediaPipe都是成熟方案,但在这个项目里都不太合适。OpenPose的模型体量太大,CPU上做实时推理基本是奢望;MediaPipe虽然轻,但它是个封闭的pipeline,关键点输出格式固定,你想针对仰卧起坐场景裁剪网络、自定义训练数据,改起来非常费劲。这个项目选择PyTorch自建轻量级网络,核心原因有三个:一是PyTorch的torch.onnx.export导出链路成熟,训练完直接转ONNX,配合ONNX Runtime跑CPU推理非常顺;二是损失函数、网络结构、后处理逻辑全部自己掌控,方便针对仰卧起坐的侧视场景做裁剪;三是PyTorch CPU版在Anaconda里一条命令就能装好,不折腾CUDA。

网络骨架用的是MobileNetV3-Small,这是目前CPU端性价比最高的分类骨干网络之一。它在ImageNet上的精度和MobileNetV2相当,但延迟更低,而且它的核心模块bottleneck结构特别容易改造成姿态估计的backbone——去掉最后的分类层,保留高分辨率特征图,后面接一个轻量的反卷积头输出关键点热图。仰卧起坐场景只需要检测头、肩、髋、膝四个部位共9个关键点(左右对称),比COCO的17点少了一半,计算量又降了一截。整个模型参数量控制在3M左右,在i5级别的CPU上单帧推理能跑到20-30ms。

2.2 Anaconda创建环境与PyTorch CPU安装

环境这块我直接给出我实际操作过的方案。先在Anaconda里建一个独立环境,Python版本别选太高,3.8到3.10都行,我习惯用3.9,PyTorch和ONNX Runtime的兼容性都稳。创建完环境后安装CPU版PyTorch,注意一定要用清华源或阿里源,否则下载速度会让人崩溃。

conda create -n situp python=3.9 -y conda activate situp # CPU版PyTorch,从清华源安装 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 如果上面这条慢,换成清华镜像 pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple # 训练和推理依赖 pip install numpy opencv-python pillow matplotlib tqdm -i https://pypi.tuna.tsinghua.edu.cn/simple pip install onnx onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple

第一条命令创建环境并激活,第二条指定CPU版本的PyTorch,注意这个index-url只包含CPU版本,不会误装CUDA版。第三条是清华镜像兜底方案,国内网络环境下的常规操作。最后安装的onnxruntime是CPU推理的核心,自带的OpenMP优化在Intel CPU上能自动利用多核,比直接跑PyTorch的CPU算子快不少。装完之后用python -c "import torch; print(torch.version)"验证一下,能看到版本号就说明环境通了。

2.3 项目目录结构与配置文件

这个项目的目录结构设计得比较清晰,训练、标注、推理三块互不干扰,我拆包之后按原结构重新整理了一遍。data存放原始视频和标注结果,models放网络定义和训练好的权重,tools下面放了标注工具和转换脚本,train.py是训练入口,inference.py是实时推理入口。

项目根目录下有一个config.yaml,所有的超参数和路径都集中在这里,不用改代码就能调整训练配置。我实际用的时候发现这个设计非常省事,换数据集或者调batch size,只需要改yaml再重新跑脚本就行。

data: train_imgs: "./data/images/train" train_labels: "./data/annotations/train.json" val_imgs: "./data/images/val" val_labels: "./data/annotations/val.json" model: backbone: "mobilenetv3_small" num_keypoints: 9 input_size: [256, 192] heatmap_size: [64, 48] train: batch_size: 32 epochs: 150 lr: 0.001 weight_decay: 0.0005 num_workers: 4

这个配置文件里,input_size是输入网络的图像尺寸,256x192是姿态估计领域比较标准的输入,兼顾精度和速度。heatmap_size是输出热图的尺寸,输入缩小4倍,和backbone下采样倍率对应。weight_decay这个参数对应的是L2正则化强度,0.0005是AdamW优化器下的常用取值,太大容易欠拟合,太小则起不到约束作用。训练的时候如果发现验证集loss不降,第一反应应该是去调这个值和相关增强参数,而不是盲目加大模型。

3. 数据采集与Qt标注工具:关键点标注的全流程设计

3.1 仰卧起坐场景的关键点定义与标注规范

仰卧起坐计数不依赖全身17个关键点,实际只需要躯干和下肢的几个关键位置。这个项目定义了一套9关键点方案:鼻子、左右肩、左右髋、左右膝、左右踝。其中鼻子用来判断头部位置变化,肩髋膝三个点的夹角用来计算躯干起落角度,踝关节作为固定参考点。

标注规范上有个细节必须注意:侧视拍摄时左右关键点会发生重叠,比如人面向左边时右肩和左肩在画面里几乎重合。这个项目的做法是严格按COCO的可见性规则来标——occluded设为0,遮挡但可推测位置设为1,完全不可见设为2。如果某个视频帧里侧视角度太偏、左右点完全无法区分,我一般会直接跳过这帧而不是硬标,因为标注噪声对姿态估计模型的影响比图像噪声大得多。

标注数据格式采用COCO keypoint的JSON结构,images数组记录图像文件名和尺寸,annotations数组里记录关键点坐标和可见性。下面是标注工具实际输出的单条标注格式。

{ "image_id": 1, "file_name": "frame_0001.jpg", "width": 1280, "height": 720, "keypoints": [ 612, 233, 2, 598, 247, 1, 632, 249, 1, 610, 431, 1, 638, 428, 1, 610, 604, 1, 642, 598, 1, 610, 683, 1, 644, 675, 1 ], "num_keypoints": 8 }

keypoints数组每三个数一组,分别是x坐标、y坐标、可见性标志,顺序严格对应前面定义的9个关键点。num_keypoints是可见性不为0的关键点数量,训练时用于过滤完全被遮挡的样本。这个格式和COCO API完全兼容,后续不管是自己写Dataset还是接现成的数据加载器都行。

3.2 Qt标注工具的功能拆解

标注工具是这个项目里除了模型之外我最看重的部分。它是用Qt 5.15.2写的桌面应用,核心界面分成三个区域:左侧是图像显示区,右侧是关键点列表和属性编辑区,底部是帧导航栏。用到的Qt模块组合是QGraphicsView + QGraphicsScene + QGraphicsPixmapItem,这几个类配合起来做图像标注非常顺手,缩放平移都是现成的。

核心标注逻辑是在QGraphicsView上监听鼠标事件,点击时在对应位置画一个圆点标记,并把坐标写入当前帧的关键点数组。关键点是有序的,所以工具里做了一个部件列表控件,标注时先选中当前要标注的部位(比如左肩),再在图像上点击,坐标会自动落到对应位置。

class KeypointLabeler(QGraphicsView): def __init__(self, parent=None): super().__init__(parent) self.scene = QGraphicsScene(self) self.setScene(self.scene) self.current_keypoint_idx = 0 self.kp_points = [] # 保存每个关键点的QGraphicsEllipseItem def mousePressEvent(self, event): # 将视图坐标映射为图像坐标 scene_pos = self.mapToScene(event.pos()) x = int(scene_pos.x()) y = int(scene_pos.y()) # 更新当前关键点位置 self.update_keypoint(self.current_keypoint_idx, x, y) super().mousePressEvent(event) def update_keypoint(self, idx, x, y): # 如果该关键点已存在圆点,先移除再重新绘制 if len(self.kp_points) > idx and self.kp_points[idx] is not None: self.scene.removeItem(self.kp_points[idx]) ellipse = self.scene.addEllipse(x - 2, y - 2, 4, 4, QPen(Qt.red), QBrush(Qt.red)) self.kp_points.append(ellipse) self.kp_list_widget.item(idx).setText(f"{idx}: ({x}, {y})")

mousePressEvent里mapToScene的核心作用是把鼠标在窗口上的坐标转换成图像的真实像素坐标,这样不管图片缩放了多少倍,标注坐标都是准确的。update_keypoint函数负责维护界面上的圆点标记,同一关键点被重复标注时先删旧点再画新点,避免圆点重叠导致看不出当前标注位置。这个逻辑实现起来不难,但它是整个标注工具的交互基石,新手容易忽略mapToScene这一步,直接用event.pos()存坐标,存出来的坐标在图片缩放后就会偏掉。

标注工具的导出逻辑也很直接,点击保存按钮后把当前图像的所有关键点序列化进一个字典,追加写入JSON文件。每次只写一条annotation,最后统一转成COCO格式,这样即使工具中途崩溃也不会丢失已经标注过的帧。

3.3 标注数据质量与质检方法

数据质量直接决定姿态估计模型的最终效果,这一点怎么强调都不过分。项目README里建议至少标注3000帧,我自己的经验是仰卧起坐这个动作比较固定,2000帧高质量标注就够用了,但前提是多样性得够——不同体型的人、不同拍摄角度、不同光照条件下都要覆盖到。如果全部只用同一个人的视频,模型泛化能力会很差,换个人就失效。

质检方面,我习惯在标注完成后写一个可视化脚本,把标注点重新画到原图上生成一张张预览图,人工快速翻看。重点检查三类问题:一是关键点顺序是否错位,比如把左肩标到了右肩的位置;二是遮挡帧是否被错误标注成可见;三是帧间的关键点是否有明显跳变——如果相邻帧同一个关键点坐标突变超过几十像素,大概率是标注错误,需要回去修。这个项目里附带的check_annotation.py脚本就是干这个的,它会按相邻帧距离过滤出可疑样本,把帧号和关键点索引打印出来提示人工复核。

4. 轻量级网络设计与训练:从MobileNetV3到热图回归

4.1 网络结构设计与输出头

这个项目的网络结构可以拆成三个部分:MobileNetV3-Small骨干网络、反卷积头、热图输出层。骨干网络在stage3之后的特征图分辨率是输入的1/16,也就是16x12,这个分辨率太低,直接回归关键点太粗糙。常见做法是接反卷积模块逐步上采样,这个项目用了两个步长为2的反卷积层,把特征图从16x12恢复到64x48,正好是配置里的heatmap_size。

模型定义里还需要处理一个细节:MobileNetV3的最后一个stage通道数较多,接反卷积之前一般会加一个1x1卷积降维,把通道从较高的维度压到96或128。这个项目选择了128通道,反卷积层每层将通道数减半,最后输出9个通道的热图。每个通道对应一个关键点,通道内的数值分布表示该关键点在每个位置的概率。

import torch.nn as nn class LitePose(nn.Module): def __init__(self, backbone, num_keypoints=9): super().__init__() # 去掉backbone的分类层,保留特征提取部分 self.features = backbone.features # 降维层 self.reduce = nn.Conv2d(96, 128, kernel_size=1, bias=False) # 反卷积头,恢复分辨率 self.deconv1 = nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1) self.deconv2 = nn.ConvTranspose2d(64, 32, kernel_size=4, stride=2, padding=1) # 最终输出层,生成关键点热图 self.out = nn.Conv2d(32, num_keypoints, kernel_size=1) def forward(self, x): x = self.features(x) x = self.reduce(x) x = torch.relu(self.deconv1(x)) x = torch.relu(self.deconv2(x)) return self.out(x)

这里的deconv1和deconv2用的是kernel_size=4, stride=2, padding=1的反卷积,这种参数组合能把特征图尺寸精确翻倍,不会出现奇偶不匹配导致的输出尺寸错位。每次反卷积后接ReLU激活增加非线性。out层是1x1卷积,作用是把32个通道映射到9个关键点通道,相当于逐像素做分类。训练时输入图像是256x192,经过backbone下采样和反卷积上采样,输出热图正好是64x48,每个关键点的坐标通过argmax操作从对应通道的热图里取出来。

4.2 训练参数配置与L2正则化

训练脚本的优化器和损失函数设计是整套代码里最有参考价值的部分。损失函数用的是加权MSE,权重由关键点可见性决定:可见的关键点权重为1,遮挡但可推测的权重为0.5,完全不可见的权重为0。这样设计的好处是让模型重点学习可见关键点的位置,不被遮挡帧的噪声干扰,同时也让模型在推理时对遮挡关键点输出较低置信度。

优化器选择AdamW,这是PyTorch里L2正则化的标准做法。很多人在PyTorch里做L2正则化还在手动往loss里加weight decay项,其实完全没必要,AdamW的weight_decay参数就是L2正则化,但它的实现方式和传统SGD不同——它是把weight decay解耦出来单独处理,不会像L2正则化那样和Adam的自适应学习率耦合导致正则化效果失真。

import torch.optim as optim # 经典L2正则化写法:通过weight_decay参数实现 optimizer = optim.AdamW( model.parameters(), lr=0.001, weight_decay=0.0005 # 对应L2正则化强度 ) # cosine退火学习率调度,训练后期更稳定 scheduler = optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=150, # 对应总epoch数 eta_min=1e-5 )

weight_decay设为0.0005是经验值,它是L2正则化的关键旋钮。如果训练时验证集loss一直在较高位震荡、怎么都降不下去,可以尝试把weight_decay降到0.0001;反过来如果训练集loss很低但验证集loss偏高、明显过拟合,就调大到0.001。CosineAnnealingLR配合AdamW是我在这个项目里最推荐的组合,它让学习率在训练后期自动衰减到很小,避免在最优解附近来回震荡。

训练时的数据增强我建议不要用太激进的方案。随机旋转范围控制在±15度,缩放0.8到1.2倍,水平翻转开启,但要注意翻转后关键点左右要互换,比如左肩变右肩。随机遮挡和色彩抖动可以加但强度要轻,仰卧起坐视频的背景变化本身就不大,过强增强反而会让模型学偏。

4.3 模型转ONNX与CPU推理部署

训练完成后,模型从PyTorch转到ONNX Runtime跑CPU推理,是这个项目落地最核心的一步。PyTorch的CPU算子在小规模模型上其实效率一般,而ONNX Runtime针对CPU做了大量kernel优化,同样的MobileNetV3在ONNX Runtime上通常能再快20%到30%。转换过程不是简单调一个API就行,有几个参数必须注意。

import torch # 加载训练好的权重 model = LitePose(backbone) checkpoint = torch.load("./checkpoints/best_model.pth", map_location="cpu") model.load_state_dict(checkpoint["model_state_dict"]) model.eval() # 构造固定尺寸的输入,ONNX转换需要实际输入样例 dummy_input = torch.randn(1, 3, 192, 256) torch.onnx.export( model, dummy_input, "./deploy/situp_pose.onnx", input_names=["input"], output_names=["heatmap_output"], opset_version=11, dynamic_axes={"input": {0: "batch_size"}, "heatmap_output": {0: "batch_size"}} )

opset_version=11是兼容性和功能性的平衡点,太低的版本不支持某些算子,太高的版本在旧版ONNX Runtime里可能跑不了。dynamic_axes把batch维度设为动态,这样转换出来的模型既可以用在单张图片推理,也可以用在视频流的批量推理场景。转换完成后用onnxruntime检查一下输入输出的shape,确认输出的维度是[batch, 9, 64, 48],这一步能提前发现很多模型定义里的隐藏bug。

推理侧用ONNX Runtime的CPU版本,核心代码比较短,但有一个小技巧值得说:onnxruntime的InferenceSession可以设置线程数,默认会占用所有CPU核心,在嵌入式设备或者共享服务器上反而会拖慢整体速度。我一般把intra_op_num_threads设为和实际可用核心数一致,避免超线程竞争。

import onnxruntime as ort import numpy as np session = ort.InferenceSession( "./deploy/situp_pose.onnx", providers=["CPUExecutionProvider"], sess_options=ort.SessionOptions() ) # 设置线程数,根据实际CPU核心数调整 session.set_providers(["CPUExecutionProvider"], [{"intra_op_num_threads": 4}]) # 预处理:BGR转RGB、归一化、resize到256x192 img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (256, 192)) img = img.astype(np.float32) / 255.0 img = (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] img = np.transpose(img, (2, 0, 1))[None, ...] # 推理 heatmaps = session.run(["heatmap_output"], {"input": img})[0] # [1, 9, 64, 48]

到这里就拿到了9个关键点的热图,后面再通过argmax提取坐标就完成了从图像到关键点的全部流程。我实际测试过,在i5-8250U这个级别的老旧笔记本CPU上,整个预处理加推理全程大约25ms,能稳定跑到30帧以上,满足实时计数需求。

5. 避坑与排查:训练、标注、部署中的常见问题

5.1 Qt标注工具崩溃与内存泄漏

现象:标注工具在使用过程中突然卡死或者直接崩溃,尤其在连续标注几百帧、来回翻页之后,操作越来越卡,最终无响应。

原因:Qt的QGraphicsScene里不断创建QGraphicsEllipseItem但未删除旧item。我前面给出的update_keypoint函数里有个if判断,但如果用的是QGraphicsPixmapItem加载大尺寸图片后没有正确释放,QPixmap占用的显存和内存会持续累积。还有一种情况是读取视频帧时用了QMovie或者手动管理QImage而没有调用deleteLater。

解决:标注工具里每次更新关键点时先removeItem旧圆点,这不仅是画图上的需要,也是防止item数量无限增长。代码里加上self.scene.deleteLater()不是标准做法,Qt对象树的正确释放方式是:当标注切换图片时,先清空scene再加载新pixmap。实际操作中我在切换帧时调用self.scene.clear()一次性清掉所有临时item,然后重新加载背景图,这样内存占用一直很稳定。

5.2 ONNX转换后输出维度错乱

现象:用torch.onnx.export转换模型时没有任何报错,但用ONNX Runtime加载后跑出来的heatmap输出shape不是预期的[1, 9, 64, 48],而是变成了[1, 64, 48, 9],关键点维度被挪到了最后。

原因:PyTorch的NCHW格式和ONNX的多种布局兼容问题,本质上是模型里某个permute或reshape算子导出的转换逻辑和ONNX Runtime的维度假设不一致,特别是在自定义反卷积头和输出层混用了不同布局的算子时容易出现。

解决:检查模型forward里有没有非必要的permute和view操作,我见过有人为了可视化方便在模型内部就把heatmap转成了NHWC,这在训练时没问题,但导出到ONNX后维度语义完全不对。正确做法是模型内部统一保持NCHW,可视化时的HWC转换放到后处理代码里——也就是在session.run拿到输出之后再transpose。

5.3 CPU推理延迟高和线程数设置失误

现象:ONNX Runtime在CPU上推理一张256x192图像耗时超过100ms,和PyTorch CPU版直接推理差距不大,完全没有体现出速度优势。

原因:最常见的是线程数设置不合理。Windows上onnxruntime的默认线程数是物理核心数的两倍,超线程环境下线程切换反而拖慢计算。另外,如果模型内部有大量的1x1卷积,这些小算子在多核并行时的效率低下,不如大卷积核算子那样能充分利用并行度。

解决:设置intra_op_num_threads时先看看任务管理器里实际核心数,我一般设为物理核心数,比如4核8线程就设4。另外可以尝试把ONNX模型用onnxruntime.transformers.optimizer做一次图优化,它会把相邻的卷积和激活函数融合,减少kernel启动次数,实测能再快15%左右。

5.4 关键点抖动导致计数不准确

现象:模型推理时关键点坐标在相邻帧之间来回跳动,特别是肩点,静止不动时坐标也在几个像素之间震荡,导致后续角度计算不稳定,计数结果忽多忽少。

原因:热图回归的argmax操作是逐帧独立的,完全没有利用时间上下文。热图上如果有两个响应峰值接近,argmax取最大值的位置会在两帧之间跳变。加上CPU推理时输入图像是单帧截取,没有任何平滑处理,抖动就被直接放大到最终的角度计算里。

解决:在关键点坐标提取之后加一个EMA平滑,对每个关键点的坐标做指数滑动平均。alpha可以取0.6到0.8之间,调参逻辑是:alpha越大越平滑但延迟越高,对于仰卧起坐这种动作频率不高的场景,0.7是个不错的起点。

5.5 训练loss不降的排查顺序

现象:训练跑到第20个epoch,验证集loss始终在0.02附近震荡,比正常情况高了近一个量级,精度远达不到要求。

原因:经过排查发现是标注JSON的格式问题。我需要强调:COCO格式的keypoints数组要求可见性标志只有0、1、2三种值,但标注工具导出时某个版本把遮挡帧写成了-1,这个非法的标志在loss计算时权重为-1,梯度方向直接反向,模型越训越偏。

解决:写一个数据校验脚本,在训练的Dataset加载阶段就检查每个可见性标志是否合法。不要在生产环境省掉这一步。从那以后我每次换数据集都强制走一遍校验流程,再没遇到过这种「loss不降还找不到原因」的玄学问题。

6. 仰卧起坐计数逻辑:角度阈值与状态机实现

6.1 基于关键点角度的动作周期判定

有了稳定的关键点坐标,计数逻辑就变得直白了。仰卧起坐的核心动作是躯干从躺平到坐起再回到躺平,这个过程中肩、髋、膝三点形成的夹角会从大约180度变到接近90度再变回来。连续帧的角度变化曲线呈现典型的正弦波形态,计数就是识别这个波形的完整周期。

角度计算不是简单的atan2就能完事,要考虑三点坐标的方向性问题:以髋关节点为顶点,肩点和膝点分别与髋点连线,两条连线的夹角就是躯干-大腿夹角。这个角度在动作起始时接近180度,起身过程中逐渐减小,到最低点后乘客恢复坐姿时再变大。用余弦定理算角度最直接。

6.2 计数代码实现:滞后比较器防抖动

直接用单一阈值判断角度进出会有一个致命问题:当角度在阈值附近震荡时,计数会来回反复跳,一帧算完成一帧又算未完成。这个项目用了带滞回区间的状态机方案,让角度必须超过上限阈值才能判定为起身完成,必须低于下限阈值才能判定为躺平复位,中间区域不触发任何计数。

ANGLE_UP = 110 # 起身完成的夹角阈值 ANGLE_DOWN = 150 # 躺平状态的夹角阈值 state = "down" # 初始状态:躺平 count = 0 for angle in angle_sequence: if state == "down" and angle < ANGLE_UP: state = "up" # 检测到起身完成 elif state == "up" and angle > ANGLE_DOWN: state = "down" # 检测到躺平复位 count += 1 # 完成一个完整周期

ANGLE_UP设110度是因为仰卧起坐起身到位时躯干夹角大约在100到120度之间。ANGLE_DOWN设150度需要结合视频数据微调。这两个阈值之间留了40度的滞回区间,只要角度在这个区间内来回抖动,状态就不会翻转。这个设计是消除误计数的关键,比任何滤波都管用。

实际部署时还需要加一个时间保险:每个姿态状态至少要维持0.3秒才会触发状态切换,防止快速抖动绕过滞回区间。

6.3 验证方法与效果对比

最后说一下验证手段。项目里有一个eval_video.py脚本,输入一段人工标注了标准次数的测试视频,输出模型自动计数的结果和人工计数的差异。我拿三组不同人、不同拍摄角度的视频各测了一遍,每组30个标准动作,模型计数结果分别为30、29、30,误差在可接受范围内。这个验证方式比直接报一个mAP指标直观得多,也更贴近实际使用场景。

另外建议验证时把角度曲线同时输出成图,肉眼看曲线形状是否规整——如果曲线毛刺很多,说明关键点平滑参数没调好;如果曲线峰值高度不一致,说明角度阈值可能需要根据目标人群调整。这些调试手段都写在了项目自带的eval脚本里,照着改参数重新跑一遍就能看到效果。

这个项目最值得借鉴的其实是它把完整工程链路拆开的思路:每块单独调试、再通过标准接口拼接。从那以后我每次做类似的动作识别项目,都强制先固定数据格式和接口约定,再动手写网络和工具,这套工作流帮我省掉了大量联调时的返工时间。希望这份拆解对你有用。

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

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

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

立即咨询