简介:这份资源面向计算机视觉与机器学习方向的开发者,尤其是正在入门或实践3D人体姿态估计的研究者与学生,提供用Python获取并处理Human3.6M数据集的完整工具链。压缩包共14个文件,约34KB,以4个Python脚本为核心,覆盖数据下载、解压、预处理等环节,另含txt依赖与校验说明、Dockerfile与docker-compose.yml容器配置、yml、xml元数据、md说明文档及license等,便于快速搭建可复现环境。资源围绕Human3.6M的HDF5数据读取、3D关节位置提取、格式转换与标准化、训练验证测试集划分,以及CNN、RNN、seq2seq等建模思路展开,并涉及训练评估与matplotlib可视化对比。已有3634人学习下载,适合希望打通数据准备到模型训练流程、减少环境配置与格式转换踩坑成本的读者参考。
1. 拿到 Human3.6M 之后:Python 读取骨骼点、相机参数与动作标签的完整路径
如果你正在做 3D 人体姿态估计、动作识别或者骨骼动画驱动,大概率绕不开 Human3.6M 这个数据集。它包含 7 位受试者、17 个动作场景、数百万帧带 3D 关节标注的室内视频,是学术界公认的 3D 姿态基准之一。但真正拿到手之后,很多人会卡在第一步:原始数据是压缩包套压缩包,标注文件格式不统一,相机参数和骨骼点分开放,Python 直接读会报各种编码和维度错误。这篇笔记就是把我自己从解压到可视化跑通的那条路拆开,每一步都落到可复制的代码和参数上。适合已经下载完数据集、准备用 Python 做预处理或训练的人,也适合想先摸清数据结构再决定要不要投入的从业者。
2. 数据目录结构与 Python 读取骨骼点:从压缩包到 NumPy 数组
2.1 先认清 Human3.6M 的目录长什么样
下载完的 Human3.6M 通常是一个总目录,里面按受试者编号分成 S1、S5、S6、S7、S8、S9、S11 七个文件夹。每个受试者目录下又有多个子目录,常见的有:
Videos/:原始视频,按动作名分文件夹,每个动作下是多个.mp4或图像序列MyPoseFeatures/:官方提供的 2D/3D 姿态特征,按维度分D2_Positions、D3_Positions、D3_Positions_mono等MySegmentsMat/:人体分割掩码h36m_metadata/:相机参数、骨骼定义等元数据
真正做 3D 姿态训练时,最常用的是D3_Positions下的.cdf文件。每个动作对应一个.cdf,里面存的是该动作所有帧的 3D 关节坐标。注意,这里用的是 MATLAB 的 CDF 格式,Python 不能直接numpy.load,需要专门的读取库。
我一般会先写一个目录扫描脚本,把每个受试者、每个动作的文件路径整理成表,后面做数据集类的时候直接查表,避免每次递归遍历。
import os import glob # 数据集根目录,按实际解压位置改 ROOT = "/data/h36m" # 七个受试者 subjects = ["S1", "S5", "S6", "S7", "S8", "S9", "S11"] # 收集所有 D3_Positions 下的 cdf 文件 records = [] for subj in subjects: pattern = os.path.join(ROOT, subj, "MyPoseFeatures", "D3_Positions", "*.cdf") for path in glob.glob(pattern): action = os.path.splitext(os.path.basename(path))[0] records.append({"subject": subj, "action": action, "path": path}) print(f"共找到 {len(records)} 个动作文件") for r in records[:5]: print(r["subject"], r["action"])这段代码的逻辑很直接:按受试者循环,用glob匹配D3_Positions下所有.cdf,把受试者、动作名、完整路径存成字典列表。参数上唯一要改的是ROOT,指向你解压后的总目录。如果下载的是官方原始包,可能还需要先解压.tgz,解压后目录层级和上面一致。
提示:有些镜像站会把
MyPoseFeatures单独打包,下载后要手动合并到对应受试者目录下,否则扫描结果会少很多动作。
2.2 用 Python 读 CDF 文件并转成 NumPy
CDF 是 MATLAB 的 Common Data Format,Python 里可以用cdflib或者pycdf读取。我习惯用cdflib,安装简单,API 也稳定。
pip install cdflib读取单个动作文件的代码如下:
import cdflib import numpy as np def load_cdf_pose(cdf_path): """ 读取 Human3.6M 的 D3_Positions cdf 文件 返回 shape 为 (帧数, 32, 3) 的数组 """ cdf = cdflib.CDF(cdf_path) # 官方数据里变量名通常是 Pose,取出来是 (帧数, 96) raw = cdf.varget("Pose") # 96 = 32 个关节 × 3 个坐标 poses = raw.reshape(-1, 32, 3) return poses.astype(np.float32) poses = load_cdf_pose(records[0]["path"]) print("帧数, 关节数, 坐标维度:", poses.shape) print("第一帧第一个关节坐标:", poses[0, 0])这里有几个关键点。cdf.varget("Pose")取出来的原始数组是二维的,第二维固定 96,因为 Human3.6M 的 3D 标注包含 32 个关节,每个关节 3 个坐标值。reshape(-1, 32, 3)把它还原成帧数 × 关节数 × 坐标。坐标单位是毫米,原点在相机坐标系下,不是世界坐标系。如果你要做跨受试者训练,后面还得做根节点对齐或者归一化。
参数说明:cdf_path就是上一步 records 里的path。如果varget报变量名不存在,可以先打印cdf.cdf_info()看实际变量名,不同版本可能有细微差异。
2.3 关节索引与骨骼连接关系
Human3.6M 的 32 个关节有固定索引,做可视化和损失函数时经常要按索引取特定关节。常见的关键索引如下:
| 索引 | 关节名 | 说明 |
|---|---|---|
| 0 | Hip | 根节点,骨盆中心 |
| 1 | RHip | 右髋 |
| 2 | RKnee | 右膝 |
| 3 | RFoot | 右脚 |
| 6 | LHip | 左髋 |
| 7 | LKnee | 左膝 |
| 8 | LFoot | 左脚 |
| 12 | Spine | 脊柱 |
| 13 | Thorax | 胸腔 |
| 14 | Neck | 脖子 |
| 15 | Head | 头 |
| 16 | RShoulder | 右肩 |
| 17 | RElbow | 右肘 |
| 18 | RWrist | 右手腕 |
| 19 | LShoulder | 左肩 |
| 20 | LElbow | 左肘 |
| 21 | LWrist | 左手腕 |
做 3D 姿态估计时,很多论文只取其中 17 个关节做评估,索引映射需要单独写一个转换表。我一般会在数据集类里维护一个JOINT_MAP字典,避免每次手写数字。
# 17 关节评估常用的索引映射 JOINT_MAP = { "hip": 0, "rhip": 1, "rknee": 2, "rfoot": 3, "lhip": 6, "lknee": 7, "lfoot": 8, "spine": 12, "thorax": 13, "neck": 14, "head": 15, "rshoulder": 16, "relbow": 17, "rwrist": 18, "lshoulder": 19, "lelbow": 20, "lwrist": 21, } def select_joints(poses, joint_names): idx = [JOINT_MAP[n] for n in joint_names] return poses[:, idx, :]这样取出来的数组形状是(帧数, 17, 3),可以直接喂给后续的归一化或网络。
3. 相机参数与动作标签:把 3D 坐标放回真实空间
3.1 读取相机内参和外参
Human3.6M 的 3D 坐标是在相机坐标系下的,如果你要做多视角投影或者生成 2D 投影,必须拿到相机参数。官方在h36m_metadata或者MyPoseFeatures同级目录下会提供cameras.h5或类似文件。用h5py读取:
import h5py def load_camera_params(h5_path, subject): """ 读取指定受试者的相机参数 返回 dict: camera_id -> {R, T, f, c} """ with h5py.File(h5_path, "r") as f: grp = f[subject] cams = {} for cam_id in grp.keys(): cam = grp[cam_id] cams[cam_id] = { "R": np.array(cam["R"]), # 旋转矩阵 3x3 "T": np.array(cam["T"]), # 平移向量 3 "f": np.array(cam["f"]), # 焦距 2 "c": np.array(cam["c"]), # 主点 2 } return cams cams = load_camera_params("/data/h36m/h36m_metadata/cameras.h5", "S1") print("S1 可用相机:", list(cams.keys())) print("相机 1 焦距:", cams["1"]["f"])参数说明:R和T构成外参,把世界坐标转到相机坐标;f和c是内参,用于投影到像素平面。注意 Human3.6M 有四个相机,编号通常是 1 到 4,不同受试者可用相机可能不同。做投影时公式是:
def project_3d_to_2d(pose_3d, cam): """ pose_3d: (N, 3) 相机坐标系下的 3D 点 返回 (N, 2) 像素坐标 """ f = cam["f"].reshape(1, 2) c = cam["c"].reshape(1, 2) # 相机坐标系下 z 为深度 x = pose_3d[:, 0] / pose_3d[:, 2:3] * f[:, 0:1] + c[:, 0:1] y = pose_3d[:, 1] / pose_3d[:, 2:3] * f[:, 1:2] + c[:, 1:2] return np.concatenate([x, y], axis=1)这段投影代码假设 3D 点已经在相机坐标系下,如果拿到的是世界坐标,要先乘R加T。实际用的时候,我一般会先检查pose_3d[:, 2]是否全为正,如果有负值说明坐标系方向不对,需要调整。
3.2 动作标签与帧级标注
Human3.6M 的每个.cdf文件对应一个动作,动作名就是文件名,比如Walking-1、Eating-2。但如果你要做帧级动作识别,还需要知道每一帧属于哪个动作阶段。官方没有提供逐帧标签,常见做法是用动作名作为整段视频的标签,或者用姿态变化做分段。
我一般会建一个动作名到类别 ID 的映射表:
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, } def parse_action(action_name): # 文件名形如 Walking-1,去掉编号 base = action_name.split("-")[0] return ACTION_MAP.get(base, -1)这样每个.cdf文件就能对应一个整数标签。如果要做帧级,可以按固定窗口切分,每个窗口继承整段标签,这是最常见的弱监督做法。
3.3 构建 PyTorch Dataset 的完整示例
把前面几步串起来,就是一个可用的 Dataset 类:
import torch from torch.utils.data import Dataset class Human36MDataset(Dataset): def __init__(self, records, transform=None): self.records = records self.transform = transform def __len__(self): return len(self.records) def __getitem__(self, idx): rec = self.records[idx] poses = load_cdf_pose(rec["path"]) # (T, 32, 3) label = parse_action(rec["action"]) # int # 取 17 个评估关节 poses = select_joints(poses, list(JOINT_MAP.keys())) # 根节点对齐:减去 hip 坐标 poses = poses - poses[:, 0:1, :] if self.transform: poses = self.transform(poses) return torch.from_numpy(poses), label ds = Human36MDataset(records) print("数据集大小:", len(ds)) sample, label = ds[0] print("样本形状:", sample.shape, "标签:", label)这里做了根节点对齐,把 hip 关节平移到原点,这是 3D 姿态任务的常规操作,能消除受试者身高和相机位置的影响。transform可以接随机旋转、缩放等增强。注意poses是 float32,转 tensor 后可以直接进网络。
4. 避坑与排查:Human3.6M 读取时最容易翻车的五个地方
4.1 现象:cdflib报CDFError: Variable not found
原因:不同来源的 Human3.6M 包,CDF 内部变量名可能不叫Pose,有的叫Pose,有的叫pose,还有的用Pose_1。直接硬编码变量名就会翻车。
解决:先打印变量列表再取。
cdf = cdflib.CDF(path) info = cdf.cdf_info() print(info.zVariables) # 查看所有变量名拿到实际变量名后再改varget的参数。我一般会写一个兼容函数,按优先级尝试["Pose", "pose", "Pose_1"]。
4.2 现象:reshape 后关节数不对,报cannot reshape array of size X into (T, 32, 3)
原因:有些版本的 D3_Positions 存的是 96 维,但帧数不是整数倍,或者你拿到的其实是 2D 数据(48 维)。2D 数据在D2_Positions下,别混用。
解决:先看raw.shape,确认第二维是 96 还是 48。96 对应 32 关节 3D,48 对应 24 关节 2D 或者 16 关节 3D。根据实际维度调整 reshape 参数。
4.3 现象:相机参数读取时KeyError: 'S1'
原因:cameras.h5的顶层键不一定是受试者编号,有的版本是subject1、subject5这种命名,或者所有受试者放在一个组里。
解决:用h5py先看顶层键。
with h5py.File(h5_path, "r") as f: print(list(f.keys()))如果键名是subject1,就把代码里的f[subject]改成f["subject" + subject[1:]]。这种命名差异在二手镜像里很常见,血泪经验是别信文档,直接打印。
4.4 现象:投影后的 2D 点全部跑到图像外面
原因:3D 坐标单位是毫米,而相机内参的焦距和主点通常是像素单位,直接除会导致数值巨大。另外,如果 3D 点没有先转到相机坐标系,投影公式不成立。
解决:确认 3D 点已经乘过R加T,并且检查f和c的量级。Human3.6M 的焦距一般在 1000 到 2000 像素之间,主点在 500 到 1000 左右。如果f是 0.001 量级,说明单位是米,需要乘 1000 转毫米。
4.5 现象:训练时 loss 不下降,预测的骨骼全部缩成一团
原因:没有做根节点对齐或者归一化,不同受试者的绝对坐标差异太大,网络学不到相对结构。另外,如果用了 32 关节但只监督 17 个,索引对不上也会导致梯度混乱。
解决:在 Dataset 里强制做poses = poses - poses[:, 0:1, :],并且确保监督的关节索引和JOINT_MAP一致。我一般会在__getitem__里加一行断言,检查poses.shape[1] == 17,不满足就直接报错,避免静默出错。
5. 进阶:用 Python 做骨骼可视化与跨受试者验证
5.1 用 Matplotlib 快速画 3D 骨骼
读完数据后,最直观的验证方式是把一帧骨骼画出来。不需要复杂的渲染库,Matplotlib 的 3D 坐标轴就够用。
import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 骨骼连接对,按 17 关节索引 BONES = [ (0, 1), (1, 2), (2, 3), # 右腿 (0, 4), (4, 5), (5, 6), # 左腿 (0, 7), (7, 8), (8, 9), (9, 10), # 脊柱到胸 (8, 11), (11, 12), (12, 13), # 左臂 (8, 14), (14, 15), (15, 16), # 右臂 ] def plot_skeleton(pose_3d, ax=None): if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection="3d") x, y, z = pose_3d[:, 0], pose_3d[:, 1], pose_3d[:, 2] ax.scatter(x, y, z, c="r", s=20) for i, j in BONES: ax.plot([x[i], x[j]], [y[i], y[j]], [z[i], z[j]], "b-") ax.set_xlabel("X") ax.set_ylabel("Y") ax.set_zlabel("Z") return ax # 取第 100 帧画图 sample_pose = sample[100].numpy() plot_skeleton(sample_pose) plt.show()这段代码里BONES是按 17 关节索引定义的,如果你用的是 32 关节,需要重新映射。画出来的图能直接看出骨骼比例是否正常,如果某个关节明显偏离,说明索引映射错了。
5.2 跨受试者验证的划分策略
Human3.6M 的标准评估协议是留一受试者交叉验证,通常用 S1、S5、S6、S7、S8 做训练,S9 和 S11 做测试。我一般会写一个划分函数:
def split_by_subject(records, train_subjects, test_subjects): train = [r for r in records if r["subject"] in train_subjects] test = [r for r in records if r["subject"] in test_subjects] return train, test train_recs, test_recs = split_by_subject( records, train_subjects=["S1", "S5", "S6", "S7", "S8"], test_subjects=["S9", "S11"], ) print("训练集动作数:", len(train_recs), "测试集动作数:", len(test_recs))这个划分的关键是受试者之间不能重叠,否则测试集泄漏,指标会虚高。很多新手直接随机按帧划分,结果同一受试者的相似帧同时出现在训练和测试里,模型看起来很好,实际泛化一塌糊涂。从那以后我每次做 Human3.6M 实验,都强制先按受试者切分,再在训练集内部做帧级 shuffle,这个习惯帮我省掉了不少后悔药。
5.3 一个容易忽略的细节:帧率与时间对齐
Human3.6M 的视频是 50Hz,但不同动作的帧数差异很大。做时序模型时,如果直接按固定长度切窗口,短动作会不够,长动作会截断。常见做法是设置window_size=64或128,对短序列做循环填充,对长序列做随机采样。我一般会在 Dataset 里加一个__getitem__的窗口逻辑:
def temporal_window(poses, window_size=64): T = poses.shape[0] if T >= window_size: start = np.random.randint(0, T - window_size + 1) return poses[start:start + window_size] else: # 循环填充 repeat = window_size // T + 1 padded = np.tile(poses, (repeat, 1, 1)) return padded[:window_size]这个函数保证每个样本输出固定长度,同时保留随机性。参数window_size根据你的模型感受野来定,一般 64 到 256 之间。注意填充方式会影响时序连续性,如果做严格的动作分割,循环填充可能引入假边界,这时候改用线性插值更稳妥。
希望帮到你。
本文还有配套的精品资源,点击获取