简介:围绕 Python 获取与处理 Human3.6M 3D 人体姿态数据集整理的项目包,面向计算机视觉、机器学习方向的学习者与研究者。Human3.6M 涵盖多视角视频、3D 关节位置、相机参数与动作注释,是姿态估计和重建研究常用的基准数据,这份资料聚焦数据下载、元数据解析、批量处理与压缩包提取,为后续 HDF5 读取、关节坐标整理和实验准备提供可复用的脚本基础。包内共 14 个文件,以 4 个 Python 脚本为核心,辅以 txt 校验与配置说明、md 文档、xml 元数据,以及 Dockerfile、docker-compose.yml、requirements.txt 等环境文件,另有 license 和忽略规则,压缩包约 34KB,体量轻便,便于按模块查阅与二次修改。脚本按下载、处理、提取职责拆分,配合校验文件便于确认数据完整性。已有 3638 人学习,适合需要快速搭建 Human3.6M 数据获取流程、了解项目组织方式,或为课程设计与科研预处理寻找起步模板的读者参考。
1. 一个几十 GB 的数据包,和一条能重跑的下载命令
接手 3D 人体姿态估计的复现任务时,第一道墙往往不是模型,而是数据。某 3D 人体姿态数据集(下文统称 Pose3D-H)把它拆成几十个压缩包:视频帧、相机标定、2D 关键点、3D 关键点分散在不同目录,官方页面上一串链接,手动点到一半断网,进度全部作废。这个 Python 工程就是专门用来解决这一步的:把“获取数据”做成一条可重跑、可校验、可增量更新的流水线,跑完之后本地目录结构和标注索引都是确定的。它适合三类人:想跑通姿态估计 baseline 的算法同学、要接自采数据做微调的工程同学、以及被数据集目录结构劝退过、想找一套能抄的下载+解析脚手架的开发者。机器学习项目里最不起眼、却最容易卡住三天的一环,就在这里。
2. 工程骨架:下载器、校验器、解析器怎么拆
拿到这类数据集工程,先别急着跑python main.py。我的习惯是先看目录——目录结构基本等于作者的架构思路,比 README 诚实。Pose3D-H 的获取工程一共就三件事:把远端文件拉到本地、确认拉下来的东西没坏、把压缩包里的二进制标注翻译成能喂给模型的数组。所以代码分层也顺着这三件事走,source(数据源描述)、fetcher(下载与校验)、parser(标注解析与导出),三层之间只通过配置文件和数据清单耦合,任何一层都能单独换掉。
2.1 三层拆分与目录结构
先看树。这套目录是我按项目正文的功能描述整理出来的典型形态,实际工程里文件名可能略有出入,但分层逻辑基本一致:
pose3d-h-fetch/ ├── configs/ │ ├── source_p3d.yaml # 远端地址、分片规则、目录映射 │ └── export_coco.yaml # 导出格式与关键点顺序映射 ├── fetchkit/ │ ├── source.py # 数据源抽象:list_files() / url_of() │ ├── fetcher.py # 分块下载、断点续传、限速 │ ├── verify.py # sha256 校验、幂等判断 │ └── unpack.py # 安全解压、目录拍平 ├── parsekit/ │ ├── h5reader.py # 读标注里的 h5 结构 │ ├── camera.py # 相机内外参、投影对齐 │ └── exporter.py # 导出 COCO / npz ├── scripts/ │ ├── download.py # 入口一:只下载不解析 │ └── build_index.py # 入口二:扫描本地生成索引 └── data/ # 落盘目录,按 subject 分文件夹source.py是唯一知道“远端长什么样”的模块,它对外只暴露两个方法:列出所有文件、给出某个文件的下载地址。这样以后换成自建对象存储或者另一个数据集,只要再写一个 Source 子类,fetcher 和 parser 一行不用改。这是这套骨架最值钱的地方——把“数据从哪来”和“数据怎么处理”彻底解耦。
fetcher.py负责搬运,verify.py负责判真,unpack.py负责落地。三者串行执行,但每一步都能单独重跑:下载失败不会污染校验结果,校验失败不会触发解压,解压失败不会写索引。这种“阶段隔离”在动辄几十 GB 的场景里是刚需,否则一次失败就得从头再来,血泪经验。
依赖上没什么玄学,requests负责 HTTP、tqdm出进度条、h5py读标注、numpy处理数组、PyYAML读配置。虚拟环境用 conda 或 venv 都行,关键是锁版本——h5py在不同大版本之间读同一个文件的返回类型会变,这点后面避坑章会细说。
2.2 配置驱动的数据清单
不要把这些参数写死在代码里:远端根地址、每个压缩包对应的本地子目录、是否启用断点续传、并发数、限速值。一旦写死,换台机器就要改代码。常见做法是抽一份 YAML,代码只读配置:
# configs/source_p3d.yaml root: "https://example.invalid/p3d/release" # 远端根路径(示例占位) chunk_size: 4194304 # 4MB 一块,太小握手开销大,太大断点粒度粗 max_workers: 4 # 并发下载的包数,不建议超过 8 rate_limit_kbps: 0 # 0 表示不限速;共享带宽时设成 2048 之类 retries: 5 # 单个分块的失败重试次数 subjects: ["S1", "S5", "S6", "S7", "S8", "S9", "S11"] assets: - name: "videos_S1.tar" target: "raw/videos/S1" sha256: "<64 位十六进制摘要>" - name: "annotations.tar" target: "raw/annot" sha256: "<64 位十六进制摘要>"chunk_size是第一个需要调的参数。4MB 是个经验值:再小,HTTP 往返次数暴涨,速度上不去;再大,断点续传时丢的那一段太大,重试成本高。max_workers不是越大越好,很多静态资源服务器对单 IP 并发有限制,开到 16 大概率被限流甚至返回 403,表现就是“进度条卡住不动但也不报错”,很迷惑。retries建议给 5 次,配合指数退避,能扛住大部分网络抖动。
读取这份配置的代码很短,但要加一层校验,避免手写 YAML 时缩进错了却静默失败:
# configs 加载:缺字段直接抛错,别让默认值掩盖配置问题 from pathlib import Path import yaml REQUIRED = ("root", "assets") def load_source_cfg(path: str) -> dict: cfg = yaml.safe_load(Path(path).read_text(encoding="utf-8")) missing = [k for k in REQUIRED if k not in cfg] if missing: raise KeyError(f"配置缺字段: {missing}") # 早失败,别拖到下载阶段 for item in cfg["assets"]: # name 和 target 必须成对出现,否则落盘路径无从推断 assert "name" in item and "target" in item, item return cfg这里的assert只用于开发期防御,生产脚本里建议换成显式抛异常,因为python -O会把assert全部剥掉。target指向的是“解压后应该在的位置”,不是压缩包本身的位置,压缩包统一放在data/_cache/下,这样即使解压失败,压缩包还在,不用重下。这个设计省下的流量非常可观。
2.3 入口脚本的职责边界
scripts/download.py只做编排,不写业务细节。它按顺序调Source.list_files()、Fetcher.fetch_all()、Verify.check_all()、Unpack.extract_all(),中间任何一步抛异常就停下,并把失败清单写到data/_failed.json。下次启动先读这个文件,优先重跑失败项。
提示:入口脚本不要吞异常。很多人为了“跑完不中断”在循环里套
try/except: pass,结果数据缺了一半,训练时 loss 曲线像心电图,回头查三天才发现是下载阶段静默丢了两个包。
scripts/build_index.py是第二个入口,它不联网,只扫本地data/raw/生成一份索引 JSON:每个 subject、每个动作、每条序列对应哪些文件、标注在哪个 h5 的哪个 key 下。有了索引,后面的解析和导出就不用反复遍历目录树。索引文件很小,可以进版本库,团队协作时新人拉下来直接能用。
3. 下载与校验核心:断点续传、哈希、安全解压
这一步是整个工程最容易翻车的地方,也是最值得读源码的部分。三个模块分别解决三个问题:网络不稳怎么办、文件是不是完整的、解压会不会把目录搞乱。
3.1 分块下载与断点续传
朴素写法是requests.get(url).content一次性读进内存,几十 GB 的文件这么干会直接把内存打爆,而且断网就全丢。正确姿势是用Range头分块拉,每块落盘后记录偏移量:
# fetcher.py:分块 + 断点续传的核心循环 import os import time import requests def download_with_resume(url: str, dst: str, chunk_size: int = 4 << 20, retries: int = 5, session: requests.Session | None = None): session = session or requests.Session() os.makedirs(os.path.dirname(dst), exist_ok=True) # 已有文件的大小就是续传起点;文件不存在则从 0 开始 pos = os.path.getsize(dst) if os.path.exists(dst) else 0 for attempt in range(retries): headers = {"Range": f"bytes={pos}-"} if pos else {} try: with session.get(url, headers=headers, stream=True, timeout=(10, 60)) as r: # 206 表示服务端接受了续传请求;200 表示它不认 Range,只能从头来 if pos and r.status_code == 200: pos = 0 open(dst, "wb").close() r.raise_for_status() mode = "ab" if pos else "wb" with open(dst, mode) as f: for chunk in r.iter_content(chunk_size=chunk_size): if not chunk: continue f.write(chunk) pos += len(chunk) # 每写一块就更新偏移 return dst except (requests.ConnectionError, requests.Timeout) as e: wait = 2 ** attempt # 指数退避:1s,2s,4s,8s,16s print(f"[retry {attempt+1}] {e} -> sleep {wait}s, resume at {pos}") time.sleep(wait) raise RuntimeError(f"下载失败且重试耗尽: {url}")timeout=(10, 60)是连接超时和读超时的元组,读超时给到 60 秒是因为有些服务器在发送大文件时会间歇性停顿。r.status_code == 200那个分支很关键:并不是所有服务端都支持Range,遇到不支持的就老老实实从头写,否则会得到一个“前半段新数据+后半段旧数据”的拼接文件,哈希永远对不上,还查不出原因。
mode = "ab" if pos else "wb"这一行决定是追加还是覆盖。注意pos的判断必须在打开文件之前完成,顺序错了就会把续传写成覆盖。
session复用是性能细节:同一个Session会复用 TCP 连接,下载几十个文件时能省掉大量 TLS 握手。并发场景下每个线程一个Session,别共享,requests.Session不是线程安全的。
3.2 哈希校验与幂等
下载完不算完,必须校验。压缩包在传输过程中被截断、被中间层改写、或者上一轮的半成品残留,都会导致后面解压报“文件损坏”,而错误信息往往指向解压库,误导性极强。校验逻辑要满足幂等:跑一次和跑十次结果一致,已通过的跳过。
# verify.py:流式 sha256,避免把大文件整个读进内存 import hashlib from pathlib import Path def sha256_of(path: str, block: int = 1 << 20) -> str: h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(block), b""): h.update(chunk) return h.hexdigest() def check_asset(path: str, expect: str, stamp_dir: str) -> bool: stamp = Path(stamp_dir) / (Path(path).name + ".ok") if stamp.exists(): return True # 校验过的留个戳,下次直接跳过 if not Path(path).exists(): return False actual = sha256_of(path) if actual != expect: print(f"校验失败 {path}\n expect={expect}\n actual={actual}") return False stamp.parent.mkdir(parents=True, exist_ok=True) stamp.write_text(actual, encoding="utf-8") return Trueiter(lambda: f.read(block), b"")是流式读的标准写法,哨兵值b""表示读到文件尾。别用f.read()一把梭,几个 GB 的文件会让内存直接飙上去。
.ok戳文件的内容写的是实际摘要,而不是空文件——这样以后想核对“这个戳是哪次校验留下的”时有据可查。戳文件的命名用原文件名加后缀,别用哈希值命名,否则压缩包改名后戳就对不上了。
3.3 安全解压与并发限速
tarfile.extractall()有一个经典问题:压缩包里的路径如果带../,解压时会把文件写到目标目录外面去。数据集来源可信时影响不大,但只要脚本会被别人拿去跑,就得防一手。Python 3.12 起extractall有了filter="data"参数,低版本则要自己检查成员路径:
# unpack.py:先过滤路径穿越,再解压;解压后拍平多余层级 import os import tarfile from pathlib import Path def safe_extract(tar_path: str, dst: str, strip: int = 0): dst = os.path.abspath(dst) os.makedirs(dst, exist_ok=True) with tarfile.open(tar_path, "r:*") as tf: members = [] for m in tf.getmembers(): target = os.path.abspath(os.path.join(dst, m.name)) if not target.startswith(dst + os.sep): raise ValueError(f"非法路径: {m.name}") # 直接拒绝,不做静默跳过 if strip and m.name.count("/") <= strip: continue m.name = "/".join(m.name.split("/")[strip:]) if strip else m.name members.append(m) tf.extractall(dst, members=members)strip参数用来拍平压缩包自带的顶层目录。很多数据集打包时会套一层Pose3D-H/或者release_v1/,不拍平的话本地路径会变成raw/annot/Pose3D-H/annotations/...,后面所有解析代码都得跟着多写一层,非常难受。
并发下载要注意两件事:一是max_workers别超过服务端容忍度,二是进度条在多线程下会串行输出。tqdm的写法是每个文件一个 bar,position参数传线程序号,或者干脆用一个总进度条按字节累加。
注意:限速(
rate_limit_kbps)不是可选项。在共享带宽的办公网里不限速拉满,同网段其他人的会议会直接卡掉,然后你就会收到投诉。
4. 标注解析:把 h5 里的二进制翻成可训练张量
下载解压只是把原料搬进厨房,真正决定后面模型能不能训起来的,是标注解析这一步。Pose3D-H 的标注是 h5 格式,结构大致是:每个 subject 一个文件,文件里按动作名分组,每组下挂poses(3D 关节坐标)、S(相机外参)、K(相机内参)、以及 2D 关键点。读之前先把结构打印出来,别凭记忆写 key。
4.1 h5 结构探测与读取
第一步永远是把树打出来看,不同发布批次的 key 名字经常有细微差别:
# h5reader.py:先把结构打出来,再按路径取数 import h5py import numpy as np def dump_tree(path: str, max_depth: int = 3): def _walk(node, prefix, depth): if depth > max_depth: return for k in node.keys(): child = node[k] if isinstance(child, h5py.Dataset): print(f"{prefix}{k} shape={child.shape} dtype={child.dtype}") else: print(f"{prefix}{k}/") _walk(child, prefix + " ", depth + 1) with h5py.File(path, "r") as f: _walk(f, "", 0) def read_sequence(path: str, group: str, action: str): with h5py.File(path, "r") as f: g = f[group][action] # 3D 关键点:常见形状 (帧数, 32, 3),单位是米,世界坐标系 poses3d = np.asarray(g["poses"], dtype=np.float32) # 相机:每帧一个外参 3x4,内参 3x3 extr = np.asarray(g["S"], dtype=np.float32) intr = np.asarray(g["K"], dtype=np.float32) # 2D 关键点:形状 (帧数, 32, 2),像素坐标 poses2d = np.asarray(g["positions_2d"][group][action], dtype=np.float32) return poses3d, poses2d, extr, intrnp.asarray(..., dtype=np.float32)这一步别省。h5 里存的可能是float64,直接拿去组 batch 会让显存占用翻倍,而且很多算子对float64支持不好。转换放在读取时做,比在 DataLoader 里做省事。
positions_2d的嵌套层级比poses多一层,这是历史原因造成的,写代码前用dump_tree看一眼就能避免取错。
另外注意 h5py 返回的 Dataset 是惰性对象,文件句柄关闭后就失效了。所以必须在with块内完成np.asarray转换,出块之后再访问会报 “Unable to synchronously open object”。
4.2 相机投影与坐标系对齐
3D 关键点在世界坐标系,2D 关键点在像素坐标系,两者之间差着一个投影变换。做对齐校验是判断数据是否读对的最快手段:把 3D 点投到 2D,跟标注的 2D 点比残差,正常应该在几个像素以内。
# camera.py:世界坐标 -> 相机坐标 -> 像素坐标 import numpy as np def project(points3d: np.ndarray, extr: np.ndarray, intr: np.ndarray) -> np.ndarray: # points3d: (N, 3) 世界坐标,单位米 # extr: (3, 4) 外参 [R|t];intr: (3, 3) 内参 R, t = extr[:, :3], extr[:, 3:] cam = points3d @ R.T + t.T # (N,3) 转到相机坐标系 z = cam[:, 2:3] z = np.where(np.abs(z) < 1e-6, 1e-6, z) # 防止除零 uv = (cam @ intr.T)[:, :2] / z # 透视除法得归一化平面,再乘内参 return uv def root_center_and_scale(p3d: np.ndarray, root_idx: int = 0, scale: float = 1000.0) -> np.ndarray: # 以根关节为原点做平移,再按固定系数缩放,避免不同 subject 身高差异 return (p3d - p3d[:, root_idx:root_idx + 1, :]) * (scale / 1000.0)project里那个除零保护不是多余的。有些帧的 3D 点会跑到相机后方(标注误差或者遮挡导致),z接近 0,除法直接产出inf,后面算 loss 就是 NaN,训练十分钟才发现 loss 变 nan 是常事。
root_center_and_scale做的是姿态归一化。不同 subject 身高差 20 厘米很常见,不归一化的话模型会先学“这个人高不高”而不是“姿势是什么”。根关节索引按关键点定义来,常见是骨盆那个点。缩放系数用固定值就行,别用每帧的实际骨长,否则同一动作的不同帧尺度不一致,反而引入噪声。
坐标系还有一处容易忽略:有些数据集的 3D 标注是相对根节点的,有些是世界坐标。通常看根节点坐标是否恒为零就能判断。这个判断必须写进代码而不是靠记忆:
def is_root_relative(p3d: np.ndarray, root_idx: int = 0) -> bool: # 根节点在所有帧上都接近原点 => 已经是相对坐标 return bool(np.abs(p3d[:, root_idx, :]).max() < 1e-4)4.3 导出 COCO 与 npz
解析完的中间结果直接喂模型也行,但团队协作时最好落一份标准格式,方便别人用现成的 dataloader。COCO 是关键点任务里通用度最高的,导出时要注意keypoints是[x, y, v, x, y, v, ...]展平的一维数组,v=2表示可见、v=1表示遮挡、v=0表示未标注。
# exporter.py:导出 COCO 风格的 keypoints 标注 import json import numpy as np def to_coco(samples, out_json: str, kp_names: list[str]): images, annotations = [], [] for i, s in enumerate(samples): # s["kp2d"]: (K,2) 像素坐标;s["vis"]: (K,) 0/1/2 flat = [] for (x, y), v in zip(s["kp2d"], s["vis"]): flat.extend([float(x), float(y), int(v)]) images.append({"id": i, "file_name": s["file_name"], "width": s["width"], "height": s["height"]}) annotations.append({"id": i, "image_id": i, "category_id": 1, "keypoints": flat, "num_keypoints": int((s["vis"] > 0).sum()), "bbox": s["bbox"], "iscrowd": 0}) coco = {"images": images, "annotations": annotations, "categories": [{"id": 1, "name": "person", "keypoints": kp_names, "skeleton": []}]} # skeleton 按关键点定义补边 with open(out_json, "w", encoding="utf-8") as f: json.dump(coco, f)num_keypoints必须是可见点数量,不是全部关键点数量。有些可视化工具靠这个字段决定画不画,填错会导致图上只有骨架没有点。
skeleton留空不影响训练,但影响可视化连线,建议按关键点定义补上父子边。
如果团队用的是自定义格式,导出npz更省事:一个文件里放kp2d、kp3d、cam、meta四个数组,np.load一行读进来,比 JSON 快一个数量级,体积也小。
5. 避坑与排查:五类真实翻车现场
这一章记录的都是跑这类数据获取脚本时反复出现的故障,按“现象 → 原因 → 解决”写,遇到对得上的直接照着改。
5.1 下载阶段:进度条卡住、哈希永远对不上
现象一:进度条停在某个百分比不动,也不报错,等十分钟还是那样。
原因:并发数开太大被服务端限流,连接被挂起而不是被拒绝,requests的读超时没触发。
解决:把max_workers降到 4 以下,同时给timeout显式设置读超时。如果再出现,就在重试逻辑里加“连续 N 秒无数据则主动断开重连”。
现象二:文件大小看起来对,但 sha256 每次都不一样。
原因:上一次中断留下的半成品没清掉,续传的Range起点对了,但服务端不支持Range返回了 200 全量数据,代码按追加模式写入,新旧数据拼在一起。
解决:先删掉本地文件重下一次;然后在代码里加那个status_code == 200的判断,一旦服务端返回 200 就切回覆盖模式。
提示:怀疑文件有问题时,先看开头几个字节。tar 包正常以文件名开头,gzip 是
\x1f\x8b。用head -c 16 file.tar | xxd一眼就能看出是不是下成了 HTML 错误页——这种情况在链接过期时特别常见,文件大小才几 KB,却能骗过“存在即跳过”的检查。
5.2 解压阶段:目录多套一层、文件名乱码
现象:解压完发现路径变成raw/annot/release_v1/annotations/...,后面所有相对路径都错位。
原因:压缩包自带顶层目录,而配置里的target是按“已拍平”的假设写的。
解决:用 3.3 节里的strip参数拍平,或者在配置里把target写全。前者更稳,因为不同批次的压缩包顶层目录名可能不一样。
现象:解压出来的文件名是乱码。
原因:压缩包在打包时用了非 UTF-8 编码,tarfile默认按系统编码解码。
解决:打开时传encoding="utf-8", errors="replace",或者干脆别依赖文件名,索引里用 h5 内部的 key 定位数据。
5.3 解析阶段:2D 和 3D 对不上、关键点顺序错
现象:把 3D 投到 2D 上做可视化,骨架整体偏移或者镜像翻转。
原因:相机外参的坐标系约定不一致(有的用右手系,有的用左手系),或者内参的cx/cy单位搞错(像素 vs 归一化)。
解决:先只投一帧、只投根节点,看单个点的残差。如果整体平移,是t的符号问题;如果整体镜像,是坐标轴顺序问题;如果随距离放大,是内参尺度问题。逐项排除比整体调试快得多。
现象:模型怎么训都不收敛,loss 卡在一个值附近。
原因:关键点索引顺序在不同数据源之间不一致,比如左手在源 A 是第 5 号、在源 B 是第 6 号,混着训等于给模型喂噪声。
解决:在配置里维护一张显式的映射表,导出前统一重排,并且写一个断言检查左右对称点的数量是否成对。
5.4 环境与依赖:h5py 版本差异
现象:同一份代码在同事机器上能跑,在自己机器上报KeyError或者返回类型不对。
原因:h5py不同大版本对某些存储类型的解码行为有变化,尤其是字符串和变长数组。
解决:锁版本,把h5py、numpy写进requirements.txt并固定小版本号。数据获取脚本对环境敏感度不高,但解析脚本很敏感,这两类依赖最好分开管。
5.5 磁盘与权限:写到一半没空间
现象:解压到 80% 报OSError: [Errno 28] No space left on device,之后所有操作都失败。
原因:这类数据集体积大,解压后通常是压缩包的两到三倍。
解决:解压前先算一遍所需空间,用shutil.disk_usage检查剩余量,不够就直接退出并给出明确提示。另外,解压失败留下的半截目录要能识别出来——在target下写一个.done标记,只有标记存在才算解压完成,避免下一轮误以为已经解压好了。
6. 二次开发:把获取脚本改成自己的数据流水线
这套骨架真正的价值不在“能下某个数据集”,而在于它是一个可扩展的模板。我接手过几个自采数据的项目,每次都是从这套结构开始改,改完就能用。
扩展新数据源,只需要实现一个Source子类,两个方法:list_files()返回文件描述列表,url_of(name)返回下载地址。fetcher、verify、unpack 全部复用。
# source.py:新增一个自建数据源,只改这一个文件 from fetchkit.source import BaseSource class MyLabSource(BaseSource): """自采数据放在内部对象存储上,按日期分目录""" def __init__(self, cfg): self.cfg = cfg self._cache = None def list_files(self): if self._cache is None: # 返回统一的描述结构,字段名必须和基类一致 self._cache = [ {"name": f"{d}/session.tar", "target": f"raw/{d}", "sha256": None} # 内部源可不校验,靠 size 兜底 for d in self.cfg["dates"] ] return self._cache def url_of(self, name: str) -> str: return f"{self.cfg['root']}/{name}"关键点在于list_files()返回的字典字段要和基类约定的完全一致,name用于拼 URL,target用于决定落盘位置,sha256可以为None表示跳过校验。只要这三个字段对齐,上层的下载、断点续传、解压逻辑一行都不用动。
导出阶段的可扩展性靠配置。关键点顺序、骨架边定义、是否做根节点归一化、输出 COCO 还是 npz,这些都放export_coco.yaml:
| 配置项 | 作用 | 常见取值 |
|---|---|---|
kp_order | 关键点重排映射 | 源索引列表,长度等于关键点数 |
root_idx | 归一化基准关节 | 0(骨盆)或按定义选 |
normalize | 是否根节点居中 | true / false |
out_format | 导出格式 | coco / npz |
min_vis | 最低可见度过滤 | 0 / 1 |
验证改造是否正确的办法很朴素:抽一条序列,把 3D 投影到 2D 上叠在原图上,肉眼看一下骨架贴合不贴合。写个十行的可视化脚本,比任何单元测试都直观。我一般会在scripts/下常备一个check_one.py,接收 subject 和 action 两个参数,跑完直接弹图。
还有一个容易忽略的改造点:索引文件的增量更新。数据分批次到位时,build_index.py要能只扫新增目录,而不是每次全量重建。做法是索引里给每个条目记一个mtime和size,扫描时对比,没变就跳过。几十万条序列全量扫一遍要几分钟,增量扫是秒级,日常开发体感差别很大。
从那以后我每次改造这类获取脚本,都会强制先跑一遍“单序列全链路”:下载一个包、校验一个哈希、解压一个目录、解析一条标注、投影一张图,五步全过再放开跑全量。全量跑到一半才发现解析错位,重来一次的成本太高了。希望帮到你。
本文还有配套的精品资源,点击获取