twanghcmut's picture
|
download
raw
8.98 kB

Is the predicted object flow (Phi) correct? — causal ck-25000, val/199

Answer: no, and two separate things are wrong. Also note this run predicts RGB + object flow only — there is no depth. ABLATION=+flow sets gamma_sdf=0, and the live training log confirms loss_sdf=0.000000, sdf_valid_frac=0.000000. The SDF/depth term has never been on in this run (the gate-4 depth anchor failed earlier and was left out).

Finding 1 — the _flow.mp4 rollout videos contain NO ground truth

scripts/rollout_replay_traj.py:646:

videos_cat_flow = np.concatenate([videos_flow, videos_flow], axis=-3)

Both halves of the flow panel are the same predicted Phi, duplicated to match the RGB video's height. Measured difference between the halves: 0.85/255 = pure mp4 codec noise. Any "GT vs pred" read off those videos is comparing the prediction against itself.

Finding 2 — 73% of every rollout is a frozen action against a frozen target

val/199 is a 33-frame episode (video_length=33, latents (33,4,24,40)). The replay runs 22 chunks covering GT frame ids 0..88. get_traj_info clamps out-of-range ids to the last frame, so:

chunk gt_frame_ids action spread
0 0–4 6.267
1–5 4–24 0.05–0.12
6–21 24–88 0.00000

Chunks 8–21 contain 1 unique action row out of 70. From chunk 6 on — 16 of 22 chunks — the model is fed a constant frozen action and scored against a frozen GT frame.

This invalidates the interpretation of every PSNR number gathered so far: causal 16.9, base 20.0, and the whole guidance sweep are ~73% measured over a dead segment where nothing is asked to change. It also fully explains the earlier "stable, no divergence for 85 frames" result — nothing was moving. Rollouts should use ~7 chunks for a 33-frame episode.

Finding 3 — predicted Phi lands where GT Phi is not

Comparing predicted Phi against genuinely decoded GT Phi latents, over the valid chunks only (frames 0–32), active pixel = |Phi − grey| > 12:

view GT active GT mean|Phi| PRED mean|Phi| IoU(pred, GT)
ext1 0.72% 0.916 1.599 0.008
ext2 1.31% 1.190 2.070 0.003
wrist 7.11% 2.474 1.427 0.004

The control that makes this readable — GT Phi against itself:

view IoU(t, t+1) IoU(t, t+2) IoU(t, random t)
ext1 0.148 0.065 0.036
ext2 0.169 0.052 0.038
wrist 0.110 0.070 0.052

So prediction-vs-GT (0.003–0.008) sits 5–10x below the random-frame baseline (0.036–0.052). The predicted flow is not merely imprecise; it occupies regions where the GT flow essentially never is.

The magnitudes point the same way: the model over-produces flow in the two external views (1.6–2.1 vs GT 0.9–1.2) and under-produces it in the wrist view (1.4 vs GT 2.5) — which is the view with by far the most real motion (7.11% active vs 0.72%/1.31%).

The caveat that matters

GT Phi is itself only 0.11–0.17 IoU between adjacent frames. The supervision target is so sparse and temporally incoherent that it is weak to learn from at all. Finding 3 should not be read purely as "the Phi head is broken" — the target it is being trained against is close to noise from one frame to the next. Fixing the flow-extraction stability is likely a prerequisite to any Phi head working.


Follow-up: WAFT is fine. The post-processing destroys it.

latent_videos_flow (raw WAFT) and latent_videos_phi (the Phi actually trained on) both exist for val/99, so they can be compared directly. Each stream has a different zero-motion reference — measured medians: raw WAFT = 254 (WHITE, colour-wheel centre), Phi = 128 (GREY). Scoring WAFT against grey makes a static background look 100% "active"; that is a measurement artifact, not saturation.

Measured against the correct reference for each:

stream view active% mean dist IoU(t,t+1)
WAFT raw ext1 5.62% 3.53 0.359
WAFT raw ext2 6.07% 4.08 0.425
WAFT raw wrist 62.62% 39.28 0.666
Phi ext1 3.44% 1.85 0.271
Phi ext2 1.56% 1.38 0.204
Phi wrist 6.88% 2.32 0.128

Raw WAFT is 2–5x more temporally coherent. The construction on top of it discards 90% of the wrist signal and 74% of ext2, and drops coherence from 0.67 to 0.13.

