ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

单目图像重建HOI:人与物体交互的3D重建实战

单目图像重建HOI:人与物体交互的3D重建实战

去年年中我接手了一个让人又爱又恨的任务:给定一张普通照片,要恢复出照片里那个人和TA手中、身下、脚边物体的三维形态。人好办,物体也还好办,难的是“人和物体叠在一起”这件事本身。你一眼能看出一个人坐在椅子上、伸手够杯子,但让算法从一张二维图像里同时猜出人体姿态、物体形状、双方接触关系,还得保证两者不互相穿透,这中间有一堆靠拍脑袋解决不了的细节。这个项目我们内部代号叫PICO,主题就是单张图像重建HOI(Human-Object Interaction,人与物体交互)。

在往下展开之前,先说清楚这篇文章适合谁。如果你在做三维视觉、AR/VR内容制作、具身智能仿真,或者只是对单目3D重建感兴趣,都可以参考。我不会只贴一张效果图完事,而是把pipeline怎么设计、损失函数怎么调、数据怎么搞、坑怎么踩,从头到尾讲一遍。想看理论和想看实操的人,都能找到对应段落。

1. 单张图像重建HOI到底难在哪

1.1 单目重建的家底:人体和物体各自都有成熟方案

单目三维人体重建的路子,近十年基本稳定:先用2D关键点检测器从图像里找出人体关键点,再拿一个参数化人体模型去拟合,让渲染出来的形状投回2D时和检测到的2D关键点、人体分割掩码对得上,同时加姿态先验防止出现反人类的扭曲。这套“2D观测约束+参数化模型先验”的组合,在单人、无遮挡、画面干净的场景下已经相当能打。

单目物体重建也有自己的套路:给定物体类别,从图像中估计类别、位姿和尺寸,再套上模板网格或者生成式模型。甚至对于室内场景,还有一批基于预训练深度大模型的方法,直接回归出整个房间的三维结构。

但这两条线通常各玩各的:人体方法输出的就是一个人体mesh,物体方法输出的是一个物体mesh。它们之间没有关系,没有接触,没有遮挡,没有谁支撑谁。单独跑起来没问题,一旦要求输出“人和物体在同一个三维空间里、以合理的交互状态共存的场景”,麻烦立刻成倍增加。

1.2 交互带来的三个麻烦:遮挡、深度歧义、物理合理性

人和物体一接触,三个麻烦就同时冒出来。

第一个是相互遮挡。人的手臂可能被杯子挡住,膝盖可能被椅子挡住;反过来,物体也可能被人的身体挡掉一大半。关键点检测一旦漏了局部,单独拟合人体就容易飘。更麻烦的是,物体被遮挡的部分在2D图像上完全没有信息,记住“这里缺了一块”本身就是一个需要模型显式处理的事情,但大多数单目方法默认输入是完整可见的。

第二个是深度歧义。同一个2D画面里,物体可以在人身前也可以在人身后。纯几何投影损失无法区分“手在杯子前面”和“手在杯子后面”。很多方法收敛到的结果从自己的相机视角看完全正确,但换个视角就露馅——杯子跑到人的背后去了,可图像里明明看到杯沿压着手臂。

第三个是物理合理性。坐在椅子上的人,臀部必须落在椅面上;抱着的球,手掌必须包着球的外表面。如果你只是分别重建人和物体,出来的很可能是“人悬空坐在椅子上方”或者“手掌和球互相交叉”。所谓合理性,不只是说几何上不穿透,还包括接触部位符合日常经验。只要有一个手指插进杯子里,人眼一眼就能发现,根本不需要专业训练。

所以我一直觉得,HOI重建的核心不只是“把人和物体各自重建出来”,而是“把交互关系也重建出来”。后者才是真正拉开难度的地方。

1.3 把交互拆成接触、遮挡、支撑三类可计算约束

交互是一个很模糊的词,落地到代码必须把它拆成可以被数学表达的东西。我们习惯拆成三件:接触、遮挡、支撑。

接触解决“哪些几何区域应该贴在一起”,遮挡解决“深度上谁在前谁在后”,支撑可以看作接触的一种特殊形式,额外带上力学语义——比如坐着时,人的重量通过臀部传递到椅面。好消息是,对大多数常见交互,支撑性和接触性高度相关,所以很多方法并不显式建模力学,而是用接触约束把两者“拉近”。

