ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Human3.6M环境搭建与数据加载实战:从零跑通3D姿态估计

Human3.6M环境搭建与数据加载实战:从零跑通3D姿态估计

简介:这份资源面向计算机视觉与机器学习方向的开发者,尤其是正在入门或实践3D人体姿态估计的研究者与学生,围绕Human3.6M这一标准数据集,提供用Python获取与处理数据的完整工程代码。压缩包共14个文件,约34KB,以4个Python脚本为核心,覆盖数据下载、处理与提取等环节,另含txt校验与依赖说明、Dockerfile与docker-compose.yml容器配置、yml、xml元数据、md说明文档及license等,便于快速搭建可复现的运行环境。资源已有3634人学习下载,热度较高。读者可借助脚本完成HDF5数据读取、3D关节位置提取、格式转换与标准化,并参考说明进行训练集、验证集、测试集划分,为CNN、RNN或seq2seq等姿态估计模型的构建、训练与评估打好数据基础,同时理解多视角、多动作数据的组织方式与工程化处理思路。

1. 从一次数据加载失败说起:Human3.6M 环境搭建到底卡在哪

第一次在实验室服务器上跑 Human3.6M 的姿态估计训练,我信心满满地装完 PyTorch,结果卡在了数据加载这一步。报错信息翻来覆去就那几句:找不到h36m目录、.npz文件解析失败、torch.utils.data的collate_fn直接抛异常。折腾了大半天才反应过来——问题根本不在模型代码,而是这套数据集的环境搭建本身就有一堆前置条件:目录结构要对、标注文件要转格式、Python 版本和依赖库版本还得对得上。Human3.6M 是 3D 人体姿态估计领域最常用的基准数据集之一,做机器学习方向的同学几乎绕不开它,但它的原始数据格式和主流框架之间隔着一层不薄的适配工作。这篇笔记记录的就是我从零搭好这套环境、跑通第一个数据加载脚本的完整过程,适合刚接触 3D 姿态估计、正准备在本地或服务器上部署 Human3.6M 的开发者。

2. 环境准备:Python 版本、依赖库与目录结构

2.1 版本选择与依赖清单

Human3.6M 本身是 MATLAB 格式的标注文件,Python 这边要读它,核心依赖是h5py和numpy。但真正决定你能不能跑通的,是深度学习框架的版本。我试过三种组合,最后稳定下来的是 Python 3.8 + PyTorch 1.10 + CUDA 11.3。Python 3.9 以上在装h5py时偶尔会遇到 wheel 不匹配的问题,Python 3.7 又太老,部分新版的numpy已经不支持了。

下面是我实际用的依赖清单,直接写进requirements.txt:

# requirements.txt numpy==1.21.6 h5py==3.6.0 torch==1.10.0 torchvision==0.11.1 matplotlib==3.5.3 tqdm==4.64.1

numpy锁在 1.21.6 是因为 1.22 之后np.float被彻底移除,而 Human3.6M 的一些旧版处理脚本里还在用这个别名。h5py3.6.0 是我测下来读写.h5标注文件最稳的版本,再新的版本在读取某些旧格式的 Human3.6M 标注时会报OSError: Unable to open file。torch和torchvision的版本要对应,1.10 配 0.11.1 是官方验证过的组合。

安装命令:

# 创建虚拟环境,避免污染系统 Python python3.8 -m venv h36m_env source h36m_env/bin/activate # 安装 PyTorch,CUDA 11.3 版本 pip install torch==1.10.0+cu113 torchvision==0.11.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装其余依赖 pip install -r requirements.txt

这里有个细节:PyTorch 的 CUDA 版本要和服务器驱动匹配。我用的服务器驱动是 465.19,支持 CUDA 11.3,所以选了cu113的 wheel。如果你的驱动版本更低,比如 450 系列,那就得降到 CUDA 11.0 对应的 PyTorch 1.7.1。查驱动版本用nvidia-smi,右上角那行CUDA Version就是上限。

2.2 目录结构规划

Human3.6M 原始数据集下载下来是一堆压缩包,解压后目录很乱。我一般会先规划好一个固定的目录结构,后面写 DataLoader 的时候直接按这个路径来,省得每次改代码:

