CNN人体姿态与动作识别:从关键点热图到动作分类
2026/9/12 1:58:17 网站建设 项目流程

简介:面向计算机相关专业毕业设计及项目实战练习,这份基于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_size=256): 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, kpts

kpts第三列是可见性:2表示可见,1表示被遮挡但存在,0表示未标注。训练时要把0的点从loss中剔除,否则未标注点会把热图回归拉偏。bbox扩边比例取1.2~1.3,太小会让肘腕出图,太大则目标占比小,模型对小姿态不敏感。

2.3 生成热图真值的核心逻辑

如果直接把关键点坐标作为回归目标,让CNN回归两个浮点数,模型很难收敛,因为同一个坐标值在语义上可以对应多种视觉形态。常见做法是生成高斯热图,让网络预测"关键点出现在每个像素位置的概率分布",训练目标是逐像素的MSE。

def make_heatmap(kpts, out_size=64, sigma=2): # out_size通常取输入尺寸的1/4,保持热图语义粒度 heatmaps = np.zeros((kpts.shape[0], out_size, out_size), dtype=np.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 heatmaps

sigma=2对应64×64热图;若热图是128×128,sigma常见取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特征图。这个分辨率对肩肘腕来说太粗了,腕部可能只占一个像素,后级热图上采样回来是糊的。

常见做法是把最后两个stride=2的下采样层改成stride=1并配合空洞卷积保持感受野,使特征图变成stride=16或8。我用得比较多的是stride=16的ResNet50,再接一层转置卷积上采样两倍,得到stride=8的中间表示。这个改法只多一个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_joints=17): super().__init__() self.reduce = nn.Sequential( nn.Conv2d(in_ch, 128, 1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), ) self.deconv = nn.ConvTranspose2d( 128, 128, kernel_size=4, stride=2, padding=1 ) 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的输出通道数必须和训练数据的关键点数一致。转置卷积的stride=2会把16×16的输入扩到32×32,如果真值热图是64×64,就再补一层上采样或stride=4的转置卷积。

3.3 损失函数与训练脚本

关键点回归的loss用MSE,但要对可见性mask加权:真值标注为0的位置loss直接置0。把可见性大于等于1的点看作有效,是我默认的mask规则。训练循环的核心如下:

criterion = nn.MSELoss(reduction='none') 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-3,warmup 5个epoch过大会让热图峰值乱跳
热图sigma64热图用2,128热图用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, window=15, n_classes=4): self.win = deque(maxlen=window) self.n_classes = n_classes def step(self, angle_feat, model, label_names): logits = model(torch.tensor(angle_feat, dtype=torch.float32).unsqueeze(0)) cls = int(torch.argmax(logits, dim=1)) 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写入管道,后续的计数、可视化都不需要再碰模型,直接消费这份输出即可。

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

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

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

立即咨询