Skip to content

Ch16 | 多模态动作获取:文生动作与视频学习

本章定位:Part IV(单形态实战)第四章。Ch15 建立了 motion tracking 的完整工程能力——给定参考动作,策略可以忠实复现。但参考动作从哪来?本章回答这个问题:通过文本描述("walk forward slowly")、视频(YouTube 上的舞蹈)、或单 RGB 相机实时遥操作获取参考动作。这是从"人工提供参考"到"自动获取参考"的关键跨越。

参考:✅ ProtoMotions(CALM/MaskedMimic)· ✅ TextOp(arXiv 2602.07439 预印本)· ✅ HumanPlus(CoRL'24)· ✅ HDMI(CMU)· ✅ WholeBodyVLA(ICLR'26 Poster)/ LeVERB(投稿 ICLR'26,审稿中)

机器人:G1/H1 · 累积项目C(续)


前置自测

📋 答不出 \(\ge\) 3 题 → 先回前置章节复习

本章直接依赖 Ch15 的 motion tracking 经验。如果你还没有在 Ch15 中跑通 BeyondMimic tracking task 和 ProtoMotions AMP,强烈建议先完成。

  1. [Ch15] BeyondMimic 的 body_position_tracking reward 为什么要在 base frame 下计算,而不是 world frame?
  2. [Ch15] AMP 判别器与直接跟踪(Mimic)的核心区别是什么?AMP 的 gradient penalty 的作用是什么?
  3. [Ch15] ProtoMotions 中从 AMP 切换到 CALM 需要修改哪些配置?约几行差异?
  4. [Ch14] G1 的 per-joint action scale 公式是什么?为什么不能统一设置?
  5. [NLP] CLIP 模型的 text encoder 输出什么格式的向量?维度通常是多少?

本章目标

学完本章后,你应该能够:

  1. 用 CALM 实现文本条件控制:在 ProtoMotions 中配置 CALM,用自然语言 prompt 控制 G1 的动作风格
  2. 理解 MaskedMimic 的 motion inpainting 思想:解释为什么部分约束(文本/关键帧/目标位置)可以补全完整动作
  3. 用 TextOp 实现实时文生动作管线:理解 RobotMDAR + Tracker 的两层架构,从文本到真机全流程
  4. 精读 HumanPlus 的 Shadowing Transformer:理解如何从单 RGB 相机实时控制人形机器人
  5. 精读 HDMI 的视频 → loco-manipulation 管线:理解如何从单目视频中同时提取人体和物体轨迹
  6. 在三种动作来源(MoCap/视频/文本)之间做出工程选型

Ch15 的 BeyondMimic 和 AMP 解决了"如何跟踪参考动作"的问题。但到目前为止,参考动作的来源要么是高质量 MoCap 数据(AMASS),要么是手动准备的 CSV 文件。本章探索三种更灵活的来源:文本描述、视频和实时遥操作。

16.1 算法回顾:从 MoCap 到文本/视频 ⭐⭐

这一节解决什么问题:建立动作获取的技术谱系——MoCap、文本生成、视频估计的优劣对比和技术基础。

动作来源的三个时代

时代 动作来源 代表方法 精度 成本 灵活性
MoCap 时代 光学/惯性 MoCap AMASS, Vicon 极高 (<1mm) 极高(设备 + 场地) 低(需要演员穿戴)
视频时代 单目/多目 RGB 4DHumans, WHAM, HybrIK 中等 (~5cm) 低(只需相机) 高(任何视频)
文本时代 自然语言描述 MDM, T2M-GPT, CALM 低 (~10cm+) 极低(只需打字) 极高(想象 = 输入)

跨领域类比:这三个时代就像绘画的三种方式。MoCap 是"照相写实"——精确但昂贵且受限于现场。视频是"写生"——看着真实场景画,精度中等但便宜灵活。文本是"命题创作"——只给一个题目("画一个人在雨中跑步"),画出来的内容取决于画家(模型)的能力,最灵活但最不可控。

Motion Diffusion Model (MDM) 核心思想

MDM(Tevet et al., ICLR 2023)是文生动作的里程碑。它把动作序列看作一种"图像"——在时间维度和关节维度上都有结构——然后用扩散模型从噪声中去噪生成:

\[q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t} x_{t-1}, \beta_t I) \quad \text{(前向过程:加噪)}\]
\[p_\theta(x_{t-1} | x_t, c) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t, c), \Sigma_\theta(x_t, t, c)) \quad \text{(反向过程:去噪,条件 c=文本)}\]

其中 \(x_0\) 是一段动作序列(\(T\)\(\times\) \(J\) 关节),\(c\) 是 CLIP text embedding。

工程含义:MDM 生成的是运动学轨迹(关节角度随时间变化),不是控制信号。这个轨迹不保证物理可行——可能包含脚在地面以下、质心超出支撑多边形等问题。必须经过 Ch15 的 tracking policy 转化为物理可行的控制信号。

这就是为什么 Ch15 是 Ch16 的前置——Ch15 的 tracker 是 Ch16 所有方法的物理执行层

CLIP Text Embedding → Motion Latent 映射

文生动作的核心是把文本语义映射到运动空间。CLIP(Radford et al., 2021)提供了一个预训练的 text encoder,把自然语言描述映射到 512 维向量:

import clip
model, preprocess = clip.load("ViT-B/32")
text = clip.tokenize(["a person walks forward slowly"])
text_embedding = model.encode_text(text)  # (1, 512)

这个 512 维向量包含了文本的语义信息——"walks" 对应运动类型,"forward" 对应方向,"slowly" 对应速度。模型需要学习从这个语义空间到运动空间的映射。

反事实推理:如果不用 CLIP 而是训练一个专用的 text encoder 会怎样? CLIP 的优势是在约 4 亿(400M)图文对上预训练,对各种自然语言描述有泛化能力。专用 text encoder 只在动作-文本配对数据上训练——数据量有限(BABEL 为约 43 小时 AMASS 提供动作标注,含 28k+ 序列级标签和 63k+ 帧级标签),泛化能力差。对于"a person moonwalks"这种训练集中没有的描述,CLIP 可能还有一定理解能力,专用 encoder 几乎肯定失败。

视频姿态估计技术栈

从视频中获取动作需要"人体姿态估计"——从 RGB 像素中恢复人体的 3D 关节位置和朝向。主要工具:

工具 输入 输出 精度 速度 特点
4D-Humans 单目 RGB 视频 SMPL body mesh + 跟踪 ~5cm ~15 FPS 全身 SMPL body(不含手部)
WHAM 单目 RGB 视频 SMPL body + global translation ~4cm ~20 FPS 全局平移更准
HybrIK 单帧 RGB 图像 SMPL pose/shape(混合解析-神经 IK) ~5cm ~30 FPS 由 3D 关节解算 SMPL 旋转
HaMeR 单帧 RGB 图像 MANO hand ~2cm ~25 FPS 专门做手部

HumanPlus 的选择:WHAM(body)+ HaMeR(hands)→ SMPL-X → retarget 到 H1。这个组合在全身 + 手部的精度和速度之间取得了好的平衡。

工程陷阱:所有视频姿态估计都有遮挡问题——当人体被物体或自身部位遮挡时,估计精度急剧下降。KungfuBot(Ch15.5)的 physics filter 就是为了过滤这些不准确的帧。

VQ-VAE Motion Tokenization

VQ-VAE(Vector Quantized Variational Autoencoder)把连续的运动序列离散化为 token 序列——类似于 NLP 中把句子切分为 token。

VQ-VAE 的三个组件