# 我用的目录结构 data/ ├── h36m/ │ ├── annotations/ │ │ ├── h36m_train.npz │ │ └── h36m_test.npz │ ├── images/ │ │ ├── s_01_act_02_subact_01_ca_01/ │ │ ├── s_01_act_02_subact_01_ca_02/ │ │ └── ... │ └── metadata.xml └── scripts/ ├── prepare_data.py └── dataset.py

annotations放转换后的.npz文件,images放原始图像序列,metadata.xml是数据集自带的相机参数和动作标签。这个结构不是官方强制的,但社区里大部分开源项目的 DataLoader 都默认按这个来,比如VideoPose3D和MotionBERT的预处理脚本。提前对齐能省掉后面改路径的麻烦。

创建目录的命令:

mkdir -p data/h36m/annotations data/h36m/images data/scripts

提示:如果你的数据盘空间紧张,images目录可以放到挂载的大容量盘上,然后用软链接链回来。Human3.6M 的原始图像有几十 GB,全量下载前先确认磁盘余量。

2.3 验证 Python 环境是否就绪

装完依赖别急着跑数据,先验证一下核心库能不能正常导入:

# check_env.py import numpy as np import h5py import torch print(f"numpy version: {np.__version__}") print(f"h5py version: {h5py.__version__}") print(f"torch version: {torch.__version__}") print(f"CUDA available: {torch.cuda.is_available()}") print(f"CUDA device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU only'}")

跑python check_env.py,如果CUDA available输出True,说明 PyTorch 和显卡驱动对上了。如果输出False,先别往下走,回去检查 CUDA 版本和驱动版本是否匹配。这一步偷懒,后面训练时会在model.cuda()那里直接崩掉。

3. 数据准备:从原始标注到可加载的 npz 文件

3.1 原始数据下载与解压

Human3.6M 的原始数据需要从官方渠道获取,下载下来是多个压缩包,按受试者(subject)和动作(action)分卷。我一般会先下载标注文件和几个受试者的图像数据做测试,不用一上来就拉全量。标注文件是h36m_annotations.rar或类似的压缩包,解压后得到.h5文件和metadata.xml。

解压命令:

# 假设下载的压缩包放在 ~/Downloads 下 cd ~/Downloads unzip h36m_annotations.zip -d ./h36m_raw # 如果压缩包是分卷的,先合并再解压 cat h36m_images_part_* > h36m_images_full.zip unzip h36m_images_full.zip -d ./h36m_raw/images

解压后检查一下文件完整性,标注文件应该有h36m_train.h5和h36m_test.h5两个,图像目录下每个动作序列对应一个文件夹,里面是.jpg格式的帧图像。

3.2 标注文件格式转换

原始.h5文件的结构比较绕,直接读也能读,但写 DataLoader 的时候不方便。我一般会先转成.npz格式,把关键字段提取出来:

# prepare_data.py import h5py import numpy as np import os def convert_h36m_annotations(h5_path, output_path): """ 将 Human3.6M 的 h5 标注文件转换为 npz 格式 提取 3D 关节点坐标、2D 投影坐标和相机参数 """ with h5py.File(h5_path, 'r') as f: # 遍历所有受试者和动作 for subject in f.keys(): for action in f[subject].keys(): # 3D 关节点坐标,形状为 (帧数, 32, 3) poses_3d = f[subject][action]['3d_joints'][:] # 2D 投影坐标,形状为 (帧数, 32, 2) poses_2d = f[subject][action]['2d_joints'][:] # 相机外参 camera_param = f[subject][action]['camera_param'][:] # 保存为 npz,每个动作序列一个文件 save_name = f"{subject}_{action}.npz" np.savez_compressed( os.path.join(output_path, save_name), poses_3d=poses_3d, poses_2d=poses_2d, camera_param=camera_param ) print(f"Saved {save_name}, 3D shape: {poses_3d.shape}") if __name__ == "__main__": convert_h36m_annotations( h5_path="data/h36m/annotations/h36m_train.h5", output_path="data/h36m/annotations/" )

