简介:面向单张人物图片直接生成全身三维模型的开源实现,源自 Facebook 研究团队的 PIFuHD 项目。它主要服务于虚拟现实、游戏开发、数字人与三维内容创作等场景,让研究者与开发者省去多视角采集和手工建模环节,仅凭一张照片即可得到较完整的网格模型。压缩包共六十五个文件,大小近四百 KB,核心为三十五个 Python 源码文件,覆盖数据加载、网络定义、网格重建与渲染等完整模块,同时附带 zbak 备份、fs/vs 着色器、csv 数据表、sh 脚本、md 文档、测试图片与关键点标注,便于对照理解每类文件的具体作用。目前已有 101 人学习下载,包内除推理与训练入口外,还提供核心网络模型、数据集封装、网格清理与环绕渲染等模块,能支撑完整复现或二次开发。请注意仅用于学习交流,勿作商业用途,下载前可留意作者分享的示例图片、许可证与说明文档,便于确认使用边界。
1. 单张照片变成可驱动的全身模型:这条 3D 人体重建工具链在做什么
你未必需要扫描仪和多机位摄影棚。用手机拍一张全身照,丢进这套基于单张图片的 3D 人体重建工具,十几分钟后就能拿到带骨骼、有皮肤细节、能在 Blender 里直接摆姿势的全身 3D 模型。这是个人学习 3D 建模时非常值得跑一遍的工具链:它用 SMPL-X 参数化人体模型先估算姿态与骨架,再用 PIFuHD 隐式曲面重建补出衣服褶皱和面部细节,把“单张图片”变成一条可复现的全身建模流水线。
解决的是谁的问题?做游戏角色预演、虚拟试穿适配、动作研究时急需人体资产的人。模型师拿它做前期草稿,初学者拿它理解单目重建原理,都比从零手捏一个人体快得多。下面按原理、环境、流程、踩坑的顺序,把这份工具写到能照着复现。
2. 原理与选型:参数化人体模型和隐式曲面怎么接力
从一张 2D 照片重建 3D 全身,表面看是个病态问题:深度信息被丢弃了,背面完全不可见。但人体不是任意物体,它有关节点位置、身体比例、骨骼拓扑这些强先验。整套工具链的思路是把“类别先验”拆给两个模型去承担,而不是让一个黑匣子从零开始猜。SMPL-X 负责骨架和姿态,PIFuHD 负责表面几何细节,两者接力,单图重建才谈得上可驱动、有细节。
2.1 参数化人体模型:SMPL-X 为什么是全身重建的骨架
SMPL-X 是 SMPL 的全身扩展版。SMPL 的姿态参数只覆盖身体主干,手指和面部是缺失的;SMPL-X 把关节扩展到约 54 个,包含左右手各 45 维姿态、下颚 3 维,以及 50 维表情参数。它本质上是一个参数化生成器:给定全局旋转、身体姿态、手部姿态、形状、表情这套参数,就能生成拓扑固定的网格,顶点数固定为 10475。顶点一一对应意味着蒙皮权重、顶点颜色、姿态迁移都有稳定载体,这是后续做角色绑定的前提。
选型上,社区里同样在做单图重建的还有体素回归和直接回归点云的方案,但它们的问题很统一:输出拓扑不固定,拿不到骨骼层级,顶点索引对不上,动画管线直接拒收。SMPL-X 输出的网格天然带骨骼对应关系,驱动起来只需要把关节旋转写进去,所以把它放在链路的骨架位是最稳的选择。加载模型验证一下:
import smplx # 加载中性性别的 SMPL-X 模型 model = smplx.create( model_path="models", # 资源包里的模型目录 model_type="smplx", gender="neutral", # neutral 对性别不明的输入更稳 use_pca=False # 不用 PCA 压缩,拿完整手部姿态 ) vertices = model().vertices # 1 x 10475 x 3 joints = model().joints # 1 x 54 x 3 print(vertices.shape, joints.shape)这段代码不是推理步骤,而是验证模型文件是否完整、参数维度是否符合预期。gender会影响形状参数 β 的先验分布,neutral在男女体型差异不大时最不容易跑偏;use_pca=False会直接拿到完整手部姿态,代价是拟合时手部自由度更大,后面避坑部分会提到怎么约束它。
2.2 隐式曲面重建:PIFuHD 到底补了什么
PIFuHD 用的是像素对齐的隐式函数:对任意一个三维点,把它投影回图像坐标,在图像特征图上采样对应特征,再通过多层感知器判断这个点落在表面内部的概率。所有采样点判断完之后得到一个 occupancy field,最后用 marching cubes 抽取等值面。它学到的能力是“从单图可见区域推断被遮挡区域”,所以头发、衣服褶皱、鞋底这类照片里看不全的部位,它也能猜出一个合理的封闭表面。
这套机制的选型理由很直接:体素回归分辨率撑不上来,神经辐射场需要多视角输入,传统 MVS 在人体这种自遮挡严重的类别上会破出大量洞。PIFuHD 是这个场景下少见的单帧输入、高分辨率输出、显存还可控的方案。它和 SMPL-X 的配合关系如下表:
| 模型 | 输出物 | 在链路中的角色 |
|---|---|---|
| SMPL-X | 10475 顶点的全身网格、54 关节骨骼 | 提供姿态、骨架和形状先验 |
| PIFuHD | 高分辨率三角网格(点云可导出) | 补充衣服、头发、面部等表面细节 |
| FLAME | 面部参数化模型 | 可选,用于修复侧脸崩坏 |
如果省掉 SMPL-X 直接跑 PIFuHD,你会得到一个静态高模,能看不能用,导入 Blender 就是一堆死顶点;如果省掉 PIFuHD 只用 SMPL-X,结果是光溜溜的人体模型,衣服和头发全丢。两条腿缺一条,这套工具链都不成立。
3. 环境搭建:这套工具链的版本匹配是第一个坑
单图重建本身跑起来不算慢,真正耗时间的是把依赖装齐。SMPL-X 拟合官方代码偏老,PIFuHD 又带自定义 CUDA 算子,两者挤在同一个 Python 环境里时,版本冲突几乎不可避免。常见做法是用 conda 单独建一个环境,把 PyTorch、Open3D、MediaPipe 这些包隔离起来,避免和日常开发环境互相污染。
3.1 conda 环境与 PyTorch 版本的选择
我用的是 Python 3.8 + PyTorch 1.13 + CUDA 11.7 这个组合,兼容性最稳。Python 3.10 以上跑 SMPL-X 的官方依赖偶尔会遇到 NumPy 接口被移除的问题,PyTorch 2.x 跑 PIFuHD 的旧算子又容易碰到编译错误,所以不要图新版本:
conda create -n fullbody python=3.8 -y conda activate fullbody pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 \ --index-url https://download.pytorch.org/whl/cu117逻辑说明:torch==1.13.1+cu117里的cu117表示 CUDA 11.7 运行时,要和显卡驱动匹配。先跑nvidia-smi看驱动支持的 CUDA 版本,再决定装哪个后缀。--index-url指定 PyTorch 官方 wheel 源,避免 pip 从 PyPI 拉到 CPU 版本。
另一个关键点是 PIFuHD 的自定义 CUDA 扩展在安装时会根据显卡算力编译。GTX 10 系算力是 6.1,RTX 20 系是 7.5,RTX 30 系是 8.6,RTX 40 系是 8.9。编译前设置环境变量:
export TORCH_CUDA_ARCH_LIST="8.6" # 按自己显卡算力填,30系填8.6 pip install -e .如果TORCH_CUDA_ARCH_LIST不设置,某些显卡会触发自动探测失败或者编译出无法加载的算子,运行时报no kernel image available。这一步看着不起眼,实际是新手最容易卡住半小时的地方。
3.2 核心依赖安装顺序与离线权重准备
紧接着装通用依赖,顺序建议是先装 Open3D 这类重依赖,再装模型相关的小包,避免 pip 解析依赖时版本打架:
pip install smplx open3d trimesh scikit-image opencv-python pip install mediapipe说明:smplx负责加载参数化模型,open3d用来做点云对齐、距离验证和可视化,trimesh负责网格读写,mediapipe负责提取 2D 关键点。这个依赖清单是这套链路里最精简的组合,缺一个都会在对应步骤报 ModuleNotFoundError。各依赖的版本建议如下表:
| 依赖 | 用途 | 版本建议 |
|---|---|---|
| python | 解释器 | 3.8 |
| torch | 深度学习框架 | 1.13.1+cu117 |
| smplx | SMPL-X 模型加载 | 最新即可 |
| open3d | 点云处理与验证 | 0.17 左右 |
| mediapipe | 2D 关键点提取 | 0.10.x |
| trimesh | 网格读写 | 4.x |
权重文件方面,SMPL-X 和 PIFuHD 的预训练权重要预先放到资源包的 models 目录。首次运行如果没有预置权重,脚本会自动触发下载,网络不通会直接失败。我一般会先把权重文件手动放到约定目录再离线跑,这样后续断网也不影响复现。检查models/smplx/下有 SMPL-X 的 npz 文件,PIFuHD/checkpoints/下有 PIFuHD 的 pt 文件,再往下走。
4. 从照片到网格:2D 关键点、姿态拟合与 PIFuHD 推理三段式
整套流程可以拆成四个动作:提取 2D 关键点、SMPL-X 姿态拟合、PIFuHD 高模重建、蒙皮权重传递。前两步解决“骨架和姿态”,第三步解决“表面细节”,第四步把前两步的结果绑定到一起,让最终模型可驱动。
4.1 第一步:用 MediaPipe 提取 2D 关键点并做坐标归一化
选 MediaPipe 而不是 OpenPose,是因为它一个模型就能同时输出身体 33 个关键点、左右手各 21 个、脸部 468 个,还支持 CPU 跑。OpenPose 精度略高,但依赖 GPU 和额外的模型下载,单图场景没必要。关键是坐标要统一:MediaPipe 返回的是归一化坐标,后续拟合脚本通常需要像素坐标或者它自己的约定范围,这里先除以宽高再存一份,兼容两种需求:
import cv2 import json import mediapipe as mp mp_holistic = mp.solutions.holistic def extract_2d_keypoints(image_path, out_json): img = cv2.imread(image_path) h, w = img.shape[:2] with mp_holistic.Holistic( static_image_mode=True, min_detection_confidence=0.5 ) as holistic: result = holistic.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) kpts = [] # 身体 33 + 左手 21 + 右手 21 + 脸部 468,按拟合脚本需要的顺序拼装 if result.pose_landmarks: kpts += [[lm.x, lm.y] for lm in result.pose_landmarks.landmark] if result.left_hand_landmarks: kpts += [[lm.x, lm.y] for lm in result.left_hand_landmarks.landmark] if result.right_hand_landmarks: kpts += [[lm.x, lm.y] for lm in result.right_hand_landmarks.landmark] if result.face_landmarks: kpts += [[lm.x, lm.y] for lm in result.face_landmarks.landmark] with open(out_json, "w") as f: json.dump({"keypoints": kpts, "width": w, "height": h}, f) extract_2d_keypoints("input.jpg", "keypoints.json")static_image_mode=True对单张照片是必须的,否则 MediaPipe 会按视频流模式处理,首帧结果不稳定。min_detection_confidence=0.5已经够用,低于这个阈值的关键点建议在后续拟合时剔除,否则脏数据会把姿态优化器带偏。如果你发现手部关键点在照片里对得不准,问题往往出在光照和遮挡,而不是阈值,先换图再调参。
4.2 第二步:SMPL-X 拟合得到姿态参数与身体网格
拟合的本质是一个优化问题:调整 SMPL-X 的姿态参数和形状参数,让模型关节重投影到图像上的位置尽量接近 2D 关键点,同时用姿态先验约束避免出现反关节。常见做法是用 SMPLify-X 系的拟合脚本,命令是:
python fit_smplx.py \ --config configs/fit_smplx.yaml \ --img input.jpg \ --keypoints keypoints.json \ --gender neutral \ --output_dir smplx_output \ --num_iters 100 \ --pose_prior_weight 300逻辑说明:--num_iters是优化迭代次数,100 次对单图已经足够,再多只会增加时间,不会显著提升精度;--pose_prior_weight是姿态先验权重,调大可以让手部更自然但会牺牲对图片的贴合度。不同资源包的拟合脚本参数名可能略有差异,跑之前先python fit_smplx.py --help确认一下。
这一步输出的smplx_output/mesh.obj是一个光溜溜的身体网格,但带有 54 个关节的骨骼结构和蒙皮权重。它是后续所有对齐的地基,先检查网格的姿态和输入照片是否一致,再继续。
4.3 第三步:PIFuHD 重建并与 SMPL-X 网格对齐
PIFuHD 直接从原图生成带衣服细节的高模。分辨率参数是显存和细节之间的权衡点,我一般在 11G 显存的卡上跑 256,细节够用且不会溢出:
python -m apps.auto_recon \ --input_path input.jpg \ --out_path pifuhd_out \ --resolution 256 \ --use_rect说明:--use_rect会使用检测到的人体边界框,裁掉背景干扰,让隐式函数把容量集中在人体区域。PIFuHD 内部会自动做人体分割和头部、身体分段重建,输出result.ply。注意它的坐标系可能和 SMPL-X 不一致,第一步先做一次 ICP 刚体对齐,再谈蒙皮。
4.4 第四步:蒙皮权重从 SMPL-X 传递到 PIFuHD 网格
PIFuHD 生成的网格没有骨骼信息,要让它在 Blender 里动起来,得把 SMPL-X 的蒙皮权重迁移过去。常见做法是找 PIFuHD 网格每个顶点在 SMPL-X 网格上的最近邻,用距离加权插值权重:
import numpy as np import trimesh from scipy.spatial import cKDTree body_mesh = trimesh.load("smplx_output/mesh.obj") detail_mesh = trimesh.load("pifuhd_out/result.obj") skinning = np.load("smplx_output/skinning_weights.npy") # J 个关节的蒙皮权重 tree = cKDTree(body_mesh.vertices) dist, idx = tree.query(detail_mesh.vertices, k=3) w = 1.0 / (dist + 1e-6) # 取最近 3 个 SMPL-X 顶点的蒙皮权重,按距离倒数加权合并 detail_weights = (skinning[idx] * w[:, :, None]).sum(1) / w.sum(1)[:, None] np.save("detail_skinning.npy", detail_weights)k=3表示取最近 3 个邻居做插值,太少会产生权重断层,太多会抹掉关节细节。距离权重的分母加1e-6是防止完全重合时除以零。权重传完之后,把 PIFuHD 网格和detail_skinning.npy导入 Blender,在骨骼约束里挂上这组顶点组,模型就能跟着骨骼动了。
注意:蒙皮传递只解决“骨肉对应”,不解决“穿模”。如果衣服袖子宽大,手臂抬起时袖口和身体会穿插,这是基于单图重建的固有局限,需要用 Blender 的骨骼校正修改器微调。
5. 避坑:单图重建最常见的五个翻车点与处置
单图重建最有趣的部分就是不确定性。下面五条是我实际跑这套流程时反复遇到的问题,每条都按现象、原因、解决的顺序写,方便你对照排查。
5.1 显存溢出:跑一半进程被杀
现象:PIFuHD 推理阶段进程直接被 kill,或者报CUDA out of memory。
原因:PIFuHD 默认分辨率 512 时显存占用接近 10G,如果同时还开着 Open3D 可视化窗口、SMPL-X 拟合也没释放显存,8G 卡基本必爆。
解决:跑推理前先强制--resolution 256 --batch_size 1,并用CUDA_VISIBLE_DEVICES=0把进程限定到单卡;拟合完成后加一行torch.cuda.empty_cache()释放显存再跑 PIFuHD。如果显存还是不够,检查后台有没有残留的 python 进程,nvidia-smi看清占用后再决定是否换 12G 卡。
5.2 手部指节拧成麻花
现象:重建出来的网格手指明显反关节,手掌和手臂的相对位置不对。
原因:MediaPipe 在手指被身体遮挡时给出的 2D 关键点置信度很低,而 SMPL-X 手部姿态自由度又很大,优化器为了贴合脏数据会把手指推到一个不自然的位置。
解决:拟合时把hand_pca_components降到 6,减少手部自由度数;同时把姿态先验权重提高到 300 以上。更彻底的办法是在提取关键点时记录置信度,把低于 0.4 的手部点直接剔除,不让它们参与重投影误差计算。手指是最容易翻车的部位,没有之一,第一次跑就要有心理准备。
5.3 头顶和鞋底出现破洞
现象:PIFuHD 生成网格的头顶、鞋底区域有明显的洞,翻转视角能看到内部。
原因:单张照片根本没有这些部位的可视信息,隐式函数只能靠先验去猜,而采样点落在可见范围外时 occupancy 预测值不置信,marching cubes 就抽不出面。
解决:把 PIFuHD 网格和 SMPL-X 网格做一次闭合处理。用 SMPL-X 网格作为深度锚点,把离锚点近的孔洞三角面补上,再做一次 Poisson 重建。Open3D 里有现成的create_from_point_cloud_poisson,参数depth设 9 左右能兼顾细节和封闭性。如果你只需要正视角效果,这一步可以跳过,但导出 3D 打印或做旋转展示时必须有闭合网格。
5.4 导入 Blender 后坐标轴翻转
现象:模型导入后左右镜像,或者骨盆朝向不对,旋转骨骼时动作是反的。
原因:SMPL-X 的坐标系是右手系、Y 轴向上,PIFuHD 输出的是图像坐标系,两者在导出 FBX/GLB 时如果没做轴转换,就会产生一次镜像翻转。
解决:在导出脚本里统一加一个旋转矩阵,把 Y-up 转成 Z-up:
import numpy as np R = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]], dtype=float) vertices = vertices @ R.T如果你拿到的资源包已经处理过坐标轴,这步就跳过。判断标准很简单:导入 Blender 后先看左手,如果模型举的是右手,那就是翻了一次。Blender 里也可以直接 Ctrl+A 应用旋转再导出,但每次重新跑流程都要手动操作一遍,不如写进脚本。
5.5 脸部崩成马赛克
现象:脸部区域网格坑坑洼洼,五官完全认不出来。
原因:PIFuHD 对脸部重建依赖正脸视角,侧面图的脸部特征被遮挡太多;另一个常见原因是输入照片人脸区域像素太少,宽度低于 500 像素时采样不足。
解决:先用 OpenCV 的仿射变换把人脸矫正成正脸,重建完再把头部网格旋转回原姿态。更省事的方式是放弃 PIFuHD 的脸部,改用 FLAME 拟合出面部网格,再和身体网格焊接。如果只是拿来做体型示意,脸部细节可以直接容忍,毕竟不是每个场景都需要面部精度。
6. 进阶:用 Open3D 做网格质量验证与姿态微调
重建完不等于能用,尤其是蒙皮权重插值之后,不验证直接上骨骼,动起来肯定露馅。我习惯在导出前用 Open3D 做一次自动体检:算 PIFuHD 网格和 SMPL-X 网格之间的点云距离,看看蒙皮传递有没有把网格带偏:
import open3d as o3d src = o3d.io.read_point_cloud("pifuhd_out/result.ply") tgt = o3d.io.read_point_cloud("smplx_output/mesh.ply") # 先做一次 ICP 刚体对齐,消除两套模型坐标系差异 reg = o3d.pipelines.registration.registration_icp( src, tgt, 5.0, o3d.pipelines.registration.TransformationEstimationPointToPlane()) src.transform(reg.transformation) dists = src.compute_point_cloud_distance(tgt) print("平均距离(mm):", dists.mean()) print("超过3mm的点占比:", (dists > 3.0).mean())registration_icp的max_correspondence_distance=5.0表示以 5mm 为阈值寻找对应点,超过这个距离的点对不算数;compute_point_cloud_distance返回每个源点到最近目标点的距离。平均距离在 3mm 以下说明对齐良好,超过这个值就要回到第四步检查蒙皮权重,而不是继续往后做。这一步三分钟能跑完,能省掉后面在 Blender 里手动修正大量错位顶点的时间。
如果验证通过,导入 Blender 后不要急着手动刷权重。用 DataTransfer 修改器把 SMPL-X 的顶点组按最近点映射到 PIFuHD 网格上,映射模式选 Nearest Face Interpolated,一两分钟就能完成蒙皮,比手动刷快一个量级。映射完在姿态模式下转一下手肘和肩关节,重点看衣服区域有没有不自然的拉伸;单图重建的衣服褶皱是静止的,关节一动就穿帮,这种地方要么用骨骼校正修改器,要么接受它是静态展示模型。
从那以后,我每次跑完拟合都会先拉一遍手肘和肩关节的角度,再用 Open3D 看一眼距离分布,最后才丢给 PIFuHD 做高模重建。这套前置检查帮我省掉了一半以上的网格后处理时间,也少走很多弯路。希望这套流程和踩坑记录能帮你把这份工具真正用起来。
本文还有配套的精品资源,点击获取