Files
vla-1.7b-qwen3-v2/tools/eval/decode_agent_tokens.py
ModelHub XC 42b9c1e645 初始化项目,由ModelHub XC社区提供模型
Model: EmpathicRobotics/vla-1.7b-qwen3-v2
Source: Original Platform
2026-08-31 04:48:17 +08:00

269 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""
Decode adaptive PCHIP agent tokens into 3D pose trajectories.
Takes the raw token string that the VLA model produces and reconstructs
the full (n_frames, 17, 3) skeleton trajectory via PCHIP interpolation.
Usage:
# Decode tokens from stdin
echo "<fps_30> <pelvis> <pelvis_t_0> ..." | python decode_agent_tokens.py
# Decode from a text file
python decode_agent_tokens.py --input generated_tokens.txt
# Decode and save JSON
python decode_agent_tokens.py --input tokens.txt --output poses.json
Token format (one 8-frame window):
<fps_30>
<pelvis> <pelvis_t_0> <pelvis_x_N> <pelvis_y_N> <pelvis_z_N>
<pelvis_t_7> <pelvis_x_N> <pelvis_y_N> <pelvis_z_N> </pelvis>
<r_hip> <r_hip_t_0> <r_hip_x_N> <r_hip_y_N> <r_hip_z_N> ... </r_hip>
...17 joints...
Dequantization: coord = N / 255.0 * 4.0 - 2.0 (metres, range [-2, 2])
Time tokens: frame index 0-7 within the 8-frame window
Reconstruction: PCHIP interpolation over control points -> 8 frames
H36M joint order (17 joints):
0 pelvis 4 l_hip 8 thorax 12 l_elbow 16 r_wrist
1 r_hip 5 l_knee 9 nose 13 l_wrist
2 r_knee 6 l_ankle 10 head_top 14 r_shoulder
3 r_ankle 7 spine 11 l_shoulder 15 r_elbow
"""
import argparse
import json
import re
import sys
import numpy as np
from scipy.interpolate import PchipInterpolator
WINDOW_FRAMES = 8
COORD_RANGE = 2.0
JOINT_NAMES = [
"pelvis", "r_hip", "r_knee", "r_ankle",
"l_hip", "l_knee", "l_ankle",
"spine", "thorax", "nose", "head_top",
"l_shoulder", "l_elbow", "l_wrist",
"r_shoulder", "r_elbow", "r_wrist",
]
JOINT_INDEX = {name: i for i, name in enumerate(JOINT_NAMES)}
N_JOINTS = len(JOINT_NAMES)
def dequantize(n: int) -> float:
return n / 255.0 * (2.0 * COORD_RANGE) - COORD_RANGE
def parse_window(tokens: list[str]) -> dict:
"""Parse tokens for a single 8-frame window into per-joint control points.
Args:
tokens: list of token strings like ['<fps_30>', '<pelvis>', '<pelvis_t_0>', ...]
Returns:
dict with fps (int) and joints (dict mapping joint name to
t_indices ndarray and cp_coords ndarray of shape (n_cp, 3)).
"""
fps = 30
if tokens and tokens[0].startswith("<fps_"):
fps = int(re.match(r"<fps_(\d+)>", tokens[0]).group(1))
tokens = tokens[1:]
joints = {}
i = 0
while i < len(tokens):
m = re.match(r"^<([a-z_]+)>$", tokens[i])
if not m or m.group(1) not in JOINT_INDEX:
i += 1
continue
name = m.group(1)
close = f"</{name}>"
i += 1
t_indices = []
coords = []
while i < len(tokens) and tokens[i] != close:
tm = re.match(rf"<{name}_t_(\d+)>$", tokens[i])
if tm and i + 3 < len(tokens):
t_indices.append(int(tm.group(1)))
xm = re.match(rf"<{name}_x_(\d+)>$", tokens[i + 1])
ym = re.match(rf"<{name}_y_(\d+)>$", tokens[i + 2])
zm = re.match(rf"<{name}_z_(\d+)>$", tokens[i + 3])
if xm and ym and zm:
coords.append([
dequantize(int(xm.group(1))),
dequantize(int(ym.group(1))),
dequantize(int(zm.group(1))),
])
i += 4
continue
i += 1
if i < len(tokens) and tokens[i] == close:
i += 1
if t_indices:
joints[name] = {
"t_indices": np.array(t_indices, dtype=int),
"cp_coords": np.array(coords, dtype=np.float32),
}
return {"fps": fps, "joints": joints}
def reconstruct(parsed: dict) -> np.ndarray:
"""PCHIP-interpolate sparse control points into a full per-frame trajectory.
Window length is inferred from the data itself (max t_index + 1 seen
across all joints) rather than a fixed constant -- 2026-07-22 (REPORT.md
#38): windows are now 24 frames (t up to 23), but this decoder is also
used on older 8-frame data (t up to 7), so it must handle both without
being told which convention a given token string uses.
Returns ndarray of shape (window_frames, 17, 3) in metres, root-centred.
"""
max_t = WINDOW_FRAMES - 1
for jdata in parsed["joints"].values():
if len(jdata["t_indices"]):
max_t = max(max_t, int(jdata["t_indices"].max()))
window_frames = max_t + 1
t_out = np.arange(window_frames, dtype=np.float64)
traj = np.zeros((window_frames, N_JOINTS, 3), dtype=np.float32)
for name, jdata in parsed["joints"].items():
j = JOINT_INDEX[name]
t_cp = jdata["t_indices"].astype(np.float64)
cp = jdata["cp_coords"]
if len(t_cp) < 2:
traj[:, j, :] = cp[0]
continue
for d in range(3):
traj[:, j, d] = PchipInterpolator(t_cp, cp[:, d])(t_out)
return traj
def decode(token_str: str) -> list[np.ndarray]:
"""Decode a token string into a list of (8, 17, 3) trajectories.
Handles both single windows and multiple consecutive windows.
"""
all_tokens = re.findall(r"<[^>]+>", token_str)
if not all_tokens:
return []
# Split on <fps_N> boundaries — each is one window
window_starts = [i for i, t in enumerate(all_tokens) if t.startswith("<fps_")]
if not window_starts:
parsed = parse_window(all_tokens)
return [reconstruct(parsed)]
trajectories = []
for wi, start in enumerate(window_starts):
end = window_starts[wi + 1] if wi + 1 < len(window_starts) else len(all_tokens)
parsed = parse_window(all_tokens[start:end])
trajectories.append(reconstruct(parsed))
return trajectories
def to_json(trajectories: list[np.ndarray], fps: int = 30) -> dict:
"""Convert decoded trajectories to a JSON-serialisable dict.
Per-window frame count is read from each trajectory's own shape (not a
fixed constant) -- see reconstruct()'s docstring, 2026-07-22."""
windows = []
cum_frames = 0
for i, traj in enumerate(trajectories):
n_frames_this = traj.shape[0]
motion = np.linalg.norm(traj[-1] - traj[0], axis=-1)
top_movers = sorted(
[(JOINT_NAMES[j], round(float(motion[j]), 4)) for j in range(N_JOINTS)],
key=lambda x: x[1], reverse=True,
)
n_missing = sum(1 for name in JOINT_NAMES if name not in
{JOINT_NAMES[j] for j in range(N_JOINTS) if np.any(traj[:, j, :] != 0)})
windows.append({
"window": i,
"time_sec": round(cum_frames / fps, 4),
"trajectory": traj.tolist(),
"value_range_m": [round(float(traj.min()), 4), round(float(traj.max()), 4)],
"top_movers": top_movers[:5],
"joints_all_zero": n_missing,
})
cum_frames += n_frames_this
total_frames = sum(traj.shape[0] for traj in trajectories)
return {
"n_windows": len(trajectories),
"total_frames": total_frames,
"duration_sec": round(total_frames / fps, 4),
"shape": [len(trajectories)] + list(trajectories[0].shape) if trajectories else [],
"value_range_m": [round(float(min(t.min() for t in trajectories)), 4),
round(float(max(t.max() for t in trajectories)), 4)] if trajectories else [0, 0],
"joint_names": JOINT_NAMES,
"windows": windows,
}
def main():
p = argparse.ArgumentParser(description="Decode agent tokens to 3D poses.")
p.add_argument("--input", "-i", default=None,
help="File containing agent tokens (default: read stdin)")
p.add_argument("--output", "-o", default=None,
help="Save decoded poses to JSON file")
args = p.parse_args()
if args.input:
with open(args.input, "r") as f:
token_str = f.read()
else:
token_str = sys.stdin.read()
token_str = token_str.strip()
if not token_str:
print("No tokens provided.", file=sys.stderr)
sys.exit(1)
trajectories = decode(token_str)
if not trajectories:
print("Could not parse any agent windows from input.", file=sys.stderr)
sys.exit(1)
result = to_json(trajectories)
print(f"Decoded {result['n_windows']} windows "
f"({result['total_frames']} frames, {result['duration_sec']}s)")
print(f"Shape: {result['shape']} (windows, frames, joints, xyz)")
print(f"Value range: {result['value_range_m']} m")
for w in result["windows"][:3]:
print(f"\n Window {w['window']} (t={w['time_sec']}s):")
if w["joints_all_zero"] > 0:
print(f" WARNING: {w['joints_all_zero']} joints are all-zero (missing)")
print(f" Top movers: ", end="")
print(", ".join(f"{name} {d:.3f}m" for name, d in w["top_movers"]))
if result["n_windows"] > 3:
print(f"\n ... {result['n_windows'] - 3} more windows")
if args.output:
with open(args.output, "w") as f:
json.dump(result, f, indent=2)
print(f"\nSaved to: {args.output}")
if __name__ == "__main__":
main()