WAFT_raw_vs_Phi_3views_val99.mp4 shows why. At frame 18 WAFT gives a crisp, clean flow field — the arm silhouette in both exterior views, a smooth ego-motion gradient in the wrist — while the Phi built from it reads 0.3% / 0.1% / 0.0% active. The wrist Phi is completely empty.

Where the signal goes

extract_causal_latent.py:1034

keep = (~rob) & (lat > args.phi_noise_m) & np.isfinite(mag)
  1. ~rob removes the robot. In the exterior views the robot arm is the dominant moving thing, so the strongest, cleanest part of WAFT's output is deleted by design.
  2. lat > phi_noise_m with phi_noise_m = 0.01 m/step. At the 5 Hz video rate that is a 5 cm/s floor — most genuine object motion in these trajectories is slower than that.

What survives is a flickering ~1% residue (IoU 0.13–0.27 between adjacent frames), which is the target the Phi head is being trained against.

Corroborating signal from causal_aux/val/199.npz: gripper_contact is 0 for all 33 frames, and contact never exceeds 0.282, on a trajectory whose instruction is "Move the green towel to the right". motion_evidence peaks at 0.015 against m_ref = 0.0518. The contact gate — the core switch of the causal factorization — never fires, because the Phi it is computed from has almost nothing in it.

Conclusion: the flow estimator is not the problem. phi_noise_m and the ~rob mask are.


The fix: Phi from 2-D WAFT flow, no depth at all

Why the metric anchoring existed, and why it goes

The contract wanted Phi's magnitude to mean "how fast is the object moving relative to the end-effector": m = |v_obj| / (kappa * |v_ee|). EE speed comes from kinematics in m/s, so the object's speed had to be in m/s too -- pixel flow is not comparable across depths. Hence Depth-Anything-3 + RANSAC anchoring to convert pixels to metres.

Legitimate goal, but the price is a monocular-depth chain that fails on 43% of trajectories, and the payoff is only a normalisation. The model consumes Phi as a latent image; pixels vs metres is a choice of scale, not of information.

Phi[...,0] = clip(u / flow_max[view], -1, 1)
Phi[...,1] = clip(v / flow_max[view], -1, 1)
Phi[...,2] = 0
Phi = 0 inside the (dilated) robot silhouette

Same 3-channel [-1,1] layout as before, so "no motion = grey" and zero_phi_latent.pt stay valid and nothing on the model side changes.

flow_max is per-view, because the cameras are not alike

Non-robot flow magnitude, 40 sampled trajectories:

view p50 p90 p95 p99 p99.9
ext1 0.03 px 0.19 0.40 6.09 22.1
ext2 0.03 px 0.19 0.46 6.42 21.9
wrist 3.66 px 23.0 31.0 49.3 88.0

The exterior cameras are genuinely static (p90 = 0.19 px). The wrist camera moves with the arm. One shared flow_max would either saturate the wrist or push exterior object motion below what the VAE can resolve, so: flow_max = 20 / 20 / 50.

Validation before committing to a re-extract

Boundary leakage -- is the surviving Phi real motion, or flow bleeding off the mask edge? Active Phi pixels bucketed by distance to the robot silhouette, 25 sampled trajectories:

view 0-3 px 3-8 px 8-20 px >20 px
ext1 7.1% 17.0% 23.6% 52.2%
ext2 6.9% 13.0% 19.4% 60.8%
wrist 0.4% 1.4% 3.8% 94.4%

Only ~7% hugs the silhouette; the majority sits >20 px away. Not leakage.

Result on val/99 (NEW_phi2d_vs_OLD_val99.mp4):

view OLD active NEW active NEW IoU(t,t+1)
ext1 3.54% 4.90% 0.298
ext2 1.61% 2.50% 0.311
wrist 7.07% 42.33% 0.474

Honest reading: in the exterior views this is a moderate gain (~1.4x the active pixels, comparable coherence). The point is not density -- it is that the new Phi does not ride on a depth estimate that fails 43% of the time.

Wrist: keeping ego-motion, deliberately

The wrist camera moves, so its non-robot flow is the scene sliding past (77.5% of pixels per frame), not object motion -- which is why the original code excluded the wrist from motion_evidence. The options were to zero it, to compensate ego-motion (needs depth again), or to keep it. Keeping it was chosen deliberately by the user, accepting that the wrist Phi channel is ego-motion rather than object motion. Recording it here so a later reader does not mistake it for an oversight.

Xet Storage Details

Size:
8.98 kB
·
Xet hash:
50780834c7929bfff067aa178009ad98019cd556ed60d2a2a567050fed0365ff

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.