PICO也沿用了这个思路:用一个接触感知的约束头,预测人体mesh表面和物体mesh表面之间的接触区域,再把这部分拉近,其余部分推开。遮挡则单独用一个深度顺序一致性约束处理,后面我会详细展开。先记住一个结论:HOI问题的求解,本质上就是这三组约束的联合求解。

2. 为什么PICO选择“先独立再联合”这条路线

2.1 端到端体积回归的诱惑与陷阱

设计一个HOI重建方案,第一个摆在桌面上的问题是:用什么形式表达结果?有两个大方向:直接回归体素或者TSDF场,或者用参数化模型拟合。

端到端回归体素看起来更酷,一张图进去,一个三维场出来,中间不需要任何人类先验。我见过不少团队一开始都走这条路,觉得“让模型自己学到交互模式”。但实际效果被两个问题卡死。

第一个是标注数据量。HOI的三维真值标注极其稀缺,因为要同时标注人体姿态、物体形状、接触关系,成本远高于标2D框。体素回归是像素级预测任务,没有海量带真值的数据根本学不动。第二个是数据分布太稀疏。交互场景变化极大,椅子可以出现在客厅、办公室、街头,坐着的人姿态千奇百怪,抱猫、拿书、撑伞、骑车……在高维空间里这些都是非常稀疏的样本点。直接学一个体素生成器,很容易在训练集上过拟合,测试时稍微遇到一种新交互形式就崩。

参数化方案则把问题分层:人体姿态用SMPL这类体模型表达,物体用类别模板+位姿表达。这样虽然上限受限于模板库,但每个子问题都有成熟的2D监督可以利用,鲁棒性明显好。PICO最终采用的是“先独立初始化、再联合优化”的混合思路:第一步把人体和物体各自估计出来,第二步在同一个优化问题里同时改进两者,让接触约束和遮挡关系参与进来。这个选择不是因为它看起来新颖,而是因为它在实际算力、数据、稳定性的三重约束下,是性价比最高的方案。

2.2 人体侧:参数化体模型为什么绕不开

做人体重建绕不开SMPL这类参数化体模型。可以把SMPL理解成一个可微分的人体变形器:输入姿态参数θ和形状参数β,输出一个带顶点坐标的人体三角网格。θ控制关节旋转,β控制高矮胖瘦。一个普通成年人的人体表面有几万个顶点,直接预测每个顶点的坐标是浪费自由度,而且很容易预测出非人的形状;但通过θ和β,你能把这些顶点压缩到一个低维流形里,而且这个流形上的每个点都对应一个合理的、像人的mesh。

之所以大家都用这类参数化模型,另一个关键原因是它天然连着梯度。θ、β经过mesh生成、顶点变换、可微渲染、投影误差,整个链条都是可微的,梯度可以从2D像素一路传回姿态参数。你在做接触损失、碰撞惩罚时,也是在直接优化这个低维姿态空间,比起优化几万个顶点坐标,稳定性和收敛速度都不可同日而语。

PICO在人体初始化阶段用的是一个多任务的人体估计器,同时输出2D关键点、分割掩码和初步的θ、β。拿到初始θ、β后,把mesh渲染出来和原图做对齐,这一步主要是为了给后面的联合优化提供一个不差的起点。这里有个经验:初始化的质量决定后面一切。如果初始化姿态偏了30度,后面的接触损失会非常激进地把手臂拽向物体,最后得到一个扭曲的中间值。所以宁可初始化弱一点,也要先在2D层面保证大致的布局。

2.3 物体侧:类别模板加六自由度位姿,不追求形状精细

物体没有像SMPL那样统一的人体先验,因为物体形状千差万别。但观察HOI常见场景会发现,交互中出现的物体类别其实相对集中:椅子、桌子、滑板、球、杯子、箱子、手机、书本……这就给类别级模板方案提供了空间。对图像中的物体先做2D检测和类别分类,再在该类别的模板网格上估计一个全局位姿,也就是旋转矩阵R和平移向量t,再加上一个统一的尺度缩放s。

