| |
| """Audit all T-Rex track caches and investigate near-static wrist tracks.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import hashlib |
| import json |
| import math |
| from collections import Counter |
| from concurrent.futures import ThreadPoolExecutor, as_completed |
| from pathlib import Path |
|
|
| import cv2 |
| import numpy as np |
| import pyarrow.parquet as pq |
|
|
| VIEW_SLICES = { |
| "head_left": (0, 100), |
| "left_wrist": (100, 175), |
| "right_wrist": (175, 250), |
| } |
| WRIST_GROUPS = { |
| "left_wrist": {"background": (100, 125), "hand": (125, 175)}, |
| "right_wrist": {"background": (175, 200), "hand": (200, 250)}, |
| } |
| IMAGE_SCALE = np.array([320.0, 180.0], dtype=np.float32) |
| WINDOW_FRAMES = 768 |
| WINDOW_OVERLAP = 64 |
| WINDOW_STEP = WINDOW_FRAMES - WINDOW_OVERLAP |
|
|
|
|
| def _parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--dataset-root", |
| type=Path, |
| default=Path("/scratch1/home/zhicao/dreamzero/data/trex_full_force"), |
| ) |
| parser.add_argument( |
| "--output-dir", |
| type=Path, |
| default=Path( |
| "/scratch1/home/zhicao/dreamzero/data/" |
| "trex_full_force/audit/track_quality" |
| ), |
| ) |
| parser.add_argument("--static-span-px", type=float, default=2.0) |
| parser.add_argument("--video-samples", type=int, default=5) |
| parser.add_argument("--video-workers", type=int, default=8) |
| parser.add_argument("--skip-video-check", action="store_true") |
| return parser.parse_args() |
|
|
|
|
| def _quantiles(values: list[float]) -> dict[str, float]: |
| array = np.asarray(values, dtype=np.float64) |
| return { |
| name: float(np.percentile(array, percentile)) |
| for name, percentile in ( |
| ("min", 0), |
| ("p01", 1), |
| ("p05", 5), |
| ("median", 50), |
| ("p95", 95), |
| ("p99", 99), |
| ("max", 100), |
| ) |
| } |
|
|
|
|
| def _point_span_px(tracks: np.ndarray) -> np.ndarray: |
| pixel_tracks = np.asarray(tracks, dtype=np.float32) * IMAGE_SCALE |
| return np.sqrt( |
| np.ptp(pixel_tracks[..., 0], axis=0) ** 2 |
| + np.ptp(pixel_tracks[..., 1], axis=0) ** 2 |
| ) |
|
|
|
|
| def _seam_metrics( |
| tracks: np.ndarray, |
| visibility: np.ndarray, |
| ) -> tuple[float, float, float]: |
| frames = int(tracks.shape[0]) |
| boundaries = list(range(WINDOW_FRAMES, frames, WINDOW_STEP)) |
| if not boundaries: |
| return math.nan, math.nan, math.nan |
|
|
| pixel_tracks = np.asarray(tracks, dtype=np.float32) * IMAGE_SCALE |
| delta = np.linalg.norm(np.diff(pixel_tracks, axis=0), axis=-1) |
| visible_pair = (visibility[1:] > 0.5) & (visibility[:-1] > 0.5) |
| seam_indices = np.asarray([boundary - 1 for boundary in boundaries], dtype=np.int64) |
| seam_values = delta[seam_indices][visible_pair[seam_indices]] |
|
|
| regular_indices = np.unique( |
| np.rint(np.linspace(0, max(0, frames - 2), min(128, frames - 1))).astype( |
| np.int64 |
| ) |
| ) |
| regular_indices = regular_indices[ |
| ~np.isin(regular_indices, seam_indices) |
| ] |
| regular_values = delta[regular_indices][visible_pair[regular_indices]] |
| seam_p95 = ( |
| float(np.percentile(seam_values, 95)) if seam_values.size else math.nan |
| ) |
| regular_p95 = ( |
| float(np.percentile(regular_values, 95)) |
| if regular_values.size |
| else math.nan |
| ) |
| ratio = ( |
| seam_p95 / max(regular_p95, 0.1) |
| if np.isfinite(seam_p95) and np.isfinite(regular_p95) |
| else math.nan |
| ) |
| return seam_p95, regular_p95, ratio |
|
|
|
|
| def _audit_track( |
| path: Path, |
| episode_index: int, |
| expected_frames: int, |
| ) -> tuple[dict[str, object], list[str]]: |
| errors: list[str] = [] |
| with np.load(path, allow_pickle=False) as payload: |
| tracks = np.asarray(payload["tracks"], dtype=np.float32) |
| visibility = np.asarray(payload["vis"], dtype=np.float32) |
| if tracks.shape != (expected_frames, 250, 2): |
| errors.append(f"tracks shape {tracks.shape} != {(expected_frames, 250, 2)}") |
| if visibility.shape != (expected_frames, 250): |
| errors.append( |
| f"visibility shape {visibility.shape} != {(expected_frames, 250)}" |
| ) |
| if errors: |
| return {"episode_index": episode_index, "frames": expected_frames}, errors |
|
|
| finite = np.isfinite(tracks).all(axis=-1) |
| in_frame = ((tracks >= 0.0) & (tracks <= 1.0)).all(axis=-1) |
| binary_visibility = (visibility == 0.0) | (visibility == 1.0) |
| if not finite.all(): |
| errors.append("non-finite track coordinates") |
| if not in_frame.all(): |
| errors.append("out-of-range normalized track coordinates") |
| if not binary_visibility.all(): |
| errors.append("non-binary visibility") |
|
|
| row: dict[str, object] = { |
| "episode_index": episode_index, |
| "frames": expected_frames, |
| "finite_fraction": float(finite.mean()), |
| "in_frame_fraction": float(in_frame.mean()), |
| "binary_visibility_fraction": float(binary_visibility.mean()), |
| } |
| for view, (start, end) in VIEW_SLICES.items(): |
| view_tracks = tracks[:, start:end] |
| view_visibility = visibility[:, start:end] |
| span = _point_span_px(view_tracks) |
| row[f"{view}_visibility"] = float(view_visibility.mean()) |
| row[f"{view}_span_median_px"] = float(np.median(span)) |
| row[f"{view}_span_p95_px"] = float(np.percentile(span, 95)) |
| row[f"{view}_static_point_fraction"] = float(np.mean(span < 1.0)) |
| if episode_index >= 1737: |
| seam_p95, regular_p95, seam_ratio = _seam_metrics( |
| view_tracks, |
| view_visibility, |
| ) |
| else: |
| seam_p95, regular_p95, seam_ratio = math.nan, math.nan, math.nan |
| row[f"{view}_seam_jump_p95_px"] = seam_p95 |
| row[f"{view}_regular_jump_p95_px"] = regular_p95 |
| row[f"{view}_seam_jump_ratio"] = seam_ratio |
|
|
| for view, groups in WRIST_GROUPS.items(): |
| for group, (start, end) in groups.items(): |
| span = _point_span_px(tracks[:, start:end]) |
| row[f"{view}_{group}_span_median_px"] = float(np.median(span)) |
| row[f"{view}_{group}_span_p95_px"] = float( |
| np.percentile(span, 95) |
| ) |
| return row, errors |
|
|
|
|
| def _video_path(root: Path, episode_index: int, view: str) -> Path: |
| return ( |
| root |
| / "videos" |
| / f"chunk-{episode_index // 1000:03d}" |
| / f"observation.images.{view}" |
| / f"episode_{episode_index:06d}.mp4" |
| ) |
|
|
|
|
| def _sample_video_motion( |
| root: Path, |
| episode_index: int, |
| view: str, |
| sample_count: int, |
| ) -> dict[str, object]: |
| path = _video_path(root, episode_index, view) |
| cap = cv2.VideoCapture(str(path)) |
| frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| frames: list[np.ndarray] = [] |
| if frame_count > 0: |
| sample_indices = np.rint( |
| np.linspace(0, frame_count - 1, sample_count) |
| ).astype(int) |
| for frame_index in sample_indices: |
| cap.set(cv2.CAP_PROP_POS_FRAMES, int(frame_index)) |
| ok, frame = cap.read() |
| if not ok: |
| continue |
| gray = cv2.resize( |
| cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), |
| (80, 45), |
| interpolation=cv2.INTER_AREA, |
| ).astype(np.float32) |
| frames.append(gray) |
| cap.release() |
| if len(frames) < 2: |
| return { |
| "episode_index": episode_index, |
| "view": view, |
| "video_error": f"decoded only {len(frames)} sampled frame(s)", |
| } |
| adjacent = [ |
| float(np.mean(np.abs(second - first))) |
| for first, second in zip(frames, frames[1:]) |
| ] |
| return { |
| "episode_index": episode_index, |
| "view": view, |
| "video_frames": frame_count, |
| "video_adjacent_mad_mean": float(np.mean(adjacent)), |
| "video_adjacent_mad_max": float(np.max(adjacent)), |
| "video_first_last_mad": float(np.mean(np.abs(frames[-1] - frames[0]))), |
| "video_first_frame_hash": hashlib.sha256( |
| frames[0].astype(np.uint8).tobytes() |
| ).hexdigest()[:16], |
| "video_sample_hash": hashlib.sha256( |
| np.stack(frames).astype(np.uint8).tobytes() |
| ).hexdigest()[:16], |
| } |
|
|
|
|
| def _arm_motion(root: Path, episode_index: int) -> dict[str, float]: |
| path = ( |
| root |
| / "data" |
| / f"chunk-{episode_index // 1000:03d}" |
| / f"episode_{episode_index:06d}.parquet" |
| ) |
| column = pq.read_table(path, columns=["observation.state"]).column(0) |
| array = column.combine_chunks() |
| values = np.asarray(array.values.to_numpy(zero_copy_only=False)).reshape( |
| len(array), |
| -1, |
| ) |
| result: dict[str, float] = {} |
| for name, selection in ( |
| ("left_arm", slice(0, 7)), |
| ("right_arm", slice(29, 36)), |
| ): |
| arm = values[:, selection].astype(np.float32, copy=False) |
| result[f"{name}_joint_path_l2"] = float( |
| np.linalg.norm(np.diff(arm, axis=0), axis=1).sum() |
| ) |
| result[f"{name}_end_delta_l2"] = float( |
| np.linalg.norm(arm[-1] - arm[0]) |
| ) |
| result[f"{name}_max_joint_range"] = float( |
| np.ptp(arm, axis=0).max() |
| ) |
| return result |
|
|
|
|
| def main() -> int: |
| args = _parse_args() |
| root = args.dataset_root.expanduser().resolve() |
| output_dir = args.output_dir.expanduser().resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| episodes = [ |
| json.loads(line) |
| for line in (root / "meta" / "episodes.jsonl").read_text().splitlines() |
| if line.strip() |
| ] |
| rows: list[dict[str, object]] = [] |
| errors: list[dict[str, object]] = [] |
| for position, episode in enumerate(episodes, start=1): |
| episode_index = int(episode["episode_index"]) |
| path = ( |
| root |
| / "tracks_trex_track_force_v2" |
| / f"episode_{episode_index:06d}.npz" |
| ) |
| try: |
| row, track_errors = _audit_track( |
| path, |
| episode_index, |
| int(episode["length"]), |
| ) |
| except Exception as exc: |
| row = { |
| "episode_index": episode_index, |
| "frames": int(episode["length"]), |
| } |
| track_errors = [f"{type(exc).__name__}: {exc}"] |
| row["task"] = " | ".join(episode.get("tasks", [])) |
| rows.append(row) |
| if track_errors: |
| errors.append( |
| { |
| "episode_index": episode_index, |
| "errors": track_errors, |
| } |
| ) |
| if position % 250 == 0 or position == len(episodes): |
| print(f"Audited tracks: {position}/{len(episodes)}", flush=True) |
|
|
| static_threshold = float(args.static_span_px) |
| candidates: list[tuple[int, str]] = [] |
| for row in rows: |
| for view in ("left_wrist", "right_wrist"): |
| value = row.get(f"{view}_background_span_median_px") |
| if isinstance(value, float) and value < static_threshold: |
| candidates.append((int(row["episode_index"]), view)) |
| candidate_keys = set(candidates) |
| candidate_episode_indices = sorted({episode for episode, _ in candidates}) |
|
|
| video_results: list[dict[str, object]] = [] |
| video_targets = [ |
| (int(row["episode_index"]), view) |
| for row in rows |
| for view in ("left_wrist", "right_wrist") |
| ] |
| if not args.skip_video_check: |
| with ThreadPoolExecutor(max_workers=max(1, int(args.video_workers))) as pool: |
| futures = { |
| pool.submit( |
| _sample_video_motion, |
| root, |
| episode_index, |
| view, |
| max(2, int(args.video_samples)), |
| ): (episode_index, view) |
| for episode_index, view in video_targets |
| } |
| for position, future in enumerate(as_completed(futures), start=1): |
| video_results.append(future.result()) |
| if position % 500 == 0 or position == len(futures): |
| print( |
| f"Checked wrist videos: " |
| f"{position}/{len(futures)}", |
| flush=True, |
| ) |
|
|
| video_by_key = { |
| (int(result["episode_index"]), str(result["view"])): result |
| for result in video_results |
| } |
| rows_by_episode = { |
| int(row["episode_index"]): row |
| for row in rows |
| } |
| video_frame_count_mismatches = [ |
| { |
| "episode_index": int(result["episode_index"]), |
| "view": str(result["view"]), |
| "expected_frames": int( |
| rows_by_episode[int(result["episode_index"])]["frames"] |
| ), |
| "actual_frames": int(result["video_frames"]), |
| } |
| for result in video_results |
| if "video_frames" in result |
| and int(result["video_frames"]) |
| != int(rows_by_episode[int(result["episode_index"])]["frames"]) |
| ] |
| video_near_static_keys = { |
| key |
| for key, video in video_by_key.items() |
| if not video.get("video_error") |
| and float(video.get("video_adjacent_mad_mean", math.inf)) < 1.0 |
| and float(video.get("video_first_last_mad", math.inf)) < 2.0 |
| } |
| video_error_keys = { |
| key |
| for key, video in video_by_key.items() |
| if video.get("video_error") |
| } |
| relevant_keys = candidate_keys | video_near_static_keys | video_error_keys |
| relevant_episode_indices = sorted( |
| {episode for episode, _ in relevant_keys} |
| ) |
| arm_by_episode: dict[int, dict[str, float]] = {} |
| for position, episode_index in enumerate( |
| relevant_episode_indices, |
| start=1, |
| ): |
| arm_by_episode[episode_index] = _arm_motion(root, episode_index) |
| if position % 250 == 0 or position == len(relevant_episode_indices): |
| print( |
| f"Checked relevant arm motion: " |
| f"{position}/{len(relevant_episode_indices)}", |
| flush=True, |
| ) |
|
|
| classifications: dict[str, list[dict[str, object]]] = { |
| "source_video_near_static_with_moving_arm": [], |
| "track_near_static_with_moving_video_and_arm": [], |
| "stationary_arm_or_low_motion": [], |
| "video_check_error": [], |
| } |
| for episode_index, view in sorted(relevant_keys): |
| row = rows[episode_index] |
| video = video_by_key.get((episode_index, view), {}) |
| arm_name = "left_arm" if view == "left_wrist" else "right_arm" |
| arm = arm_by_episode[episode_index] |
| max_joint_range = float(arm[f"{arm_name}_max_joint_range"]) |
| video_error = video.get("video_error") |
| key = (episode_index, view) |
| video_near_static = key in video_near_static_keys |
| track_near_static = key in candidate_keys |
| arm_moving = max_joint_range >= 0.1 |
| record = { |
| "episode_index": episode_index, |
| "view": view, |
| "frames": int(row["frames"]), |
| "task": row["task"], |
| "background_span_median_px": row[ |
| f"{view}_background_span_median_px" |
| ], |
| "hand_span_median_px": row[f"{view}_hand_span_median_px"], |
| "video_adjacent_mad_mean": video.get( |
| "video_adjacent_mad_mean" |
| ), |
| "video_first_last_mad": video.get("video_first_last_mad"), |
| "video_first_frame_hash": video.get("video_first_frame_hash"), |
| "video_sample_hash": video.get("video_sample_hash"), |
| "arm_max_joint_range": max_joint_range, |
| "track_near_static": track_near_static, |
| "video_near_static": video_near_static, |
| } |
| if video_error: |
| record["video_error"] = video_error |
| classifications["video_check_error"].append(record) |
| elif video_near_static and arm_moving: |
| classifications[ |
| "source_video_near_static_with_moving_arm" |
| ].append(record) |
| elif track_near_static and not video_near_static and arm_moving: |
| classifications[ |
| "track_near_static_with_moving_video_and_arm" |
| ].append(record) |
| else: |
| classifications["stationary_arm_or_low_motion"].append(record) |
|
|
| seam_anomalies: list[dict[str, object]] = [] |
| for row in rows: |
| for view in VIEW_SLICES: |
| ratio = row.get(f"{view}_seam_jump_ratio") |
| seam_p95 = row.get(f"{view}_seam_jump_p95_px") |
| if ( |
| isinstance(ratio, float) |
| and isinstance(seam_p95, float) |
| and np.isfinite(ratio) |
| and np.isfinite(seam_p95) |
| and ratio > 5.0 |
| and seam_p95 > 5.0 |
| ): |
| seam_anomalies.append( |
| { |
| "episode_index": int(row["episode_index"]), |
| "view": view, |
| "seam_jump_p95_px": seam_p95, |
| "regular_jump_p95_px": row[ |
| f"{view}_regular_jump_p95_px" |
| ], |
| "ratio": ratio, |
| } |
| ) |
|
|
| metric_distributions: dict[str, dict[str, float]] = {} |
| for view in VIEW_SLICES: |
| for suffix in ("visibility", "span_median_px", "seam_jump_ratio"): |
| key = f"{view}_{suffix}" |
| values = [ |
| float(row[key]) |
| for row in rows |
| if isinstance(row.get(key), float) |
| and np.isfinite(float(row[key])) |
| ] |
| if values: |
| metric_distributions[key] = _quantiles(values) |
| for view in WRIST_GROUPS: |
| for group in ("background", "hand"): |
| key = f"{view}_{group}_span_median_px" |
| metric_distributions[key] = _quantiles( |
| [float(row[key]) for row in rows] |
| ) |
|
|
| csv_path = output_dir / "episode_metrics.csv" |
| fieldnames = sorted({key for row in rows for key in row}) |
| with csv_path.open("w", newline="") as file: |
| writer = csv.DictWriter(file, fieldnames=fieldnames) |
| writer.writeheader() |
| writer.writerows(rows) |
|
|
| video_csv_path = output_dir / "wrist_video_metrics.csv" |
| if video_results: |
| video_fieldnames = sorted( |
| {key for result in video_results for key in result} |
| ) |
| with video_csv_path.open("w", newline="") as file: |
| writer = csv.DictWriter(file, fieldnames=video_fieldnames) |
| writer.writeheader() |
| writer.writerows( |
| sorted( |
| video_results, |
| key=lambda result: ( |
| int(result["episode_index"]), |
| str(result["view"]), |
| ), |
| ) |
| ) |
|
|
| frozen_records = classifications[ |
| "source_video_near_static_with_moving_arm" |
| ] |
| frozen_episode_indices = sorted( |
| {int(record["episode_index"]) for record in frozen_records} |
| ) |
| frozen_views_by_episode: dict[int, set[str]] = {} |
| for record in frozen_records: |
| frozen_views_by_episode.setdefault( |
| int(record["episode_index"]), |
| set(), |
| ).add(str(record["view"])) |
| frozen_episode_breakdown = Counter( |
| "both" if len(views) == 2 else next(iter(views)) |
| for views in frozen_views_by_episode.values() |
| ) |
| frozen_frame_count = sum( |
| int(rows_by_episode[episode_index]["frames"]) |
| for episode_index in frozen_episode_indices |
| ) |
| first_frame_hash_counts = Counter( |
| str(record["video_first_frame_hash"]) |
| for record in frozen_records |
| if record.get("video_first_frame_hash") |
| ) |
| repeated_frozen_frames = [ |
| {"first_frame_hash": frame_hash, "view_count": count} |
| for frame_hash, count in first_frame_hash_counts.most_common() |
| if count > 1 |
| ] |
| all_classification_records = [ |
| {"classification": name, **record} |
| for name, records in classifications.items() |
| for record in records |
| ] |
| classification_path = output_dir / "wrist_static_classifications.json" |
| classification_path.write_text( |
| json.dumps(all_classification_records, indent=2) + "\n" |
| ) |
| blacklist_path = output_dir / "frozen_wrist_episode_indices.json" |
| blacklist_path.write_text( |
| json.dumps(frozen_episode_indices, indent=2) + "\n" |
| ) |
|
|
| summary = { |
| "dataset_root": str(root), |
| "total_episodes": len(rows), |
| "total_frames": int(sum(int(row["frames"]) for row in rows)), |
| "track_integrity_error_count": len(errors), |
| "track_integrity_errors": errors[:100], |
| "static_background_threshold_px": static_threshold, |
| "near_static_wrist_view_count": len(candidates), |
| "near_static_episode_count": len(candidate_episode_indices), |
| "wrist_video_check_count": len(video_results), |
| "wrist_video_check_error_count": len(video_error_keys), |
| "wrist_video_frame_count_mismatch_count": len( |
| video_frame_count_mismatches |
| ), |
| "wrist_video_frame_count_mismatches": ( |
| video_frame_count_mismatches[:100] |
| ), |
| "source_video_near_static_view_count": len( |
| video_near_static_keys |
| ), |
| "source_video_near_static_episode_count": len( |
| {episode for episode, _ in video_near_static_keys} |
| ), |
| "classification_counts": { |
| name: len(records) |
| for name, records in classifications.items() |
| }, |
| "classification_episode_counts": { |
| name: len( |
| { |
| int(record["episode_index"]) |
| for record in records |
| } |
| ) |
| for name, records in classifications.items() |
| }, |
| "classification_examples": { |
| name: records[:30] |
| for name, records in classifications.items() |
| if records |
| }, |
| "focus_episode_5463": [ |
| record |
| for record in all_classification_records |
| if int(record["episode_index"]) == 5463 |
| ], |
| "repeated_frozen_first_frame_groups": repeated_frozen_frames[:30], |
| "unique_frozen_first_frames": len(first_frame_hash_counts), |
| "frozen_view_breakdown": dict( |
| Counter(str(record["view"]) for record in frozen_records) |
| ), |
| "frozen_episode_breakdown": dict(frozen_episode_breakdown), |
| "frozen_episode_frame_count": frozen_frame_count, |
| "frozen_episode_frame_fraction": ( |
| frozen_frame_count |
| / sum(int(row["frames"]) for row in rows) |
| ), |
| "seam_anomaly_count": len(seam_anomalies), |
| "seam_anomalies": seam_anomalies[:100], |
| "metric_distributions": metric_distributions, |
| "episode_metrics_csv": str(csv_path), |
| "wrist_video_metrics_csv": ( |
| str(video_csv_path) if video_results else None |
| ), |
| "wrist_static_classifications_json": str(classification_path), |
| "frozen_wrist_episode_indices_json": str(blacklist_path), |
| } |
| summary_path = output_dir / "summary.json" |
| summary_path.write_text(json.dumps(summary, indent=2) + "\n") |
| print(f"Wrote {summary_path}") |
| print(f"Wrote {csv_path}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|