Download scripts/data/audit_trex_tracks.py from zhicao/dreamzero: direct link, hf CLI and curl.
- Browser
- Download file 23.5 kB
-
https://huggingface.co/zhicao/dreamzero/resolve/main/scripts/data/audit_trex_tracks.py
- Command line
-
hf download hf://zhicao/dreamzero/scripts/data/audit_trex_tracks.py
-
curl -L -o audit_trex_tracks.py https://huggingface.co/zhicao/dreamzero/resolve/main/scripts/data/audit_trex_tracks.py
23.5 kB
| #!/usr/bin/env python3 | |
| """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: # noqa: BLE001 | |
| 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()) | |