这里要泼一盆冷水:物体模板的形状不需要太精细。因为单目图像本身丢失了大量三维细节,杯子把手有没有、椅子扶手曲线什么样,在像素层面很难恢复。PICO用的是相对简化的模板,重点放在位姿和尺度上——换句话说,先把“这个物体在哪里、有多大、朝向什么方向”搞对,形状细节留给后续的网格细化模块去做。这个细化模块可以是一个独立的形状变形网络,输入初始位姿和图像特征,输出顶点偏移量。实验下来,对比直接预测精细形状,先粗定位再细化的方式在接触准确率上提升了接近三成,因为初始位姿对了,接触约束才能生效,细化才有正确的前置条件。

物体尺度估计是HOI最容易翻车的一环。单目图像里,一个远处的篮球和近处的网球在2D上可能一样大,前者需要的平移深度和尺度完全不同。PICO的处理是引入人体作为尺度锚点:利用已经在图像上检出的2D人体框和估计出的身体尺寸,约束物体尺度躲在一个与人类常识匹配的范围内。试想一个成年人身侧的包被重建得比人还大,一眼就不对劲。这个先验简单粗暴,但在实际项目里帮我们挡住了大量离谱结果。

2.4 遮挡推理:单目里最容易被忽视的一环

遮挡关系在单目里是隐式的,因为图像只记录了从相机视角看到的可见表面。但我们可以从图像观测中提取线索:谁在谁的轮廓边缘上切过,谁在画面上被谁盖住。

PICO的做法是把人体和物体分别渲染出深度图,对比两者在重叠区域的深度顺序,再和图像里观测到的遮挡模式做比对。如果图像里人的手遮挡了杯子的下沿,而当前重建结果里杯子深度在人前面,那就会被一个遮挡一致性损失纠正。

这个模块看着小,实际非常关键。我们做过消融实验,把遮挡项从损失里拿掉之后,联合优化比之前更容易收敛到“物体的真实位置在人的另一侧”这种自洽但错误的解。原因是接触损失和投影损失在优化过程中协同作用下,可能找到一个在2D上完美一致、但3D上是左右翻转的镜像解。有了遮挡约束,模型被迫选择与实际可见性一致的深度排序,这类错误解基本被压掉。

3. 损失函数设计:接触、穿透、遮挡怎么平衡

3.1 三大损失线:数据项、先验项、交互项

联合优化的整体目标可以拆成三组。第一组是数据一致项:人体关键点的2D重投影误差、人体掩码误差、物体2D框与掩码误差。第二组是正则项:姿态先验、形状先验、物体尺度的均匀先验。第三组是交互项:接触、穿透、遮挡。

一个典型的优化目标大致是:

L_total = λ1*L_kp2d + λ2*L_mask + λ3*L_contact + λ4*L_penetration + λ5*L_prior

用这组损失之前,要做几件事:配好每个损失的权重,确定优化顺序(先人体后物体还是联合优化),控制学习率,防止某一项梯度太大把姿态拉飞。我们实验里最常见的失败是λ4穿透惩罚开太大,直接把人的膝盖怼出椅子表面,代价是姿态扭曲成诡异角度。

权重该怎么定?我有个笨办法:先在测试图上把每一项单独加上,观察它对输出mesh的影响方向和幅度。比如单独加λ4,如果输出立刻“僵住”——所有人都不敢靠近物体——那就说明它太大了。反过来,单独加λ3如果输出里人和物体几乎贴成一体,说明接触权重过大,出现了“过度拥抱”现象。把每个单项调到“有作用但不抢戏”的状态,再组合起来微调。这个流程大概需要一到两周。

3.2 接触损失的具体实现与加权技巧

接触损失的本质是“预测哪些点对应该接近,再把它们拉近”。PICO里有一个接触映射预测头,输入人体mesh顶点特征和物体mesh顶点特征,输出一个接触置信度矩阵,表示人体顶点i与物体顶点j在交互中发生接触的概率。

有了置信度,接触损失就可以写成加权距离:

def contact_loss(verts_h, verts_o, confidence): # verts_h: [Vh, 3], verts_o: [Vo, 3] dist = torch.cdist(verts_h, verts_o) # [Vh, Vo] # 只惩罚高置信度且距离过大的点对 mask = (confidence > 0.5) & (dist > 0.03) if mask.sum() == 0: return torch.tensor(0.0, requires_grad=True) loss = (confidence * dist)[mask].mean() return loss

