简介:这份资源面向计算机视觉与机器学习方向的开发者,尤其是正在入门或实践3D人体姿态估计的研究者与学生,提供用Python获取并处理Human3.6M数据集的完整工具脚本。压缩包共14个文件,约34KB,以4个Python脚本为核心,覆盖数据下载、解压、处理与提取等环节,另含txt依赖与校验说明、Dockerfile与docker-compose.yml容器配置、yml、xml元数据、md说明文档及license等,便于快速搭建可复现的运行环境。目前已有3634人学习下载,说明其在同类资源中具备一定参考价值。借助这些脚本,读者可省去手动整理数据流程的繁琐,直接对接HDF5读取、3D关节位置提取、格式转换与数据集划分等步骤,并在此基础上构建CNN、RNN或seq2seq等姿态估计模型,完成训练、评估与可视化对比,适合作为3D姿态理解项目的起步脚手架。
1. 拿到 Human3.6M 之后:Python 读取骨骼点、相机参数与动作标签的完整路径
如果你正在做 3D 人体姿态估计、动作识别或者骨骼动画驱动,大概率绕不开 Human3.6M 这个数据集。它包含 11 位受试者、17 个动作场景、数百万帧图像,以及配套的 2D/3D 关节点坐标和相机标定参数。但真正上手时你会发现,原始数据不是一份 CSV 就能搞定——视频帧、MAT 标注、相机内参分散在不同目录,标签体系还有两套(17 关节和 32 关节)。这篇笔记记录的是我用 Python 从零把 Human3.6M 读成可训练张量的过程,包括目录结构解析、MAT 文件读取、坐标系对齐、动作标签映射,以及几个让我卡了大半天的坑。适合已经下载完数据集、准备写 DataLoader 的开发者。
2. 目录结构与数据格式:先搞清楚你手里有什么
2.1 原始目录的层级逻辑
Human3.6M 下载下来通常是一个顶层文件夹,下面按受试者编号分目录,比如S1、S5、S6、S7、S8、S9、S11。每个受试者目录里再按动作场景分,比如Directions、Discussion、Eating、Greeting、Phoning、Posing、Purchases、Sitting、SittingDown、Smoking、Photo、Waiting、Walking、WalkDog、WalkTogether。每个动作目录下一般有:
imageSequence/或视频文件:原始 RGB 帧annotations.mat:该动作的 2D 关节点、3D 关节点、相机参数等boundingBoxes.mat:人体检测框
我拿到的版本里,图像是.jpg序列,标注是 MATLAB 的.mat文件。这里第一个容易翻车的地方是:不同来源的 Human3.6M 副本,目录命名和文件组织方式不完全一致。有的把视频单独放在videos/下,有的把标注统一放在annotations/里。所以第一步不是写代码,而是先用os.walk把目录树打出来看一眼。
import os root = "/path/to/human36m" for dirpath, dirnames, filenames in os.walk(root): # 只打印到动作目录层级,避免输出爆炸 depth = dirpath.replace(root, "").count(os.sep) if depth <= 2: print(dirpath) for f in filenames[:3]: print(" ", f)这段代码的作用是快速确认目录深度和文件类型。depth <= 2限制只看到受试者/动作这一层,避免把几万张图片全列出来。如果你发现某个动作目录下没有.mat文件,那说明标注可能被集中放在别处,需要调整后续读取路径。
2.2 MAT 文件里到底存了什么
Human3.6M 的标注文件是 MATLAB 的 v7.3 格式,普通scipy.io.loadmat读不了,必须用h5py。文件里的键通常包括:
| 键名 | 含义 | 维度示例 |
|---|---|---|
S | 主体结构体,包含所有标注 | struct |
S/pose | 3D 关节点(世界坐标) | 32×N |
S/pose2d | 2D 关节点(图像坐标) | 32×N |
S/camParam | 相机参数 | struct |
S/box | 检测框 | 4×N |
注意:不同版本里键名可能是小写s或者嵌套更深。我一般会先写一个探测脚本,把顶层键和子键都打印出来。
import h5py mat_path = "/path/to/human36m/S1/Directions/annotations.mat" with h5py.File(mat_path, "r") as f: def print_keys(name, obj): if isinstance(obj, h5py.Dataset): print(f"{name}: shape={obj.shape}, dtype={obj.dtype}") else: print(f"{name}: group") f.visititems(print_keys)visititems会递归遍历所有节点。h5py.Dataset是实际存数据的数组,h5py.Group是嵌套结构。这一步能帮你确认pose到底是 32 关节还是 17 关节,以及数据是否已经转置。我遇到过pose的 shape 是(N, 32)而不是(32, N),如果直接按列取就会全错。
2.3 两套关节体系的区别
Human3.6M 提供 32 关节和 17 关节两套标注。32 关节包含更多末端节点(如手指、脚趾),17 关节是常用简化版,对应 Human3.6M 论文里的标准评估协议。如果你要做 SOTA 对比,必须用 17 关节;如果只是做骨骼驱动或可视化,32 关节更细腻。
17 关节的索引映射在原始 MAT 里不是直接给出的,需要自己按论文定义写死。常见做法是定义一个JOINT_MAP列表,把 32 关节里的对应索引挑出来。这个映射表在网上有多个版本,我建议以你下载的数据集附带的 README 为准,不要随便抄。
3. 用 Python 读取 3D 关节点与相机参数
3.1 读取单帧 3D 坐标
假设你已经确认pose的 shape 是(32, N),其中 N 是帧数。读取某一帧的 3D 坐标:
import h5py import numpy as np def load_3d_pose(mat_path, frame_idx): with h5py.File(mat_path, "r") as f: # 注意:h5py 读取后需要转置,因为 MATLAB 是列优先 pose = f["S/pose"][:] # shape (32, N) pose = pose.T # 转成 (N, 32) frame_pose = pose[frame_idx] # (32,) # 转成 (32, 3) 的 xyz xyz = frame_pose.reshape(-1, 3) return xyz这里的关键是转置。MATLAB 存矩阵是列优先,h5py读出来是原始维度,但 Python 习惯行优先。如果不转置,pose[frame_idx]取到的就是第 frame_idx 个关节在所有帧上的值,完全错了。我当初就是在这里卡了很久,可视化出来是一团乱线。
reshape(-1, 3)把 32×3 的坐标展平成 32 个关节的 xyz。注意 Human3.6M 的 3D 坐标单位是毫米,原点在相机坐标系下,不是世界坐标系。如果你要做全局动作分析,需要额外做坐标系变换。
3.2 相机参数解析与投影
相机参数存在S/camParam里,通常包含内参矩阵K、旋转R、平移T。读取方式:
def load_camera(mat_path): with h5py.File(mat_path, "r") as f: cam = f["S/camParam"] # 具体键名需要探测,常见为 K, R, T K = cam["K"][:] # (3, 3) R = cam["R"][:] # (3, 3) T = cam["T"][:] # (3, 1) return K, R, T拿到K、R、T后,可以把 3D 点投影到 2D 图像平面,验证标注是否对齐:
def project_3d_to_2d(xyz, K, R, T): # xyz: (32, 3) # 转成齐次坐标 ones = np.ones((xyz.shape[0], 1)) xyz_h = np.hstack([xyz, ones]) # (32, 4) # 外参 RT = np.hstack([R, T.reshape(3, 1)]) # (3, 4) cam_coord = (RT @ xyz_h.T).T # (32, 3) # 内参投影 uv = (K @ cam_coord.T).T # (32, 3) uv = uv[:, :2] / uv[:, 2:3] # 归一化 return uv这段代码的逻辑是:先把 3D 点从世界坐标转到相机坐标(乘外参),再用内参投影到像素坐标。如果你发现投影结果和pose2d对不上,大概率是R和T的方向搞反了,或者T需要取负。不同受试者的相机参数是独立的,不能混用。
3.3 批量读取与内存控制
Human3.6M 全量数据很大,一次性把所有 MAT 读进内存会爆。我一般用生成器逐动作、逐帧读取:
def iter_poses(root, subjects, actions): for subj in subjects: for action in actions: mat_path = os.path.join(root, subj, action, "annotations.mat") if not os.path.exists(mat_path): continue with h5py.File(mat_path, "r") as f: pose = f["S/pose"][:].T # (N, 32) for i in range(pose.shape[0]): yield subj, action, i, pose[i].reshape(-1, 3)yield让每次只返回一帧,配合 PyTorch 的DataLoader可以做到流式加载。注意h5py.File在生成器里打开后,如果外部提前中断,文件句柄可能不会立即释放。稳妥做法是把读取逻辑封装成类,在__del__里关闭文件。
4. 动作标签映射与数据集划分
4.1 动作名称到类别 ID
Human3.6M 的 15 个动作(去掉Photo和WalkingDog的某些变体)需要映射成整数标签。我一般用字典硬编码:
ACTION_MAP = { "Directions": 0, "Discussion": 1, "Eating": 2, "Greeting": 3, "Phoning": 4, "Posing": 5, "Purchases": 6, "Sitting": 7, "SittingDown": 8, "Smoking": 9, "Photo": 10, "Waiting": 11, "Walking": 12, "WalkDog": 13, "WalkTogether": 14 }注意Photo在某些协议里被排除,WalkDog和WalkTogether有时合并。如果你要复现某个论文的结果,先去查它的评估协议,不要直接用这个表。
4.2 标准训练/测试划分
Human3.6M 的标准划分是:S1、S5、S6、S7、S8 做训练,S9、S11 做测试。这个划分在几乎所有 3D 姿态论文里通用。写代码时:
TRAIN_SUBJECTS = ["S1", "S5", "S6", "S7", "S8"] TEST_SUBJECTS = ["S9", "S11"]如果你要做跨受试者泛化实验,可以进一步从训练集里留一个受试者做验证。但注意不要用测试集调参,否则结果没有可比性。
4.3 构建 PyTorch Dataset
把前面几块拼起来,一个最小的 Dataset 长这样:
import torch from torch.utils.data import Dataset class Human36MDataset(Dataset): def __init__(self, root, subjects, actions, transform=None): self.samples = [] for subj in subjects: for action in actions: mat_path = os.path.join(root, subj, action, "annotations.mat") if os.path.exists(mat_path): self.samples.append((mat_path, ACTION_MAP[action])) self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): mat_path, label = self.samples[idx] with h5py.File(mat_path, "r") as f: pose = f["S/pose"][:].T # (N, 32) # 这里取中间帧做示例,实际应按帧索引 mid = pose.shape[0] // 2 xyz = pose[mid].reshape(-1, 3).astype(np.float32) if self.transform: xyz = self.transform(xyz) return torch.from_numpy(xyz), label这个 Dataset 把每个动作文件当成一个样本,取中间帧。实际训练时应该按帧展开,或者用滑动窗口取序列。ACTION_MAP在外部定义,transform可以做归一化或随机旋转。
5. 避坑与排查:那些让我重跑实验的细节
5.1 现象:读取的 3D 坐标全是 NaN
原因:MAT 文件里某些帧的pose是空值或填充值,h5py读出来是NaN。Human3.6M 原始数据里确实存在少量缺失帧。
解决:在 Dataset 里加过滤,np.isnan(xyz).any()为真就跳过或补零。我一般选择跳过,因为补零会引入虚假的骨骼长度。
5.2 现象:投影后的 2D 点和pose2d偏差几百像素
原因:相机参数里的R和T是相对于世界坐标系的,而pose可能已经是相机坐标系下的值。两者混用会导致双重变换。
解决:先确认pose的坐标系。如果pose已经是相机坐标,投影时只需要乘K,不要再乘R、T。可以拿一帧手动算一下,看哪个组合能让投影点和pose2d重合。
5.3 现象:不同受试者的骨骼长度不一致
原因:Human3.6M 的 3D 坐标是绝对坐标,不同受试者身高不同,骨骼长度自然不同。如果直接拿绝对坐标训练,模型会学到受试者身份而不是动作。
解决:做骨骼归一化。常见做法是以髋关节为原点,用躯干长度做尺度归一化。或者直接预测相对坐标(每个关节减去父关节)。
5.4 现象:h5py打开文件报OSError: Unable to open file
原因:MAT 文件不是 v7.3 格式,而是更老的 v7 或 v6。h5py只支持 v7.3。
解决:用scipy.io.loadmat试一下,如果能读就用 scipy。或者用 MATLAB 重新保存为 v7.3。我遇到过混合格式的数据集,需要写一个兼容层,先试h5py,失败再试scipy。
5.5 现象:训练时 loss 不下降,准确率随机
原因:标签映射错了。比如ACTION_MAP里Walking和WalkDog的 ID 写反,或者训练集里混入了测试集受试者。
解决:打印每个 batch 的标签分布,确认没有某个类别全是 0。再检查TRAIN_SUBJECTS和TEST_SUBJECTS是否有交集。这个坑很隐蔽,因为数据加载不会报错,只是模型学不到东西。
6. 进阶:用 17 关节协议做标准化评估与可视化验证
6.1 17 关节映射与 MPJPE 计算
要做论文级对比,必须把 32 关节转成 17 关节,并计算 MPJPE(Mean Per Joint Position Error)。映射表我一般这样写:
# 32 关节到 17 关节的索引映射(以常见协议为准) JOINT_17 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] # 实际需要根据你的 MAT 文件调整,这里只是占位MPJPE 计算:
def mpjpe(pred, gt): # pred, gt: (N, 17, 3) return np.mean(np.linalg.norm(pred - gt, axis=-1))注意:计算前要先做根节点对齐(通常是髋关节),否则误差会被全局平移放大。标准协议里还会做尺度对齐,但 Human3.6M 的评估一般只做根对齐。
6.2 可视化验证:用 Matplotlib 画骨架
写完读取代码后,一定要可视化验证。我习惯用 Matplotlib 画 3D 骨架:
import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D def plot_skeleton(xyz, edges): fig = plt.figure() ax = fig.add_subplot(111, projection="3d") for i, j in edges: ax.plot([xyz[i, 0], xyz[j, 0]], [xyz[i, 1], xyz[j, 1]], [xyz[i, 2], xyz[j, 2]], "b-") ax.scatter(xyz[:, 0], xyz[:, 1], xyz[:, 2], c="r") plt.show()edges是骨骼连接列表,需要按 17 关节或 32 关节定义。画出来如果是一个扭曲的人形,说明坐标读取正确;如果是一团乱麻,回去检查转置和 reshape。
6.3 一个我常用的验证习惯
每次换数据集副本或改读取代码后,我会固定做三件事:第一,取 S1 的Directions第一帧,打印 3D 坐标的范围,正常应该在 -1000 到 1000 毫米之间;第二,投影到 2D 和pose2d对比,误差应小于 5 像素;第三,用 17 关节算一遍 MPJPE,如果大于 200 毫米,说明映射或对齐有问题。这三步走完,基本能排除 90% 的读取错误。从那以后我每次接新数据集都强制走一遍这个流程,省下了大量重跑实验的时间。希望帮到你。
本文还有配套的精品资源,点击获取