# VQ-VAE motion tokenizer(概念性实现)
class MotionVQVAE(nn.Module):
    def __init__(self, motion_dim=263, latent_dim=512, codebook_size=512):
        super().__init__()
        # 1. Encoder: 连续动作 → 连续 latent
        self.encoder = nn.Sequential(
            nn.Conv1d(motion_dim, 512, kernel_size=4, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv1d(512, latent_dim, kernel_size=4, stride=2, padding=1),
        )

        # 2. Codebook: K 个离散向量
        self.codebook = nn.Embedding(codebook_size, latent_dim)

        # 3. Decoder: 离散 token → 重建动作
        self.decoder = nn.Sequential(
            nn.ConvTranspose1d(latent_dim, 512, kernel_size=4, stride=2, padding=1),
            nn.ReLU(),
            nn.ConvTranspose1d(512, motion_dim, kernel_size=4, stride=2, padding=1),
        )

    def quantize(self, z_e):
        """找到 codebook 中最近的向量"""
        # z_e: (B, latent_dim, T')
        # codebook: (K, latent_dim)
        distances = torch.cdist(
            z_e.permute(0, 2, 1),        # (B, T', latent_dim)
            self.codebook.weight.unsqueeze(0)  # (1, K, latent_dim)
        )
        indices = distances.argmin(dim=-1)  # (B, T') — token indices
        z_q = self.codebook(indices).permute(0, 2, 1)  # (B, latent_dim, T')
        return z_q, indices

    def forward(self, motion):
        """
        motion: (B, motion_dim, T) — 原始动作序列
        """
        z_e = self.encoder(motion)          # (B, latent_dim, T')
        z_q, indices = self.quantize(z_e)   # 量化到离散 token
        motion_recon = self.decoder(z_q)    # 重建

        # Loss: 重建 + codebook alignment + commitment
        recon_loss = F.mse_loss(motion_recon, motion)
        codebook_loss = F.mse_loss(z_q.detach(), z_e) + 0.25 * F.mse_loss(z_q, z_e.detach())

        return motion_recon, indices, recon_loss + codebook_loss

工程含义:token 化后可以用 NLP 的自回归模型(GPT 风格)来生成动作——输入是 text token 序列,输出是 motion token 序列,然后通过 VQ-VAE decoder 恢复为连续动作。T2M-GPT 使用了这种"离散 motion token + GPT"的范式。(注意:TextOp 的高层生成器用的是自回归 motion diffusion 模型,属于另一类范式,详见 16.4。)

codebook_size 的选择:太小(如 64)→ 动作种类被过度压缩,重建质量差。太大(如 4096)→ 很多 token 使用率极低,浪费容量。512-1024 是文献中的常见选择。

从 MDM 到机器人控制的完整链路

文本描述: "walk forward slowly"
    ↓ CLIP text encoder
文本向量: (512,)
    ↓ MDM / T2M-GPT / TextOp RobotMDAR
运动学轨迹: (T, J) — 关节角度序列
    ↓ ★ 关键步骤: 物理可行性检查 (Ch15.5 physics filter)
过滤后轨迹: (T', J)
    ↓ ★ 关键步骤: tracking policy (Ch15.1 BeyondMimic)
关节控制信号: 50Hz joint position targets
    ↓ PD 控制器
关节力矩: → 机器人执行

两个 ★ 步骤是 Ch15 和 Ch16 的连接点。没有 Ch15 的 tracker,文生动作只是一段好看的动画——无法在物理世界中执行。这就是为什么本章是 Ch15 的后续而不是独立章节。

视频姿态估计的工程管线

以 WHAM 为例,从安装到输出的完整流程:

# 安装 WHAM
git clone https://github.com/yohanshin/WHAM.git
cd WHAM
pip install -e .

# 下载预训练模型
bash fetch_demo_data.sh

# 运行姿态估计
python demo.py \
    --video_path input_video.mp4 \
    --output_dir results/ \
    --save_pkl \
    --render                    # 可选:渲染叠加可视化

# 输出文件结构
# results/
# ├── wham_output.pkl          # SMPL 参数
# ├── rendered_video.mp4       # 叠加可视化
# └── poses/                   # 逐帧 3D 关节位置

WHAM 输出的 pkl 结构

import pickle
with open('results/wham_output.pkl', 'rb') as f:
    data = pickle.load(f)

# data 包含:
# 'body_pose':    (T, 69)  — 23 个 SMPL body joint × 3 axis-angle
# 'global_orient': (T, 3)  — root 朝向 (axis-angle)
# 'transl':       (T, 3)   — root 全局平移
# 'betas':        (10,)    — 体形参数
# 'pred_cam':     (T, 3)   — 预测的相机参数

WHAM vs 4DHumans 的选择:WHAM 的 transl 字段提供了全局平移估计——这对机器人控制至关重要(机器人需要知道"走了多远",不只是"关节怎么弯")。4DHumans 的全局平移估计较弱。如果你的任务需要全局位移(如行走、跑步),用 WHAM。如果只需要局部姿态(如站立手势),4DHumans 和 WHAM 都可以。

⚠️ 常见陷阱

⚠️ 编程陷阱:CLIP text embedding 维度不匹配。错误做法:假设所有 CLIP 模型输出 512 维。实际上 ViT-B/32 输出 512 维,ViT-L/14 输出 768 维。模型和维度必须配对,否则 downstream 网络的输入维度不匹配。正确做法:在配置中明确指定 CLIP model name 和对应的 embedding dim。

💡 概念误区:文生动作生成的动作直接可用。MDM/T2M-GPT 生成的是运动学轨迹,不保证物理可行。必须经过 tracking policy(Ch15)转化为物理控制信号。直接把运动学轨迹发送给关节 PD 控制器——如果轨迹要求脚在空中悬停(物理不可行),PD 控制器会产生巨大力矩,机器人猛烈振荡。

🧠 思维陷阱:视频估计精度 ~5cm 够用了。5cm 的平均误差看起来不大,但某些关键帧(如接触瞬间、转向起始)的误差可能远大于平均值。这些关键帧的误差会导致 tracking policy 在物理上不可行——Ch15.5 的 physics filter 就是为了解决这个问题。

SMPL 表示基础

几乎所有视频姿态估计和动作生成方法都使用 SMPL(Skinned Multi-Person Linear model)作为中间表示。理解 SMPL 的参数结构对于后续所有 retarget 工作至关重要。

SMPL 参数(详见附录 G):

参数 维度 含义 用途
betas (10,) 体形参数 控制身高、胖瘦,retarget 时用于骨骼缩放
global_orient (3,) root 朝向 axis-angle 表示,需要转换到机器人坐标系
body_pose (69,) 23 关节 \(\times\) 3 axis-angle 核心运动参数,需要 IK 转为机器人关节角
transl (3,) root 位置 全局平移,需要身高比缩放

关键工程概念:SMPL 的每个关节是 3-DoF ball joint(用 axis-angle 表示),但 G1 的每个关节是 1-DoF hinge joint(用单个角度表示)。这意味着 SMPL 的 \(23\times3=69\) 维关节空间不能直接映射到 G1 的 29 维——需要通过 IK(逆运动学) 求解。TextOp 的一个关键创新就是绕过了 SMPL,直接在 G1 的 29 维空间中训练(Robot-Skeleton 表示),避免了 retarget 误差。

WHAM 输出处理示例

从 WHAM 输出到可用的关节数据,需要以下处理:

# WHAM 输出处理(实际可运行代码片段)
import numpy as np
import pickle

# 加载 WHAM 输出
with open('results/wham_output.pkl', 'rb') as f:
    wham_data = pickle.load(f)

print("WHAM 输出结构:")
for key, val in wham_data.items():
    if isinstance(val, np.ndarray):
        print(f"  {key}: shape={val.shape}, dtype={val.dtype}")

# 典型输出:
#   body_pose: shape=(300, 69), dtype=float32   — 10s × 30fps = 300 帧
#   global_orient: shape=(300, 3), dtype=float32
#   transl: shape=(300, 3), dtype=float32
#   betas: shape=(10,), dtype=float32

# 基本信息提取
T = wham_data['body_pose'].shape[0]
fps = 30  # WHAM 输出帧率 = 视频帧率
duration = T / fps
print(f"动作长度: {duration:.1f}s, 帧数: {T}, 帧率: {fps}Hz")

# 检查全局平移范围
transl = wham_data['transl']
print(f"移动范围: x=[{transl[:,0].min():.2f}, {transl[:,0].max():.2f}], "
      f"y=[{transl[:,1].min():.2f}, {transl[:,1].max():.2f}], "
      f"z=[{transl[:,2].min():.2f}, {transl[:,2].max():.2f}]")

本质洞察:本章所有方法——无论是文本生成(CALM/TextOp)、视频估计(WHAM/HDMI)还是实时遥操(HumanPlus)——都可以看作"参考动作来源"的不同实例。它们的输出最终都要经过 Ch15 的 tracking policy 转化为物理可行的关节控制信号。区别只在于获取参考动作的方式和精度——MoCap 最精确但最昂贵,文本最灵活但最不精确,视频居中。理解了这个统一视角,你就能在任何新方法出现时迅速定位它在管线中的位置。

练习

  1. [概念题] 解释 MoCap、视频估计和文本生成三种动作来源的精度-成本 trade-off。在什么场景下你会选择每一种?
  2. [计算题] CLIP ViT-B/32 的 text embedding 是 512 维。如果你要把它映射到一个 150 帧 \(\times\) 29 关节的动作序列,中间需要什么结构?估算参数量。
  3. [跨章综合题,Ch15+Ch16] Ch15 的 BeyondMimic tracking policy 的 obs 中有 ~60 维的 motion reference terms。如果参考动作来自 MDM(而不是 MoCap),这些 reference terms 的质量会如何变化?对 tracking 效果有什么影响?

Ch15.2 已经介绍了 ProtoMotions 中 AMP → ASE → CALM 的配置切换。本节深入 CALM 的工程实现——如何把文本描述注入判别器,让策略生成符合文字描述的动作。

16.2 CALM 文本条件控制 ⭐⭐⭐

这一节解决什么问题:精读 CALM 的工程实现——text embedding 如何注入判别器,以及如何在 ProtoMotions 中完成从 AMP 到 CALM 的切换。

CALM 的核心思想

回顾 Ch15.2 的 AMP:判别器区分"策略动作"和"参考动作",但不关心哪种参考动作。AMP 学到的是"像参考数据集中的某个动作"——但你无法指定"像走路"还是"像跑步"。

CALM(Conditional Adversarial Latent Models,Tessler et al., SIGGRAPH 2023)在 AMP 的基础上加入条件——判别器不仅区分真/假,还检查动作是否匹配文字描述

\[D_{CALM}(s, s', c) = \text{判断 state transition (s, s') 是否匹配文字描述 c}\]

CALM 继承 AMP 的 LSGAN(最小二乘)判别器\(D_{CALM}\) 输出回归值(参考且匹配 \(c\) 时回归到 \(+1\),否则 \(-1\)),风格 reward 沿用 AMP 的 LSGAN 形式:

\[r_{CALM} = \max\!\left[0,\; 1 - 0.25\,\big(D_{CALM}(s, s', c) - 1\big)^2\right]\]

从 AMP 到 CALM 的工程差异

Ch15.2 已经给出了配置差异的概览。本节深入实现细节:

AMP 的判别器(无条件):

class AMPDiscriminator(nn.Module):
    def __init__(self, obs_dim, hidden_dims=(1024, 512)):
        super().__init__()
        input_dim = obs_dim * 2  # [s_t, s_{t+1}] 拼接
        self.net = build_mlp(input_dim, hidden_dims, output_dim=1)

    def forward(self, obs, next_obs):
        x = torch.cat([obs, next_obs], dim=-1)
        return self.net(x)

CALM 的判别器(条件——text embedding 注入):

class ConditionalAMPDiscriminator(nn.Module):
    def __init__(self, obs_dim, condition_dim=512, hidden_dims=(1024, 512)):
        super().__init__()
        # 输入多了 condition_dim 维
        input_dim = obs_dim * 2 + condition_dim
        self.net = build_mlp(input_dim, hidden_dims, output_dim=1)

    def forward(self, obs, next_obs, text_embedding):
        # 关键:text_embedding 与状态拼接
        x = torch.cat([obs, next_obs, text_embedding], dim=-1)
        return self.net(x)

差异只在 input_dim 多了 condition_dim,以及 forward 多了 text_embedding 参数。

Text embedding 的来源

class CLIPTextEncoder(nn.Module):
    def __init__(self, model_name="ViT-B/32"):
        super().__init__()
        self.model, _ = clip.load(model_name, device="cuda")
        self.model.eval()
        for param in self.model.parameters():
            param.requires_grad = False  # 冻结 CLIP

    def forward(self, text_list):
        tokens = clip.tokenize(text_list).to("cuda")
        with torch.no_grad():
            embedding = self.model.encode_text(tokens)  # (B, 512)
        return embedding.float()

CLIP encoder 是冻结的——不在 CALM 训练过程中更新。这保证了 text embedding 的语义一致性。如果 CLIP 被 fine-tune,文本语义可能在训练过程中漂移,导致判别器学到的条件映射失效。

Gradient Penalty 的实现

Ch15 已经提到 gradient penalty 是 AMP 训练稳定的关键。在 CALM 中,gradient penalty 同样重要——条件判别器如果过度自信会导致 reward 二值化:

# CALM gradient penalty 实现
def compute_gradient_penalty(discriminator, real_obs, real_next_obs, text_emb):
    """计算 WGAN-GP 风格的 gradient penalty"""
    real_obs.requires_grad_(True)
    real_next_obs.requires_grad_(True)

    # 判别器对真实数据的输出
    d_real = discriminator(real_obs, real_next_obs, text_emb)

    # 对输入计算梯度
    grad_obs = torch.autograd.grad(
        outputs=d_real,
        inputs=[real_obs, real_next_obs],
        grad_outputs=torch.ones_like(d_real),
        create_graph=True,
        retain_graph=True,
    )

    # 梯度范数
    grad_norm = sum(g.reshape(g.size(0), -1).norm(dim=1) for g in grad_obs)

    # 惩罚偏离 1 的梯度
    penalty = ((grad_norm - 1.0) ** 2).mean()

    return penalty

gradient_penalty_weight = 5.0 的含义:判别器的总 loss = GAN loss + 5.0 \(\times\) gradient penalty。较大的 penalty weight 让判别器更"保守"——它的输出更平滑,策略获得的 reward 信号更连续。如果 penalty weight 太小(如 0.1),判别器容易过拟合到训练数据,对策略新生成的动作给出极端的判断。

CALM 的完整训练 Loss

# CALM 训练的 loss 组成
def calm_training_step(agent, batch):
    """CALM 的一步训练"""
    obs = batch['obs']
    next_obs = batch['next_obs']
    actions = batch['actions']
    text_labels = batch['text_labels']

    # === 1. PPO Loss(策略优化) ===
    # 与 Ch15 的 PPO 完全相同
    ppo_loss = compute_ppo_loss(agent.actor, agent.critic, batch)

    # === 2. 判别器 Loss ===
    # 2a. 对真实数据(AMASS motions + 对应 text)
    text_emb = agent.text_encoder(text_labels)
    real_obs, real_next_obs = sample_real_transitions(batch['motion_data'])
    d_real = agent.discriminator(real_obs, real_next_obs, text_emb)

    # 2b. 对策略生成的数据
    d_fake = agent.discriminator(obs, next_obs, text_emb)

    # 2c. LSGAN(最小二乘)判别器 loss——AMP/CALM 用回归目标:参考→+1,策略→-1
    disc_loss = 0.5 * torch.mean((d_real - 1.0) ** 2) \
              + 0.5 * torch.mean((d_fake + 1.0) ** 2)

    # 2d. Gradient penalty
    gp = compute_gradient_penalty(agent.discriminator, real_obs, real_next_obs, text_emb)

    disc_total = disc_loss + 5.0 * gp

    # === 3. AMP/CALM 风格 Reward(LSGAN 形式,给 PPO 使用的额外 reward) ===
    with torch.no_grad():
        amp_reward = torch.clamp(1.0 - 0.25 * (d_fake - 1.0) ** 2, min=0.0)

    # === 4. Encoder Loss(ASE/CALM 特有) ===
    if hasattr(agent, 'encoder'):
        latent = agent.encoder(obs, text_emb)
        encoder_loss = compute_encoder_loss(latent, batch)

    return ppo_loss, disc_total, amp_reward

CALM 训练监控

Tensorboard Panel 健康趋势 异常信号 对应动作
disc/loss 稳定在 0.5-1.5 降到 ~0 判别器过度自信 → 增大 gp_weight
disc/accuracy 稳定在 50-70% >90% 判别器过拟合 → 增大 gp_weight
amp_reward 缓慢上升 不涨或下降 策略没学到匹配风格
policy/entropy 缓慢下降 快速降到 0 entropy 坍塌 → 加 entropy bonus
text_diversity 多样的 prompt 集中在少数 训练数据标注不均衡

关键监控disc/accuracy 应该在 50-70%。如果 >90%,判别器太强了——策略获得的 reward 信号太弱。如果 <50%,判别器太弱——策略没有受到有效的风格约束。

ProtoMotions 中的 CALM 配置

# examples/experiments/calm/mlp.yaml
agent:
  _target_: protomotions.agents.calm.CALM

  # 条件判别器
  discriminator:
    _target_: protomotions.modules.ConditionalAMPDiscriminator
    hidden_dims: [1024, 512]
    gradient_penalty_weight: 5.0
    discriminator_weight: 1.0
    condition_dim: 512  # CLIP ViT-B/32

  # 条件 skill encoder
  encoder:
    _target_: protomotions.modules.ConditionalSkillEncoder
    latent_dim: 64
    hidden_dims: [512, 256]
    condition_dim: 512
  encoder_weight: 1.0

  # CLIP text encoder
  text_encoder:
    _target_: protomotions.modules.CLIPTextEncoder
    model_name: "ViT-B/32"

  learning_rate: 5.0e-4
  gamma: 0.99

训练数据准备:Motion + Text 配对

CALM 需要动作和文字描述的配对数据。AMASS 本身没有文字标注——需要额外的标注数据集:

数据集 动作数 文字标注 适用
BABEL ~43h AMASS(28k+ 序列标签 / 63k+ 帧标签) 自然语言动作描述 文本-动作标注
TEACH ~3K 标注 时间对齐的文字-动作对 高质量对齐
HumanML3D ~14K 每个动作 3 段文字描述 MDM/T2M-GPT 训练
KIT-ML ~3K 英文动作描述 辅助数据

Motion YAML manifest 中的 text 字段(Ch15.2 已提到):

motions:
  - file: data/amass/walking_slow.npy
    fps: 30
    text: "A person walks forward slowly with relaxed arms"
  - file: data/amass/running_fast.npy
    fps: 30
    text: "A person runs forward quickly with swinging arms"
  - file: data/amass/jumping.npy
    fps: 30
    text: "A person jumps in place"

AMP 忽略 text 字段,CALM 使用它。这种设计让同一个 manifest 文件可以被所有算法复用。

CALM 训练流程

# Step 1: 准备带文字标注的 motion 数据
# 使用 BABEL/TEACH 标注的 AMASS 子集
python scripts/prepare_calm_data.py \
    --amass_dir data/amass/ \
    --babel_dir data/babel/ \
    --output data/calm_dataset.yaml

# Step 2: CALM 训练
python protomotions/train_agent.py \
    +exp=calm_mlp \
    +robot=g1 \
    +simulator=isaacgym \
    motion_file=data/calm_dataset.yaml \
    num_envs=4096 \
    max_iterations=15000 \
    experiment_name=g1_calm

# Step 3: 文本条件评估
python protomotions/eval_agent.py \
    +robot=g1 +simulator=isaacgym \
    checkpoint=results/g1_calm/last.ckpt \
    text_prompt="walk forward slowly"

可控性实验:同一 prompt 不同 seed

CALM 的一个关键特性是同一 prompt 可以生成多样化的动作——因为 latent space 的不同采样会产生风格变化:

# 同一 prompt,不同 seed
for seed in 1 2 3 4 5; do
    python protomotions/eval_agent.py \
        +robot=g1 +simulator=isaacgym \
        checkpoint=results/g1_calm/last.ckpt \
        text_prompt="walk forward slowly" \
        seed=$seed \
        --record_video
done

# 比较 5 个视频的差异
# 预期:都在走路,但步幅、摆臂幅度、节奏有差异

本质洞察:CALM 的条件控制不是"精确指定动作"——它是约束动作的风格分布。"walk forward slowly"约束了速度(慢)和方向(前),但没有约束步幅(大步慢走 vs 小步慢走)或摆臂(自然摆 vs 手插口袋)。这种"约束但不完全指定"的特性既是优势(灵活性)也是局限(不可控的维度可能产生不自然的行为)。

⚠️ 常见陷阱

⚠️ 编程陷阱:text_embedding 在训练中被错误更新。错误做法:CLIP encoder 的 requires_grad=True,导致 CALM 的 discriminator loss 反向传播到 CLIP 中。现象:训练初期正常但后期 text embedding 语义漂移,相似文本产生完全不同的动作。正确做法:冻结 CLIP encoder,for param in clip_model.parameters(): param.requires_grad = False

💡 概念误区:CALM 可以响应任意文本。CALM 的文本理解能力受限于训练数据的标注覆盖。如果 BABEL/TEACH 中没有"moonwalk"的标注,CALM 对"do a moonwalk"的响应是不可预测的——可能生成一个和"walk"类似的动作(CLIP 空间中"moonwalk"和"walk"接近),也可能完全不对。

🧠 思维陷阱:CALM 的 latent_dim 越大越好。latent_dim=64 意味着 skill space 是 64 维的。太大(如 256)会导致 encoder 难以学到紧凑的表示——很多维度是冗余的,采样时容易产生 out-of-distribution 的 latent。太小(如 8)会导致表达能力不足——所有动作被压缩到 8 维空间,不同风格无法区分。

练习

  1. [实验题] 在 ProtoMotions 中训练 CALM,使用 5 个不同的 text prompt(走、跑、跳、转身、蹲下)。记录每个 prompt 下策略生成的动作质量。哪些 prompt 的效果好?为什么?
  2. [配置题] 写出从 ASE 切换到 CALM 需要修改的具体配置字段。标注哪些是"替换",哪些是"新增"。
  3. [设计题] 如果你要让 CALM 支持中文文本(如"慢慢走"),需要做哪些修改?CLIP 支持中文吗?如果不支持,有什么替代方案?

CALM 用文本描述控制动作风格,但只能给出"整体约束"(如"慢走")。MaskedMimic 更进一步——它可以从部分约束(一个关键帧、一个目标位置、一段文字)补全整个动作序列。

16.3 MaskedMimic 的 Motion Inpainting ⭐⭐⭐

这一节解决什么问题:理解 MaskedMimic 如何用"部分信息 → 补全完整动作"的思想统一多种控制模态。

MaskedMimic 的核心思想

MaskedMimic(Tessler et al., SIGGRAPH Asia 2024)的关键洞察是:

Key insight: 所有控制指令都可以看作部分的运动描述。文本描述指定了风格但不指定关节角度;关键帧指定了某些时刻的姿态但不指定中间过渡;目标位置指定了末端位置但不指定到达路径。一个统一的 "motion inpainting" 模型可以从任意部分描述中补全完整动作。

这个思想类似于 NLP 中的 masked language model(BERT):给定一个句子中的部分词,预测被遮挡的词。MaskedMimic 给定一个动作的部分信息(mask),预测被遮挡的部分。

两阶段训练

MaskedMimic 在 ProtoMotions 中的训练分为两个阶段:

Phase 1: 全约束专家(PPO)

这一阶段和 Ch15 的 motion tracking 完全相同——训练一个能精确跟踪参考动作的 expert policy。

# Phase 1: 全约束 expert
python protomotions/train_agent.py \
    +exp=mimic_mlp +robot=g1 +simulator=isaacgym \
    motion_file=data/amass_full.yaml \
    max_iterations=50000 \
    experiment_name=g1_expert

Expert 的 obs 包含完整的参考运动信息(~160 维,和 Ch15 的 tracking task 一致)。

Phase 2: Masked 蒸馏(在线 teacher-student distillation)

Phase 2 把 expert 的全信息策略蒸馏到一个能处理部分信息的 transformer。注意这是在线蒸馏:训练中机器人被初始化到随机动作的随机帧,expert(Phase 1 冻结策略)观察未遮挡的未来动作并实时给出 action,student 观察随机遮挡版本并预测 expert 的 action——监督信号是 expert 的在线输出,而不是预录的离线数据集。它不是 PPO(不重新探索),但也不是从固定数据集做离线 BC:

# MaskedMimic Phase 2 蒸馏(概念性实现)
class MaskedMimicTrainer:
    def __init__(self, expert_policy, student_model, mask_ratio=0.7):
        self.expert = expert_policy
        self.expert.eval()  # 冻结 expert
        self.student = student_model  # transformer
        self.mask_ratio = mask_ratio
        self.optimizer = torch.optim.Adam(self.student.parameters(), lr=1e-4)

    def train_step(self, obs_full, motion_description):
        """
        obs_full: 完整的 observation(包含全部参考运动信息)
        motion_description: 结构化的运动描述(关键帧 + 文本 + 目标位置等)
        """
        # 1. 用 expert 生成 ground truth action
        with torch.no_grad():
            expert_action = self.expert(obs_full)

        # 2. 随机 mask motion_description
        mask = self.generate_random_mask(motion_description)
        masked_description = motion_description * mask

        # 3. Student 从 masked description 预测 action
        student_action = self.student(obs_full[:, :PROPRIO_DIM], masked_description)
        # 注意:student 的 obs 只有 proprio 部分 + masked description
        # 不包含完整的 motion reference(那是 expert 专属的)

        # 4. BC loss: 模仿 expert 的 action
        loss = F.mse_loss(student_action, expert_action)

        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        return loss.item()

    def generate_random_mask(self, description):
        """随机遮挡部分运动描述"""
        mask = torch.ones_like(description)

        # 随机选择遮挡类型
        mask_types = ['keyframes', 'text', 'target_pos', 'body_parts']
        selected = random.sample(mask_types, k=random.randint(1, 3))

        for mt in selected:
            if mt == 'keyframes':
                # 遮挡某些时间帧的姿态
                frame_mask = torch.rand(description.shape[1]) > self.mask_ratio
                mask[:, frame_mask, :KEYFRAME_DIM] = 0
            elif mt == 'text':
                # 遮挡文本 embedding
                if random.random() < self.mask_ratio:
                    mask[:, :, TEXT_START:TEXT_END] = 0
            elif mt == 'target_pos':
                # 遮挡目标位置
                if random.random() < self.mask_ratio:
                    mask[:, :, TARGET_START:TARGET_END] = 0
            elif mt == 'body_parts':
                # 遮挡某些身体部位
                parts_to_mask = random.sample(range(NUM_BODY_PARTS),
                                              k=int(self.mask_ratio * NUM_BODY_PARTS))
                for part in parts_to_mask:
                    mask[:, :, BODY_STARTS[part]:BODY_ENDS[part]] = 0

        return mask

Phase 2 的关键设计: - Student 模型是 transformer(不是 MLP)——因为 mask 后的输入长度和内容是变化的,transformer 通过 attention 机制自然处理变长输入 - 监督式蒸馏 loss(MSE 到 expert action)而不是 PPO——在线查询 expert 直接学习比重新探索更稳定(属于在线 teacher-student distillation,不是离线数据集 BC) - 每个 batch 的 mask 组合不同——模型在训练中看到所有可能的 mask 模式

推理时的多模态控制

训练完成后,MaskedMimic 支持多种控制方式——只需要提供不同的 mask:

控制方式 提供的信息 遮挡的信息 应用场景
文本控制 text embedding 关键帧、目标位置 "walk slowly"
关键帧控制 起始和终止姿态 文本、中间帧 动画插值
目标导航 目标 (x, y, yaw) 文本、关键帧 走到指定位置
混合控制 文本 + 起始姿态 目标位置、中间帧 "从当前姿态开始跑步"
全约束 全部信息 等同于 expert

反事实推理:如果不用 mask 而是为每种控制方式训练一个独立模型会怎样? 需要 5 个独立模型(每种控制方式一个),训练和维护成本是 MaskedMimic 的 5 倍。更关键的是,独立模型之间不共享知识——文本控制模型不知道关键帧控制模型学到的运动规律。MaskedMimic 通过共享 transformer 参数,让不同控制模式之间隐式传递知识。

与 HOVER 的对比

维度 HOVER(Ch14.6) MaskedMimic
Mask 对象 身体部位(root/手臂/头...) 信息类型(关键帧/文本/位置...)
Phase 1 全身跟踪 teacher 全约束 tracking expert
Phase 2 DAgger 蒸馏 在线 teacher-student 蒸馏
输出 关节位置目标 关节位置目标
用途 多控制模态(导航/操作/遥操) 多信息源(文本/关键帧/目标)

两者的思想高度相似——都是"训练一个全信息的 expert,然后蒸馏到能处理部分信息的 student"。区别在于 mask 的维度不同——HOVER 遮挡身体部位,MaskedMimic 遮挡信息类型。

ProtoMotions 中的 MaskedMimic 训练

# Phase 1: 训练全约束 expert
python protomotions/train_agent.py \
    +exp=mimic_mlp +robot=g1 +simulator=isaacgym \
    motion_file=data/amass_full.yaml \
    max_iterations=50000 \
    experiment_name=g1_expert

# Phase 2: 在线 teacher-student 蒸馏
python protomotions/train_masked_mimic.py \
    +robot=g1 \
    expert_checkpoint=results/g1_expert/last.ckpt \
    motion_file=data/amass_full.yaml \
    mask_ratio=0.7 \
    max_iterations=100000 \
    experiment_name=g1_masked_mimic

注意:MaskedMimic 的 Phase 2 是在线 teacher-student 蒸馏而不是 PPO。它仍然 rollout 与环境交互(student 用自己的动作推进),但学习信号来自实时查询冻结 expert 的 action(监督式蒸馏),而不是 PPO 的 reward 探索;它也不是从一个固定离线数据集做 BC。

⚠️ 常见陷阱

⚠️ 编程陷阱:MaskedMimic 的 Phase 2 是监督式蒸馏而非 RL 探索。错误做法:用 PPO 训练 Phase 2。现象:训练不稳定,因为 mask 后的输入信息不足以让 PPO 有效探索。正确做法:用在线 teacher-student 蒸馏,从 Phase 1 expert 实时取 action 作为监督。

💡 概念误区:MaskedMimic 可以从任意少的信息恢复完整动作。如果 mask 掉 99% 的信息(只给一个词"walk"),恢复的动作质量会很差。mask_ratio 有一个实用范围(~0.3-0.8),太高会导致补全质量下降。

练习

  1. [架构分析题] MaskedMimic 和 BERT 都使用了 mask 机制。列出 3 个相同点和 2 个不同点。
  2. [设计题] 如果你要用 MaskedMimic 做"给定起始姿态和目标位置,补全中间过渡动作",mask 应该怎么设置?哪些信息保留、哪些遮挡?
  3. [跨章综合题,Ch14+Ch16] MaskedMimic 的 Phase 2 蒸馏和 Ch14 HOVER 的 DAgger 蒸馏都是从 expert → student 的知识传递。对比两者在以下维度的差异:(a) expert 的训练方式,(b) student 的架构(MLP vs transformer),(c) mask 的含义,(d) 蒸馏 loss 的类型。

MaskedMimic 和 HOVER 的统一视角

回顾 Ch14.6 的 HOVER 和本节的 MaskedMimic,它们实际上是同一个思想的两种实例——"训练全信息 expert → 蒸馏到能处理部分信息的 student"

组件 HOVER (Ch14.6) MaskedMimic (Ch16.3)
Expert 训练 PPO,全身 tracking reward PPO,全约束 motion tracking
Expert 的 obs 全身参考运动 + privileged 完整参考运动 + 环境信息
Student 架构 MLP Transformer
Mask 维度 身体部位(root/手臂/头/腿) 信息类型(文本/关键帧/目标/场景)
蒸馏方法 DAgger(在线,rollout 中收集数据) 在线 teacher-student 蒸馏(rollout 中实时查询 expert action)
Student 的 obs 部分身体的参考 + proprio 部分信息(masked description)+ proprio
推理时输入 特定身体部位的控制信号 文本/关键帧/目标位置等组合

HOVER 与 MaskedMimic 的蒸馏都在线进行。 HOVER 用 DAgger——student 在自己的 rollout 中向 expert 查询标签。MaskedMimic 也是在线 teacher-student 蒸馏——student 用自己的动作 rollout,同时实时查询冻结 expert 在未遮挡观察下的 action 作为监督目标;两者都在 rollout 分布上学习,因此都能缓解纯离线 BC 的 distribution shift 问题。区别更多在 mask 的维度(身体部位 vs 信息类型)而非"在线/离线"。

工程启示:如果你要构建一个支持多种控制模态的统一控制器,有两条路线:

路线 A(HOVER 模式):
  按身体部位分 mask → 训练 → 部署时选择控制哪些部位
  适用:遥操作、导航+操作解耦

路线 B(MaskedMimic 模式):
  按信息类型分 mask → 训练 → 部署时选择提供什么信息
  适用:文本控制、关键帧插值、目标导航

两条路线可以组合——理论上可以同时在身体部位和信息类型两个维度上做 mask。但这会导致训练样本需求指数增长(mask 组合数太多),实际中通常选一条路线。


CALM 和 MaskedMimic 是学术方法——它们展示了概念的可行性。TextOp 把这些概念工程化为一个可部署的实时系统——从键盘输入文本到 G1 执行动作,端到端闭环。

16.4 TextOp 实时文生动作管线 ⭐⭐⭐

这一节解决什么问题:精读 TextOp 的两层架构和工程实现——如何实现"打字 → G1 立即动"的实时系统。

TextOp 是什么

TextOp(arXiv 2602.07439 预印本,项目页 text-op.github.io,代码 TeleHuman/TextOp)是一个实时交互式文生动作系统。它允许用户在 G1 运行过程中随时修改文本指令——机器人平滑地从当前动作过渡到新指令描述的动作。

两层架构

用户文本输入 (streaming)
       ↓
┌──────────────────────────────────┐
│  High-Level: RobotMDAR           │
│  (Robot Motion Diffusion          │
│   AutoRegressive Model)           │
│                                    │
│  输入: text embedding + 最近动作   │
│  输出: 短时域运动学轨迹 (~1s)      │
│  推理: ~20ms per chunk            │
└──────────────────────────────────┘
       ↓ 运动学轨迹
┌──────────────────────────────────┐
│  Low-Level: BeyondMimic Tracker   │
│  (Ch15 的 tracking policy)         │
│                                    │
│  输入: proprio + reference motion  │
│  输出: joint position target       │
│  频率: 50 Hz                       │
└──────────────────────────────────┘
       ↓ 关节命令
       Robot (G1)

关键设计:RobotMDAR 是自回归的——它不是一次生成整段动作(像 MDM 那样),而是每次生成 ~1 秒的短片段,基于当前文本和最近的动作上下文。这意味着用户可以在运行中修改文本,RobotMDAR 在下一个 chunk 中就会响应新指令。

RobotMDAR 的实现

TextOp 的 RobotMDAR 基于 DART(Diffusion-based AutoRegressive Transformer)的重构:

# RobotMDAR 的推理循环(概念性)
class RobotMDAR:
    def __init__(self, model, clip_encoder, chunk_length=50):
        self.model = model  # 扩散 + 自回归 transformer
        self.clip_encoder = clip_encoder
        self.chunk_length = chunk_length  # 50 帧 = 1 秒 @ 50Hz
        self.motion_context = []  # 最近的动作上下文

    def generate_chunk(self, text_prompt, current_state):
        """生成下一个 ~1 秒的运动学轨迹"""
        # 文本编码
        text_emb = self.clip_encoder(text_prompt)  # (512,)

        # 拼接动作上下文
        if len(self.motion_context) > 0:
            context = torch.cat(self.motion_context[-3:], dim=0)
        else:
            context = current_state.unsqueeze(0)

        # 扩散去噪生成
        noise = torch.randn(self.chunk_length, 29)  # 29-DoF G1
        trajectory = self.model.denoise(noise, text_emb, context)
        # trajectory: (chunk_length, 29) — 关节角度序列

        # 更新上下文
        self.motion_context.append(trajectory)
        if len(self.motion_context) > 5:
            self.motion_context.pop(0)

        return trajectory

TextOp 的工程亮点

1. Robot-Skeleton Motion Representation

TextOp 不使用 SMPL ball-and-socket 表示——它使用机器人骨骼的单自由度关节表示。SMPL 的每个关节是 3-DoF ball joint(用 axis-angle 或 quaternion 表示),但 G1 的每个关节是 1-DoF hinge joint。

SMPL 表示: 24 joints × 3 axis-angle = 72 维
G1 表示: 29 joints × 1 angle = 29 维

直接用 SMPL 表示训练 → 生成的动作需要从 72 维转换到 29 维 → 会引入转换误差。TextOp 从一开始就在 G1 的 29 维空间中训练,避免了这个问题。

2. Generator-Augmented Training Data

Tracking policy 在 MoCap 数据上训练,但部署时要跟踪 RobotMDAR 生成的动作——两者的分布不同。TextOp 把 RobotMDAR 生成的动作也加入 tracker 的训练数据中,缩小分布差距:

Tracker 训练数据 = MoCap 数据 (AMASS) + RobotMDAR 生成数据

3. Streaming 指令修改

TextOp 支持在运行中修改文本指令——不需要停下来重新开始。这通过 RobotMDAR 的自回归特性实现:每个 chunk 独立依赖当前文本,文本变化时下一个 chunk 自动响应。

TextOp 代码结构

TeleHuman/TextOp/
├── TextOpRobotMDAR/     # 高层文生动作模型
│   ├── model/           # DART-based diffusion autoregressive
│   ├── data/            # 数据处理
│   └── train.py         # 训练脚本
├── TextOpTracker/       # 低层 tracking policy
│   ├── (built on BeyondMimic)
│   └── train.py
├── TextOpDeploy/        # 部署
│   ├── sim2sim/         # MuJoCo sim-to-sim
│   ├── sim2real/        # G1 真机
│   └── keyboard_joystick.py  # 键盘输入文本
├── dataset/             # 数据预处理脚本
│   ├── retarget_smpl_to_g1.py
│   └── prepare_babel_annotations.py
└── deps/                # 第三方依赖

训练和部署流程

TextOp 的训练分为三个独立阶段——每个阶段可以独立跑通再进入下一阶段:

阶段 1:数据预处理

# Step 1a: AMASS → G1 29-DoF retarget
python dataset/retarget_smpl_to_g1.py \
    --amass_dir data/amass/ \
    --output_dir data/g1_motions/ \
    --fps 50  # 目标帧率

# Step 1b: BABEL 文本标注对齐
# BABEL 为 AMASS 中的部分动作提供了自然语言标注
python dataset/prepare_babel_annotations.py \
    --babel_dir data/babel/ \
    --motion_dir data/g1_motions/ \
    --output data/g1_motions_with_text.yaml

# 输出的 YAML manifest 格式:
# motions:
#   - file: data/g1_motions/walking_01.npy
#     fps: 50
#     text: "A person walks forward slowly with relaxed arms"
#     duration: 3.2
#   - ...

数据量参考:TextOp 论文使用 AMASS 的 BABEL-TEACH 标注子集(~6K 条有文字标注的动作)+ LAFAN1 数据集。总训练数据约 15 小时的运动学轨迹。

阶段 2:训练 Tracker

# Step 2: 训练 Tracker(复用 BeyondMimic 架构)
cd TextOpTracker
python train.py --config configs/g1_tracker.yaml \
    --motion_file ../data/g1_motions_with_text.yaml \
    --num_envs 4096 \
    --max_iterations 30000

# Tracker 的特殊之处:训练数据包含 RobotMDAR 生成的动作
# 初次训练时还没有 RobotMDAR → 先用纯 MoCap 训练
# RobotMDAR 训练好后 → 生成额外数据 → 重新训练 tracker

Generator-Augmented Training 的工程实现

# TextOp tracker 的混合训练数据
def build_tracker_training_data(mocap_dir, generated_dir=None, gen_ratio=0.3):
    """构建 tracker 训练数据(混合 MoCap + 生成数据)"""
    motions = []

    # MoCap 数据(主体)
    for f in glob(f"{mocap_dir}/*.npy"):
        motions.append({"file": f, "source": "mocap", "weight": 1.0})

    # RobotMDAR 生成数据(辅助)
    if generated_dir:
        for f in glob(f"{generated_dir}/*.npy"):
            motions.append({"file": f, "source": "generated", "weight": gen_ratio})

    print(f"Tracker data: {len(motions)} motions "
          f"({sum(1 for m in motions if m['source']=='mocap')} mocap + "
          f"{sum(1 for m in motions if m['source']=='generated')} generated)")

    return motions

为什么要加入生成数据? RobotMDAR 生成的运动学轨迹和 MoCap 的分布不同——生成轨迹可能更"平滑"但关节范围更小。如果 tracker 只在 MoCap 数据上训练,部署时跟踪 RobotMDAR 的输出会有 distribution shift。加入 30% 的生成数据可以缩小这个 gap。

阶段 3:训练 RobotMDAR

# Step 3: 训练高层文生动作模型
cd TextOpRobotMDAR
python train.py --config configs/g1_mdar.yaml \
    --motion_file ../data/g1_motions_with_text.yaml \
    --text_encoder clip_vit_b_32 \
    --chunk_length 50 \
    --num_diffusion_steps 100 \
    --batch_size 256 \
    --max_epochs 500

# RobotMDAR 训练不需要物理引擎
# 它只学习 text → 运动学轨迹 的映射
# 训练在 CPU/GPU 上进行,不需要 Isaac Lab 或 MuJoCo

阶段 4:部署

# Step 4: sim-to-sim 部署测试(MuJoCo)
cd TextOpDeploy
python sim2sim/run.py \
    --tracker_ckpt ../TextOpTracker/checkpoints/last.pt \
    --mdar_ckpt ../TextOpRobotMDAR/checkpoints/last.pt \
    --keyboard  # 键盘输入文本

# 键盘操作:
# 输入文本后回车 → G1 开始执行对应动作
# 新文本覆盖旧文本 → G1 平滑过渡到新动作
# Ctrl+C 退出

# Step 5: sim-to-real 部署(真机 G1)
python sim2real/deploy.py \
    --tracker_ckpt ../TextOpTracker/checkpoints/last.pt \
    --mdar_ckpt ../TextOpRobotMDAR/checkpoints/last.pt \
    --robot_ip 192.168.123.xxx \
    --text "walk forward slowly"

TextOp 训练的资源需求

阶段 GPU 时间 说明
数据预处理 CPU ~2h AMASS retarget + BABEL 对齐
Tracker 训练 1 \(\times\) 4090 ~8h PPO, 4096 envs, 30K iter
RobotMDAR 训练 1 \(\times\) 4090 ~12h Diffusion, 500 epochs
Tracker 重训(含生成数据) 1 \(\times\) 4090 ~6h 加入 MDAR 生成数据
总计 ~28h 单 4090

⚠️ 常见陷阱

⚠️ 编程陷阱:Tracker 和 RobotMDAR 的动作表示不一致。错误做法:RobotMDAR 在 SMPL 空间训练,Tracker 在 G1 关节空间训练——中间需要 retarget 转换。现象:实时部署时转换引入延迟和误差。正确做法:TextOp 的设计就是为了避免这个问题——两者都在 G1 29-DoF 空间中工作。

💡 概念误区:TextOp 的响应延迟是 chunk 长度。chunk_length=50 帧 = 1 秒 ≠ 响应延迟 1 秒。RobotMDAR 的推理时间约 20ms——用户修改文本后 ~20ms 内就开始生成新 chunk。感知到的延迟是 RobotMDAR 推理时间 + 动作过渡时间,通常 < 200ms。

🧠 思维陷阱:TextOp 可以完全替代 MoCap。TextOp 生成的动作自然度和精度不如 MoCap + BeyondMimic。对于需要精确复现特定动作的场景(如舞蹈编排),MoCap 仍然更好。TextOp 的优势是交互性和灵活性——不需要预录 MoCap,用户实时输入文本就能控制。

练习

  1. [实验题] 使用 TextOp 的 sim-to-sim 部署,依次输入 "walk forward", "turn left", "jump"。观察 G1 的过渡是否平滑。
  2. [设计题] TextOp 的 RobotMDAR 每次生成 1 秒 (50 帧) 的 chunk。如果改为 0.5 秒 (25 帧),响应更快但质量可能下降。如果改为 2 秒 (100 帧),质量更好但响应更慢。你会怎么选择?为什么?
  3. [跨章综合题,Ch15+Ch16] TextOp 的 Tracker 复用了 Ch15 的 BeyondMimic。列出 Tracker 需要的修改:(a) 训练数据(加入 RobotMDAR 生成数据),(b) observation(是否需要额外信息),(c) reward(是否需要调整 σ)。

16.5 LeVERB 端到端 VLA ⭐⭐

这一节解决什么问题:理解 VLA(Vision-Language-Action)的端到端架构与 TextOp 分层架构的差异。

从分层到端到端

TextOp 是分层架构:高层(文本→运动学轨迹)和低层(运动学轨迹→关节控制)明确分离。LeVERB(Xue, Huang et al., arXiv 2506.13751)代表另一条路线——端到端 VLA

分层架构(TextOp):
  文本 → [RobotMDAR] → 运动学轨迹 → [Tracker] → 关节控制

端到端架构(LeVERB):
  文本 + 视觉 → [VLM] → latent action → [WBC] → 关节控制

核心区别:TextOp 的中间表示是运动学轨迹(人类可理解);LeVERB 的中间表示是latent action(模型学到的、人类不可理解的压缩表示)。

LeVERB 的双层系统

LeVERB 使用 Kahneman 的"System 1/System 2"类比:

层级 角色 输入 输出 训练
System 2(高层 VLM) "慢思考" 视觉 + 语言指令 latent action (CVAE) 合成渲染的运动学 demo
System 1(低层 WBC) "快反应" proprio + latent action 关节位置目标 RL (PPO) in Isaac Lab

CVAE(Conditional VAE)latent action space:高层 VLM 不直接输出关节角度——它输出一个 latent vector,低层 WBC 解码这个 latent 为关节控制。

# LeVERB CVAE latent action space(概念性)
class LatentActionCVAE(nn.Module):
    def __init__(self, obs_dim, action_dim, latent_dim=32):
        super().__init__()
        # Encoder: obs + action → latent (训练时)
        self.encoder = nn.Sequential(
            nn.Linear(obs_dim + action_dim, 256),
            nn.ReLU(),
            nn.Linear(256, latent_dim * 2),  # mean + logvar
        )
        # Decoder: obs + latent → action (推理时)
        self.decoder = nn.Sequential(
            nn.Linear(obs_dim + latent_dim, 256),
            nn.ReLU(),
            nn.Linear(256, action_dim),
        )

    def encode(self, obs, action):
        h = self.encoder(torch.cat([obs, action], dim=-1))
        mean, logvar = h.chunk(2, dim=-1)
        return mean, logvar

    def decode(self, obs, latent):
        return self.decoder(torch.cat([obs, latent], dim=-1))

    def reparameterize(self, mean, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mean + eps * std

CVAE 的好处是 latent space 是连续且紧凑的(~32 维),高层 VLM 只需要在这个低维空间中做决策——远比直接预测 29 维关节角度容易。

LeVERB 的关键数字

  • 评估基准:150+ tasks from 10 categories
  • Zero-shot 简单导航:80% 成功率
  • 总体成功率:58.5%
  • 对比 naive hierarchical VLA:7.8× 提升
  • 评估平台:Unitree G1 in Isaac Lab

与 TextOp 的详细选型对比

维度 TextOp(分层) LeVERB(端到端)
中间表示 运动学轨迹(可解释) latent action(不可解释)
视觉输入 有(RGB)
训练数据 AMASS + BABEL 文字标注 合成渲染的视觉 demo
部署复杂度 中(两个独立模型) 高(VLM + WBC)
响应速度 ~200ms ~500ms(VLM 推理更慢)
灵活性 高(文本可实时修改) 更高(文本 + 视觉)
可调试性 高(可检查中间轨迹) 低(latent 不可解释)
适用场景 纯文本交互控制 需要视觉理解的复杂任务
成熟度 较高(有预训练模型) 研究前沿(代码部分开源)
GPU 需求 1 × 4090(推理) 多 GPU(VLM 推理更重)

跨领域类比:TextOp 和 LeVERB 的区别就像导航中的"文字路线指引"和"带地图的语音导航"。文字路线指引(TextOp)简单直接——"直走100米,左转"——你不需要看到路况也能执行。语音导航+地图(LeVERB)更强大——它能看到路况、避开拥堵、适应变化——但系统更复杂、需要更多计算资源。对于简单的路线(直走到目的地),文字就够了;对于复杂的路线(需要看路况决策),需要地图+视觉。

本质洞察:分层架构(TextOp)和端到端架构(LeVERB)的根本 trade-off 是可解释性 vs 信息整合。分层架构的中间表示(运动学轨迹)是人类可理解的——你可以检查、可视化、手动修改它。端到端架构的中间表示(latent action)是模型学到的——它可能包含比运动学轨迹更丰富的信息(如对环境的理解),但你无法直接检查它的含义。在工程实践中,分层架构因为可调试性更受青睐;在研究前沿,端到端架构展示了更强的任务泛化能力。

⚠️ 常见陷阱

💡 概念误区:端到端一定比分层好。端到端避免了信息瓶颈(中间表示的信息损失),但引入了可调试性问题——当系统失败时,你不知道是高层理解错了还是低层执行错了。TextOp 失败时你可以查看中间轨迹:如果轨迹对但执行错 → tracker 问题;如果轨迹不对 → RobotMDAR 问题。

🧠 思维陷阱:VLA 只需要一个大模型就够了。LeVERB 的"端到端"并不是说一个模型从 RGB → 关节控制。它仍然有两层——高层 VLM 和低层 WBC——只是中间表示从运动学轨迹变成了 latent action。低层 WBC 仍然需要单独的 RL 训练。

LeVERB 的 WBC 低层训练

LeVERB 的 System 1(低层 WBC)本质上是一个 conditioned-on-latent 的 tracking policy:

# LeVERB WBC actor(概念性实现)
class WBCActor(nn.Module):
    def __init__(self, proprio_dim, latent_dim=32, action_dim=29):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(proprio_dim + latent_dim, 512),
            nn.ELU(),
            nn.Linear(512, 256),
            nn.ELU(),
            nn.Linear(256, action_dim),
        )

    def forward(self, proprio, latent_action):
        """
        proprio: (N, proprio_dim) — robot joint pos/vel/gravity
        latent_action: (N, latent_dim) — from high-level VLM
        """
        x = torch.cat([proprio, latent_action], dim=-1)
        return self.net(x)

WBC 的训练使用 PPO,reward 包含 tracking + task 组件。训练时 latent_action 由 CVAE encoder 从 ground truth action 编码得到(不需要 VLM 参与),部署时 latent_action 由 VLM 生成。

与 Ch15 tracker 的关系:LeVERB 的 WBC 和 Ch15 的 BeyondMimic tracker 在结构上高度相似——都是"接收某种形式的参考信号 + proprio → 关节控制"。区别在于参考信号的形式:BeyondMimic 接收运动学轨迹(~60 维),LeVERB 接收 latent action(~32 维)。

LeVERB 的训练数据构造

LeVERB 的高层 VLM 训练数据来自合成渲染的运动学 demo——不需要真机数据:

训练数据构造流程:
  1. 在 Isaac Lab 中创建多种任务场景
  2. 用 expert policy 执行任务,记录:
     - 每步的 egocentric RGB 渲染
     - 每步的语言描述(自动标注或手写)
     - 每步的 action(关节控制信号)
  3. 通过 CVAE encoder 把 action → latent_action
  4. 训练数据 = (RGB, text, latent_action) 三元组
  5. 高层 VLM 学习: (RGB, text) → latent_action

这种合成数据方法避免了真机数据收集的高成本——但代价是 sim-to-real gap 可能更大(渲染的 RGB 和真实 RGB 有视觉差异)。

练习

  1. [对比题] TextOp 和 LeVERB 分别在什么场景下更适合?列出 3 个场景及推荐方案。
  2. [设计题] 如果你要设计一个"看到桌上的杯子 → 走过去拿起来"的系统,用 TextOp 需要什么额外模块?用 LeVERB 呢?哪个更合适?为什么?
  3. [架构分析题] LeVERB 的 CVAE latent_dim=32。如果改为 4 或 128,分别有什么 trade-off?提示:从信息瓶颈和 VLM 预测难度两个角度分析。

16.6 视频 → 机器人完整管线 ⭐⭐

这一节解决什么问题:建立从 YouTube 视频到 G1 执行动作的完整数据管线。

端到端管线

Video (MP4)
  ↓ WHAM / 4DHumans
SMPL 参数 (body_pose, global_orient, transl)
  ↓ retarget_smpl_to_g1.py
G1 关节角度 + root 轨迹
  ↓ physics_filter.py (Ch15.5)
过滤后的物理可行动作
  ↓ csv_to_npz.py (Ch15.1)
G1 motion.npz
  ↓ BeyondMimic tracking (Ch15.1)
Tracking Policy
  ↓ ONNX export + RoboJuDo
G1 真机执行

每一步的工具和注意点

Step 1: 视频 → SMPL

# 使用 WHAM(全局平移更准),命令以官方 demo.py 为准
python demo.py \
    --video input.mp4 \
    --output_pth results/ \
    --save_pkl

# 输出: results/<视频名>.pkl
# 该 pkl 按 subject id 组织为 dict,每个 subject 含:
#   pose (T, 72), trans (T, 3), pose_world (T, 72), trans_world (T, 3), betas (10,), verts, frame_ids
# 其中 pose[:, :3] 是 root orient,pose[:, 3:] 是 body pose;world 版为世界坐标系结果

Step 2: SMPL → G1 retarget

# retarget 的关键步骤(概念性)
import numpy as np

def retarget_smpl_to_g1(smpl_pkl_path, output_csv_path, person_id=0):
    """SMPL → G1 29-DoF retarget(WHAM 输出按 subject id 组织,先选人再拆字段)"""
    import pickle
    with open(smpl_pkl_path, 'rb') as f:
        data = pickle.load(f)

    subj = data[person_id]                 # WHAM pkl 顶层是 {subject_id: {...}}
    pose = subj['pose_world']              # (T, 72):前 3 维 root orient,其余 body pose
    global_orient = pose[:, :3]            # (T, 3)
    body_pose = pose[:, 3:]               # (T, 69) = 23 joints × 3 axis-angle
    transl = subj['trans_world']          # (T, 3) 世界坐标系平移

    T = len(body_pose)
    g1_joints = np.zeros((T, 29))
    g1_root_pos = np.zeros((T, 3))
    g1_root_quat = np.zeros((T, 4))

    for t in range(T):
        # SMPL FK → 24 个 joint 的 3D 位置
        smpl_joints_3d = smpl_forward_kinematics(
            body_pose[t], global_orient[t], transl[t]
        )

        # IK 求解 G1 关节角度
        g1_joints[t] = inverse_kinematics_g1(
            smpl_joints_3d,
            joint_limits=G1_JOINT_LIMITS
        )

        # Root 位置和朝向
        g1_root_pos[t] = transl[t] * (1.32 / 1.70)  # 身高比例缩放
        g1_root_quat[t] = axis_angle_to_quat(global_orient[t])

    # 保存为 CSV
    save_csv(output_csv_path, g1_root_pos, g1_root_quat, g1_joints)

Step 3: Physics filter(复用 Ch15.5 的 KungfuBot filter)

Step 4: CSV → NPZ(复用 Ch15.1 的 csv_to_npz.py)

Step 5: BeyondMimic tracking(复用 Ch15.1 的训练流程)

质量评估:retarget 后的 MuJoCo 回放

在训练 tracking policy 之前,先在 MuJoCo 中回放 retarget 后的动作,目视检查质量:

# MuJoCo 回放
python scripts/playback_motion.py \
    --mjcf g1_29dof.xml \
    --motion data/g1_walking.npz \
    --viewer viser

检查要点: - 脚是否在地面以上?(脚底穿地 → retarget 的 root 高度不对) - 关节角度是否在限位内?(某个关节卡在限位 → IK 求解失败) - 动作是否连续?(跳变 → 帧间插值问题) - 自碰撞?(手臂穿过身体 → IK 没有考虑碰撞约束)

Retarget 后的自动化质量评估

除了目视检查,应该用脚本自动检测常见问题:

# 自动化质量评估脚本
def assess_retarget_quality(motion_npz_path, robot_model_path):
    """评估 retarget 后动作的物理可行性"""
    import mujoco
    import numpy as np

    motion = np.load(motion_npz_path)
    root_pos = motion['root_pos']      # (T, 3)
    root_quat = motion['root_quat']    # (T, 4)
    joint_pos = motion['joint_pos']    # (T, 29)

    m = mujoco.MjModel.from_xml_path(robot_model_path)
    issues = {"ground_penetration": 0, "joint_limit": 0, 
              "velocity_spike": 0, "total_frames": len(root_pos)}

    for t in range(len(root_pos)):
        # 检查 1: 脚底是否穿地
        # (简化: 检查 root 高度是否太低)
        if root_pos[t, 2] < 0.3:  # G1 root 最低约 0.4m
            issues["ground_penetration"] += 1

        # 检查 2: 关节角度在限位内
        for j in range(m.njnt):
            if m.jnt_limited[j]:
                lo, hi = m.jnt_range[j]
                if j < len(joint_pos[t]):
                    if joint_pos[t, j] < lo - 0.05 or joint_pos[t, j] > hi + 0.05:
                        issues["joint_limit"] += 1

        # 检查 3: 相邻帧速度突变
        if t > 0:
            dt = 0.02  # 50 Hz
            joint_vel = np.abs(joint_pos[t] - joint_pos[t-1]) / dt
            if np.max(joint_vel) > 20.0:  # rad/s
                issues["velocity_spike"] += 1

    # 输出质量报告
    T = issues["total_frames"]
    report = {
        "ground_penetration_rate": issues["ground_penetration"] / T,
        "joint_limit_violation_rate": issues["joint_limit"] / (T * 29),
        "velocity_spike_rate": issues["velocity_spike"] / T,
        "overall_pass": (issues["ground_penetration"] / T < 0.05 and
                        issues["velocity_spike"] / T < 0.02),
    }

    print(f"Quality Report for {motion_npz_path}:")
    print(f"  Ground penetration: {report['ground_penetration_rate']:.1%}")
    print(f"  Joint limit violations: {report['joint_limit_violation_rate']:.1%}")
    print(f"  Velocity spikes: {report['velocity_spike_rate']:.1%}")
    print(f"  Overall: {'✅ PASS' if report['overall_pass'] else '❌ FAIL'}")

    return report

Post-Processing: 修复常见问题

当自动化检查发现问题时,以下 post-processing 步骤可以修复大部分问题:

def postprocess_retarget(motion, robot_model):
    """修复 retarget 后动作的常见问题"""

    # 1. 脚底穿地修复: 整体抬高 root
    min_foot_height = compute_min_foot_height(motion, robot_model)
    if min_foot_height < 0.0:
        motion['root_pos'][:, 2] -= min_foot_height + 0.01  # 抬到地面以上
        print(f"Lifted root by {-min_foot_height + 0.01:.3f}m")

    # 2. 关节角度 clamp 到限位
    for j in range(robot_model.njnt):
        if robot_model.jnt_limited[j]:
            lo, hi = robot_model.jnt_range[j]
            motion['joint_pos'][:, j] = np.clip(
                motion['joint_pos'][:, j], lo + 0.01, hi - 0.01
            )

    # 3. 速度突变平滑: 低通滤波
    from scipy.signal import savgol_filter
    for j in range(29):
        motion['joint_pos'][:, j] = savgol_filter(
            motion['joint_pos'][:, j],
            window_length=11,  # 11 帧窗口 @ 50Hz = 0.22s
            polyorder=3,
        )

    # 4. 坐标系转换 (Y-up → Z-up)
    # SMPL: Y-up → MuJoCo: Z-up
    if is_y_up(motion):
        R = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]])  # Y→Z 旋转
        motion['root_pos'] = motion['root_pos'] @ R.T
        motion['root_quat'] = rotate_quaternions(motion['root_quat'], R)

    return motion

Savgol 滤波器的窗口选择:window_length=11(0.22 秒)是经验值。太小(如 5)保留了大部分噪声;太大(如 21)会把快速动作(如挥拳)也平滑掉。对于高动态动作(功夫、跑酷),建议用 window_length=7。

完整的视频→训练管线脚本

#!/bin/bash
# 完整的 YouTube 视频 → G1 tracking 管线
set -e

VIDEO_URL="$1"
OUTPUT_DIR="output/$(date +%Y%m%d_%H%M%S)"
mkdir -p $OUTPUT_DIR

echo "Step 1: 下载视频"
yt-dlp -f 'best[height<=720]' -o "$OUTPUT_DIR/input.mp4" "$VIDEO_URL"

echo "Step 2: WHAM 姿态估计"
cd WHAM
python demo.py --video "$OUTPUT_DIR/input.mp4" \
    --output_pth "$OUTPUT_DIR/wham/" --save_pkl
cd ..

echo "Step 3: SMPL → G1 retarget"
python retarget_smpl_to_g1.py \
    --input "$OUTPUT_DIR/wham/input.pkl" \
    --output "$OUTPUT_DIR/g1_raw.csv"

echo "Step 4: CSV → NPZ (帧率对齐)"
python csv_to_npz.py \
    --input-csv "$OUTPUT_DIR/g1_raw.csv" \
    --output-npz "$OUTPUT_DIR/g1_motion.npz" \
    --input-fps 30 --output-fps 50

echo "Step 5: Post-processing + 质量评估"
python postprocess_and_assess.py \
    --motion "$OUTPUT_DIR/g1_motion.npz" \
    --robot g1_29dof.xml \
    --output "$OUTPUT_DIR/g1_motion_clean.npz"

echo "Step 6: Physics filter"
python physics_filter.py \
    --motion "$OUTPUT_DIR/g1_motion_clean.npz" \
    --robot g1_29dof.xml \
    --output "$OUTPUT_DIR/g1_motion_filtered.npz"

echo "Step 7: MuJoCo 回放检查"
python playback_motion.py \
    --mjcf g1_29dof.xml \
    --motion "$OUTPUT_DIR/g1_motion_filtered.npz" \
    --viewer viser --record "$OUTPUT_DIR/playback.mp4"

echo "管线完成!检查 $OUTPUT_DIR/playback.mp4"
echo "如果回放质量好,继续训练 tracking policy:"
echo "  uv run train Mjlab-Tracking-Flat-Unitree-G1 \\"
echo "    --env.commands.motion.motion-file $OUTPUT_DIR/g1_motion_filtered.npz"

⚠️ 常见陷阱

⚠️ 编程陷阱:SMPL 和 G1 的坐标系不同。SMPL 使用 Y-up 坐标系,MuJoCo 使用 Z-up。不转换会导致 G1 "躺在地上"。正确做法:在 retarget 中做 Y-up → Z-up 旋转。

⚠️ 编程陷阱:视频帧率和动作帧率不匹配。视频通常是 24/30 FPS,WHAM 输出与视频帧率相同。G1 tracking 需要 50 Hz。必须用插值对齐帧率。

练习

  1. [实践题] 选择一段 YouTube 舞蹈视频(10 秒以内),用 WHAM 提取 SMPL 参数,retarget 到 G1,在 MuJoCo 中回放。记录遇到的问题和解决方案。
  2. [设计题] 如果视频中有多个人,WHAM 的输出是什么?如何选择要跟踪的那个人?

16.7 精读:HumanPlus Shadowing Transformer ⭐⭐⭐

这一节解决什么问题:理解 HumanPlus 如何用单 RGB 相机实现实时全身遥操作——从姿态估计到 HST 到 HIT 的完整链路。

HumanPlus 系统概览

HumanPlus(Fu, Zhao, Wu, Wetzstein, Finn, CoRL 2024, MarkFzp/humanplus)是 Stanford 的全栈人形遥操作系统。它的核心贡献是:

一个 RGB 相机就能实时控制人形机器人做复杂任务——穿鞋、叠衣服、打乒乓球。

两阶段架构

Stage 1: Humanoid Shadowing Transformer (HST) — 低层控制器
  训练: AMASS (40h MoCap) + PPO in IsaacGym
  输入: robot proprio + retargeted target pose
  输出: 19-D joint position target (H1)
  硬件: 单 RTX 4090
  特点: zero-shot sim-to-real

Stage 2: Humanoid Imitation Transformer (HIT) — 高层技能策略
  训练: 真机遥操作数据 (40 demos/task)
  输入: egocentric RGB + robot proprio
  输出: target pose for HST
  硬件: 真机 H1 + 两个头部 RGB 相机

HST 的实现

HST 是一个 decoder-only transformer,在 AMASS 数据集上用 PPO 训练:

# HST 架构(概念性简化)
class HumanoidShadowingTransformer(nn.Module):
    def __init__(self, obs_dim, action_dim=19, hidden_dim=256, n_heads=4, n_layers=4):
        super().__init__()
        self.embed = nn.Linear(obs_dim, hidden_dim)
        self.transformer = nn.TransformerDecoder(
            nn.TransformerDecoderLayer(hidden_dim, n_heads, dim_feedforward=512),
            num_layers=n_layers
        )
        self.action_head = nn.Linear(hidden_dim, action_dim)

    def forward(self, obs_sequence):
        """
        obs_sequence: (batch, seq_len, obs_dim)
        包含: robot joint pos, joint vel, target pose from retarget
        """
        x = self.embed(obs_sequence)

        # Causal mask (decoder-only: 只看历史)
        mask = nn.Transformer.generate_square_subsequent_mask(x.size(1))

        x = self.transformer(x, x, tgt_mask=mask)
        action = self.action_head(x[:, -1, :])  # 只取最后一步的输出
        return action

为什么用 transformer 而不是 MLP? AMASS 包含极其多样的动作——从慢走到快跑到跳跃。MLP 处理固定长度输入,缺少对时间上下文的建模能力。Transformer 通过 attention 机制可以关注历史中的关键帧——比如"上一步是左脚着地"这个信息对决定"这一步应该抬右脚"很重要。

HST 的 obs 结构

# HST 每步的 obs 组成
obs = torch.cat([
    robot_joint_pos,          # (19,)  当前关节角度
    robot_joint_vel,          # (19,)  当前关节速度
    base_angular_velocity,    # (3,)   IMU 角速度
    projected_gravity,        # (3,)   重力方向投影
    target_joint_pos,         # (19,)  retarget 的目标关节角度
    target_joint_vel,         # (19,)  retarget 的目标关节速度
], dim=-1)  # 总共 ~82 维

HST 接收窗口化的 obs 序列(如最近 10 步),而不是单帧 obs。这让 transformer 的 attention 能够关注时间上下文。

HST 的训练 Reward

# HumanPlus HST reward(论文 Table 1 的 reward terms)
rewards = {
    # 指令跟踪(论文用指数核匹配目标速度)
    "target_xy_velocity":  "exp 匹配 target forward/lateral 速度",
    "target_yaw_velocity": "exp 匹配 target yaw 速度",
    # 姿态跟踪(L2 惩罚)
    "target_joint_positions": "-||q - q_tg||^2",
    "target_roll_pitch":      "-||(roll,pitch) - (roll,pitch)_tg||^2",
    # 正则与稳定项
    "energy":         "-||τ ⊙ dq||^2  (torque × velocity)",
    "feet_contact":   "feet contact 与 feet slipping 惩罚",
    "alive":          "存活 bonus",
}

注意:上表对应 HumanPlus 论文 Table 1 的实际项(target xy/yaw velocities、target joint positions、target roll & pitch、energy、feet contact/slipping、alive),与 Ch15 通用 motion tracking reward 不完全相同——HumanPlus 跟踪的是目标速度与目标关节角,而非完整 root 位姿轨迹。HST 的优势在于它在整个 AMASS(约 40 小时、11,000+ motions)上训练,是一个通用低层控制器。

HST 的训练配置

参数 说明
训练数据 AMASS (40+ 小时) 11,000+ motions
机器人 Unitree H1 (33-DoF) 含 6-DoF Inspire 手
策略输出 19 维 身体关节 setpoints(手部不经 HST,直接给 PD)
并行环境 IsaacGym 数千并行(具体 env 数/训练时长以官方实现为准)
HST 控制频率 50 Hz 策略推理频率
身体姿态估计 25 fps WHAM body retarget(RTX 4090)
手部姿态估计 10 fps HaMeR hand retarget(RTX 4090)
Sim-to-Real Zero-shot 无 fine-tuning

实时遥操作的数据流

外部 RGB 相机
    ├─ WHAM: 身体姿态估计 (25 fps) → SMPL body_pose + global_orient + transl
    │      ↓ retarget → H1 body target(19 个身体关节角)
    │      ↓ 频率提升 (25 → 50 Hz, 线性插值)
    │      ↓ HST 输入: robot proprio + body target
    │      ↓ HST 推理 (~1ms on 4090) → 19 维身体 joint position commands (50 Hz)
    │      ↓ PD 控制器 → 身体关节力矩
    └─ HaMeR: 手部姿态估计 (10 fps) → MANO hand_pose
           ↓ retarget → Inspire 手 target joint angles
           ↓ 直接送入 PD 控制器(不经 HST)→ 手部关节力矩

频率对齐是工程关键:身体姿态 25 fps、手部姿态 10 fps,而 HST 策略推理 50 Hz。身体 target 在两步策略推理之间插值一次即可(25 Hz 下相邻帧姿态变化小);手部 target 角直接进 PD,不经过 HST。

HIT 的数据收集和训练

HST 解决了"如何从 target pose 控制机器人"。HIT 解决了"如何从视觉观察生成 target pose"——让机器人从遥操作 demo 中学会自主执行任务。

数据收集流程:
  1. 人站在 H1 旁边做动作(穿鞋、叠衣服...)
  2. 外部 RGB 相机捕捉人的动作
  3. WHAM + HaMeR → SMPL-X → retarget → target pose
  4. target pose → HST → H1 实时执行(shadowing)
  5. H1 的 egocentric RGB + joint state 记录为 demo

  每个任务: 约 25-40 个 demos;单条任务时长不一(论文中 5-50 秒不等,
            如 Greeting ~5s、Rearrange ~10s、Warehouse/Fold Clothes ~20s、Wear a Shoe ~50s)

自主技能训练:
  6. 训练 HIT (decoder-only transformer)
     输入: egocentric RGB (ResNet-18 features) + robot proprio
     输出: target pose for HST
  7. HIT 部署: 头部 RGB → HIT → target pose → HST → 自主执行

40 个 demos 的工程意义:传统 imitation learning 需要数千个 demo。HumanPlus 用 40 个就够,关键在于 HST 提供了一个强大的低层控制器——HIT 只需要学"做什么"(高层策略),不需要学"怎么做"(低层控制)。这种分层设计极大降低了 demo 需求。

HumanPlus HIT 自主任务成功率(论文 Table 5,whole-task success):

任务 demos 数 成功率 难度
叠衣服 (Fold Clothes) 40 100% 中——灵巧操作
整理物体 (Rearrange Objects) 30 90% 中——全身协调
打字 "AI" (Type AI) 30 80% 高——精细操作
双机器人打招呼 (Two-Robot Greeting) 30 90%
仓库搬运 (Warehouse) 25 90% 中——全身协调
穿鞋并行走 (Wear a Shoe and Walk) 40 60% 高——接触丰富

注意:打乒乓球(table tennis)在论文中是 shadowing(实时遥操作) 的展示任务,不是 HIT 自主策略的 40-demo 任务,不应列入上面的自主成功率表。

⚠️ 常见陷阱

⚠️ 编程陷阱:HumanPlus 基于 IsaacGym Preview 4(非 Manager-Based)。代码结构与 mjlab/Isaac Lab 不同,不能直接复用。如果要在 mjlab 中实现类似功能,需要重新实现 HST 的训练循环。

💡 概念误区:HST 就是 Ch15 的 tracking policy 加了 transformer。HST 确实是一种 tracking policy,但它的训练数据覆盖范围远大于 Ch15 的单条/少量动作——HST 在整个 AMASS(40 小时)上训练,是一个通用 motion tracker。Ch15 的 BeyondMimic 通常在特定任务的数据子集上训练。

⚠️ 编程陷阱:HumanPlus 的 retarget 包含手部映射。H1 使用 Inspire RH56DFX 6-DoF 手,SMPL-X 手部有 15 个关节 per hand。retarget 需要同时处理身体(WHAM → H1 body)和手部(HaMeR → Inspire hand)。如果只做身体 retarget 忽略手部,穿鞋/叠衣服等精细任务无法执行。

HumanPlus 的 DR 配置

HST 的 zero-shot sim-to-real 依赖于训练时的 Domain Randomization:

# HumanPlus DR 配置(基于论文 Table 2)
dr_config = {
    # 论文 Table 2 的随机化项与范围
    "base_payload":        [-3.0, 3.0],    # kg, 基座负载
    "end_effector_payload":[0.0, 0.5],     # kg, 末端负载
    "center_of_base_mass": [-0.1, 0.1],    # m, 基座质心偏移(每个轴)
    "motor_strength":      [0.8, 1.1],     # 电机力矩缩放
    "friction":            [0.3, 0.9],     # 摩擦系数
    "control_delay":       [0.02, 0.04],   # s, 控制延迟
}

上表对应 HumanPlus 论文 Table 2 的实际项(base payload、end-effector payload、center of base mass、motor strength、friction、control delay)。若要加入 restitution、电机零位、观测噪声、随机推力等其它 DR 项,应作为"可选扩展",不属于论文 Table 2。

为何 DR 范围相对保守:HST 需要在极其多样的动作(约 40 小时 AMASS)上工作——如果 DR 太强,策略在困难动作(如单脚站立)上就难以收敛。

反事实推理:如果 HST 训练时完全不加 DR 会怎样? 仿真中 HST 的 tracking 质量可能更高(没有噪声干扰),但部署到真机后立即崩溃——真机的摩擦、电机特性和传感器噪声与仿真不同。DR 是 zero-shot sim-to-real 的必要条件。

HumanPlus 在 mjlab 中的等价路径

如果你想在 mjlab 中复现 HumanPlus 的 HST,有以下选项:

  1. 最简路径:用 Ch15 的 BeyondMimic + 整个 AMASS 数据集训练。BeyondMimic 的 MLP 策略不如 transformer 强大,但对于大多数遥操作场景够用。
  2. 进阶路径:修改 RSL-RL 的 actor 为 transformer 架构,在 mjlab 的 tracking task 中训练。需要自己实现 transformer actor 和 obs 历史窗口化。
  3. 最佳路径:等待 mjlab 社区贡献的 transformer actor 模块——社区活跃,可能已有或即将有实现。

练习

  1. [架构分析题] HST 使用 decoder-only transformer。如果改用 encoder-only(BERT 风格)会有什么问题?提示:考虑因果性——策略只能看到过去,不能看到未来。
  2. [跨章综合题,Ch14+Ch15+Ch16] HumanPlus 的 HST 和 Ch14 的 HOVER、Ch15 的 BeyondMimic 都是 tracking policy。列出三者在以下维度的对比:(a) 训练数据规模,(b) 支持的控制模态,(c) 网络架构,(d) 部署平台。
  3. [设计题] 如果你要用 HumanPlus 的方法让 G1(而不是 H1)学会穿鞋,需要哪些修改?列出 retarget、HST 训练和 HIT 数据收集三个方面的具体变化。

16.8 精读:HDMI 单目视频 → loco-manipulation ⭐⭐

这一节解决什么问题:理解 HDMI 如何从单目视频中同时提取人体和物体轨迹,训练人形的全身交互技能。

HDMI 的独特价值

前面的方法(CALM、TextOp、HumanPlus)都关注人体运动——机器人在空旷空间中移动。HDMI(Weng, Li et al., CMU, arXiv 2509.16757)解决的是人与物体的交互——从视频中同时提取人的运动和物体的轨迹,训练机器人的 loco-manipulation 技能。

三阶段管线

Stage 1: 视频提取(离线)
  单目 RGB 视频
    → 人体姿态估计 (SMPL)
    → 物体检测 + 位姿估计 (6-DoF)
    → retarget 到 G1
    → 输出: (human_motion, object_trajectory) 配对

Stage 2: RL 训练(离线)
  co-tracking reward:
    r = w_h * r_human_tracking + w_o * r_object_tracking + w_i * r_interaction
  三个关键设计:
    1. 统一物体表示
    2. Residual action space
    3. 统一交互 reward

Stage 3: Zero-shot 策略部署(无需 fine-tuning)
  ONNX → G1 真机
  注意: 论文真机实验在机器人 pelvis 和被操作物体上贴 mocap markers,
        以获取 root link 全局位姿和物体状态作为 policy observation,
        并非仅靠机载视觉。论文 Limitations 将"转向机载 RGB/depth"列为未来方向。

三个关键工程设计

1. 统一物体表示

不同物体(门、箱子、杯子)有不同的几何形状和交互方式。HDMI 使用一个统一的 6-DoF 表示——不管什么物体,都用 (position, orientation) + 动力学状态表示:

# 统一物体表示
object_obs = torch.cat([
    object_position,       # (3,) 物体中心世界坐标
    object_orientation,    # (4,) 物体朝向四元数
    object_linear_vel,     # (3,) 物体线速度
    object_angular_vel,    # (3,) 物体角速度
    relative_position,     # (3,) 物体相对于机器人的位置
    relative_orientation,  # (4,) 物体相对于机器人的朝向
], dim=-1)  # 总共 20 维

为什么使用统一表示而不是针对每种物体设计不同的 obs? 因为统一表示让不同物体类型复用同一套观察接口和训练框架——门、箱子、行李箱都用同一组物体状态字段,省去为每类物体重写 obs 结构。需要强调的是:HDMI 论文的 Limitations 明确说明当前是每个技能训练一个专用策略(one policy per skill),并非单一策略 zero-shot 泛化到任意新物体;跨新物体的鲁棒性主要依赖训练随机化覆盖,而不是"见过门和箱子就能直接处理没见过的抽屉"。

2. Residual Action Space

标准 action space 是关节位置增量 \(a = q_{default} + \Delta q\),策略需要从零学习完整的关节控制。HDMI 使用 residual action:

\[a = q_{ref}(t) + \Delta q_{residual}\]

其中 \(q_{ref}(t)\) 来自视频提取的参考动作。策略只需要学习"相对于参考动作的微调"。

# Residual action space 的实现
class ResidualActionEnv(BaseEnv):
    def apply_actions(self, actions):
        """
        actions: (N, 29) — residual action from policy
        """
        # 获取当前帧的参考动作
        ref_action = self.motion_ref.get_joint_pos(self.current_frame)  # (N, 29)

        # 最终 action = reference + residual
        final_action = ref_action + actions * self.residual_scale
        # residual_scale 通常设为 0.1-0.3(限制 residual 幅度)

        # 应用到 PD 控制器
        self.robot.set_joint_position_target(final_action)

工程好处: - 探索空间被压缩——residual 只需要在 ±0.3 rad 内搜索 - 训练初期策略输出接近零 → 执行的动作接近参考 → 已经有合理的行为 - 对于接触丰富的任务(开门),接触点附近需要极精确的关节控制——residual 让策略只需微调

反事实推理:如果不用 residual 而是标准 action space 会怎样? 策略需要从零学习"走到门前"+"伸手"+"抓把手"+"推门"的完整动作序列。在接触丰富的任务中,探索空间极大,PPO 需要大量样本才能发现"抓住把手"这个关键行为。Residual action 把参考动作提供的"大致正确"作为起点,策略只需要学习"微调"来应对物理差异。

3. 统一交互 Reward

# HDMI 的统一交互 reward(概念性实现)
class HDMIReward:
    def __init__(self):
        self.w_body = 1.0       # 人体跟踪权重
        self.w_object = 0.5     # 物体跟踪权重
        self.w_contact = 0.3    # 接触奖励权重

    def compute(self, env):
        # 人体跟踪(复用 Ch15 的 body tracking)
        r_body = body_tracking_exp(env, sigma=0.1)
        r_joint = joint_tracking_exp(env, sigma=0.3)

        # 物体跟踪(HDMI 新增)
        obj_pos_err = torch.norm(
            env.object.root_pos - env.ref_object_pos, dim=-1
        )
        r_obj_pos = torch.exp(-obj_pos_err ** 2 / 0.05 ** 2)

        obj_ori_err = quat_diff_rad(
            env.object.root_quat, env.ref_object_quat
        )
        r_obj_ori = torch.exp(-obj_ori_err ** 2 / 0.2 ** 2)

        # 交互 reward(HDMI 新增)
        # 当手接触物体时给 bonus
        hand_obj_dist = torch.norm(
            env.robot.hand_pos - env.object.root_pos, dim=-1
        )
        r_contact = torch.where(
            hand_obj_dist < 0.05,  # 手距物体 < 5cm
            torch.ones_like(hand_obj_dist),
            torch.zeros_like(hand_obj_dist),
        )

        total = (self.w_body * (r_body + r_joint)
                + self.w_object * (r_obj_pos + r_obj_ori)
                + self.w_contact * r_contact)
        return total

HDMI 的视频提取管线细节

HDMI 需要同时从视频中提取人体物体的轨迹——这比只提取人体更复杂:

视频 → 分别提取(按 HDMI 官方仓库的 processing steps):
  人体: GVHMR 估计 SMPL 运动 → GMR / LocoMujoco 转为机器人 body/joint 状态
  物体:
    1. 提取物体轨迹: position、orientation、velocities
    2. 把物体元数据写入 meta.json,并将物体 body 状态拼接到机器人 body 状态
       得到形如 [T, B_robot + B_object, 3/4] 的张量

  对齐: 人体(机器人)与物体在同一时间和坐标系下

输出: (robot_motion, object_trajectory) 配对

注意:HDMI 官方管线用的是 GVHMR + GMR/LocoMujoco,不是 WHAM/Detic/DINO;物体 6-DoF 估计工具(如 BundleSDF/FoundationPose)是可替代的通用方法,并非 HDMI 论文/仓库所采用的组件。

关键结果

任务 真机结果 说明
开门穿越 (Door) 67 次连续 双向开门 + 穿越
行李箱 (Suitcase) 7 次连续成功 接触丰富的操作
面包箱搬运 (Breadbox) 2 次完整试验(含 180° 转身) 抓取 + 搬运
泡沫垫搬运 (Foam mats) 抓取 + 侧步 + 放置成功 全身协调
Truman's Bow 3 次完整序列(上台阶/鞠躬/坐下/挥手/跳) 长序列全身协调
仿真任务 14 种 包括上述 + 更多

67 次连续开门穿越是 HDMI 最亮眼的结果——说明策略不仅能做一次,而且有足够的鲁棒性反复执行。这得益于 DR(不同门的位置、方向、阻力都随机化了)和 residual action(接触点精确控制)。

HDMI 与其他视频学习方法的对比

维度 HDMI HumanPlus KungfuBot (Ch15.5)
视频提取 人体 + 物体 人体 only 人体 only
物体交互 ✅ 核心能力
训练方法 RL + residual action RL (PPO) RL (PPO + BLO)
部署机器人 G1 H1 G1
典型任务 开门、搬箱子 穿鞋、叠衣服 功夫、跑酷
关键创新 co-tracking + unified reward HST + 40-demo learning physics filter + BLO

⚠️ 常见陷阱

⚠️ 编程陷阱:物体位姿估计的累积漂移。视频中物体位姿的估计会随时间累积误差——特别是旋转估计。可用 BundleSDF/FoundationPose 等通用 6-DoF 跟踪方法减少漂移(注意这些不是 HDMI 官方管线的组件,HDMI 提取的是 object trajectory 并写入 meta.json),但累积误差不能完全消除。

💡 概念误区:HDMI 可以学任意交互。HDMI 需要从视频中提取物体轨迹——这要求物体在视频中始终可见且可跟踪。对于小物体、透明物体或形变物体,物体跟踪的准确度会下降。

练习

  1. [分析题] HDMI 的 residual action space 和标准 action space 的区别是什么?在什么任务上 residual 更有优势?
  2. [设计题] 如果你要用 HDMI 学习"从桌子上拿起杯子喝水",需要从视频中提取什么信息?列出人体和物体各需要的轨迹。

16.9 选型决策:MoCap/视频/文本 ⭐⭐

这一节解决什么问题:在三种动作来源之间做出工程选型决策。

三种来源的完整对比

维度 MoCap 视频 文本
精度 极高 (<1mm) 中等 (~5cm) 低 (~10cm+)
成本 极高(设备+场地+演员) 低(相机+视频源) 极低(键盘)
灵活性 低(需预录) 高(任何视频) 极高(打字即可)
实时性 否(需预录) 可(实时相机,如 HumanPlus) 可(实时文本,如 TextOp)
物体交互 可(需特殊设备) 可(HDMI co-tracking) 受限(需 VLA)
物理一致性 高(真实运动) 需 physics filter 需 tracker
数据规模 有限(40h AMASS) 无限(互联网视频) 无限(想象力)
Retarget 难度 中(SMPL→G1) 中(SMPL→G1) 低(直接在 G1 空间)
代表工具 AMASS + BeyondMimic WHAM + HDMI CALM/TextOp
代表系统 Ch15 BeyondMimic HumanPlus / HDMI TextOp / LeVERB

选型决策树(完整版)

你要解决什么问题?
│
├── "精确复现特定动作"(如特定舞蹈编排、科研复现)
│   └── 有 MoCap 设备?
│       ├── 是 → 自己录 MoCap → AMASS 格式 → BeyondMimic
│       └── 否 → AMASS 数据集中有类似动作?
│           ├── 是 → AMASS 子集 → BeyondMimic
│           └── 否 → 视频录制 → WHAM → retarget → BeyondMimic
│
├── "从视频学新技能"(如模仿 YouTube 视频中的运动)
│   ├── 只有人体运动?(走路、跑步、舞蹈)
│   │   └── WHAM → retarget → physics filter → BeyondMimic/AMP
│   └── 包含物体交互?(开门、搬东西)
│       └── HDMI (人体 + 物体 co-tracking → residual RL)
│
├── "文本控制实时交互"(如语音指令控制机器人服务员)
│   ├── 需要视觉理解?(看到桌子上的物体做决策)
│   │   └── LeVERB (VLA)
│   └── 纯文本指令?("走过去"、"跳一下")
│       └── TextOp (RobotMDAR + Tracker)
│
├── "遥操作 + 自主学习"(如工厂教工人教机器人)
│   └── HumanPlus (单 RGB 遥操 → 40 demos → HIT 自主)
│
├── "风格化运动"(如"像老年人一样走路"的风格控制)
│   └── CALM (text embedding → 条件判别器)
│
└── "统一控制器"(单一策略处理多种控制输入)
    └── 多种信息源(文本+关键帧+目标位置)?
        └── MaskedMimic (motion inpainting)

工程项目的典型选型

项目类型 推荐方案 理由 开发周期
学术论文 demo BeyondMimic + 精选 MoCap 精度最高,结果最好看 1-2 周
展会演示 TextOp + 预设 prompt 库 交互性强,观众可以尝试 2-3 周
遥操作数据收集 HumanPlus HST 高效数据收集管线 1-2 周(训 HST)
仓库搬运 POC HDMI + 视频 demo 物体交互能力 3-4 周
服务机器人 MVP TextOp + LeVERB 混合 文本导航 + 视觉抓取 4-6 周
运动技能库 AMASS + PHC PMCP 大规模不遗忘 2-4 周

数据混合策略

实际项目中,通常不会只用一种数据来源——而是混合使用:

策略 组合方式 适用场景 工程注意
MoCap + 文本 MoCap 训练 tracker,CALM 加文本条件 精确 + 灵活 text 标注覆盖 MoCap 数据
MoCap + 视频 MoCap 做基础,视频做领域增强 训练数据扩增 retarget 质量对齐
视频 + 文本 视频 + BABEL 标注 → TextOp 全自动管线 需要 BABEL 标注
全部混合 AMASS + 视频 + BABEL → 大规模统一训练 通用 tracker 数据质量分层管理

数据质量分层管理:混合使用不同来源的数据时,需要根据数据质量分配权重:

# 数据混合采样(概念性)
data_sources = {
    "mocap_amass": {"weight": 0.5, "quality": "high"},      # MoCap: 高质量
    "video_wham": {"weight": 0.3, "quality": "medium"},      # 视频: 中等质量
    "generated_textop": {"weight": 0.2, "quality": "low"},   # 文生: 低质量
}

# 高质量数据更多采样,低质量数据作为增强
# 但不能完全不用低质量数据——它们提供了多样性

技术路线的发展趋势

趋势 2023 2024 2025-2026 预测
精度 MoCap 主导 视频精度提升 文生精度改善 三者差距缩小
实时性 离线为主 HumanPlus 实时遥操 TextOp 实时文本 全部实时
物体交互 几乎无 ExBody 初步 HDMI 67次开门 通用交互
端到端 初步探索 LeVERB 58.5% VLA 主导

对工程师的建议:2025-2026 年是过渡期。如果你现在开始一个项目,分层架构(TextOp 模式)的可调试性和可靠性更好。但如果你在做中长期研究,端到端 VLA(LeVERB 模式)是值得投入的方向——它的任务泛化能力会随着 VLM 的进步而快速提升。

⚠️ 常见陷阱

🧠 思维陷阱:选最先进的方法就对了。先进不等于适合。如果你只需要让 G1 走到目的地,Ch14 的 velocity task 就够了——不需要 TextOp 或 LeVERB。方法的选择应该基于任务需求,不是论文的新旧。

💡 概念误区:多种数据源混合总是更好。如果低质量数据(如噪声大的视频估计)占比过高,会拉低整体训练效果——策略学到"平均化"行为。混合时必须加权——高质量数据主导,低质量数据辅助。

⚠️ 编程陷阱:不同来源的数据坐标系不一致。MoCap 数据、WHAM 输出、TextOp 生成的数据可能使用不同的坐标系约定(Y-up vs Z-up、不同的四元数约定)。混合前必须统一。

练习

  1. [决策题] 以下三个场景分别推荐什么方案?(a) 让 G1 在展览中表演太极拳,(b) 让 G1 在仓库中搬运箱子,(c) 让 G1 根据语音指令做日常动作。解释每个选择的理由。
  2. [设计题] 你需要为一个机器人服务员项目选择动作来源方案。要求:(1) 机器人需要走路送餐,(2) 需要避开障碍物,(3) 需要把盘子放在桌上。设计一个混合方案,说明每个子任务使用什么来源、什么算法。
  3. [跨章综合题,Ch14+Ch15+Ch16] 回顾 Ch14 的 velocity task、Ch15 的 motion tracking、和本章的多模态获取。它们形成了一个能力递进链路:velocity → tracking → multimodal。画出这条链路的技术依赖图,标注每一步复用了前一步的什么组件。

本章小结

知识点 核心要点 难度
动作来源三个时代 MoCap → 视频 → 文本,精度-成本-灵活性 trade-off ⭐⭐
MDM/VQ-VAE 扩散生成运动学轨迹 + token 化动作序列 ⭐⭐
CALM text embedding 注入条件判别器,~30行配置差异 ⭐⭐⭐
MaskedMimic 部分约束 → 补全完整动作,两阶段(PPO expert + BC 蒸馏) ⭐⭐⭐
TextOp RobotMDAR + BeyondMimic Tracker 两层架构,实时文本控制 ⭐⭐⭐
LeVERB 端到端 VLA,System 1(WBC) + System 2(VLM) ⭐⭐
视频管线 WHAM → SMPL → retarget → filter → tracking ⭐⭐
HumanPlus HST(AMASS+PPO) + HIT(40 demos),单 RGB 遥操作 ⭐⭐⭐
HDMI 视频 co-tracking 人体+物体 → residual action RL → 67 次开门 ⭐⭐
选型决策 MoCap/视频/文本 → 精度-成本-实时性决策树 ⭐⭐

本章与其他章节的关系

本章知识 前置来源(回顾) 后续应用(预告)
CALM 文本条件控制 Ch15 AMP 判别器 Ch20 多模态全身控制
MaskedMimic inpainting Ch14 HOVER mask + Ch15 BC Ch20 任务级控制
TextOp 两层架构 Ch15 BeyondMimic tracker Ch22 自定义 task
HumanPlus HST Ch15 tracking + AMASS Ch23 sim-to-real
HDMI co-tracking Ch15 tracking + Ch17 manipulation Ch20 loco-manipulation
视频管线 (WHAM→retarget→filter) Ch15 physics filter Ch22 数据管线
LeVERB CVAE Ch09 Teacher-Student + Ch15 tracker Ch22 VLA 集成
VQ-VAE tokenization Ch06 Reward 设计(连续→离散) Ch20 技能组合

关键数字速查

数字 含义 来源
512 CLIP ViT-B/32 text embedding 维度 16.1/16.2
~30 行 AMP → CALM 配置差异 16.2 ProtoMotions
0.7 MaskedMimic Phase 2 默认 mask_ratio 16.3
50 帧 TextOp RobotMDAR chunk_length (= 1 秒 @ 50Hz) 16.4
~20ms TextOp RobotMDAR 单 chunk 推理时间 16.4
58.5% LeVERB 总体成功率 16.5
150+ LeVERB 评估任务数 16.5
~5cm WHAM 身体姿态估计精度 16.6
25 Hz HumanPlus 相机频率 16.7
40 HumanPlus HIT 每任务 demo 数 16.7
19 HumanPlus HST 输出维度 (H1) 16.7
67 HDMI 连续开门穿越次数 16.8
14 HDMI 仿真中的任务数 16.8
32 LeVERB CVAE latent_dim 16.5

本章建立的核心能力检查

能力 验证方式 对应小节
理解三种动作来源的 trade-off 能为具体项目写选型报告 16.1, 16.9
运行 CALM 文本控制 ProtoMotions CALM 训练收敛 16.2
理解 MaskedMimic mask 机制 能解释不同 mask 对应什么控制 16.3
理解 TextOp 两层架构 能画出完整数据流 16.4
执行视频→G1 管线 YouTube → MuJoCo 回放成功 16.6
精读 HumanPlus 能解释 HST/HIT 每个组件 16.7
精读 HDMI 能解释 residual action 的工程好处 16.8
做出选型决策 能为 3 种场景推荐合适方案 16.9

累积项目 C(续):本章新增模块

模块清单

模块 状态 说明
CALM 文本控制 (ProtoMotions) AMP → CALM 配置切换
MaskedMimic 理解 两阶段训练 + mask 机制
TextOp 文生动作 RobotMDAR + Tracker 两层架构
视频 → G1 管线 WHAM → retarget → filter → tracking
HumanPlus HST 理解 transformer + AMASS + PPO
HDMI co-tracking 理解 人体 + 物体 + residual action + 统一 reward
LeVERB VLA 理解 System 1/2 + CVAE latent action
选型决策框架 三种来源对比 + 决策树

实践里程碑(建议用时 5-6 天)

里程碑 预计用时 完成标准 前置
M1: CALM 训练 6h ProtoMotions 中 CALM 收敛,3 个 prompt 生成不同动作 Ch15 M3 完成
M2: TextOp sim-to-sim 4h 键盘输入文本→G1 在 MuJoCo 中执行 Ch15 M1 完成
M3: 视频→G1 管线 6h 选一段 YouTube 视频→WHAM→retarget→MuJoCo 回放 Ch15 M2 完成
M4: 视频→tracking 6h M3 的动作→BeyondMimic 训练→G1 物理执行 M3
M5: HumanPlus 精读 3h 能画出 HST 架构图并解释每个组件
M6: HDMI 精读 3h 能画出三阶段管线并解释 residual action
M7: 选型报告 2h 为一个具体项目写选型决策文档 M1-M6

总计 ~30 GPU-hours(RTX 4090)。M1(CALM)和 M2(TextOp sim-to-sim)是最有价值的实践——它们让你亲手体验"打字→机器人动"的全流程。M3-M4 是完整的视频管线实践。M5-M7 是精读和综合应用。

从本章到下一章

本章的多模态动作获取建立了"从文本/视频/相机获取参考动作"的能力。但这些能力集中在全身运动——走路、跑步、舞蹈、遥操作。Ch17(机械臂与灵巧手操作)将转向精细操作——抓取、放置、装配。你会发现本章的很多方法(如 HDMI 的 residual action、HumanPlus 的 demo learning)在操作场景中也有对应——但 MDP 设计(obs 中有 object pose、action 是末端位置或关节角度)会有本质区别。

技术路线延续

Ch14 人形 velocity (基础运动)
  → Ch15 Motion Imitation (跟踪参考动作)
     → Ch16 多模态获取 (文本/视频 → 参考动作) ← 本章
        → Ch17 操作 (抓取/放置/装配)
           → Ch20 全身控制 (locomotion + manipulation)

累积项目完成检查

能力 验证方式 对应小节
理解 MoCap/视频/文本三种来源的 trade-off 能为具体项目写选型报告 16.1, 16.9
在 ProtoMotions 中运行 CALM CALM 训练收敛,文本控制有效 16.2
理解 MaskedMimic 的 mask 机制 能解释不同 mask 对应什么控制方式 16.3
理解 TextOp 的两层架构 能画出 RobotMDAR + Tracker 数据流 16.4
执行视频→G1 管线 从 YouTube 视频到 MuJoCo 回放成功 16.6
精读 HumanPlus 能解释 HST/HIT 的训练和部署流程 16.7
精读 HDMI 能解释 co-tracking 和 residual action 16.8

延伸阅读

学术论文

资料 难度 会议/期刊 说明
Tevet et al., "Human Motion Diffusion Model (MDM)," 2023 ⭐⭐ ICLR 2023 文生动作的里程碑
Zhang et al., "T2M-GPT," 2023 ⭐⭐ CVPR 2023 VQ-VAE + GPT 自回归
Tessler et al., "CALM," 2023 ⭐⭐⭐ SIGGRAPH 2023 条件对抗潜模型
Tessler et al., "MaskedMimic," 2024 ⭐⭐⭐ SIGGRAPH Asia 2024 统一 motion inpainting
"TextOp," 2026 ⭐⭐⭐ arXiv 2602.07439(预印本) 实时文生动作控制
Fu et al., "HumanPlus," 2024 ⭐⭐⭐ CoRL 2024 单 RGB 遥操作全栈
Weng et al., "HDMI," 2025 ⭐⭐⭐ arXiv 2509.16757 视频 → loco-manipulation
Xue et al., "LeVERB," 2025 ⭐⭐⭐ arXiv 2506.13751(投稿 ICLR 2026,审稿中) 层级 latent VLA
Radford et al., "CLIP," 2021 ⭐⭐ ICML 2021 文本-图像对齐基础

工具和代码

资料 难度 说明
NVlabs/ProtoMotions ⭐⭐⭐ CALM + MaskedMimic 统一框架
TeleHuman/TextOp ⭐⭐⭐ 实时文生动作,含预训练模型
MarkFzp/humanplus ⭐⭐⭐ HST + HIT 全栈代码
yohanshin/WHAM ⭐⭐ 全局平移准确的视频姿态估计
shubham-goel/4DHumans ⭐⭐ 全身+手部视频估计

阅读路线

  • 最小路线(文本控制):16.1→16.2 CALM 训练
  • 标准路线(文本+视频):上述 + 16.4 TextOp + 16.6 视频管线
  • 进阶路线(全栈理解):上述 + 16.7 HumanPlus + 16.8 HDMI
  • 研究路线:上述 + 16.3 MaskedMimic + 16.5 LeVERB

🔧 故障排查手册

# 症状 可能原因 排查步骤 相关小节
1 CALM 对所有文本生成相同动作 text embedding 未正确注入 打印判别器输入维度,确认包含 condition_dim 16.2
2 CALM 动作不自然 训练数据文字标注覆盖不足 检查 manifest 中的 text 字段覆盖率 16.2
3 MaskedMimic Phase 2 loss 不降 mask_ratio 太高 降低到 0.5,逐步增大 16.3
4 TextOp 响应延迟大 RobotMDAR 推理太慢 检查 GPU 利用率,减小 chunk_length 16.4
5 TextOp tracker 跟不上生成动作 训练数据不含生成数据 加入 RobotMDAR 生成数据到 tracker 训练集 16.4
6 视频 retarget 后脚穿地 坐标系未转换 (Y-up → Z-up) 检查旋转变换 16.6
7 WHAM 估计抖动严重 视频质量差或遮挡 用低通滤波器平滑 16.6
8 HumanPlus 遥操作延迟 25Hz 相机 → 50Hz 策略的插值问题 检查线性插值逻辑 16.7
9 HDMI 物体跟踪漂移 累积位姿误差 用通用 6-DoF 跟踪(如 BundleSDF/FoundationPose)或增加关键帧校正 16.8
10 混合数据训练效果差 数据质量权重不对 高质量数据主导(≥50%),低质量辅助(≤20%) 16.9

Debug Checklist

文生动作(CALM/TextOp)

  • [ ] CLIP model 版本和 condition_dim 匹配(ViT-B/32 → 512)
  • [ ] CLIP encoder 冻结(requires_grad=False)
  • [ ] Motion manifest 包含 text 字段
  • [ ] 训练数据覆盖目标 text prompt 的语义
  • [ ] TextOp 的 tracker 训练数据包含 RobotMDAR 生成数据
  • [ ] chunk_length 合理(50 帧 = 1 秒 @ 50Hz)

视频管线

  • [ ] 坐标系对齐(Y-up → Z-up)
  • [ ] 帧率对齐(视频 FPS → 50 Hz)
  • [ ] WHAM 输出包含 global_orient 和 transl
  • [ ] retarget 后 MuJoCo 回放检查通过
  • [ ] physics filter 通过率 > 50%
  • [ ] 关节角度在 G1 限位范围内

HumanPlus 遥操作

  • [ ] 外部 RGB 相机 ≥ 25 FPS
  • [ ] WHAM + HaMeR 实时运行无掉帧
  • [ ] retarget 频率与策略频率对齐
  • [ ] HST 在目标机器人上 zero-shot 验证通过

附录 A:文生动作算法谱系

Motion Diffusion Model (MDM, 2023)
  │  CLIP text → diffusion → motion sequence
  │
  ├── T2M-GPT (2023): VQ-VAE + GPT 自回归
  │   │  离散 token + 语言模型
  │   │
  │   └── MotionGPT (2024): 统一语言+动作 token
  │
  ├── MoMask (2024): masked + VQ-VAE + transformer
  │
  ├── MoMaDiff (2025): frame-wise VAE + masked autoregressive diffusion
  │
  └── DART (2025): diffusion + autoregressive
      │
      └── TextOp RobotMDAR (2026): DART 重构 + G1 部署

各方法的关键差异

方法 动作表示 生成方式 条件输入 生成长度
MDM 连续 非自回归扩散 文本 固定
T2M-GPT 离散 token GPT 自回归 文本 可变
MoMask 离散 token Masked diffusion 文本 + mask 可变
DART 连续 chunk 自回归扩散 文本 + 上下文 流式
TextOp 连续 (G1 29-DoF) 自回归扩散 文本 + 上下文 流式

TextOp RobotMDAR 和 DART 的区别:RobotMDAR 使用机器人骨骼表示(G1 29-DoF),DART 使用 SMPL 表示(72 维)。机器人骨骼表示避免了 retarget 误差——生成的动作可以直接被 tracker 消费。


附录 B:Retarget 工具选型

工具 输入 输出 特点
PHC convert_amass.py AMASS .npz IsaacGym pickle 最成熟,SMPL→多种人形
ProtoMotions retarget AMASS .npz ProtoMotions .npy PyRoki-based,多 simulator
TextOp retarget SMPL → G1 29-DoF CSV / NPZ G1 专用,质量高
HOVER human2humanoid SMPL → H1/G1 Isaac Lab 格式 HOVER 子模块
HumanPlus retarget SMPL-X → H1 33-DoF IsaacGym 含手部 retarget
手动 IK 任意 3D joints 任意机器人 最灵活但最费力

选择建议:如果用 mjlab → TextOp retarget(G1 专用)。如果用 ProtoMotions → 内置 retarget。如果用 IsaacGym → PHC convert_amass.py 或 HumanPlus retarget。


附录 C:CLIP Text Embedding 速查

常用 CLIP 模型

模型 Embedding 维度 推理速度 精度
ViT-B/32 512 快 (~5ms)
ViT-B/16 512 中 (~8ms)
ViT-L/14 768 慢 (~15ms) 最高

CALM 和 TextOp 通常使用 ViT-B/32——512 维足够表达动作语义,推理速度最快。

Text Prompt 设计经验

好的 text prompt 应该包含三个维度:

  1. 动作类型:"walk", "run", "jump", "kick"
  2. 风格描述:"slowly", "quickly", "gracefully", "aggressively"
  3. 方向/空间:"forward", "in a circle", "to the left"

示例:

Prompt 预期效果
"walk forward slowly" 慢速直线行走
"run forward quickly" 快速直线跑步
"jump in place" 原地跳跃
"wave right hand" 右手挥手
"sit down carefully" 缓慢坐下
"dance energetically" 活泼舞蹈

不好的 prompt: - 太模糊:"do something" → CALM 不知道做什么 - 太具体:"raise left arm to 45 degrees while bending right knee 30 degrees" → 超出 CALM 的控制精度 - 超出训练分布:"moonwalk while juggling" → 训练数据中可能没有


附录 D:视频姿态估计工具对比

安装和使用

WHAM(推荐用于全身+全局平移):

git clone https://github.com/yohanshin/WHAM.git
cd WHAM && pip install -e .
bash fetch_demo_data.sh
python demo.py --video input.mp4 --output_pth results/ --save_pkl

4D-Humans(HMR2.0,全身 SMPL body 重建与跟踪;不输出手部):

git clone https://github.com/shubham-goel/4D-Humans.git
cd 4D-Humans && pip install -e .
# 视频跟踪使用 track.py(官方命令),source 可为视频/帧目录/YouTube 链接
python track.py video.source="input.mp4"

HaMeR(专用手部估计,与 WHAM 配合):

git clone https://github.com/geopavlakos/hamer.git
cd hamer && pip install -e .
python demo.py --video_path input.mp4 --output_dir results/ --save_hands

精度对比

下表精度/FPS 为典型量级,具体值依 benchmark、硬件、分辨率与实现而异,不是固定指标。

工具 身体精度 手部精度 全局平移 FPS
WHAM ~4cm ✅ 好 ~20
4D-Humans ~5cm 无(仅 SMPL body) ⚠️ 弱 ~15
HybrIK ~5cm ⚠️ 弱 ~30
HaMeR ~2cm N/A ~25

组合推荐: - 全身运动(走、跑):WHAM alone - 全身+手部(遥操作):WHAM(身体)+ HaMeR(手部) - 快速原型:4D-Humans(全身 SMPL body 跟踪,不含手部)


附录 E:多模态管线实验记录模板

experiment:
  name: g1_text_walk_v1
  date: 2026-05-21
  pipeline: TextOp / CALM / Video / HumanPlus
  robot: Unitree G1 (29-DoF)

data_source:
  type: text / video / mocap
  # 文本:
  prompt: "walk forward slowly"
  clip_model: ViT-B/32
  # 视频:
  video_path: data/youtube_dance.mp4
  pose_estimator: WHAM
  retarget_tool: TextOp retarget
  physics_filter_pass_rate: 0.85
  # MoCap:
  mocap_file: data/amass/walking_01.npz

generation:
  method: TextOp_RobotMDAR / CALM / direct_tracking
  chunk_length: 50
  inference_time_ms: 18

tracking:
  method: BeyondMimic / HST
  mpjpe_mm: 55
  episode_length_ratio: 0.88

deployment:
  target: sim / sim2sim / real
  success_rate: 0.90

observations:
  quality: "动作自然,过渡平滑"
  issues:
    - "高速时手臂摆动略不自然"
    - "转向时有 0.3s 延迟"

next_steps:
  - "增大 shoulder_pitch std"
  - "减小 chunk_length 到 30"

附录 F:本章系统的技术栈总结

系统 高层 低层 数据 部署
CALM 条件判别器 PPO actor AMASS + BABEL text ONNX
MaskedMimic BC transformer PPO expert AMASS + 多模态标注 ONNX
TextOp RobotMDAR (DART) BeyondMimic tracker AMASS + BABEL + LAFAN1 ONNX + Jetson
HumanPlus HIT (decoder transformer) HST (decoder transformer) AMASS + 40 real demos IsaacGym → H1
HDMI N/A (单层 RL) PPO + residual action 视频 co-tracking ONNX → G1
LeVERB VLM (System 2) WBC (System 1) 合成渲染 demo Isaac Lab → G1

共同点:所有系统都建立在 RL-based motion tracking 的基础上——无论高层用的是什么方法(判别器/扩散/transformer/VLM),低层都需要一个物理可行的 tracking policy 来执行动作。这就是 Ch15 的 tracker 在整个体系中的核心地位。


附录 G:SMPL 表示速查

SMPL 模型参数

SMPL(Skinned Multi-Person Linear model)是几乎所有视频姿态估计和动作生成方法的标准人体表示。理解 SMPL 的参数对于 retarget 至关重要。

参数 维度 含义
betas (10,) 体形参数——控制身高、胖瘦等
global_orient (3,) root 朝向——axis-angle 表示
body_pose (69,) 23 个身体关节 × 3 axis-angle
transl (3,) root 全局平移
left_hand_pose (45,) SMPL-X 扩展:15 个手指关节
right_hand_pose (45,) SMPL-X 扩展:15 个手指关节
expression (10,) SMPL-X 扩展:面部表情

SMPL 关节到 G1 关节的映射

SMPL 有 24 个关节(每个 3-DoF ball joint),G1 有 29 个关节(每个 1-DoF hinge joint)。映射不是一一对应的——需要 IK 求解。

SMPL 关节 (24个):                  G1 关节 (29个):
  0: pelvis                          0-2: left_hip (3 DoF)
  1: left_hip                        3: left_knee (1 DoF)
  2: right_hip                       4-5: left_ankle (2 DoF)
  3: spine1                          6-8: right_hip (3 DoF)
  4: left_knee                       9: right_knee (1 DoF)
  5: right_knee                      10-11: right_ankle (2 DoF)
  6: spine2                          12-14: waist (3 DoF)
  7: left_ankle                      15-17: left_shoulder (3 DoF)
  8: right_ankle                     18: left_elbow (1 DoF)
  9: spine3                          19-20: left_wrist (2 DoF)
  10: left_foot                      21-23: right_shoulder (3 DoF)
  11: right_foot                     24: right_elbow (1 DoF)
  12: neck                           25-26: right_wrist (2 DoF)
  13: left_collar                    27-28: head (2 DoF)
  14: right_collar
  15: head
  16: left_shoulder
  17: right_shoulder
  18: left_elbow
  19: right_elbow
  20: left_wrist
  21: right_wrist
  22: left_hand
  23: right_hand

关键映射问题: - SMPL 的 hip 是 1 个 3-DoF ball joint → G1 的 hip 是 3 个 1-DoF hinge joint(需要分解) - SMPL 有 3 个 spine 关节 → G1 只有 1 个 waist (3-DoF)(需要合并) - SMPL 的 left_foot/right_foot 在 G1 中没有直接对应(G1 没有脚趾关节)

坐标系约定

系统 上方向 前方向 四元数约定
SMPL Y-up -Z
MuJoCo Z-up X (w, x, y, z)
PhysX/IsaacGym Z-up X (x, y, z, w)

Y-up → Z-up 转换

# 坐标系转换
def y_up_to_z_up(position):
    """SMPL (Y-up) → MuJoCo (Z-up)"""
    # x → x, y → z, z → -y
    return np.stack([position[..., 0], 
                     -position[..., 2], 
                     position[..., 1]], axis=-1)

这个转换是整个视频管线最容易出错的地方——忘记转换会导致 G1 "躺着"或"面朝天花板"。每次 retarget 后都要用 MuJoCo 回放检查。


附录 H:本章技术路线的时间线

年份 方法 关键贡献 本章位置
2021 CLIP text-image 对齐预训练 16.1 基础
2021 AMP 判别器替代 hand-crafted reward Ch15 前置
2023 MDM 文生动作扩散模型 16.1
2023 T2M-GPT VQ-VAE + GPT 动作生成 16.1
2023 CALM 条件判别器 + text embedding 16.2
2024 MaskedMimic 统一 motion inpainting 16.3
2024 HumanPlus 单 RGB 遥操作 + 40-demo 学习 16.7
2025 HDMI 视频 co-tracking 人体+物体 16.8
2025 DART 自回归扩散 16.4 前置
2025 LeVERB 端到端 VLA + CVAE 16.5
2026 TextOp 实时文生动作部署 16.4
2026 SafeFlow TextOp + 安全门控 延伸

趋势:从 2023 年的"概念验证"(MDM/CALM 在仿真中展示可行性)到 2025-2026 年的"工程落地"(TextOp/HDMI 在真机上部署),领域正在快速成熟。下一个前沿可能是多模态统一——一个模型同时接受文本、视觉、触觉输入,生成包含 locomotion + manipulation 的全身控制信号。

各系统的 GitHub 活跃度(截至 2026 年)

仓库 ⭐ 数 维护状态 推荐场景
NVlabs/ProtoMotions ~1.4k ✅ 积极维护 CALM/MaskedMimic 研究和教学
TeleHuman/TextOp ~370 ✅ 活跃 实时文生动作,含预训练模型
MarkFzp/humanplus ~1.0k ✅ 活跃 HST + HIT 全栈遥操作
yohanshin/WHAM ~800 ✅ 活跃 视频 → SMPL 姿态估计

工程建议:2026 年开始做多模态动作获取,最推荐的入门路径是 ProtoMotions CALM(文本控制的学术基础)或 TextOp(最接近产品级的文生动作系统)。HumanPlus 适合需要遥操作数据收集管线的项目。HDMI 适合需要物体交互的 loco-manipulation 项目。

从本章到后续章节的技术承接

本章能力 直接承接的后续章节 承接内容
CALM 文本控制 Ch20 全身控制 文本作为高层控制信号
MaskedMimic inpainting Ch20 统一控制器 mask 机制复用
TextOp 两层架构 Ch22 自定义 env 两层架构范式
视频管线 Ch22 数据管线 WHAM→retarget→filter
HumanPlus 遥操作 Ch23 sim-to-real 数据收集和部署
HDMI co-tracking Ch20 loco-manipulation 人体+物体联合跟踪

结语:动作来源的选择——MoCap、视频还是文本——不是"哪个更好"的问题,而是"任务需要什么"的问题。本章展示了每种来源的完整工程管线和代表系统(CALM/MaskedMimic、TextOp、HumanPlus、HDMI、LeVERB),以及它们之间的混合策略。所有这些系统都建立在 Ch15 的 motion tracking 基础之上——Ch15 的 tracker 是 Ch16 所有方法的物理执行层。如果你能理解"文本/视频 → 运动学轨迹 → tracking policy → 关节控制"这条链路中每一步的工程实现和限制,你就掌握了多模态动作获取的核心工程能力。

下一章(Ch17 机械臂与灵巧手操作)将从全身运动转向精细操作——MDP 设计、reward 和 obs 都会有本质区别。但本章建立的选型框架(MoCap vs 视频 vs 文本)和分层架构思想(高层决策 + 低层执行)在操作场景中同样适用。

本章的核心收获:不是记住每个系统的细节——而是建立"文本/视频/MoCap → 运动学轨迹 → tracking policy → 物理执行"这条完整链路的工程直觉。新系统出现时,你只需要问三个问题:(1) 它替换了链路中的哪个环节?(2) 它的输入和输出格式是什么?(3) 它与现有环节的接口如何对齐?