这段代码看着简单,但有一个非常实际的坑:如果直接用所有点对的距离均值,会被大量无关点对稀释。一个杯子和一个完整的人体之间可能有几万个点对,真正接触的点也许只占百分之几。如果不做筛选,损失会被绝大多数无关点对的零距离淹没,训练时梯度信号非常弱。

我们实验里试过两种筛选方式。一种是阈值筛选,只保留置信度大于0.5的点对参与计算,收敛稳定但初期慢,因为初始置信度普遍低;另一种是top-k采样,每次取距离最近的前k个点对参与计算,初期收敛快,因为梯度集中在最有希望的候选对上。两个方案最终效果接近,但top-k在调试初期更友好——你能直观看到“模型正在尝试抓住哪一块区域”。后期建议切回阈值加权,精度更细腻。

还有一个细节值得提:接触距离的阈值0.03米不是拍脑袋写的,它取决于mesh的顶点密度。如果人体mesh平均边长接近2厘米,那么两个mesh表面点距离小于3厘米,就可以认为已经“贴上了”。你要是用一个顶点稀疏的模板,阈值要相应放大,否则模型永远追不上目标,损耗就一直在。

3.3 穿透惩罚:法向“推出去”和SDF两种做法对比

接触是把该贴的贴上,穿透则是把不该穿的推开。判断“穿了没有”不能只看顶点距离,要看物体表面的法向。一个常见做法是,对每个穿透的人体顶点,计算物体表面最近点以及该点法向,如果人体顶点在物体内部,就沿着法向推出去,惩罚量正比于侵入深度。

我推荐用符号距离函数,也就是把物体表达成一个SDF场,查询任意一个三维点,输出它的有符号距离:正数表示在物体外部,负数表示在物体内部,绝对值是到表面的距离。穿透损失就可以非常自然地写成:

def penetration_loss(verts_h, sdf_query_fn): # 把人体顶点输入物体的SDF,返回负数表示内部 sdf_vals = sdf_query_fn(verts_h) # [Vh] inside = sdf_vals < 0 if inside.sum() == 0: return torch.tensor(0.0, requires_grad=True) loss = (-sdf_vals[inside]).square().mean() return loss

这里的SDF不需要真的训一个网络,实践中用模板mesh的最近点距离近似即可:把每个顶点投影到物体mesh表面,用射线和法向的符号判断顶点在表面哪一侧。这个近似足够用来惩罚典型的大尺度穿透,比如整只手插入杯壁、半个臀部陷入椅面。但对于指尖嵌入表面这种毫米级穿透,效果有限,建议后期换用更精确的几何碰撞库做精修。

说到底,接触和穿透天生是一对矛盾:接触项在推近,穿透项在推远。如果两个损失的权重匹配不好,模型就会在“差一点点没贴上”和“刚好贴上但深入了”之间来回震荡。我们最后采用的策略是给穿透项加一个软边际:只惩罚深入超过1厘米的部分,1厘米以内不管。这样既允许轻微的、不可见的侵入,又能压制大的几何错误,训练稳定非常多。

3.4 交替优化与联合优化的节奏控制

这是实操里绕不开的问题。联合优化同时更新人体θ、β和物体R、t、s,理论上更优雅,但梯度来源复杂,容易陷入局部最优,尤其当初始姿态偏差较大时,接触损失会把整个手臂拽到杯子上,姿态瞬间崩坏。交替优化则把问题拆成两步:固定物体优化人体,固定人体优化物体,循环若干轮。交替更稳,因为每一步的子问题更简单、梯度更干净。

PICO实测下来,初始化的那几轮必须用交替优化,等姿态和物体位姿大致合理后,再用联合优化做精调。这也是很多这类方法的通用套路,如果你自己搭方案,建议别一上来就端到端联合优化。

具体节奏我们是这样调的:前20步,固定物体,优化人体姿态,让人的手、臀、脚去够物体的相应区域;接着固定人体,优化物体位姿和尺度,让物体反过来贴合人的接触部位;如此交替5轮,每轮30步左右。粗调阶段结束后,解除固定,用联合优化再跑100步,此时接触和穿透损失的权重可以适当调高,让表面细节收敛。整张图的推理时间大约在10秒内,主要花在可微渲染和迭代优化上。

