ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

LRM与HOI重建:从单目图像到交互场景的三维重建

LRM与HOI重建:从单目图像到交互场景的三维重建 在 3D 重建与具身智能的研究中Large Reconstruction ModelsLRMs与 Human-Object InteractionHOI重建正在快速融合。传统的 HOI 重建通常依赖类别模板、多视角图片或长时间的优化迭代而 LRMs 提供另一条路径让模型从图片中直接推理出人和物体的三维表示。这篇文章围绕这一主题整理 LRM 用于人与物体交互重建时的概念、环境、实现、验证和排错方法。与普通单物体重建不同人与物体交互重建要同时解决三类问题人类姿态与形状估计、物体形状与姿态估计、以及二者之间的接触与遮挡关系。LRM 的优势在于它可以把大量真实世界交互场景的先验压缩进前馈网络推理时不需要逐帧优化。下面从任务定义开始讲清楚这套方案为什么成立再给出一个最小可运行的实现框架。1. 先理解 LRM 为什么适合人与物体交互重建1.1 人与物体交互重建到底在重建什么Human-Object InteractionHOI重建的目标是从一张或多张图像中恢复人体、物体以及二者交互关系的三维表示。常见的输出包括人体网格或隐式表面通常用 SMPL、SMPL-X 等参数化模型表达。物体网格或隐式表面可以用 SDF、NeRF、3D Gaussian Splatting 或显式 mesh 表达。人体与物体的相对位姿包括旋转、平移和尺度。接触区域比如手掌与杯子柄、臀部与椅子面、脚底与地面。单纯把人体和物体分开重建并不是真正的 HOI 重建。交互场景中通常存在大面积遮挡物体可能被人的手遮挡人体肢体也可能被物体遮挡。如果两个分支独立重建最终会把人体穿过物体或者手部悬空。因此LRM 的建模方式需要让两个目标共享图像特征同时输出一个“一致的三维场景”而不是两个孤立的网格。1.2 传统方法的两类瓶颈类别模板与优化速度在 LRM 出现之前HOI 重建的主流方法大致分成两类。第一类是参数化优化方法。它基于 SMPL 拟合人的姿态再对物体做位姿估计或模板匹配最后用物理约束和接触约束优化相对位置。这类方法的优点是精度高可以结合几何先验但缺点是每张图都要迭代几百到几千步速度慢。容易陷入局部最优特别是手部与物体互相遮挡时。对物体类别敏感已知类别的模板效果尚可未知类别很难处理。第二类是类别专用方法。例如针对椅子、桌子、杯子等常见交互物体分别训练检测器和形状重建网络。优点是类别内精度高缺点是无法扩展到开放世界。真实场景中的交互物体类别几乎无限不可能为每个类别都准备精细模板。LRM 的思路介于两者之间。它不把物体当作固定类别而是把“从图像到三维形状”当作一个可学习的前馈映射。训练数据覆盖足够广时模型可以泛化到训练集中没有见过的物体形状。这正是 LRM 适合 HOI 重建的深层原因。1.3 LRM 的建模思路前馈生成一个三维表示一个典型的 LRM 包含下面几部分图像编码器通常是 Vision TransformerViT把输入图片切分成 patch 并编码成 token。三平面解码器从 token 生成三个正交平面的特征每个平面保存局部几何和纹理信息。可微渲染器对任意空间点查询三平面特征再解码成颜色、密度或 SDF并通过体渲染或 splatting 生成图像。在人与物体交互场景中可以把人体和物体看作是同一个三维空间中的两个语义区域。LRM 不需要分别重建后再拼接而是在同一个 triplane 或 3D Gaussian 空间里同时表达人和物体然后增加一个语义分支区分“这个位置属于人、属于物体、还是属于背景”。这种设计有几个好处人体和物体的相对位置天然一致不需要后处理对齐。遮挡区域可以通过可微渲染从像素损失中学习。接触关系体现在同一空间场的拓扑连接中比单独预测接触点更自然。当然LRM 也有自己的代价。它需要大规模 HOI 训练数据显存占用较高并且对小物体的细节恢复不如专用优化方法。下面进入工程落地时最需要关注的依赖和数据准备。2. 环境与数据准备先对齐依赖再谈模型2.1 依赖环境和版本选择LRM 模型通常使用 PyTorch 实现训练和推理都依赖 CUDA。部分实现还会用到 3D Gaussian Splatting 的可微光栅化器或者 NeRF 的渲染模块。下面是研究实验环境中最常见的依赖组合conda create -n lrm_hoi python3.10 -y conda activate lrm_hoi pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy trimesh pyrender pip install mcubes scikit-image lpips tensorboard实际项目中LRM 的代码仓库可能会锁定某些依赖版本。安装前先看仓库的requirements.txt或environment.yml不要直接使用最新版本。不同版本的 PyTorch 对 CUDA 算子兼容性差异很大尤其是diff-gaussian-rasterization这类需要自行编译的扩展。组件推荐版本区间说明Python3.9 到 3.11版本过低可能导致新算子不支持PyTorch2.0 以上自动求导和编译器能力更稳定CUDA11.8 或 12.1具体以 PyTorch 预编译包为准diff-gaussian-rasterization以项目仓库为准3DGS 渲染需要自行编译trimesh4.x用于 mesh 导入导出mcubes最新稳定版从 SDF 提取 mesh 时使用2.2 数据集与标注格式HOI 重建的数据比普通单物体重建更复杂。常见公开数据集包括 BEHAVE、H2O、InterCap 等它们通常提供多视角视频、人体 SMPL 参数、物体 mesh 和相机参数。使用前要确认 License 和数据用途不同数据集的标注规范差异很大。一份基础的数据组织方式可以设计成这样dataset/ sequence_01/ rgb/ 000000.png 000001.png mask_human/ 000000.png 000001.png mask_object/ 000000.png 000001.png cam_meta.json smplx/ tracked_people.json object/ object_mesh.obj关键字段在cam_meta.json中至少需要包含图像宽高。相机内参矩阵 K。外参 R、T。时间戳或帧索引。对于人体推荐使用 SMPL-X 或 SMPL 参数对于物体最好提供对象 mesh 或至少提供深度图。如果数据集中没有物体的真实 mesh可以通过多视角重建得到伪标签但这会增加训练噪声。2.3 项目目录设计研究性代码很容易演变成一串混乱的 notebook 和脚本。稍微克制一点项目目录可以这样组织lrm_hoi/ configs/ train_hoi.yaml inference_hoi.yaml data/ datasets/ transforms.py models/ lrm_hoi.py renderer.py losses.py utils/ camera.py mesh_utils.py metrics.py scripts/ train.py export_mesh.py evaluate.py outputs/ checkpoints/ visualizations/这个结构不复杂但能保证训练、推理、评估、可视化彼此解耦。接下来直接在models/lrm_hoi.py中实现一个最小化接口。3. 一个最小可运行框架从单目 RGB 到交互场景网格3.1 图片预处理与相机归一化LRM 的输入通常是归一化到一定尺寸的 RGB 图像例如 512x512。图像本身要进行中心裁剪或缩放相机内参也需要同步归一化。如果忽略这一步输入分布偏移会直接影响重建结果。import cv2 import numpy as np import torch def preprocess_image(image_path, target_size512): image cv2.imread(image_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w image.shape[:2] scale target_size / max(h, w) new_w, new_h int(round(w * scale)), int(round(h * scale)) image cv2.resize(image, (new_w, new_h)) image image.astype(np.float32) / 127.5 - 1.0 # 中心填充到 target_size h_, w_ image.shape[:2] pad_h target_size - h_ pad_w target_size - w_ top pad_h // 2 left pad_w // 2 image cv2.copyMakeBorder( image, top, pad_h - top, left, pad_w - left, cv2.BORDER_CONSTANT, value(-1, -1, -1) ) tensor torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0) return tensor # 相机内参也需要按相同的缩放和填充做归一化 def normalize_intrinsics(K, original_size, target_size): h, w original_size scale target_size / max(h, w) K_norm K.copy() K_norm[0, 0] * scale K_norm[1, 1] * scale K_norm[0, 2] K[0, 2] * scale (target_size - w * scale) / 2.0 K_norm[1, 2] K[1, 2] * scale (target_size - h * scale) / 2.0 return K_norm这里的关键是图像缩放后相机主点坐标也必须跟着移动否则后续的 ray casting 全部错位。实际项目里这部分代码必须写单元测试。3.2 骨干网络与 LRM 解码器下面代码描述的是一个简化但完整的模型结构图像编码器提取 token解码器生成 triplane再由一个查询模块从 triplane 得到密度和颜色。它用于说明接口设计不能直接当完整训练脚本使用。import torch import torch.nn as nn class LRMHOI(nn.Module): def __init__(self, image_size512, triplane_res128, plane_channel32): super().__init__() # 图像骨干可以替换为任意 ViT 编码器 from torchvision.models import vit_b_16 self.image_encoder vit_b_16(weightsNone) # 将编码器输出映射为 triplane 特征 # 三平面XY, XZ, YZ每个平面是 plane_channel 维特征 self.triplane_head nn.Sequential( nn.Linear(768, 256), nn.ReLU(inplaceTrue), nn.Linear(256, triplane_res * triplane_res * plane_channel * 3) ) self.triplane_res triplane_res self.plane_channel plane_channel def forward(self, image): tokens self.image_encoder(image) features self.triplane_head(tokens) B, _ features.shape C self.plane_channel R self.triplane_res features features.view(B, 3, C, R, R) return { triplane_xy: features[:, 0], triplane_xz: features[:, 1], triplane_yz: features[:, 2] } def query_triplane(self, triplanes, points): # points: [B, N, 3] features [] for plane_name in [triplane_xy, triplane_xz, triplane_yz]: plane triplanes[plane_name] # 实际项目这里会做双线性采样并求和 features.append(torch.zeros(points.shape[:-1] (self.plane_channel,))) return sum(features)由于 LRM 通常使用预训练 ViT且不同仓库的实现差异很大实际使用时要直接复用原作者定义的模型类。上面代码只是把“图像到 triplane”的核心流程表达清楚。3.3 人体、物体与接触区域的分支设计HOI 场景需要模型在同一个空间里输出多个语义标签。常见做法是在 triplane 查询后增加几个 head人体 head输出人体 SDF 值和语义概率。物体 head输出物体 SDF 值和语义概率。接触 head输出该空间点是接触区域的概率。这样设计的好处是人体和物体共享同一个 triplane 特征但解码层可以保留各自语义。物体类别多样化时物体 head 可以做得更深人体类别相对单一人体 head 可以加入 SMPL 先验约束。class HOIDecoder(nn.Module): def __init__(self, in_channels32, hidden64): super().__init__() self.shared_mlp nn.Sequential( nn.Linear(in_channels, hidden), nn.ReLU(inplaceTrue) ) self.human_head nn.Linear(hidden, 1) self.object_head nn.Linear(hidden, 1) self.contact_head nn.Linear(hidden, 1) self.color_head nn.Linear(hidden, 3) def forward(self, plane_feature): shared self.shared_mlp(plane_feature) human_sdf self.human_head(shared) object_sdf self.object_head(shared) contact self.contact_head(shared) color self.color_head(shared) return { human_sdf: human_sdf, object_sdf: object_sdf, contact: contact, color: color }这里没有把人体和物体分开成两套完全独立的网络而是先共享特征、再分支预测。理由是在交互场景中接触边界附近的人体和物体特征高度相似强行分开会让梯度互相屏蔽。3.4 训练循环示例训练一个 LRM 风格的 HOI 重建模型核心循环并不复杂。关键是保证每个 batch 里包含图像、相机参数、人体 mask、物体 mask、采样点真值 SDF 或深度。def train_step(model, batch, optimizer): image batch[image] # [B, 3, H, W] mask_human batch[mask_human] # [B, H, W] mask_object batch[mask_object] # [B, H, W] points batch[points] # [B, N, 3] sdf_human_gt batch[sdf_human_gt] # [B, N] sdf_object_gt batch[sdf_object_gt] # [B, N] rgb_gt batch[rgb_gt] # [B, N, 3] triplanes model.encode(image) plane_feature model.query_triplane(triplanes, points) pred model.decode(plane_feature) loss_human sdf_loss(pred[human_sdf], sdf_human_gt) loss_object sdf_loss(pred[object_sdf], sdf_object_gt) loss_color torch.nn.functional.l1_loss(pred[color], rgb_gt) loss loss_human loss_object 0.5 * loss_color optimizer.zero_grad() loss.backward() optimizer.step() return { loss: loss.item(), loss_human: loss_human.item(), loss_object: loss_object.item(), loss_color: loss_color.item() }如果使用体渲染而不是直接回归 SDF训练循环里还要增加渲染步骤。这个示例没有渲染是为了把“由 triplane 查询得到 SDF”这件事讲清楚。实际复现时渲染器和 SDF 回归往往需要同时存在并用可微渲染将pred[color]与图像像素对齐。3.5 推理与导出训练完成后从模型输出 mesh 的标准流程是先构建一个稠密网格查询每个顶点的 SDF再用 Marching Cubes 提取等值面。下面是常用流程import numpy as np import mcubes import trimesh def extract_mesh(model, triplanes, res256, level0.0): xs np.linspace(-1.0, 1.0, res) ys np.linspace(-1.0, 1.0, res) zs np.linspace(-1.0, 1.0, res) grid np.stack(np.meshgrid(xs, ys, zs, indexingij), axis-1) flat grid.reshape(-1, 3) with torch.no_grad(): pred model.query_triplane(triplanes, torch.from_numpy(flat).float().unsqueeze(0)) object_sdf pred[object_sdf].squeeze(0).squeeze(-1).numpy() object_sdf object_sdf.reshape(res, res, res) vertices, triangles mcubes.marching_cubes(object_sdf, level) # 顶点坐标映射回 [-1, 1] 空间 vertices vertices / (res - 1) * 2.0 - 1.0 mesh trimesh.Trimesh(verticesvertices, facestriangles) return mesh这里只提取物体 mesh 作为演示。实际项目通常会把人体 mesh 和物体 mesh 分别提取再保留接触区域。输出 mesh 后可以用trimesh.export导出为.obj也可以用 open3d 或 pyrender 可视化。注意上面代码中的model.query_triplane和model.decode都只是接口示意。不同 LRM 实现的三平面采样方式不同网格范围也可能不是[-1, 1]落地前要以对应仓库的实际坐标约定为准。4. 关键参数、损失函数与输出含义4.1 输入分辨率与 token 密度的取舍LRM 类模型对输入分辨率比较敏感。分辨率提高意味着 ViT 切出的 patch token 更多模型有更多信息恢复细节但显存占用也会快速上升。常见选择224x224适合快速验证和消融实验显存占用小但小物体重建效果差。384x384平衡速度与效果适合大多数单物体和 HOI 实验。512x512 或更高适合精细 mesh 输出需要至少 24GB 显存。在 HOI 场景中手部和小物体通常只占图像的一小部分。如果输入分辨率过低物体可能只有十几个 token重建结果会非常粗糙。实际项目中可以先在 384x384 上跑通流程再针对小物体训练高分辨率模型。4.2 损失函数Mask、RGB、深度和接触损失LRM 训练时通常需要组合多种损失每种损失负责不同维度。Mask 损失帮助模型区分人、物体、背景。RGB 损失保证从新视角渲染出的颜色与真实图片一致。深度或 SDF 损失监督几何形状避免表面漂移。Eikonal 正则让 SDF 在空间中满足梯度范数为 1提升表面稳定性。接触损失在人体 mesh 和物体 mesh 最近点之间施加小距离约束减少穿插。接触损失是 HOI 重建里最需要小心的部分。太大会让模型把人体和物体强行压在一起太小又会导致手部漂浮。推荐在训练初期给一个较低权重等 mask 和 SDF 分支基本收敛后再调高。def contact_loss(human_verts, object_verts, threshold0.05): human_verts: [N, 3] object_verts: [M, 3] 返回接触区域的近似距离损失。 # 使用 scipy 的 KDTree 找最近邻 from scipy.spatial import KDTree tree KDTree(object_verts.detach().cpu().numpy()) dist, idx tree.query(human_verts.detach().cpu().numpy()) dist_tensor torch.from_numpy(dist).float().to(human_verts.device) # 只惩罚距离小于阈值的点避免把整个人拉向物体 contact_mask (dist_tensor threshold).float() loss (dist_tensor * contact_mask).mean() return loss注意KDTree 在反向传播时不可导所以示例里使用了detach()。实际实现通常会改用可微距离场查询但思路是一样的只约束接触点附近的距离。4.3 关键超参数速查表参数名建议范围调大影响调小影响错误表现输入分辨率224 到 512细节更多显存更大速度快但小物体丢失显存溢出或细节模糊triplane 分辨率64 到 256几何表达更强表面更粗表面空洞或伪影triplane 通道数16 到 64容量增加容量减少颜色和形状欠拟合采样点数64 到 256渲染更精细训练更快表面不连续接触损失权重0.01 到 1.0接触更紧密容易穿插漂浮穿插或悬浮Eikonal 权重0.01 到 0.1SDF 更稳定表面抖动重建表面杂散以上数值不是固定的应该以自己数据集上的验证集指标为准。做消融实验时每次只改一个参数否则很难定位是哪个因素引起指标变化。4.4 学习环境与生产环境的差异研究实验环境可以接受“离线训练、人工看可视化结果”。但如果要把 LRM 推理能力用到批量数据生产或实时服务中必须考虑额外工程化问题。维度研究实验环境批量生产环境推理速度每秒处理几张即可需要批处理或异步任务显存控制尽量用最大 batch 提高速度需要限制单卡并发和动态 batch模型版本训练日志 checkpoint 即可需要模型注册和版本回滚数据输入手工挑选少量图片需要校验图片质量、mask 完整性异常处理失败时人工重跑需要自动跳过、记录失败原因输出校验肉眼观察 mesh 是否合理需要客观指标和兜底降级策略研究阶段可以容忍训练脚本偶尔中断但生产环境必须保证一个序列失败不会影响整批任务。因此推荐在脚本开头加数据完整性检查并在每个 block 处理完后写入中间结果。5. 运行验证与常见错误模式5.1 可视化验证mesh、点云、渲染图训练完成后先不要直接看指标先可视化几个样本。推荐保存三类图像重建 mesh 的侧视图和新视角渲染图。人体 mesh 和物体 mesh 重叠后的接触区域放大图。原始输入图和 mask 叠加图。可视化脚本可以保留在scripts/export_mesh.py中python scripts/export_mesh.py \ --ckpt ./outputs/checkpoints/last.pt \ --input ./data/sequence_01/rgb/000000.png \ --output ./outputs/visualizations/sequence_01/ \ --save_obj \ --save_png如果新视角渲染明显不合理说明 triplane 内部的几何表达有问题而不是后处理步骤问题。可视化能帮助快速区分是模型问题还是数据问题。5.2 定量指标IoU、Chamfer、F-score、接触度量定量评估通常分为几何、体素和接触三类指标。指标名称作用计算方式IoU衡量占用空间重合度真值和预测网格体素化后计算交并比Chamfer Distance衡量表面点云距离双向最近点距离平均F-score衡量表面精度和召回距离小于阈值的点占比Contact Precision衡量接触区域精度预测接触点是否在真值接触范围内实际评估时建议对人和物体分别计算指标再额外输出一个“接触区域 IoU”。因为总 IoU 高不代表接触关系正确有可能人体和物体各自重建得好但位置错开了。5.3 两类典型失败模式第一类是人手和物体边缘粘连。这通常是因为 mask 中手部和物体贴得太近分割不干净。模型学到的是“两个物体边界可以融合”而不是真正的接触关系。第二类是物体完全被遮挡。比如人站在桌子后面只有上半身可见物体的下半部分完全被隐藏。LRM 只能依赖先验猜测形状如果测试数据与训练集分布不同猜测结果可能很突兀。这两类失败在单目输入下很难彻底避免。缓解方式包括使用多视角输入、在训练数据中增加截断样本、增加接触先验。5.4 排查链路从现象倒推原因当重建结果出现明显错误时可以按照下面顺序排查检查输入图片和处理后的 mask。检查相机内参归一化是否正确。检查 triplane 分辨率是否足够。检查人体和物体分支是否共享了不该共享的编码层。检查接触损失权重是否过高或过低。检查训练集与测试集分布是否有明显差异。例如如果发现重建的物体整体偏移优先怀疑相机归一化如果发现人体网格正常但物体空洞优先怀疑 object mask 或物体数据量不足如果发现人和物体相互穿插但各自指标都很好优先怀疑接触损失和真实接触标签质量。注意排查时不要一次性修改多个因素。每次只改一个条件保留输出日志和可视化结果才能形成可复现的实验记录。6. 最佳实践与下一步扩展6.1 实验前检查清单开始训练前建议逐项确认下面内容数据集是否包含相机内参、外参和 mask。训练集与验证集是否按序列划分避免同一交互序列出现在两边。图像预处理代码是否与训练代码使用同一份实现。模型输出空间的范围是否与网格提取范围一致。人体和物体 mask 是否有大面积重叠。是否保存了每个训练 step 的 loss 曲线。是否固定了随机种子确保可复现。是否在很小 batch 上做过一天步数的过拟合测试。过拟合小 batch 是这套流程里性价比最高的检查。如果模型连训练集中的一个 batch 都无法重建那么问题大概率出现在代码层面而不是数据量或超参。6.2 常见坑与规避方法常见坑为什么错推荐做法输入分辨率过低小物体 token 太少几何表达不足使用 384x384 以上或在 ROI 区域单独重建只优化 RGB 损失颜色容易收敛几何可能塌缩到下平面增加 SDF/深度损失和 Eikonal 正则接触损失权重一开始就很大早期几何未成形强行拉点导致表面变形先小权重后期再调高人体与物体分支完全独立无法共享图像特征交互关系难学习共享编码器只用轻量 head 区分语义没有做 mask 质量检查mask 边缘噪声会污染 triplane 特征对 mask 做形态学操作并按序列抽查直接使用最新版依赖算子接口或 CUDA 版本不兼容严格锁定仓库要求版本HOI 重建很容易出现“看起来差不多其实细节全错”的情况。因此定量指标和可视化必须同时使用不能只用一张渲染图判断效果。6.3 从单视图到多视图与视频输入单目 LRM 是起点实际项目往往需要多视角或视频输入来获得更稳定的结果。多视角输入时可以把不同视角的图像分别编码再用 cross-attention 融合 token。视频输入则可以利用时序一致性让相邻帧的人体姿态变化更平滑。这里有一个常见的工程选择是直接扩展 LRM 网络还是用后处理多视角融合。直接扩展网络通常效果更好但训练成本会成倍增加。后处理融合适合快速验证缺点是融合错误难以修正。建议先在后处理流程中验证多视角对齐再考虑修改网络结构。6.4 落地时最值得保留的工程化习惯最后整理几条对实际项目最有用的习惯命令行参数统一使用 YAML 配置不要散落在 python 文件里。每次实验记录 commit、seed、数据集版本和超参否则无法复现。数据集中的原始图像不要随意覆盖标注文件按序列独立保存。checkpoint 保存时同时保存 optimizer 和 scheduler 状态。评估脚本固定测试集不允许训练过程中往测试集里增加样本。LRM 用于 HOI 重建还处于快速演进阶段新的渲染器、新的三维表示和新的数据集会不断出现。但底层的工程问题不会变输入分布、数据质量、显存管理、损失平衡、评估口径。把这些基础打牢后续替换模型结构时会轻松很多。
返回列表