这段代码的核心逻辑是遍历.h5文件里的层级结构,把每个动作序列的 3D 坐标、2D 坐标和相机参数分别提取出来,存成独立的.npz文件。poses_3d的形状是(帧数, 32, 3),32 是关节点数量,3 是 XYZ 坐标。poses_2d是投影到图像平面的坐标,训练 2D 到 3D 的提升网络时会用到。camera_param包含相机的内外参数,做多视角融合或者坐标变换时要用。

注意:不同版本的 Human3.6M 标注文件里字段名可能不一样,有的用3d_joints,有的用S。跑之前先用h5py打开文件看一眼结构,f[subject][action].keys()打印出来确认字段名。

3.3 写一个可复用的 Dataset 类

转换完数据,下一步是写 PyTorch 的Dataset类。这个类要能按索引读取单个样本,返回 3D 坐标和对应的 2D 坐标:

# dataset.py import numpy as np import torch from torch.utils.data import Dataset import os class Human36MDataset(Dataset): def __init__(self, data_dir, split='train', seq_len=1): """ data_dir: npz 文件所在目录 split: 'train' 或 'test' seq_len: 每个样本包含的连续帧数 """ self.data_dir = data_dir self.seq_len = seq_len self.file_list = sorted([ f for f in os.listdir(data_dir) if f.endswith('.npz') and split in f ]) # 预加载所有数据到内存,数据量不大时可行 self.data = [] for fname in self.file_list: npz = np.load(os.path.join(data_dir, fname)) self.data.append({ 'poses_3d': npz['poses_3d'], 'poses_2d': npz['poses_2d'], 'camera_param': npz['camera_param'] }) def __len__(self): return len(self.data) def __getitem__(self, idx): sample = self.data[idx] poses_3d = torch.from_numpy(sample['poses_3d']).float() poses_2d = torch.from_numpy(sample['poses_2d']).float() return { 'poses_3d': poses_3d, 'poses_2d': poses_2d }

__init__里把每个.npz文件加载成一个字典,存到self.data列表里。__getitem__返回单个样本的 3D 和 2D 坐标,转成torch.float32张量。这里没有做序列切分,如果要做时序模型,可以在__getitem__里按seq_len截取连续帧。

测试 Dataset 是否能正常加载:

from dataset import Human36MDataset ds = Human36MDataset(data_dir='data/h36m/annotations/', split='train') print(f"Dataset size: {len(ds)}") sample = ds[0] print(f"3D shape: {sample['poses_3d'].shape}") print(f"2D shape: {sample['poses_2d'].shape}")

如果输出3D shape: torch.Size([帧数, 32, 3]),说明数据加载链路已经通了。

4. 避坑记录:Human3.6M 环境搭建中最容易翻车的五个点

4.1 h5py 读取报 OSError: Unable to open file

现象:用h5py.File('h36m_train.h5', 'r')打开标注文件时直接抛OSError,文件路径没问题,文件大小也正常。

原因:h5py3.7 及以上版本对某些旧版 HDF5 格式的兼容性变差了,Human3.6M 的标注文件恰好是用旧版 HDF5 库生成的。

解决:降级到h5py==3.6.0,命令是pip install h5py==3.6.0。如果已经装了新版,先pip uninstall h5py再装。装完用python -c "import h5py; print(h5py.__version__)"确认版本。

4.2 numpy 版本过高导致 np.float 报错

现象:跑数据预处理脚本时,报AttributeError: module 'numpy' has no attribute 'float'。

原因:numpy1.22 开始移除了np.float、np.int这些别名,但 Human3.6M 的一些社区处理脚本还在用。

解决:把numpy锁在 1.21.6,或者手动把脚本里的np.float改成np.float64。我一般选前者,因为改脚本容易漏。命令是pip install numpy==1.21.6。

4.3 CUDA 版本和 PyTorch 不匹配

现象:torch.cuda.is_available()返回False,但nvidia-smi能看到显卡。

原因:装的 PyTorch wheel 是 CPU 版本,或者 CUDA 版本和驱动不兼容。