很多人以为优化步数越多越好,实际上不是。单目问题本身存在模糊性,优化的步数一旦过长,模型会开始在2D掩码上过度拟合,丢失姿态的合理性。我们发现150步以内是甜点区,超过300步后穿透深度反而上升。这个现象后来我们理解了:投影损失在整个目标里占比高,为了让渲染出来的轮廓和图像完全重合,模型倾向于把人体姿态拧成极端角度,以补偿物体遮挡带来的信息缺失。所以适当提前停止,反而是正则化的一种手段。

4. 实操过程:模型训练、推理与评测全流程

4.1 环境搭建与渲染管线选型

我们实验环境比较朴素:PyTorch为基础,渲染用Pyrender或自研的软光栅化器做可微渲染,几何处理用trimesh,额外用了一个轻量的SDF查询库处理穿透计算。显卡用的是中高端消费级卡,单张图从初始化到精调完成大概几秒到十几秒,推理时不需要非常大的显存。

提示:可微渲染是这类项目的重头。Pyrender本身不可微,我们一般接一个soft rasterizer作为前向渲染器,或者用PyTorch3D的可微渲染管道。如果你只想验证损失设计,先用物理渲染器渲染出固定视角的图,单独观察mesh形状是否合理,再把渲染接到优化循环里。

环境配置有一个容易踩的坑:可微渲染库对CUDA版本和PyTorch版本的绑定非常严格,版本不匹配会直接编译失败。我的建议是锁定一套已经互相验证过的版本组合,不要在有多个显卡驱动的工作站上随便升级。我们团队有同事因为在一个版本杂乱的集群上装PyTorch3D,前后折腾了两天,最后在容器环境里才跑通。这个问题虽然看起来和算法无关,但实际会吃掉大量项目时间。

4.2 合成数据准备:让接触标注免费

HOI的真实标注数据非常贵。手动标注一个三维接触关系,比标2D框要难得多,所以在项目里,我们主要依赖两条腿:一部分是在公开的人-物交互数据集上训练,另一部分是自制的渲染合成数据——把人体mesh和物体mesh摆成接触姿态,用随机材质、随机光照、随机相机角度渲染出一批图像,同时天然带有精确的接触标注和深度标注。合成数据解决标注问题,真实数据解决域差异问题。

做合成数据的细节值得说一句:随机摆放时,要刻意让一部分样本存在轻微穿透,一部分样本存在轻微分离,否则模型只见过完美接触的样本,一旦真实图像里接触点略微错位,接触推理就不稳。这其实是一个数据增强思想——人为引入一定范围的接触误差,让模型学会在微扰下仍然输出正确的接触区域。我们最多的时候把一个交互动作渲染出200组不同视角、光照和背景的图像,模型的泛化能力提升非常明显。

在生成接触标注时要注意标注的粒度。接触不是一个二值属性,而是有区域的:手的整个掌面可能接触杯子,但只有指腹接触杯把。我们的标注做法是给每个接触事件记录一个接触面积,用于训练接触置信度的连续值版本,而不是简单分类。这样接触头输出的置信度矩阵就有了平滑的梯度,训练起来比0/1标签稳定得多。

4.3 接触头的训练与端到端微调

训练分成两个阶段。阶段一训练接触映射预测头:输入人体mesh顶点特征和物体点云特征,输出接触置信度,用渲染合成数据的接触标注做监督。阶段二冻结接触头的部分层,端到端微调整个重建模型,让接触信息反过来影响人体姿态和物体位姿的预测。

一个关键点是接触头的输入特征不只是几何坐标,还包含2D图像上对应位置的颜色特征。为什么?因为单靠几何无法区分“手在杯子前面还是后面”,而颜色和纹理能提供遮挡线索——杯子遮挡手的那段边缘在图像上有清晰的轮廓特征。把2D特征对齐到3D顶点上,再输入接触头,是一个很有效的小技巧。具体做法是把图像放进一个浅层CNN,得到每像素的特征向量,然后根据相机投影,把人体顶点和物体顶点分别投到图像上,采样对应位置的特征,拼接在坐标特征后面。实测这个操作能让接触预测准确率提升近10个百分点。

