EgoWM Lane B — Iter 208 Blueprint & Strategic Report
Date: 2026-07-10 · Author: Haoran Geng (@geng-haoran) with Claude Opus 4.7 (1M context) · Branch:
h200_1_devConsolidates Iter 204-207 evidence, strategic positioning, and the concrete architecture / parameter-reuse plan for the next production iteration (v10). Written after v9b (dense root_head) partial validation.
Quick links: Executive Summary · Where We Are · Evidence Base · Positioning · Architecture · Depth Head · Milestones · Downloads
Media companion
Two qualitative videos from v9b (step 200) predictions on ARCTIC val:
Hand-pose overlay — 4-clip grid, 49 frames each, GT (green/cyan) vs baseline (yellow, v8d-like) vs v9b lift (red):
3D wrist trajectory — right-hand wrist path in camera frame, view rotates:
Static reference for viewers without video playback:
Iter 205 dashboard · 6 panels: oracle MPJPE decomposition, orient collapse per source,
gt-vs-pmap scatter, per-clip s* boxplot, wrist-Z error before/after rescale, and the
ARCTIC error waterfall.
Iter 206 · GT-2D + per-clip s* wrist depth lift oracle upper bounds: ARCTIC 203 → 54 mm.
Iter 207 · v9a lift trainer wired end-to-end; pooled-features pixel-localization
ceiling identified.
0. Executive summary
- Iter 204-207 completed 6 diagnostics in ~48 h (orient oracle · pmap wrist anchor · 2D+depth+lift oracle · lift trainer · dense root_head fix · SomantisModel integration).
- Root cause of v8d's 213 mm ARCTIC MPJPE plateau:
feats.mean(dim=(2,3))over 30×52 spatial tokens strips sub-token spatial information; the head cannot localize wrist pixel below ~150 px on average, blowing past the Iter 206 oracle's σ=5 px budget. - Fix confirmed: v9b dense root_head + soft-argmax (Iter 207b) drops pixel-error median from ~300 px (v9a) to 117 px at step 200 — a −61 % median improvement. Long tail (25 % of frames >216 px) remains; caused by missing presence/visibility head.
- The 2D+depth+lift formulation (user's proposed reframing, Iter 206) has a proven Iter 206 oracle upper bound of 54 mm on ARCTIC (3.8× better than v8d's 213 mm), and a matching independent implementation exists at SomantisModel v14d (13.4 px 2D-MPJPE).
- Iter 208 architecture: dense conv on Wan feature grid → soft-argmax heatmap for root_uv + pointmap; pooled MLPs for hand articulation, MANO, camera; clip-pool MLPs for betas + per-clip metric scale (Iter 205 finding). Total ~15 M new head params on top of frozen (or last-4-unfrozen) 1.3 B Wan trunk.
- Depth head parameter reuse from VGGT-1B (32.65 M point_head weights, already at
_ckpt/VGGT-1B/model.safetensors): reuse the DPT + fusion + refinenet blocks; add a small Wan-1536 → VGGT-2048 adapter (~10 M); warm-start Stage 1 with point_head frozen → should give usable output within 500 steps. - Strategic positioning: EgoWM occupies the intersection of
[video foundation model]×[metric grounded output]×[egocentric hand-object interaction]. Fills a gap between EgoWaM (world dynamics for policy), LVP (task-conditioned video generation), and SomantisModel (hand-only 2D+lift). - Cosmos 3 Nano 16B is single-GPU FT-viable with LoRA + 8-bit optimizer + gradient checkpointing (~50-60 GB on a GH200 96 GB card). This has been confirmed by a private reference implementation; the recipe is a fork of NVIDIA's cosmos-framework with LoRA hooks + activation-return points added at the MoT (Mixture-of-Transformers) boundaries. Cosmos 3 becomes a serious parallel-track candidate for Phase 4.
1. Where we are (2026-07-09 status)
1.1 Latest ckpts on disk
| Run | Recipe | Val MPJPE | Val pix (norm MSE) | Notes |
|---|---|---|---|---|
| v8d step 3000 | frozen + unfreeze4 + depth cond, hand 61-d MSE | 213 mm | — | Baseline; 6 wks of head-only recipe tuning ceiling |
| v9a step 200 | + LiftHead (u_norm, v_norm, s_log) from pooled features | 246 mm | 0.099 | Pixel from pooled = structural cap; killed at 200 |
| v9b step 200 | + DenseRootHead on 30×52 grid → soft-argmax | 256 mm (mean, long-tailed) | 0.048 | Median 117 px pixel err vs v9a 309 mean; needs presence head |
1.2 Codebase state
New in Iter 204-207 (all diagnostic, no production code touched):
- data_engine/orient_oracle_diag.py (Iter 204)
- data_engine/wrist_depth_anchor_diag.py (Iter 205)
- data_engine/lift_2d_depth_oracle.py (Iter 206)
- data_engine/train_egowm_lift.py (Iter 207, v9a trainer)
- data_engine/train_egowm_lift_v2.py (Iter 207b, v9b dense-head trainer)
- data_engine/eval_lift_mpjpe.py (Iter 207)
- data_engine/viz_iter{205,206,207}*.py — 5 visualization scripts
- data_engine/viz_iter207_videos.py — mp4 renderers with matplotlib.animation + ffmpeg
- data_engine/build_iter207_ppt.py — 16-slide PPT builder (_viz/EgoWM_Lane_B_Iter204_207_summary_video.pptx)
1.3 Assets on disk
- 1.4 TB DA3 pointmap cache at
_cache/da3_*/— 33 077 clips × 7 datasets, 100 % coverage - VGGT-1B safetensors at
_ckpt/VGGT-1B/model.safetensors(1.26 B params, 32.65 M point_head) - SomantisModel reference (private) — champion v14d (val_mpjpe_px 13.4, PCK@0.10 0.777)
2. Evidence base (Iter 204-207 verified facts)
Each row is an oracle-level number (not a training result), so they set upper bounds on any recipe.
| # | Finding | Evidence | Numeric |
|---|---|---|---|
| E1 | Trans is 3× the lever of orient on ARCTIC | Iter 204 GT-swap oracle | 213 → oracle-orient 164 (−49) → oracle-orient+trans 17 mm (−147) |
| E2 | Orient collapse is per-source-mean, not universal rodrigues bug | Iter 204 per-source pred_spread | arctic 16.5° / h2o 51.9° (not collapsed) / ho3dv3 5.9° |
| E3 | HO3Dv3 is a training negative asset | Iter 204 + 206 GT-swap on HO3Dv3 | oracle_orient makes it worse (155→188 mm); lift oracle worse (155→212 mm) — GT labels themselves noisy |
| E4 | DA3 pointmap XY is accurate but Z has per-source-per-clip metric bias | Iter 205 wrist-anchor diag | XY 30 mm mean err; Z 102/60/199 mm before; 24/33/26 mm after per-clip s* rescale |
| E5 | Per-source s* median | Iter 205 | arctic 1.23 · h2o 0.95 · ho3dv3 1.65 |
| E6 | 2D+depth+lift oracle beats 3D-MANO 3-4× | Iter 206 21-joint MPJPE with GT-2D + per-clip s* | ARCTIC 203 → 54 mm; H2O 183 → 65 mm; HO3Dv3 155 → 212 |
| E7 | Detector accuracy: σ=5 px costs 9 mm on ARCTIC | Iter 206 noise sweep | σ=0→42 · σ=2→44 · σ=5→51 · σ=10→67 |
| E8 | Per-joint error grows with distance from wrist | Iter 206 | wrist 24 mm · MCP 30 · PIP 53 · fingertip 60-83 |
| E9 | v8d pooled-features head has structural pixel cap | Iter 207 v9a training | val_pix 0.099 (RMS ~137 px) even after 200 steps at loss weight 1.0 |
| E10 | Dense root_head + soft-argmax breaks the cap | Iter 207b v9b training | val_pix 0.145 → 0.048 in 200 steps; pixel-err median 300 → 117 |
| E11 | Data-engine debts | Iter 204 stratified sampling | asmhand val 100% masked (Iter 200 side-effect); dexycb/hot3d partial OOB in manifest |
| E12 | Pool-Wan trunk features carry orient signal | Iter 204 H2O H2O not collapsed | Suggests the trunk features CAN encode orientation; the head just can't extract it from pooled |
3. Strategic positioning
3.1 What we're building vs. adjacent projects
| Project | What it predicts | Backbone | Data | Our overlap |
|---|---|---|---|---|
| EgoWaM (Georgia Tech RL2, Danfei Xu) | Future world dynamics (VAE pixels / DINO / 3D motion flow) as auxiliary supervision for policy transfer | HPT trunk + swappable heads | Robot teleop + in-domain human + EgoVerse in-the-wild (1362 h) | Both ego video; they use hand pose as input, we predict it |
| Large Video Planner (LVP) | Video plans conditioned on scene + text task, retargeted to robot action | 14 B latent diffusion (Wan-based) | LVP-1M (1.4 M clips, Ego4D + DROID) | Wan lineage; they generate futures, we ground the present |
| SomantisModel v14d | 2D hand kp (21 kp + present + vis) → MANO via HaMeR crop + PnP lift | Frozen ViTPose-H HaMeR init | URDS (7 datasets) + InterHand + replay synth + wild coco | Same 2D+lift philosophy; we integrate it into a video foundation model |
| VGGT | Joint (depth + pointmap + camera + tracks) feed-forward | 1B DINOv2-based aggregator | ScanNet + ARKitScenes + CO3D + BlendedMVS + Waymo etc. | Same "joint geometry prediction" goal; we borrow their DPT head |
| One4D | RGB + pointmap co-diffused via decoupled LoRA | Wan2.1-Fun 14B | Video with pointmap teacher | Same "geometry heads on Wan" recipe |
| EgoWM (ours) | RGB (rectified-flow) + depth + camera + hand 3D, all with metric supervision | Wan 2.1-1.3B (frozen + last-4 unfrozen) | 33 k clips, 7 ego hand-object datasets, full DA3+VAE cache | The joint model — none of the above jointly predicts hand + camera + depth + generation |
3.2 What's unique about EgoWM
- Head-heavy adaptation on a frozen video foundation model — 15 M trainable heads over 1.3 B trunk. SomantisModel does this on ViTPose-H (hand-only); we generalize to multi-stream.
- Physics-grounded reconstruction, not policy or generation — EgoWaM predicts dynamics for policy; LVP generates plans; we predict metric structure of the present frame.
- Multi-teacher grounding — DA3 for depth (cached), URDS for hand+camera GT, SomantisModel v14d (future) for pseudo-2D-labels at web scale.
- PnP-based lift instead of direct 3D regression — respects physical constraints; Iter 204/206 already showed direct regression has a plateau.
3.3 Wan vs. Cosmos 3 (updated with TRI info)
Corrections vs. earlier internal base-model doc:
- Cosmos 3 Nano (16B) is single-GPU FT-viable on a GH200 96 GB card with standard
PEFT engineering (LoRA + 8-bit optimizer + grad checkpointing → ~50-60 GB usage).
A private reference implementation (fork of
nvidia/cosmos-frameworkwith LoRA hooks added at the MoT boundaries) confirms this. The 8×H100 recipe is NVIDIA's default full-SFT cookbook, not a hard requirement. - Cosmos 3 license is now OpenMDW 1.1 (commercial-friendly), not the earlier restrictive NVIDIA Open Model License with safety-guardrail termination. The license blocker from our docs/08 is removed.
- Cosmos 3 has native 57-D egocentric motion action embedding — potentially the exact interface we want for downstream policy work.
Recommended posture: - Phase 1-3 stays Wan 2.1-1.3B (Iter 208-215): our diagnostic evidence is on Wan, the head recipe already runs, ecosystem has 4+ joint-geometry precedents. - Phase 4 opens a parallel Cosmos 3 Nano track (Iter 220+): validate whether the reasoning transformer's features are already grounded (single-GPU inference probe first, LoRA head-only FT if promising).
4. Iter 208 architecture blueprint
4.1 Overall data flow
RGB video (B, 3, 49, 480, 832) → Wan VAE (frozen) → latent (B, 16, 13, 60, 104)
│ + cond[:, :16, :1]=frame-0 latent; cond[:, 16, :]=DA3 depth teacher (still injected)
▼
Wan 2.1 DiT trunk (1536-d, 30 blocks) — frozen except patch_embed + last-4 blocks
│ patchify (1, 2, 2) → 13 × 30 × 52 = 20 280 tokens
▼
feats: (B, 20280, 1536) → reshape feats_grid (B, 13, 30, 52, 1536)
│
├── mean(2,3) → pooled (B, 13, 1536) # per-frame global
├── mean(1,2,3) → clip_pool (B, 1536) # per-clip global
│
DENSE heads (need spatial):
RootHead(feats_grid) → heatmap → soft-argmax → root_uv (B, 13, 2, 2) ← Iter 207b tested
PointmapHead(feats_grid, DA3_cache) → (B, 13, 4, 60, 104) X, Y, Z, log_conf
← VGGT weight reuse
POOLED heads (per-frame global):
CamPoseHead(pooled) → (B, 13, 7) # t + quat
PresenceHead(pooled) → (B, 13, 2) # per-hand present logit
LogScaleHead(pooled) → (B, 13, 2) # 2D hand size log-scale
JrelHead(pooled) → (B, 13, 2, 20, 2) # root-relative 20 joints
VisHead(pooled) → (B, 13, 2, 21) # per-joint visibility
ManoRotHead(pooled) → (B, 13, 2, 16, 6) # 6D: go + 15 hand_pose
CLIP-POOLED heads (per-clip scalar):
BetasHead(clip_pool) → (B, 2, 10) # MANO betas per hand per clip
SMetricHead(clip_pool) → (B, 2) # depth rescale per hand per clip
← Iter 205 finding
Total new params (heads): ~15-30 M depending on depth head reuse strategy (see §5).
4.2 Head params breakdown
| Head | Type | Params | Warm start |
|---|---|---|---|
| RootHead | Conv2d(1536→256→256→2) + soft-argmax | 500 K | v9b step 200 |
| PointmapHead (VGGT reuse) | Adapter(1536→2048) + VGGT DPT | 32.6 M frozen + 10 M trainable | See §5 |
| CamPoseHead | LayerNorm + Linear(1536→512→7) | 800 K | v8d cam_head |
| PresenceHead | LayerNorm + Linear(1536→128→2) | 200 K | fresh |
| LogScaleHead | Same shape | 200 K | fresh |
| JrelHead | LayerNorm + Linear(1536→512→80) | 800 K | fresh (zero-init) |
| VisHead | LayerNorm + Linear(1536→256→42) | 400 K | fresh |
| ManoRotHead | LayerNorm + Linear(1536→512→192) | 1.1 M | v8d hand_head first 48 dims |
| BetasHead | LayerNorm + Linear(1536→128→20) | 200 K | v8d hand_head last 20 dims |
| SMetricHead | LayerNorm + Linear(1536→64→2) | 100 K | fresh |
| Total trainable | ~15 M (with VGGT frozen) | Mixed |
4.3 Loss recipe (Stage 2 — full head + adapter)
L = w_cam_t * MSE(cam_pred[..., :3], cam_gt[..., :3]) +
w_cam_R * geodesic(cam_pred[..., 3:], cam_gt[..., 3:]) +
w_pmap_z * SSI_depth(pmap_pred[..., 2], da3_gt[..., 2], conf=pmap_pred[..., 3]) +
w_pmap_xy * L1(pmap_pred[..., :2], da3_gt[..., :2]) +
w_pmap_conf * BCE(pmap_pred[..., 3], da3_valid_mask) +
w_present * BCE(present, hand_valid) +
w_root * 5.0 * masked_MSE(root_uv, gt_root_uv, mask=present) +
w_scale * L1(logscale, gt_logscale) +
w_jrel * masked_MSE(jrel, gt_jrel, mask=vis[:, 1:]) +
w_vis * BCE(vis, gt_vis) +
w_mano * mano_geodesic(mano_rot, gt_mano_rot) +
w_smetric * MSE(s_metric_log, GT_wrist_z_ratio_log)
Weight recommendation (informed by Iter 204/205 sensitivity):
- w_root = 5.0 (pixel is the bottleneck for downstream lift)
- w_pmap_z = 0.3, w_pmap_xy = 0.1, w_pmap_conf = 0.1
- w_smetric = 0.5 (per-clip anchor, small but critical)
- Rest = 1.0
4.4 Training curriculum
Stage 1 · Heads-only warm-up (~500 steps, all trunk frozen incl. patch_embed): - Only train new heads (~15 M params, fast) - LR 3e-4, cosine decay - Warm-start relevant params from v8d + v9b - Freeze VGGT point_head as adapter learns to project Wan features - Goal: verify all heads output reasonable values
Stage 2 · Adapter FT (~2000 steps, unfreeze patch_embed + last-4 blocks): - Recipe identical to v8d (proven working) - LR 1e-5 trunk / 1e-4 heads (param groups) - Depth cond still active on cond[:, 16] - Goal: ARCTIC 21-joint MPJPE < 70 mm (within Iter 206 oracle bound of 54 mm)
Stage 3 · Web-scale distill (~5000 steps): - Add SomantisModel v14d as pseudo-2D-labeler for the 33 k pool + wild coco - Pseudo-label loss weight 0.5 (below GT's 1.0) - Fix data-engine debts (asmhand mask, dexycb OOB, drop HO3Dv3) - Goal: val PCK@0.10 > 0.6
Stage 4 · Multi-stream unified (~10 000 steps): - Add rectified-flow RGB loss to restore generation capability - Balance pmap / cam / hand / rgb four losses - Goal: joint model + FVD matches base Wan
4.5 Inference-time metric 3D lift
def infer_metric_3d(pred, K):
# 1. Decode 21 kp 2D via SomantisModel representation.py
uv_pred = decode(pred['root_uv'], pred['logscale'], pred['jrel']) # (B, T, 2, 21, 2)
uv_pix = normalize_to_pixel(uv_pred, H_img=480, W_img=832)
# 2. FK MANO in canonical frame (trans=0)
go_R = rot6d_to_matrix(pred['mano_rot'][..., 0, :])
pose_R = rot6d_to_matrix(pred['mano_rot'][..., 1:, :])
J0_can = mano_fk(go_R, pose_R, pred['betas'].expand_time(T), trans=0) # (B, T, 2, 21, 3)
# 3. Wrist-Z init from pointmap + s_metric
z_init = pmap_at_pixel(pred['pmap'], uv_pix[..., 0, :]) * pred['s_metric'].exp()
t_init = init_translation_from_z(K, uv_pix[..., 0, :], z_init)
# 4. PnP (SomantisModel pnp.solve_transl, 30 iter Gauss-Newton)
weights = pred['vis'].sigmoid()
t = solve_transl_batch(J0_can, uv_pix, weights, K, t_init)
# 5. Assemble
kp_3d = J0_can + t.unsqueeze(-2)
return dict(kp_3d=kp_3d, uv=uv_pix, cam_pose=pred['cam'], pointmap=pred['pmap'])
5. Depth head deep-dive: VGGT parameter reuse
5.1 Current state: no active depth head
egowm/models/egowm_toy.py defines a zero-init decoupled depth head but
make_model sets z_depth=0 → head disabled. Depth is currently used only as
input conditioning into cond[:, 16]. Iter 204 (v6c) showed that with a frozen
trunk this conditioning is math-noop (patch_embedding cond channels zero-init +
frozen). Iter 207 confirmed the cond only starts affecting output when the trunk
is unfrozen.
5.2 VGGT-1B available structure (verified 2026-07-09)
At _ckpt/VGGT-1B/model.safetensors:
| Module | Params | Fraction |
|---|---|---|
| aggregator (DINOv2-based backbone) | 909.1 M | 72.4 % |
| camera_head (transformer + MLP) | 216.2 M | 17.2 % |
| track_head | 65.9 M | 5.2 % |
| point_head (DPT for pointmap) | 32.65 M | 2.6 % |
| depth_head (DPT for depth) | 32.65 M | 2.6 % |
| TOTAL | 1256.5 M | 100 % |
point_head and depth_head are identical DPT architectures (same 62 tensors),
differing only in the final output_conv2.2 output channel (4 for pointmap+conf vs.
2 for depth+conf).
5.3 VGGT DPT structure
Following the canonical DPT design (also used by DA3, MiDaS, Depth-Anything V2):
Input: ViT-like features (B*T, 2048, Hp, Wp)
│ norm (2048)
│
├── projects.0-3: 4 parallel Conv2d(2048→256/512/1024/1024, 1×1)
│ × multi-scale expansion
│
├── resize_layers.0-3:
│ [0] ConvTranspose2d(256→256, k=4, s=4) # 4× spatial upsample
│ [1] ConvTranspose2d(512→512, k=2, s=2) # 2× upsample
│ [2] Identity
│ [3] Conv2d(1024→1024, k=3, s=2) # 0.5× downsample
│
├── scratch.layer1-4_rn: 1×1 or 3×3 conv to unify → 256 channels
│
├── scratch.refinenet4/3/2/1: FeatureFusionBlock chain (bottom-up)
│ Each block = 2× ResidualConvUnit + out_conv
│ Progressively fuse multi-scale features
│
├── scratch.output_conv1: Conv2d(256→128, 3×3)
├── scratch.output_conv2:
│ [0] Conv2d(128→32, 3×3)
│ [1] SiLU (implicit)
│ [2] Conv2d(32→C_out, 1×1) where C_out = 4 (X, Y, Z, log_conf)
│
Output: (B*T, 4, H_out, W_out)
The Depth-Anything V3 reference implementation of DPT (Apache 2.0, ByteDance)
matches VGGT layer-for-layer and can be vendored directly — repo:
ByteDance/Depth-Anything-3,
file depth_anything_3/model/dpt.py.
5.4 Parameter reuse strategy: 3 options ranked
Strategy A (recommended): Adapter + VGGT point_head
class EgoWMPointmapHead(nn.Module):
"""Wan features → adapter → VGGT DPT.
Adapter: 10M trainable. VGGT DPT: 32.65M frozen (Stage 1) or LoRA (Stage 3)."""
def __init__(self, wan_dim=1536, vggt_ckpt='_ckpt/VGGT-1B/model.safetensors',
out_hw=(60, 104)):
super().__init__()
# trainable adapter — 10 M
self.adapter = nn.Sequential(
nn.LayerNorm(wan_dim),
nn.Linear(wan_dim, 2048), nn.GELU(),
nn.Linear(2048, 2048))
# VGGT DPT structure (import from vendored file)
from egowm.models.dpt import DPT
self.dpt = DPT(dim_in=2048, output_dim=3, use_sky_head=False,
patch_size=1, features=256, use_conf=True)
# weight load from VGGT.safetensors
self._load_vggt_weights(vggt_ckpt)
# freeze VGGT initially
for p in self.dpt.parameters(): p.requires_grad_(False)
# residual delta for correcting DA3 metric bias
self.delta = nn.Conv2d(2048, 4, 1)
nn.init.zeros_(self.delta.weight); nn.init.zeros_(self.delta.bias)
def _load_vggt_weights(self, ckpt_path):
from safetensors.torch import load_file
sd = load_file(ckpt_path)
point_sd = {k[len('point_head.'):]: v for k, v in sd.items() if k.startswith('point_head.')}
info = self.dpt.load_state_dict(point_sd, strict=False)
print(f'VGGT point_head warm-start: missing={len(info.missing_keys)} '
f'unexpected={len(info.unexpected_keys)}')
def forward(self, feats_grid, da3_pmap=None):
# feats_grid (B, T, Hp=30, Wp=52, 1536) → adapter → VGGT DPT
B, T, Hp, Wp, _ = feats_grid.shape
adapted = self.adapter(feats_grid).permute(0, 1, 4, 2, 3).reshape(B*T, 2048, Hp, Wp)
dpt_out = self.dpt(adapted) # (B*T, 4, H, W)
delta_out = F.interpolate(self.delta(adapted), size=dpt_out.shape[-2:],
mode='bilinear', align_corners=False)
out = (dpt_out + delta_out).view(B, T, 4, dpt_out.shape[-2], dpt_out.shape[-1])
if da3_pmap is not None:
# residual on DA3: at step 0, delta=0, VGGT gives its own pred → averaged with DA3 by loss weight
pass # or: out[..., :3] += da3_pmap resized (Stage 2)
return dict(pmap=out[..., :3, :, :], log_conf=out[..., 3:, :, :])
Pros: - ✅ 32.65 M VGGT weights come for free (proven on ScanNet + CO3D etc.) - ✅ Only 10 M adapter is trainable initially — fast Stage 1 convergence - ✅ Zero-init delta layer preserves residual learning path
Cons: - ⚠️ Need to write DPT class (~300 LoC) matching VGGT's exact layer names - ⚠️ Wan features (1536) and VGGT expected features (2048) have different distributional statistics — adapter must bridge
Strategy B: Same as A but also load depth_head (2× teachers)
Load both point_head (XYZ + conf) and depth_head (Z + conf); average their outputs or use them as two teachers. Extra 32.65 M of frozen params, 0 extra trainable.
Strategy C: DPT from scratch (no VGGT reuse)
Skip VGGT entirely, DPT random-init on Wan features. Simpler (no adapter, no weight loading), but expected to converge 3-5× slower than A/B.
5.5 Loss for depth head
def depth_loss(pmap_pred, log_conf, da3_gt, gt_wrist_z, gt_valid, s_metric):
"""SSI depth + XY L1 + confidence BCE + sparse metric anchor."""
conf = log_conf.sigmoid()
valid = torch.isfinite(da3_gt).all(dim=2) & (da3_gt[..., 2, :, :] > 1e-3)
# SSI on log-depth (per-frame median normalize)
lp = torch.log(pmap_pred[..., 2, :, :].clamp_min(1e-3))
lg = torch.log(da3_gt[..., 2, :, :].clamp_min(1e-3))
med_p = lp.masked_fill(~valid, 0).median(dim=(-1,-2), keepdim=True)
med_g = lg.masked_fill(~valid, 0).median(dim=(-1,-2), keepdim=True)
L_ssi = ((lp - med_p) - (lg - med_g)).abs()[valid] * conf[valid]
L_ssi = L_ssi.mean()
L_xy = F.l1_loss(pmap_pred[..., :2, :, :][valid], da3_gt[..., :2, :, :][valid])
L_conf = F.binary_cross_entropy(conf, valid.float())
L_metric = F.mse_loss(
s_metric[gt_valid] * pmap_at(pmap_pred[..., 2], wrist_uv)[gt_valid],
gt_wrist_z[gt_valid])
return 0.3 * L_ssi + 0.1 * L_xy + 0.1 * L_conf + 0.5 * L_metric
5.6 Improvement axes not implemented in first pass
Deliberately deferred to future iters:
- Temporal smoothness: warp depth[t] via cam_pose[t]→[t-1], L1 residual. Adds ~200 LoC. Low ROI initially since DiT already has temporal awareness.
- Multi-teacher distillation: DA3 + VGGT + DepthPro. Add once first pass validated.
- Higher output resolution: currently 60×104 (VAE latent). Upsample to 240×416 for fingertip detail. 4× compute; not needed if fingertip error already <20 mm.
- Depth ↔ hand consistency loss: wrist_z from depth vs. wrist_z from PnP-solved trans. Add in Stage 3 once both heads stable.
6. Data-engine debts to fix
Discovered during Iter 204-206 stratified sampling:
| # | Issue | Impact | Fix |
|---|---|---|---|
| D1 | asmhand val 100% masked (Iter 200 mask NaN global_orient side-effect) | 4255 clips of asmhand contribute zero signal to both train and val | Either (a) get real global_orient labels for asmhand, or (b) train a separate 2D-only head on asmhand that doesn't need MANO orient, or (c) drop asmhand |
| D2 | dexycb / hot3d_aria / hot3d_quest3 manifest OOB start indices | frame_indices out of bounds errors; eval silently skips |
Audit data_engine/build_*_manifest.py; regen manifest with per-zarr max_frames check |
| D3 | HO3Dv3 is a training negative (Iter 204 orient oracle + Iter 206 lift oracle both show GT is noisier than model) | 910 clips of HO3Dv3 lower overall metrics | Drop HO3Dv3 from Iter 208+ pool |
| D4 | ARCTIC dominates the all7 pool at 54.5 % | Per-source-mean collapse risk (Iter 204 finding) | Stratified sampling or per-source loss weighting in Stage 3 |
7. Milestone plan (Phase 1-4)
| Phase | Iters | Target metric | Deliverable | Backbone |
|---|---|---|---|---|
| 1 · Break pooled cap | 208-210 | ARCTIC 21-joint MPJPE < 70 mm | v10 ckpt + full-stack eval | Wan 2.1-1.3B |
| 2 · Web-scale distill | 211-215 | val PCK@0.10 > 0.6; depth AbsRel < 15% | 4-stream benchmark + qualitative gallery | Wan 2.1-1.3B |
| 3 · Multi-stream unified | 216-220 | joint hand+cam+depth < 30 mm; FVD ≈ base Wan | Paper-ready results | Wan 2.1-1.3B (unfrozen more) |
| 4 · Cosmos 3 parallel track | 220+ | Compare with Cosmos 3 Nano head-only FT | Cross-backbone comparison table | Cosmos 3 Nano 16B (LoRA) |
| — · Paper | 216+ | CVPR/ICCV positioning: "Grounded fine-tuning of video foundation models for egocentric scene understanding" | Manuscript | — |
8. Concrete Iter 208 implementation checklist
Ordered by dependency:
Prep (Day 1 morning):
- [ ] Copy depth_anything_3/model/dpt.py → egowm/models/dpt.py (rename, keep Apache-2.0 header)
- [ ] Copy SomantisModel/hand2d/representation.py → egowm/hand2d/representation.py
- [ ] Copy SomantisModel/hand2d/pnp.py → egowm/hand2d/pnp.py
- [ ] Verify VGGT point_head weight load into DPT class (test script)
Head implementation (Day 1 afternoon):
- [ ] egowm/models/heads/pointmap_head.py — EgoWMPointmapHead from §5.4
- [ ] egowm/models/heads/root_head.py — move DenseRootHead from train_egowm_lift_v2.py
- [ ] egowm/models/heads/hand2d_heads.py — all decoupled heads (present/logscale/jrel/vis/mano/betas/smetric)
- [ ] egowm/models/heads/cam_head.py — from v8d cam_head with quaternion normalization
Trainer (Day 2):
- [ ] data_engine/train_egowm_v10.py — Stage 1 (frozen trunk, 500 steps heads-only)
- [ ] Extend GT loader in LaneBCachedDataset.__getitem__ to output SomantisModel decoupled repr
- [ ] Multi-head loss module egowm/losses/joint_v10_loss.py
- [ ] Warm-start logic: v9b step 200 → root + s_log → new heads; v8d → hand_head → mano_rot + betas
Eval + viz (Day 3):
- [ ] data_engine/eval_full_stack.py — MPJPE + PCK@0.10 + AbsRel + PA + orient_geodesic
- [ ] data_engine/viz_iter208_full_stack.py — 3-clip video: RGB + depth heatmap + hand skeleton + pointmap 3D
- [ ] docs/EXPLORATION_LOG.md Iter 208 entry
Total: 3 focused days. Budget: 800 LoC.
9. Open questions carried forward
-
VGGT feature-space compatibility: does the Wan → VGGT 2048 adapter actually align feature distributions? Ablation: linear probe VGGT DPT on frozen Wan features vs. frozen VGGT aggregator features on ARCTIC depth GT. If the gap is small, adapter works; if huge, we may need to also unfreeze VGGT DPT early.
-
Cosmos 3 reasoning transformer features: are they already grounded (physics-aware)? Single-GPU inference probe on ARCTIC 100 clips → linear probe → GT hand kp3d. If R² > 0.7, we should port the head architecture to Cosmos 3 immediately.
-
PnP failure modes: when 2D detection has heavy tail (e.g. Iter 207b's p75=216 px), does PnP still converge? Add PnP outlier rejection (RANSAC-style) or use confidence weighting.
-
asmhand rescue: instead of drop, can we retrofit hand pose from SomantisModel v14d as pseudo-label? Then asmhand becomes a valuable diverse source rather than a debt.
-
Long-tail on dense root_head (Iter 207b p75=216 px): caused by out-of-frame / occluded frames? Presence head should filter these — verify in Iter 208.
10. Bottom line
The Iter 204-207 diagnostic arc converged on a decisive architectural pivot: from direct 3D-MANO regression via pooled features (Wan v1-v8d ceiling: 200 mm ARCTIC MPJPE) to a joint (2D + depth + camera + MANO shape) prediction with dense conv + soft-argmax + PnP-based metric 3D lift. The Iter 206 oracle upper bound (54 mm on ARCTIC) is mechanically reachable, SomantisModel v14d independently verified the recipe works at 13.4 px 2D MPJPE, and VGGT-1B's 32.65 M point_head weights are available for reuse.
Iter 208 is a 3-day / 800-LoC implementation that binds all this into a v10 checkpoint. After that, Phase 2-4 is data + scale + downstream, not more architectural search.
Downloads
Companion assets for this report:
| Asset | Path |
|---|---|
| Slide deck (16 slides, embedded videos, 8.2 MB) | assets/decks/EgoWM_Lane_B_Iter204_207_summary_video.pptx |
| Hand-pose overlay video (mp4, 2.5 MB) | assets/videos/iter207_hand_pose_overlay.mp4 |
| 3D wrist trajectory video (mp4, 1.2 MB) | assets/videos/iter207_wrist_trajectory_3d.mp4 |
| Iter 205 dashboard (PNG) | assets/figures/iter205_dashboard.png |
| Iter 205 convergence (PNG) | assets/figures/iter205_convergence.png |
| Iter 205 qualitative frames (PNG) | assets/figures/iter205_qualitative.png |
| Iter 206 lift oracle (PNG) | assets/figures/iter206_lift_oracle.png |
| Iter 207 lift training (PNG) | assets/figures/iter207_lift_train.png |
| Iter 207 hand pose overlay static (PNG) | assets/figures/iter207_hand_pose_overlay.png |
| Iter 207 wrist trajectory static (PNG) | assets/figures/iter207_wrist_trajectory.png |
Meta
- Author: Haoran Geng (@geng-haoran) with Claude Opus 4.7 (1 M context) as pair-programmer.
- Referenced commits:
f9c2e8a(Iter 203 orient collapse diag) →0e6028b(Iter 207 video PPT deck) →2dd59ce(this report's source indocs/21_*.md). - Internal cross-references: the engineering exploration log lives in
docs/EXPLORATION_LOG.md(Iter 190-207 chronology) anddocs/20_depth_fusion_analysis.md(Iter 204-206 deep dive). These are internal and not part of the public report bundle.