解决:先用nvidia-smi查驱动支持的 CUDA 上限,然后去 PyTorch 官网找对应版本的安装命令。比如驱动支持 CUDA 11.3,就装torch==1.10.0+cu113。装完在 Python 里跑import torch; print(torch.version.cuda)确认。

4.4 图像路径和标注文件对不上

现象:DataLoader 能加载标注,但读图像时报FileNotFoundError。

原因:Human3.6M 的图像目录命名规则和标注文件里的动作名称不是一一对应的,中间需要做一次映射。

解决:检查metadata.xml里的动作标签,和图像目录名做对照。我一般会写一个映射字典,把标注里的action名称转成图像目录的实际名称。这个映射关系在社区脚本里能找到参考,但不同下载渠道的文件命名可能有差异,以实际目录为准。

4.5 内存不够导致加载中断

现象:__init__里预加载所有数据时,进程被系统 kill 掉,终端显示Killed。

原因:Human3.6M 全量数据加载到内存后占用几个 GB,如果服务器内存紧张就会被 OOM killer 干掉。

解决:改成懒加载,__getitem__里再读文件,而不是在__init__里全读进来。或者用np.load的mmap_mode='r'参数做内存映射,不实际加载数据到内存。我一般会先估算一下数据量,超过 2GB 就用懒加载。

5. 装完怎么验证:跑通一个最小训练循环

环境搭好、数据加载通了,最后一步是验证整条链路能不能跑起来。我一般会写一个最小训练循环,用几行代码确认前向传播和反向传播都没问题:

# verify_pipeline.py import torch import torch.nn as nn from torch.utils.data import DataLoader from dataset import Human36MDataset # 加载数据集 ds = Human36MDataset(data_dir='data/h36m/annotations/', split='train') loader = DataLoader(ds, batch_size=4, shuffle=True, num_workers=0) # 定义一个极简的 2D 到 3D 提升网络 class SimpleLiftingNet(nn.Module): def __init__(self): super().__init__() self.fc = nn.Sequential( nn.Linear(32 * 2, 256), nn.ReLU(), nn.Linear(256, 32 * 3) ) def forward(self, x): # x: (batch, 帧数, 32, 2) batch, frames, joints, _ = x.shape x = x.view(batch * frames, joints * 2) out = self.fc(x) return out.view(batch, frames, joints, 3) # 初始化模型和优化器 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleLiftingNet().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.MSELoss() # 跑一个 epoch model.train() for batch_idx, batch in enumerate(loader): poses_2d = batch['poses_2d'].to(device) poses_3d = batch['poses_3d'].to(device) optimizer.zero_grad() pred_3d = model(poses_2d) loss = criterion(pred_3d, poses_3d) loss.backward() optimizer.step() if batch_idx % 5 == 0: print(f"Batch {batch_idx}, Loss: {loss.item():.4f}") # 只跑几个 batch 验证流程 if batch_idx >= 20: break print("Pipeline verification passed.")

这段代码的关键验证点有三个:第一,DataLoader能正常迭代,说明数据加载链路通了;第二,model(poses_2d)能跑通前向传播,说明输入输出维度对上了;第三,loss.backward()和optimizer.step()不报错,说明反向传播和参数更新正常。如果这三步都过了,环境就算搭好了。

跑的时候注意num_workers先设成 0,避免多进程加载数据时出一些玄学问题。等确认流程通了,再改成 4 或 8 加速。另外batch_size设小一点,4 就行,验证阶段不需要跑全量数据。

我一般还会额外检查一下poses_3d和pred_3d的数值范围,如果 loss 一开始就是nan,多半是数据里有异常值或者输入没归一化。Human3.6M 的 3D 坐标单位是毫米,数值范围在几百到几千之间,如果看到poses_3d里有inf或nan,回去检查标注文件转换那一步是不是漏了字段。

从那以后我每次搭新环境,都会先跑一遍这个最小验证脚本,确认数据加载、模型前向、反向传播三条链路都通了,再开始写正式的训练代码。这个习惯帮我省掉了无数次「训练跑了一晚上才发现数据没加载对」的后悔药。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进