CNN人体姿态与动作识别:从关键点热图到动作分类

发布时间:2026/9/12 1:58:21
CNN人体姿态与动作识别:从关键点热图到动作分类 简介面向计算机相关专业毕业设计及项目实战练习这份基于CNN深度学习的人体姿态与动作识别系统源码包适合需要完成毕设、课程设计或期末大作业的学生也适合希望进阶深度学习的开发者。代码经测试运行成功曾获导师认可的高分评价。资源内含6个文件以5个Python脚本和1个Markdown项目说明为主涵盖数据采集、模型训练、测试及姿态检测等环节结构简洁便于二次开发与功能扩展。压缩包仅7KB轻量易用。已有205人学习浏览足见其参考价值。下载后建议先阅读项目说明可快速了解运行流程与文件作用如需远程教学也可沟通能够帮助初学者降低上手门槛。1. 基于CNN的人体姿态与动作识别真正要解决的是两段式任务人体姿态和动作识别这个标题容易被当成一个模型搞定的事。实际做工程的时候它是由两个任务串起来的管道先用CNN在单帧图像上回归出脖子、肩、肘、腕这些关键点的坐标热图再把连续帧的坐标序列交给动作分类逻辑来判断人在干什么。健身动作计数、康复训练评估、安防行为分析甚至舞台特效驱动背后都是同一套结构。用CNN做深度学习姿态估计真正的分水岭在热图回归和动作分类的衔接而不是网络堆得多深。我见过不少项目卡在关键点坐标直接回归上loss不收敛坐标乱跳其实是坐标系归一化和热图生成出了问题。下面按数据、模型、动作分类、评估调参的顺序拆解给你一条能直接落地的Python实现路径。2. 数据准备关键点标注格式与热图真值生成2.1 选COCO还是MPII先看后级任务要什么人体关键点公开数据集主要用COCO和MPII。COCO提供17个关键点包含左右耳、左右眼、鼻子、颈、肩肘腕胯膝踝那一整套MPII是16点没有左右耳之分多一个胸骨点。常见做法是直接用COCO 17点因为动作分类很容易把左右手/左右脚作为特征维度COCO的命名和顺序更顺手。我自己会优先选COCO格式还有一个原因它的annotation里带bbox和iscrowd做单人裁剪和密集场景过滤都方便。下表是两个格式的差别数据集关键点数热图通道适合场景COCO1717健身、康复、手语这些左右对称动作多的场景MPII1616通用姿态估计人物主体清晰自标注自定义K只关心末梢关节比如手势或手指动作自建数据集时我一般把关键点统一存成像素坐标 可见性标记的三元组键名沿用COCO风格这样换主干或换框架都不动数据层。2.2 把标注JSON读成Python张量数据读取层要做的不只是把JSON读进来。训练时我一般先把图像crop到检测框内再resize到统一尺寸归一化放在resize之后因为热图真值也是在resize后的分辨率上生成的。一个最小可用的loader核心如下import json, cv2, numpy as np def load_annotation(json_path): with open(json_path) as f: data json.load(f) annos [] for item in data[annotations]: kpts np.array(item[keypoints]).reshape(-1, 3) # x, y, visibility annos.append({ bbox: item[bbox], # x, y, w, h kpts: kpts, image_id: item[image_id] }) return annos def crop_to_bbox(img, kpts, bbox, out_size256): x, y, w, h bbox # 常见做法是在bbox四周再扩20%的上下文避免手脚贴边 cx, cy x w / 2, y h / 2 side max(w, h) * 1.2 x0, y0 int(cx - side / 2), int(cy - side / 2) x0, y0 max(x0, 0), max(y0, 0) img img[y0:y0 int(side), x0:x0 int(side)] img cv2.resize(img, (out_size, out_size)) kpts[:, 0] (kpts[:, 0] - x0) / side * out_size kpts[:, 1] (kpts[:, 1] - y0) / side * out_size return img, kptskpts第三列是可见性2表示可见1表示被遮挡但存在0表示未标注。训练时要把0的点从loss中剔除否则未标注点会把热图回归拉偏。bbox扩边比例取1.2~1.3太小会让肘腕出图太大则目标占比小模型对小姿态不敏感。2.3 生成热图真值的核心逻辑如果直接把关键点坐标作为回归目标让CNN回归两个浮点数模型很难收敛因为同一个坐标值在语义上可以对应多种视觉形态。常见做法是生成高斯热图让网络预测关键点出现在每个像素位置的概率分布训练目标是逐像素的MSE。def make_heatmap(kpts, out_size64, sigma2): # out_size通常取输入尺寸的1/4保持热图语义粒度 heatmaps np.zeros((kpts.shape[0], out_size, out_size), dtypenp.float32) grid_y, grid_x np.meshgrid(np.arange(out_size), np.arange(out_size)) for i, (x, y, v) in enumerate(kpts): if v 0: continue gx, gy x / 4.0, y / 4.0 # 缩放到热图分辨率 d2 (grid_x - gx) ** 2 (grid_y - gy) ** 2 heatmaps[i] np.exp(-d2 / (2.0 * sigma ** 2)) return heatmapssigma2对应64×64热图若热图是128×128sigma常见取2.5~3。热图分辨率越高定位越准但显存涨得快。折中方案是训练用256×256输入配64×64热图精度不足时推理换成384×384。2.4 图像增强翻转时必须交换左右关键点人体姿态里水平翻转是最有价值的增强它把左右手样本量直接翻倍。注意翻转不只是镜像图像关键点索引也要互换比如left_shoulder和right_shoulder整体交换。翻转配对表如下# COCO 17点里左右互换的索引对 flip_pairs [ [1, 2], # 眼 [3, 4], # 耳 [5, 6], # 肩 [7, 8], # 肘 [9, 10], # 腕 [11, 12], # 胯 [13, 14], # 膝 [15, 16] # 踝 ] def flip_kpts(kpts, img_w): kpts[:, 0] img_w - 1 - kpts[:, 0] for a, b in flip_pairs: kpts[a], kpts[b] kpts[b].copy(), kpts[a].copy() return kpts除了翻转旋转±30度、缩放0.8~1.25、亮度抖动也是图像增强CNN算法系列的常规操作。旋转和缩放的变换矩阵需要同时作用于关键点坐标建议把所有变换合成一个仿射矩阵一次算完避免坐标多次漂移。提示增强时不要把被裁出图外的点改成未标注。标成1遮挡比0更好CNN仍能从躯干上下文推断它的位置。3. 模型构建从CNN主干到关键点热图回归3.1 backbone下采样倍数决定热图分辨率姿态估计一般拿ResNet做主干。ResNet50默认stride是32即224输入得到7×7特征图。这个分辨率对肩肘腕来说太粗了腕部可能只占一个像素后级热图上采样回来是糊的。常见做法是把最后两个stride2的下采样层改成stride1并配合空洞卷积保持感受野使特征图变成stride16或8。我用得比较多的是stride16的ResNet50再接一层转置卷积上采样两倍得到stride8的中间表示。这个改法只多一个3×3空洞卷积层和一层上采样速度损失可控。主干选择可以这样看主干输出stride热图尺寸输入256速度取向精度取向ResNet501616×16高中型目标均衡MobileNetV31616×16更高端侧部署HRNet-W32464×64低关键点级的高精度3.2 检测头1x1卷积加转置上采样主干输出后接一个1x1卷积把通道压到关键点数量K再用转置卷积把分辨率扩到和训练真值一致。激活函数统一用ReLU姿态回归这种稠密输出任务用不上GELU那种重激活。PyTorch实现可以这样写import torch.nn as nn class PoseHead(nn.Module): def __init__(self, in_ch, num_joints17): super().__init__() self.reduce nn.Sequential( nn.Conv2d(in_ch, 128, 1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) self.deconv nn.ConvTranspose2d( 128, 128, kernel_size4, stride2, padding1 ) self.out nn.Conv2d(128, num_joints, 1) # 输出17通道热图 def forward(self, x): x self.reduce(x) x self.deconv(x) return self.out(x)head的输出通道数必须和训练数据的关键点数一致。转置卷积的stride2会把16×16的输入扩到32×32如果真值热图是64×64就再补一层上采样或stride4的转置卷积。3.3 损失函数与训练脚本关键点回归的loss用MSE但要对可见性mask加权真值标注为0的位置loss直接置0。把可见性大于等于1的点看作有效是我默认的mask规则。训练循环的核心如下criterion nn.MSELoss(reductionnone) for epoch in range(num_epochs): for imgs, heatmaps, mask in train_loader: preds model(imgs) # (B, 17, 64, 64) loss criterion(preds, heatmaps) # (B, 17, 64, 64) loss loss * mask # mask为0处不计loss loss loss.sum() / mask.sum() optimizer.zero_grad() loss.backward() optimizer.step()这里没有用带权MSE而是等权MSE加可见性mask。如果某个关节总是错位比如腕可以在mask之外再给腕部热图乘一个大于1的权重这会拉高整体loss波动需要同时调低学习率。3.4 训练收敛的3个关键参数参数常见取值影响输入尺寸256×256起步384×384提精度越大定位越准显存翻倍初始学习率AdamW配1e-3warmup 5个epoch过大会让热图峰值乱跳热图sigma64热图用2128热图用2.5~3过小梯度稀疏过大定位模糊刚开始动手训练时先用512个样本、256×256输入把模型跑到200个iteration确认loss有下降趋势同时可视化热图峰值坐标是否跟着真值走。如果热图全是背景置信度优先查可见性mask和sigma设置如果峰值跟着人走但位置偏移大把输入尺寸抬到384×384。推理阶段建议做翻转融合原图和水平翻转图各预测一次翻转图的关键点坐标交换回原坐标系两张热图取平均对OKS有稳定提升。4. 动作分类姿态序列到动作标签的落地管道4.1 动作分类的两种路线骨架序列还是视频帧关键点拿到以后动作分类有两条路线一种是直接把连续帧裁剪图送进视频分类模型如UCF101上常见的3D CNN路线另一种是从骨架序列出发用角度或坐标序列做分类。我会优先选骨架序列路线因为2D姿态已经是高度压缩的表示旋转、缩放各向同性不需要再让分类器学尺度不变性。路线输入模型规模帧率要求落点视频帧分类连续RGB帧大必须有GPU动作高度依赖纹理骨架序列分类关键点坐标/角度小CPU也能跑健身、康复、行为视频帧分类在纹理差异大的场景很好用但无法区分同姿势不同人时容易过拟合。骨架序列分类对衣着、光照不敏感关键是序列要连续帧断档时动作识别直接失效。4.2 用关节角度做特征省掉时序模型如果动作只有两三类用关节角度加阈值分类完全够用不需要上LSTM。角度特征的核心优势是尺度不变同一动作不管人站在远处还是近处角度值不变。下面用三个点算一个角度def angle_between(p1, p2, p3): # p1-p2-p3三点返回p2处的夹角单位度 v1 np.array(p1[:2]) - np.array(p2[:2]) v2 np.array(p3[:2]) - np.array(p2[:2]) cos np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-6) return np.degrees(np.arccos(np.clip(cos, -1, 1))) # 以左手抬平为例肩-肘-腕夹角接近180度 arm_angle angle_between( keypoints[5], # left_shoulder keypoints[7], # left_elbow keypoints[9] # left_wrist )角度特征通常取肩肘腕、胯膝踝共8个角度拼成一个8维向量。动作分类可以用一个随机森林或3层MLP输入8维特征输出N个动作类别加一个无动作。如果你动作本身有先后顺序比如抬手—放下那么连续动作就加一层一阶差分特征把上一帧的角度连同当前帧拼成16维。4.3 滑窗推理与动作平滑单帧分类结果必然有抖动常见做法是开一个滑窗每帧计算角度特征并压入队列队列满后对窗口内的类别做多数投票。窗口长度一般取15~30帧对应0.5~1秒30fps既有响应延迟又能滤掉大部分抖动。from collections import deque class ActionSmoother: def __init__(self, window15, n_classes4): self.win deque(maxlenwindow) self.n_classes n_classes def step(self, angle_feat, model, label_names): logits model(torch.tensor(angle_feat, dtypetorch.float32).unsqueeze(0)) cls int(torch.argmax(logits, dim1)) self.win.append(cls) # 多数投票 votes [0] * self.n_classes for c in self.win: votes[c] 1 return label_names[votes.index(max(votes))]窗口越大动作切换越平滑但动作起始会滞后。如果想保留动作起始时间可以额外记录窗口内的类别切换点的帧号。这个滑窗本身不增加推理成本是纯Python的list操作CPU上跑也没有压力。5. 评估与调参一份能单帧验证的脚本5.1 用OKS评估关键点精度评估姿态估计不能只看分类准确率关键点要用OKS即目标关键点相似度。OKS公式为 exp(-d_i² / (2 s² σ_i²))其中 d_i 是检测点与真值点的欧氏距离s 是目标框的尺度σ_i 是每个关键点的归一化标准差。OKS平均值超过0.5时可以认为关键点定位可用。这个指标比单纯算平均像素误差更公平因为它把目标大小的影响归一化了。5.2 置信度阈值与NMS推理时热图上可能因为背景干扰出现多个峰值常见做法是对热图做3×3 maxpool保留峰值点中置信度最高的那个并设一个阈值0.3~0.5过滤低置信度。如果场景里有多人先把检测框送入物体检测器再对每个框各自跑一次姿态估计最后跨框做按距离的NMS。注意NMS的IoU阈值不要设到0.7以上姿态框之间重叠度本身就高阈值太松会把相邻人的框并掉。5.3 单帧验证脚本下面的脚本可以拿一张图跑出17个关键点加一个动作标签当项目里的最小验证入口import cv2, torch def infer_single_frame(img, device): crop, _ preprocess(img) # 你训练时的预处理 out model(crop.to(device))[0].cpu() # (17, H, W) kpts [] for heat in out: _, max_val, _, max_loc cv2.minMaxLoc(heat.numpy()) kpts.append((max_loc[0] * 4, max_loc[1] * 4, max_val)) feat angle_features(kpts) # 转成角度特征 action classifier(feat) return kpts, action img cv2.imread(demo.jpg) kpts, action infer_single_frame(img, device) for name, (x, y, conf) in zip(JOINT_NAMES, kpts): print(f{name}: ({x:.1f}, {y:.1f}) conf{conf:.2f}) print(action:, action)验证脚本要跑两遍一遍原图一遍翻转图。如果两次输出的关键点位置差超过10像素说明骨架不稳定先回去看sigma和数据增强比例不要急着调细化网络。把每帧的坐标和动作标签按JSON写入管道后续的计数、可视化都不需要再碰模型直接消费这份输出即可。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询