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_dev

Consolidates 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 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 lift oracle Iter 206 · GT-2D + per-clip s* wrist depth lift oracle upper bounds: ARCTIC 203 → 54 mm.

Iter 207 lift training Iter 207 · v9a lift trainer wired end-to-end; pooled-features pixel-localization ceiling identified.


0. Executive summary


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


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

  1. 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.
  2. Physics-grounded reconstruction, not policy or generation — EgoWaM predicts dynamics for policy; LVP generates plans; we predict metric structure of the present frame.
  3. Multi-teacher grounding — DA3 for depth (cached), URDS for hand+camera GT, SomantisModel v14d (future) for pseudo-2D-labels at web scale.
  4. 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:

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:

  1. 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.
  2. Multi-teacher distillation: DA3 + VGGT + DepthPro. Add once first pass validated.
  3. 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.
  4. 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.pyegowm/models/dpt.py (rename, keep Apache-2.0 header) - [ ] Copy SomantisModel/hand2d/representation.pyegowm/hand2d/representation.py - [ ] Copy SomantisModel/hand2d/pnp.pyegowm/hand2d/pnp.py - [ ] Verify VGGT point_head weight load into DPT class (test script)

Head implementation (Day 1 afternoon): - [ ] egowm/models/heads/pointmap_head.pyEgoWMPointmapHead 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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