训练时损失函数除了常规的交叉熵或MSE,还要加一个空间平滑项。因为接触区域在mesh表面是成片出现的,如果不对置信度做平滑,输出往往像斑点一样零散,优化时梯度杂乱。加入一个简单的拉普拉斯平滑项后,接触区域变成连贯的块状,后续的接触损失和穿透惩罚也能更稳定地作用。

4.4 推理流程、参数配置与评测指标

推理时输入一张图,流程是:检测人体和物体2D框 → 独立估计人体姿态和物体位姿 → 接触推理 → 联合优化若干轮 → 输出人体mesh和物体mesh,附带接触标注。

参数配置我整理了一个基线,供参考:

  • 人体初始化学习率:1e-3,联合优化阶段降到5e-4
  • 物体位姿学习率:3e-3,物体尺度学习率:5e-4
  • 接触权重λ3:0.8,穿透权重λ4:1.2
  • 优化轮次:交替阶段5轮,联合阶段100步
  • 掩码损失用L1而不是L2,边界敏感度更高

评测不能只看肉眼效果。我们常用的指标有四个:

指标计算方式说明
Chamfer距离重建mesh与真值mesh顶点间双向最近点距离均值衡量几何精度
掩码IoU重建mesh投影到2D与真值掩码的IoU衡量投影一致性
接触准确率预测接触对的精度/召回衡量交互语义
穿透深度人体顶点侵入物体内部的平均深度衡量物理合理性

评测集要额外标注接触真值,这个环节我们花了一周多,最后发现最好的办法是找一批置信度高的合成测试图加人工抽检真实图,兼顾自动化和可信度。人工抽检时我们总结了一套规则:先看投影掩码是否贴合,再看身体四肢是否出现反向折叠,最后看手和物体的关系——手是悬空还是接触,接触位置是否符合物体类别常识。这套规则后来被写成了团队的评测checklist,新来的同学上手也快。

5. 常见问题与排查实录

5.1 关键点被遮挡,姿态初始化崩坏

把真实图片喂进去,最常遇到的问题就是2D关键点检测在遮挡处失效,尤其是“人坐椅子”这种场景,臀部和膝盖有大半被椅面挡住,检测器给出的置信度低,初始化姿态直接跑偏。

排查思路:先单独跑人体姿态估计,把2D关键点和投影后的3D骨架可视化,确认哪几个关节置信度低。如果是遮挡导致的,可以调整优化权重,把低置信度关键点的监督权重降下来,同时增加姿态先验的权重,让模型更多依赖先验而不是被带偏的2D观测。如果实在不行,有个土办法:用检测器输出多个候选姿态假设,每个假设各自走一遍联合优化,最后选投影误差最小的结果。有点费算力,但确实能救回一些难例。

我们在PICO里还加了一个针对性的技巧:用一个轻量的遮挡分割网络,先标出图像里“什么东西挡住了人体的哪部分”,然后在优化时把被遮挡区域的掩码损失权重直接降到接近0。这么做的好处是,模型不会为了拟合被遮挡部分的轮廓而扭曲姿态,它只需要保证可见部分的投影一致,其余由姿态先验和接触约束兜底。

5.2 物体尺度来回跳,越调越离谱

联合优化时,物体尺度s和平移t经常互相补偿:尺度变大一点,平移朝远处挪一点,投影出来的2D大小不变,但3D结果变成一个“远处的大物体”。这是单目尺度-深度耦合的典型症状,也叫scale-depth ambiguity。

我们的解决办法是加了一个人体尺度锚定项。具体做法:根据当前重建的人体mesh,计算出关键的尺寸,比如肩宽和手臂长,作为参照系。然后把物体的尺度限制在与参照系合理匹配的区间内。比如一个人肩膀宽40厘米,他手边的物体横跨尺寸超过1米,那基本可以判定尺度预测错了,直接把这个样本判为低置信度,或者把物体尺度拉回区间内。

再不行就在优化里固定物体尺度,只优化位姿,等人体姿态稳定后再放开尺度。这种“放弃一部分自由度换取稳定性”的思路,在单目任务里是值得反复使用的。

还有一个容易忽略的点:物体检测的2D框不准,会直接传导到尺度估计。我们曾经在测试集上一半物体的尺度过大,最后排查发现是检测器对部分遮挡物体的边界框偏向宽松,导致初始尺度偏大。后来在初始化时对物体框做了一次边缘紧缩——比例大约3%——尺度误差立刻降下来。这种问题不看数据分布,光调损失是无法发现的。

