简介:针对Human3.6M 3D人体姿态数据集获取难、格式复杂的问题,这份资源提供了一套轻量级但完整的Python处理方案,覆盖数据下载、完整性校验、解压、元数据解析与批量预处理全流程,尤其适合多视角、多动作场景下的3D人体姿态研究,适合计算机视觉、机器学习方向的学生、研究人员与开发者直接参考或进行二次开发。资源共14个文件,核心为4个Python脚本,分别负责自动下载、压缩包解压、元数据查看以及全量数据处理;同时附带了依赖清单、Docker部署配置、示例配置和说明文档等辅助内容,整体压缩包仅34KB,便于跨平台使用与快速集成。目前已有3632人学习/下载,代码中针对HDF5格式实现了3D关节坐标读取、单位范数标准化、8:1:1训练/验证/测试数据划分等常用操作,并预留了基于随机森林、CNN/RNN等算法的建模接口,既能用于数据预览,也能作为姿态估计模型训练前的标准数据预处理模板。借助这套资源,可以避免从零搭建环境时常见的踩坑问题,让开发者将更多时间投入到模型设计与实验对比中。
1. 获取Human36M3D数据集:为什么需要一套自动化获取脚本
在做三维人体姿态估计时,Human36M3D 是绕不开的公开基准。它提供的多视角视频、同步帧和 3D 关节坐标,直接决定你复现论文时能不能拿到同口径的评测结果。然而真正动手获取这套数据,远没有论文附录写的那么轻松:申请访问权限后,你面对的是几十个分卷压缩包、目录层级混乱的原始文件,以及散落各处的标定参数。我基于 Python 将这些步骤拆成了可复用的自动化脚本,让一个刚起步的机器学习工程师也能通过几条命令完成从下载、解压到标定转换的完整流程。本文记录的是该工具链的设计思路、关键参数和我在实操中踩过的坑。
2. 理解Human36M3D的数据组织:申请、协议与三种数据形式
在动手写脚本之前,必须先把数据集的目录规范和坐标系搞清楚。我见过不止一个同学跳过这一步,直接拿脚本下载,结果解压完发现目录里套着一层一层冗余路径,训练时路径拼接直接报错。下面按照“目录结构 -> 申请流程 -> 数据格式”的顺序把基础打牢。
2.1 目录结构与坐标约定
Human36M3D 解压后的根目录通常不只有一个顶层目录,而是按 subject 划分成多个主体文件夹。每个 subject 目录下又有 action 子目录,例如走动、打招呼、打电话等日常动作;再往下是按拍摄批次划分的片段(Sequence)。这种层级结构很容易被压缩包内嵌顶层目录打乱,所以你需要先确认解压后实际路径与预期路径是否一致。
一个典型的解压后目录结构如下:
dataset/ ├── subject1/ │ ├── walking/ │ │ ├── Seq1/ │ │ │ ├── image_0.jpg │ │ │ ├── image_1.jpg │ │ └── Seq2/ │ └── greeting/ ├── subject2/ │ ├── walking/ │ └── eating/ └── metadata/ └── calibration.mat注意,Seq1、Seq2这些命名并不是固定不变的,有些压缩包会直接省略中间的Seq层,变成subject1/walking_0.jpg。如果你用代码硬编码遍历路径,很容易漏掉一部分文件。我一般会在解压后先打印前三级目录名,确认实际结构,再决定是否进入重排逻辑。
在坐标约定上,该数据集把原点定在拍摄空间的几何中心,x 轴指向相机矩阵的左侧,y 轴指向地面上方,z 轴朝拍摄方向。注意这个约定和常见的数据集不同,常见的是 z 轴垂直地面,因此你在做坐标变换时首先要做轴变换,否则后续三角化出来的结果会和关节角度计算直接冲突。
我建议在自动化脚本里用一个 YAML 配置来固定目录规则和坐标轴顺序,不要硬编码在 Python 代码里。比如:
dataset: root: ./data/H36M subject_dirs: [subject1, subject2, subject4, subject5] action_keyword: walking coord_axes: [x, y, z]这里的coord_axes能让你在对齐到第三方框架时快速调整轴的映射顺序。解压后你可以用tree命令检查目录,如果发现多了一层,再进入重排逻辑。
2.2 申请与权限:邮箱校验和下载链接的获取
Human36M3D 的数据并不是注册就能直接拿到,通常需要先填写一份申请表,说明研究用途。我在实操中遇到的第一步程序,是使用机构邮箱发送申请邮件,而非个人邮箱。对方的自动回复里会带有下载页的临时凭证,这个凭证有时是一条包含时间戳和签名的超长链接。
申请成功后,你会收到一封邮件,里面往往包含多个分卷的下载地址。这些地址可能在正文里,也可能在一个附件文本中。我建议先把它保存为本地 JSON 文件,方便脚本统一管理:
{ "url": "https://example.com/h36m/subject1_s1.zip?sig=abc", "expires": 1710000000, "md5": "d41d8cd98f00b204e9800998ecf8427e" }expires是过期时间的 Unix 时间戳。在开始下载前,用下面的代码校验链接是否仍然有效:
import json, time import requests def check_link_valid(link_file): with open(link_file) as f: links = json.load(f) now = int(time.time()) if now > links['expires']: print('链接已过期,请重新申请下载凭证') return False r = requests.head(links['url'], allow_redirects=True, timeout=15) if r.status_code != 200: print(f"返回状态码 {r.status_code}") return False return True这里的requests.head只取响应头,不下载正文,适合在进入全量下载前做快速探测。如果链接失效,脚本应当直接退出,而不是继续跑下载流程。另外,下载页有时会做高频访问限制,我一般会在两个分卷请求之间加 2~3 秒延时,并把重试次数设置成 5 次。
2.3 三种数据形式:原始视频、抽取帧与3D标注
这份资源里包含的数据格式主要有三种:原始视频流、按一定帧率抽取的图像序列,以及包含 3D 关节坐标和相机参数的标注文件。三者的体积和对齐方式完全不同。
| 数据形式 | 文件格式 | 典型内容 | 用途 |
|---|---|---|---|
| 原始视频 | .mp4 或 .avi | 多视角同步拍摄画面 | 需要自行抽帧 |
| 抽取帧 | .jpg | 每若干帧保存一张 | 可直接做2D检测 |
| 3D标注 | .mat | 关节坐标、相机标定 | 训练和评测 |
解析 .mat 标注文件时,你需要用到scipy.io.loadmat。这个函数加载出来的 dict 里通常包含矩阵和结构体,结构体字段首先要通过dtype查看,不能直接当 dict 操作。我踩过这个坑:以为data['pose']返回的是 Nx3 数组,结果拿到的是 shape 为 (1,1) 的 object 数组,必须再做一次.item()才能拿到真正的坐标矩阵。建议用下面的方式统一封装:
from scipy.io import loadmat def load_pose_mat(path): raw = loadmat(path, squeeze_me=False) pose_struct = raw['pose'].item() joint_xyz = pose_struct['xyz'].item() return joint_xyz加上.item()的原因在于,matlab 保存的 struct 在 scipy 看来是嵌套 object 数组,直接索引会得到无用的包裹层。squeeze_me=False保证不预先压缩掉长度为 1 的维度,避免形状意外变化。
还需要注意,抽取帧和 3D 标注之间并不是天然对齐的。每个片段可能包含同步时间戳,你需要用帧号或者时间戳把两者关联起来。我通常的做法是:先从标注文件里读出该片段的总帧数,再和图像目录下的文件名数量进行比对,如果数量不一致,优先检查是否存在缺失帧或额外帧。
3. 自动化获取脚本:环境依赖与核心模块拆解
拿到了数据组织方式的认知后,下面进入实际操作。这一章我把整个获取链路拆成三个核心模块:环境安装、下载和解压重排。每个模块都可以单独跑,这样你不需要一次性执行整套命令,也可以随时断点续跑。
3.1 环境依赖与Python版本要求
整个工具链基于 Python 3.8 及以上版本,因为 3.8 之后的zipfile处理大文件效率有明显提升,并且pathlib的 API 更稳定。我使用到的第三方库包括:requests 用于下载,tqdm 显示进度,scipy 读取标定,PyYAML 管理配置。你可以在项目根目录下建requirements.txt并安装:
requests==2.28.2 tqdm==4.64.1 scipy==1.9.3 PyYAML==6.0注意,版本号并不是强制锁死,而是我在实际运行中验证过不会互相冲突的组合。如果你在其他项目里已经有旧版 scipy,建议先建一个虚拟环境,避免覆盖原有依赖。创建环境:
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt安装完检查一下版本,如果import scipy报错,多半是 Python 3.12 对旧版 scipy 不兼容,需要升级 scipy 到支持当前解释器的版本。这类环境问题比下载本身更耗费时间,提前用虚拟环境能省掉很多脏活。
3.2 下载模块:断点续传与链接解密
下载是整个流程中最容易翻车的环节,几十个字节的文件卡到一半,网络中断,链接过期,都是家常便饭。为了应对这种情况,我把下载模块设计成了支持断点续传的状态机。
import os import requests def download_resume(url, output_path, chunk_size=8192): resume_bytes = 0 if os.path.exists(output_path): resume_bytes = os.path.getsize(output_path) print(f"已有 {resume_bytes} 字节内容,从该位置续传") headers = {'Range': f'bytes={resume_bytes}-'} with requests.get(url, stream=True, headers=headers, timeout=30) as r: r.raise_for_status() with open(output_path, 'ab') as f: for chunk in r.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) return output_path这里Range头的写法是让服务器返回从指定字节开始的后续内容。chunk_size建议设置为 8192 字节,而不是更大。因为大数据包如果单次请求返回太多,容易触发服务端限流;设置太小则 CPU 和磁盘频繁写,吞吐反而降低。
如果你遇到下载中断,重跑脚本时它不会笨拙地重新下载整个文件,而是先检查目标文件的大小。这里有一个前提:目标文件路径必须和上次保持一致,否则续传无从谈起。因此脚本里我固定一个downloads_partial/目录存放未完成的文件。
跑完所有分卷后,需要做一次校验。如果下载页提供了 MD5 清单,你就应该用 hashlib 逐文件计算 MD5 并比对,不要只看文件大小:
import hashlib def verify_md5(file_path, expected_md5): h = hashlib.md5() with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(65536), b''): h.update(chunk) return h.hexdigest() == expected_md5MD5 校验放在下载完成后执行,如果校验不通过,直接删除该文件并重新下载。这里没有用更快的缓存方式,因为大文件读取本身不算瓶颈,瓶颈基本都在网络。
3.3 解压与目录重排:按subject/action分发
下载完成后,压缩包内部目录往往带有一层冗余顶层目录。例如你解压subject1.zip,实际得到的是subject1/activities/path/thru/...这种深度结构,而你需要的是subject1为根的干净目录。我在脚本中加了一个rearrange步骤,把它摊平:
import zipfile, shutil, os def unpack_and_flatten(zip_path, dest_root): with zipfile.ZipFile(zip_path, 'r') as zf: for member in zf.infolist(): if member.is_dir(): continue parts = member.filename.split('/') if len(parts) <= 1: target_rel = member.filename else: target_rel = '/'.join(parts[1:]) target = os.path.join(dest_root, target_rel) os.makedirs(os.path.dirname(target), exist_ok=True) with zf.open(member) as src, open(target, 'wb') as dst: shutil.copyfileobj(src, dst)这里parts[1:]会去掉顶层的subject1前缀,之后把剩下的目录拼到dest_root下。如果你想让顶层目录保留,可以把parts[1:]改成parts。我一般习惯去掉,因为训练框架往往自己会加上 subject 信息作为配置字段,路径里再重复一遍反而容易出错。
zipfile.infolist()返回的是压缩包内部的条目,能拿到文件名和是否是目录。用shutil.copyfileobj做流式复制,可以避免一次性压入内存,适合像 H36M 这种动辄 1GB 的单视频文件。
3.4 参数说明表
整个脚本的主要运行参数如下,你可以根据自己下载链路情况调整:
| 参数名 | 默认值 | 作用 | 推荐调整方式 |
|---|---|---|---|
| chunk_size | 8192 | 每次读取的块大小 | 网络差时调小到 4096 |
| retry_times | 5 | 单个文件重试次数 | 稳定长传时可调大 |
| delay_seconds | 2 | 分卷之间的等待秒数 | 服务端限流时调大 |
| remove_zip | false | 解压后是否删除压缩包 | 磁盘紧张时打开 |
| verify_md5 | true | 是否校验MD5 | 官方提供清单时建议开启 |
注意,remove_zip默认关闭,是因为解压后你还需要压缩包作为原始备份。如果磁盘确实不够,建议解压成功并且目录结构确认无误后再手动删除,而不是让脚本自动删。最后,关于下载链接的解密,很多分卷链接看起来是一串签名过的 URL,实际上不需要你自行解密,只要把它原样传给 requests 即可。所谓“自动化获取”并不是去破解凭证,而是将凭证管理的流程收纳进配置,避免你在 shell 里复制粘贴一堆超长字符串。
4. 从16个摄像头到3D坐标:标定文件与坐标变换
下载不是终点。很多初学者拿到数据后直接看 .mat 里的 3D 关节坐标,以为这就是训练标签。但 Human36M3D 更常见的用法是让模型从 2D 图像估计 3D 姿态,因此你需要自己把多视角的 2D 关键点与相机标定结合起来,完成从像素坐标到世界坐标的变换。这一章重点讲标定文件的读取和三角化。
4.1 标定文件读取与相机参数解析
标定文件通常保存在一个全局的calibration.mat中,里面包含每个摄像机的内参、外参和畸变系数。读取时你可能会遇到与之前一样的嵌套 object 数组问题。下面的函数把结构体里的字段提取为 dict:
from scipy.io import loadmat def extract_cameras(calib_path): data = loadmat(calib_path) calib_struct = data['calibration'].item() cameras = {} for idx in range(calib_struct.shape[0]): cam = calib_struct[idx, 0].item() name = str(cam['name'].item()) cameras[name] = { 'R': cam['R'].item(), 't': cam['t'].item(), 'K': cam['K'].item(), 'dist': cam['dist'].item() } return cameras这里我用.item()把 matlab struct 数组转成可索引的 Python 对象。K是 3x3 内参矩阵,R是旋转向量(注意是 3x1),t是平移向量。dist是畸变系数,在后续重投影时要传给 OpenCV 的undistortPoints函数。
该数据集的相机编号通常是从 0 到 15 共 16 个视角,但具体编号在标定文件里并不是按顺序排列的。你需要以cam['name']为 key 建立映射,不要靠数组下标去猜测。
另外,从标定文件里读取的内参矩阵是针对引用分辨率的。如果你手里的抽取帧分辨率与标定文件不一致,先处理这里的问题,不要等到后面才发现重投影误差大。具体操作会在避坑指南里展开。
4.2 从2D到3D:三角化与坐标系对齐
当我们有了某个人体关键点在多个视角的 2D 像素坐标,就可以用线性三角化方法重建 3D 坐标。所谓线性三角化,就是把相机投影方程写成齐次形式,应用最小二乘求交点的最优解。
import numpy as np import cv2 def triangulate(kpts2d, cams): """ kpts2d: shape (num_views, 2) 的像素坐标列表(已去除畸变) cams: 由 extract_cameras 得到的相机参数列表 """ num_views = len(kpts2d) A = np.zeros((num_views * 2, 4)) for i, (kpt, cam) in enumerate(zip(kpts2d, cams)): K = cam['K'] R = cv2.Rodrigues(cam['R'].reshape(3, 1))[0] t = cam['t'].reshape(3, 1) P = K @ np.hstack([R, t]) # 3x4 投影矩阵 u, v = kpt A[i * 2, :] = u * P[2, :] - P[0, :] A[i * 2 + 1, :] = v * P[2, :] - P[1, :] _, _, vt = np.linalg.svd(A) X = vt[-1, :3] / vt[-1, 3] return X注意,这里的代码是简化的线性三角化版本。实际中应在用 SVD 之前将坐标转换为齐次坐标,并处理常数项。这里使用的最小奇异值对应的右奇异向量,等价于最小二乘解。cv2.Rodrigues把旋转向量转成旋转矩阵,如果你的代码不想依赖 OpenCV,可以用 scipy 的Rotation.from_rotvec。
这个函数是隐式的,前提是输入坐标已经完成畸变纠正。很多人直接拿原始像素坐标送进去,导致重投影误差达到几十像素。所以实际项目里应该先调用cv2.undistortPoints把坐标归一化到理想针孔模型下:
def undistort_points(kpts, cam): pts = np.array(kpts, dtype=np.float32).reshape(-1, 1, 2) icv = np.array([[cam['K'][0, 0], cam['K'][0, 2]], [cam['K'][1, 1], cam['K'][1, 2]]]) # 注意要传入内参矩阵作为相机矩阵 undist = cv2.undistortPoints(pts, cam['K'], cam['dist'].reshape(1, 5)).reshape(-1, 2) # 结果坐标为归一化坐标,需要乘内参恢复像素坐标 return undist这里undistortPoints返回的坐标是归一化坐标,还需要用内参矩阵将其转换回像素坐标,或者直接用于构造齐次坐标。这个细节最容易出错,我会在验证环节再强调。
4.3 验证结果:重投影误差检查
三角化出来的 3D 坐标对不对,不能只看数值是不是落在合理范围里,还需要做重投影验证。把重建的 3D 坐标重新投影到某个视角,计算与原始 2D 检测点之间的像素距离:
def reproject_error(X, cam, kpt): R = cv2.Rodrigues(cam['R'].reshape(3, 1))[0] t = cam['t'].reshape(3, 1) P = cam['K'] @ np.hstack([R, t]) Xh = np.hstack([X, 1.0]) proj = P @ Xh proj = proj[:2] / proj[2] return float(np.linalg.norm(proj - kpt))如果平均误差小于 2 像素,说明标定读取和坐标变换都正确。如果误差大于 5 像素,先检查有没有把畸变系数作用到输入坐标上,其次检查相机旋转向量是否被当成了旋转矩阵。这里有一个常见误区:直接使用cam['R']做矩阵乘法,但它实际是旋转向量,shape 可能是(3,)或(3,1);必须先转换成旋转矩阵。
在验证环节,你还可以把多个视角的重投影误差总和打印出来,作为后续训练日志的指标。如果某个视角的误差显著高于其他视角,很可能是标定文件里该视角的内参 K 和实际照片不一致,或者该视角的图像序列存在抽帧错位。这一章的代码片段已经能够覆盖从标定文件到 3D 点的完整链路。当然,真实数据里存在遮挡、检测噪声和极端姿态,还需要用 RANSAC 或多视角几何约束来筛选有效视角,这些内容我会在最后一章的进阶技巧里展开。
5. 避坑指南:下载中断、路径乱码与标注缺失的排查
即使按照前四章的流程走完,你仍然可能在某个环节遇到奇怪的行为。我把自己在踩坑中记录下来的问题按“现象 -> 原因 -> 解决”整理在这里,希望能帮你跳过这些不必要的排障时间。
5.1 下载中断后重试,文件明明是空的却显示已存在
现象:脚本在下载到某个分卷时中断,重新运行后日志显示“已有 X 字节内容”,但最终解压校验失败,原因是压缩包损坏。
原因:requests 在下载过程中写入了临时文件,但服务端响应并未完整返回,导致本地文件大小不为 0 但其实缺少尾部数据。仅靠文件大小判断续传位置是不够的,需要校验文件完整性。
解决:在续传前先判断文件扩展名是否为.part,并且下载完成后立即进行 MD5 校验。如果 MD5 不一致,删除本地文件重新从头下载。我一般会在脚本里让每个分卷的下载状态保留一个.md5标记,写完后比对。
5.2 解压后目录名出现“乱码”或中文编码错乱
现象:zip 包里的文件名在解压后显示为一堆类似“S1_thru_???.avi”的乱码,无法与标定文件中的视角名对起来。
原因:压缩包内文件名编码是 CP437 或 CP936,而 Python 的 zipfile 默认按 UTF-8 解码,导致文件名出现乱码。
解决:在ZipFile初始化时,把metadata_encoding参数指定为cp437或gbk。例如:
with zipfile.ZipFile(zip_path, metadata_encoding='cp437') as zf: ...注意,metadata_encoding参数只影响文件名的解码方式,不影响文件内容。你可以先打印一条条目名字,看到正常的中文或英文后再继续。如果还乱,就换gbk或latin1试试。
5.3 标注文件读出来全是 NaN 或缺失关节
现象:loadmat读取 .mat 文件后,pose字段里某个 sequence 的关节坐标为全 NaN,或者缺失了手指等部位的坐标。
原因:有些视频分段的标注并未覆盖到全部关节,例如当人物被遮挡时,自动标注会放弃部分关键点,填充 NaN。这是数据本身的问题,不是解析错误。
解决:在加载标注后立即做有限性检查,把包含 NaN 的帧记录下来,在构建训练集时直接过滤这些帧。你可以用numpy.isnan查找:
import numpy as np valid_frames = ~np.isnan(pose_xyz).any(axis=(1, 2))如果过滤比例超过 10%,需要重新检查组件的图像与标注是否对齐,而不是盲目把 NaN 替换成 0。
5.4 相机标定矩阵与图像尺寸不匹配
现象:重投影误差很大,但代码逻辑看起来没问题。检查后发现标定的内参矩阵 K 是 1024x1024 的图像尺寸,而实际视频帧是 1920x1080。
原因:数据集提供多种分辨率版本,标定文件默认是针对后期裁剪后的分辨率,并不是和你抽帧所用的分辨率一致。
解决:在读取标定时,根据实际图像尺寸对内参矩阵做缩放。例如原始标定的主点坐标是按 1024 宽标定的,现在图像宽为 1920,则需要把cx和fy按比例放大:
scale_x = image_width / 1024 scale_y = image_height / 1024 K[0, 0] *= scale_x K[1, 1] *= scale_y K[0, 2] *= scale_x K[1, 2] *= scale_y这个操作要在提取相机参数后立刻执行,否则后续所有三角化都基于错误的内参。
5.5 下载链接只有 24 小时有效,但分卷数量太多
现象:链接里包含大量分卷,下载速度慢,还没下完链接就过期了。
原因:数据体积比较大,服务端对链接只提供短时访问控制,普通网络条件下一天内很难下完。
解决:不要逐个下载所有分卷。我一般先把下载页里的全部链接和分卷名称抓下来放到 JSON 里,然后用脚本按优先级顺序下载。如果链接过期,重新申请时只补下之前没下载的分卷。上面的download_links.json就是为此设计的。
这五条是我遇到频率最高的,实际上还有不少边缘情况,比如磁盘文件句柄占用、Windows 路径长度限制等。遇到问题时,最有效的做法是先看脚本出错堆栈,再对照压缩包内部文件名打印,不要直接猜。
6. 进阶技巧:把数据预处理成训练格式并与主流框架对接
当你拿到了原始视频、标定和 3D 标注,下一步就是把它转换成训练框架需要的格式。以一个常见的 2D 检测 + 3D 姿态估计流水线为例,你通常需要生成一个包含图像路径、2D 关键点、3D 关节坐标和相机内参的字典。下面我会给出一个最简单的转换模板,并且分享一个下采样技巧。
6.1 转成 COCO 式 JSON
很多检测框架接受 COCO 格式的标注。你可以把每个有效帧作为一条 record,写入一个 JSON 文件:
import json def build_training_json(frames, output_path): dataset = { 'annotations': [], 'images': [] } for i, frame in enumerate(frames): img_entry = { 'id': i, 'file_name': frame.img_path, 'width': frame.width, 'height': frame.height, } ann_entry = { 'id': i, 'image_id': i, 'keypoints': frame.kpt_2d.reshape(-1).tolist(), 'num_keypoints': 17, 'pose_3d': frame.pose_3d.tolist(), 'cam_K': frame.cam_K.tolist() } dataset['images'].append(img_entry) dataset['annotations'].append(ann_entry) with open(output_path, 'w', encoding='utf-8') as f: json.dump(dataset, f)注意,COCO 标准里的keypoints是 x, y, v 形式,你还需要把坐标系顺序和可见性标志一起转换。这里我把pose_3d放在注释字段里,虽然不是 COCO 标准,但很多复现代码会兼容读取。
6.2 下采样与归一化技巧
原始视频帧率通常在 50 或 60 FPS,分类训练根本用不上。我会先按每 5 帧采样一次,再以该帧的 2D 关键点为中心做裁剪,把图片归一化到固定 256x256 尺寸。归一化时不要直接拉伸到正方形,而是先缩放再居中填充,避免关键点定位因拉伸而偏移。
import numpy as np import cv2 def normalize_crop(image, center, scale, output_size=256): w = scale * 200 h = scale * 200 tx = center[0] - w / 2 ty = center[1] - h / 2 M = np.array([[w / output_size, 0, tx], [0, h / output_size, ty]]) out = cv2.warpAffine(image, M, (output_size, output_size)) return out这里的scale是根据关键点包围盒宽度估算的,通常取关键点最大包围盒宽度的 1.2 倍。center是包围盒中心。通过仿射变换把原图裁剪并缩放,能保留关键点的相对位置,避免直接 resize 带来的比例失真。
6.3 验证转换结果
转换完成后,我习惯把标注绘制到图片上做一遍人工抽查。你可以用 OpenCV 画出 2D 关键点,再把 3D 坐标重投影到图上,把重投影点画成十字。如果看到十字和原始点偏差很大,就回去检查内参缩放问题。如果这一步没问题,再进入训练。
从那以后,我每次处理新的数据集,都会强制走一遍“下载 -> 解压 -> 标定读取 -> 坐标变换 -> 格式转换”的完整流程,并且在每一步留下日志文件。比起直接在训练脚本里硬解析原始 mat,这条流程反而更省时间。希望这些记录能帮到你,也祝你在三维姿态估计的项目上一路顺利。
本文还有配套的精品资源,点击获取