5.3 接触部位张冠李戴,手里没抓到该抓的地方

眼看接触项在起作用,人和物体也不再穿透,但仔细看,手没放在杯柄上,而是放在杯口上——接触置信度矩阵给出的语义区域完全错位。这通常是接触头的训练数据有问题,合成数据里“手抓杯子”的接触区域太单一,模型只记住了抓杯口这一类模式。

对策是扩大合成数据的接触多样性:同一个杯子,让手从侧面、上方、斜下方都去抓一遍;同一个姿态,换不同形状的物体。我们还试过在接触置信度上加入类别约束,比如属于“握柄类”的物体,优先把手部顶点映射到手接触区域。效果有,但有限,核心还是数据多样性。

调试这类问题,最快的方法是直接把接触置信度矩阵可视化,按置信度值给mesh顶点上色,看预测的“热点”到底分布在哪些区域。有一次我们发现,热点集中在大腿前侧,而真值接触区在臀部下方,原因就出在合成数据里坐姿样本生成时,人体和椅面的接触采样是从mesh全局最近点出发的,最近点落在大腿前侧而不是臀部。修正采样逻辑之后,这个问题就消失了。“可视化接触热点”这一招我们后来一直在用,凡是接触相关的bug,先上色看图,再找原因。

5.4 损失一直降,但渲染出来还是糊成一团

还有一种情况是损失一直在降,但mesh投影出去和原图对不上,说明优化陷入了某个自洽但不合观测的局部最优。这时候先检查渲染的相机参数和检测框是否对得上,再检查掩码损失是否真的在起作用。

很多次问题出在一个小地方:掩码损失用的是L2,对边缘极其不敏感。一个只差两个像素的轮廓和一个完全对不上的轮廓,L2值差别不大,梯度也差不多,模型学不到“往边上挪一点”的信号。换成一个边缘加权的距离项,或者干脆只用轮廓边界像素计算距离,效果会好非常多。

另外,相机内参错误是一个隐蔽的大坑。如果你的初始化代码里把焦距估计错了10%,投影的物体轮廓会比实际偏大或偏小,联合优化会努力用“错误尺度+错误深度”去补偿,最后收敛到一个数值上合理但视觉上完全不对的结果。所以每次跑新数据集,先把一张已知真实深度分布的测试图走完整个流程,确认相机内参对齐,再开始大规模评测。这个检查只需要几分钟,能省掉后面一整天的返工。

5.5 一个容易被忽视的工程问题:mesh顶点数量不一致

接触损失和穿透损失都要计算人体mesh顶点与物体mesh顶点之间的关系,而人体模板和物体模板的顶点数、面片密度往往差异巨大。比如人体模板有2万多个顶点,一个简化的椅子模板可能只有2000个顶点。直接计算点对距离时,人体侧的点密度远高于物体侧,导致接触区域“人体一个点对物体多个点”的情况,梯度传播时会偏向人体侧。

我们的解决办法是对物体mesh做一次自适应细分,让两个mesh在接触区域的顶点密度大致匹配。更规范的做法是用最近邻双向匹配,并给每个点对乘上一个面积加权因子。如果你只是自己搭一个快速demo,最简单的办法是先把两个mesh都采样成相同数量的点云,再做后续计算。这个细节不解决,接触损失会一直抖动,而且越训越差,表现非常迷惑。

6. 一点个人体会

跑这种项目最大的体会是:真正耗时的不是网络结构,而是数据和调试。HOI的合成数据生成看起来简单,实际涉及接触采样、姿态合理过滤、遮挡多样性、域随机化,每一步都要反复试;损失函数那几项权重更是玄学,我们调了一个多月才找到相对稳的组合。

如果让我给后来人一个建议,那就是别急着上完整方案,先把最小闭环跑通:单张简单图,一个固定类别物体,一个接触损失,一把椅子,先让它在不穿透的前提下把接触做对。把这一步做实了,再往复杂场景、多类别、多样交互扩展,路会顺很多。这个项目我们后续还在往视频序列上延伸,加上时间一致性之后,很多单目里的深度歧义会被自然消解,那是另一个有趣的故事了。

RELATED READING

延伸阅读

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