| |
| """ |
| Unified Table2 evaluation script (Retarget Error), supporting VAE and RVQ models with sliding window strategy. |
| Based on the sliding window implementation from table1_unified_sliding.py. |
| """ |
| import os, argparse, tqdm, torch, gc, time |
| import numpy as np |
| from sata.mypath import * |
| from sata.mydataset import PairedDataset, get_mi_src_tgt_all_graph, PairedGraph_collate_fn |
| from sata.skel_pose_graph import SkelPoseGraph |
| from sata.conversions.graph_to_motion import hatD_recon_motion, gt_recon_motion |
|
|
|
|
| def load_model_by_type(model_type, model_epoch, device): |
| """ |
| Dynamically load the corresponding model based on model type. |
| |
| Args: |
| model_type: "vae" or "rvq" |
| model_epoch: model checkpoint path |
| device: device |
| |
| Returns: |
| model, cfg, ms_dict |
| """ |
| if model_type == "vae": |
| from sata.test import prepare_model_test |
| print(f"[Model] Loading VAE model from: {model_epoch}") |
| elif model_type == "rvq": |
| from sata.test_vq import prepare_model_test |
| print(f"[Model] Loading RVQ model from: {model_epoch}") |
| else: |
| raise ValueError(f"Unknown model_type: {model_type}. Must be 'vae' or 'rvq'") |
| |
| model, cfg, ms_dict = prepare_model_test(model_epoch, device) |
| return model, cfg, ms_dict |
|
|
|
|
| def extract_window_from_dataset(dataset, mi, src_ri, tgt_ri, start_frame, window_size, total_frames): |
| """ |
| Extract a window from the dataset directly from its internal structure (faster than loading frame by frame). |
| |
| Args: |
| dataset: PairedDataset instance |
| mi: motion index |
| src_ri: source rig index |
| tgt_ri: target rig index |
| start_frame: window start frame |
| window_size: window size |
| total_frames: total number of frames |
| |
| Returns: |
| (src_window, tgt_window), actual_window_size |
| """ |
| end_frame = min(start_frame + window_size, total_frames) |
| actual_window_size = end_frame - start_frame |
| |
| |
| src_fi = dataset.mi_ri_2_fi[mi][src_ri] |
| tgt_fi = dataset.mi_ri_2_fi[mi][tgt_ri] |
| |
| |
| src_skel = dataset.skel_list[src_fi] |
| tgt_skel = dataset.skel_list[tgt_fi] |
| |
| |
| src_start_idx = dataset.start_frames[src_fi] + start_frame |
| tgt_start_idx = dataset.start_frames[tgt_fi] + start_frame |
| |
| src_pose_window = dataset.pose_list[src_start_idx:src_start_idx + actual_window_size] |
| tgt_pose_window = dataset.pose_list[tgt_start_idx:tgt_start_idx + actual_window_size] |
| |
| |
| src_graphs = [SkelPoseGraph(src_skel, pose) for pose in src_pose_window] |
| tgt_graphs = [SkelPoseGraph(tgt_skel, pose) for pose in tgt_pose_window] |
| |
| |
| window_data = list(zip(src_graphs, tgt_graphs)) |
| src_window, tgt_window = PairedGraph_collate_fn(window_data) |
| |
| return (src_window, tgt_window), actual_window_size |
|
|
|
|
| def process_with_sliding_window(model, model_type, ds, mi, src_ri, tgt_ri, total_frames, |
| window_size, overlap, device, debug=False): |
| """ |
| Process a motion with sliding windows, supporting overlap. |
| |
| Strategy: in overlapping regions, keep only the prediction from the earlier window. |
| - First window: keep all frames |
| - Subsequent windows: discard the first `overlap` frames (the overlapping part), keep only the non-overlapping part |
| |
| Args: |
| model: trained model |
| model_type: "vae" or "rvq" |
| ds: PairedDataset instance |
| mi: motion index |
| src_ri: source rig index |
| tgt_ri: target rig index |
| total_frames: total number of frames |
| window_size: window size |
| overlap: overlap size between consecutive windows |
| device: device |
| debug: print debug information |
| |
| Returns: |
| z_full: concatenated z across all windows (overlaps discarded) |
| hatD_full: concatenated hatD across all windows (overlaps discarded) |
| tgt_batch_full: full target batch for GT computation |
| """ |
| stride = window_size - overlap |
| |
| |
| if total_frames <= window_size: |
| num_windows = 1 |
| else: |
| num_windows = (total_frames - window_size + stride - 1) // stride + 1 |
| |
| if debug: |
| print(f"\n[DEBUG] Processing motion {mi} (src_ri={src_ri}, tgt_ri={tgt_ri}):") |
| print(f" Total frames: {total_frames}") |
| print(f" Window size: {window_size}, Overlap: {overlap}, Stride: {stride}") |
| print(f" Number of windows: {num_windows}") |
| print(f" Model type: {model_type}") |
| |
| |
| |
| tgt_fi = ds.mi_ri_2_fi[mi][tgt_ri] |
| tgt_skel = ds.skel_list[tgt_fi] |
| num_nodes_per_frame = tgt_skel.lo.shape[0] |
| |
| if debug: |
| print(f" Detected {num_nodes_per_frame} nodes per frame") |
| |
| |
| z_parts = [] |
| hatD_parts = [] |
| |
| |
| for window_idx in range(num_windows): |
| start_frame = window_idx * stride |
| |
| |
| if start_frame >= total_frames: |
| if debug: |
| print(f" Window {window_idx}: start_frame {start_frame} >= {total_frames}, skipping") |
| break |
| |
| |
| (src_window, tgt_window), window_frames = extract_window_from_dataset( |
| ds, mi, src_ri, tgt_ri, start_frame, window_size, total_frames |
| ) |
| |
| |
| src_window = src_window.to(device) |
| tgt_window = tgt_window.to(device) |
| |
| |
| with torch.no_grad(): |
| if model_type == "vae": |
| |
| z_win, hatD_win = model(src_window, tgt_window, window_frames) |
| elif model_type == "rvq": |
| |
| z_win, hatD_win, _, _ = model(src_window, tgt_window, window_frames) |
| else: |
| raise ValueError(f"Unknown model_type: {model_type}") |
| |
| |
| hatD_dim = hatD_win.shape[1] |
| |
| |
| hatD_win_reshaped = hatD_win.view(window_frames, num_nodes_per_frame, hatD_dim) |
| |
| |
| if window_idx == 0: |
| |
| keep_start_idx = 0 |
| keep_end_idx = window_frames |
| else: |
| |
| keep_start_idx = overlap |
| keep_end_idx = window_frames |
| |
| |
| z_keep = z_win[keep_start_idx:keep_end_idx] |
| hatD_keep = hatD_win_reshaped[keep_start_idx:keep_end_idx] |
| |
| if debug: |
| print(f" Window {window_idx}: frames [{start_frame}, {start_frame + window_frames})") |
| print(f" Keep: [{keep_start_idx}, {keep_end_idx}), z_keep: {z_keep.shape}, hatD_keep: {hatD_keep.shape}") |
| |
| |
| z_parts.append(z_keep) |
| hatD_parts.append(hatD_keep) |
| |
| |
| del src_window, tgt_window, z_win, hatD_win, hatD_win_reshaped |
| |
| |
| z_full = torch.cat(z_parts, dim=0) |
| hatD_full_3d = torch.cat(hatD_parts, dim=0) |
| |
| |
| actual_frames = z_full.shape[0] |
| hatD_full = hatD_full_3d.view(actual_frames * num_nodes_per_frame, -1) |
| |
| |
| if actual_frames != total_frames: |
| if debug: |
| print(f" WARNING: Expected {total_frames} frames, got {actual_frames} frames") |
| |
| |
| if torch.isnan(z_full).any() or torch.isinf(z_full).any(): |
| raise ValueError("NaN or Inf detected in z_full") |
| if torch.isnan(hatD_full).any() or torch.isinf(hatD_full).any(): |
| raise ValueError("NaN or Inf detected in hatD_full") |
| |
| if debug: |
| print(f" Final shapes: z_full {z_full.shape}, hatD_full {hatD_full.shape}") |
| print(f" Total frames collected: {actual_frames} (expected: {total_frames})") |
| |
| |
| (_, tgt_batch_full), _ = get_mi_src_tgt_all_graph(ds, mi, src_ri, tgt_ri, device) |
| tgt_batch_full = tgt_batch_full.to(device) |
| |
| return z_full, hatD_full, tgt_batch_full |
|
|
|
|
| def compute_retarget_error_with_sliding_window( |
| model, model_type, cfg, ms_dict, ds, mi, src_ri, tgt_ri, |
| total_frames, window_size, overlap, device, debug=False |
| ): |
| """ |
| Compute retarget positional error using a sliding window (consistent with table2 metrics). |
| |
| Args: |
| model: trained model |
| model_type: "vae" or "rvq" |
| cfg: model config |
| ms_dict: mean/std dictionary |
| ds: PairedDataset instance |
| mi: motion index |
| src_ri: source rig index |
| tgt_ri: target rig index |
| total_frames: total number of frames |
| window_size: window size |
| overlap: overlap size |
| device: device |
| debug: whether to print debug information |
| |
| Returns: |
| error: positional error (computed consistently with table2) |
| """ |
| |
| z_full, hatD_full, tgt_batch = process_with_sliding_window( |
| model, model_type, ds, mi, src_ri, tgt_ri, |
| total_frames, window_size, overlap, device, debug=debug |
| ) |
| |
| |
| |
| out_motion_list, _ = hatD_recon_motion( |
| hatD_full, tgt_batch, cfg["representation"]["out"], |
| ms_dict, total_frames |
| ) |
| |
| tgt_motion_list, _ = gt_recon_motion(tgt_batch, total_frames) |
| |
| |
| |
| |
| |
| |
| height = tgt_batch.go[:, 1].max().item() |
| |
| pos_ref = tgt_motion_list[0].positions(local=False) |
| pos = out_motion_list[0].positions(local=False) |
| |
| |
| err = (pos - pos_ref) * (pos - pos_ref) |
| err /= height**2 |
| |
| |
| del z_full, hatD_full, tgt_batch, out_motion_list, tgt_motion_list |
| |
| return err.mean() * 1000 |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--model_type", type=str, required=True, choices=["vae", "rvq"], |
| help="Model type: vae or rvq (REQUIRED)") |
| parser.add_argument("--model_epoch", type=str, default="ckpt0") |
| parser.add_argument("--device", type=str, default="cuda:0") |
| parser.add_argument("--data_dir", type=str, default="evaluation/motion/processed") |
| parser.add_argument("--window_size", type=int, default=64, |
| help="Sliding window size (number of frames)") |
| parser.add_argument("--overlap", type=int, default=16, |
| help="Overlap size between consecutive windows") |
| parser.add_argument("--num_samples", type=int, default=-1, |
| help="Number of samples to evaluate (-1 for all)") |
| args = parser.parse_args() |
| |
| print("="*80) |
| print("Unified Table2 Evaluation (Retarget Error, with sliding window)") |
| print("="*80) |
| print(f"Model type: {args.model_type.upper()}") |
| print(f"Model: {args.model_epoch}") |
| print(f"Device: {args.device}") |
| print(f"Data dir: {args.data_dir}") |
| print(f"Window size: {args.window_size}") |
| print(f"Overlap: {args.overlap}") |
| print(f"Stride: {args.window_size - args.overlap}") |
| print("="*80) |
| |
| |
| model, cfg, ms_dict = load_model_by_type(args.model_type, args.model_epoch, args.device) |
| |
| |
| ds = PairedDataset(min_motion_lens=8) |
| data_dir = os.path.join(DATA_DIR, args.data_dir) |
| ds.load_data_dir_pairs(data_dir) |
| |
| |
| total_samples = len(ds.mi_ri_2_fi) |
| if args.num_samples > 0 and args.num_samples < total_samples: |
| N = args.num_samples |
| else: |
| N = total_samples |
| |
| print(f"\nEvaluating {N} samples out of {total_samples} total samples\n") |
| |
| int_err, cross_err = 0.0, 0.0 |
| int_cnt, cross_cnt = 0, 0 |
| |
| model.eval() |
| |
| st = time.time() |
| |
| for mi in tqdm.tqdm(range(N)): |
| |
| fi = ds.mi_ri_2_fi[mi][0] |
| total_frames = ds.frame_cnts[fi] |
| R = len(ds.mi_ri_2_fi[mi]) |
| |
| |
| debug_mode = (mi == 0) |
| |
| |
| |
| |
| |
| |
| |
| for tgt_ri in range(1, R): |
| err = compute_retarget_error_with_sliding_window( |
| model, args.model_type, cfg, ms_dict, ds, mi, |
| src_ri=0, tgt_ri=tgt_ri, total_frames=total_frames, |
| window_size=args.window_size, overlap=args.overlap, |
| device=args.device, debug=(debug_mode and tgt_ri == 1) |
| ) |
| cross_err += err.item() |
| cross_cnt += 1 |
| |
| |
| |
| |
| |
| |
| for src_ri in range(1, R): |
| for tgt_ri in range(1, R): |
| err = compute_retarget_error_with_sliding_window( |
| model, args.model_type, cfg, ms_dict, ds, mi, |
| src_ri=src_ri, tgt_ri=tgt_ri, total_frames=total_frames, |
| window_size=args.window_size, overlap=args.overlap, |
| device=args.device, debug=False |
| ) |
| int_err += err.item() |
| int_cnt += 1 |
| |
| |
| |
| |
| |
| elapsed_time = time.time() - st |
| |
| |
| print("\n" + "="*80) |
| print("Results:") |
| print("="*80) |
| print(f"Internal error: {int_err/int_cnt:.4f}") |
| print(f"Cross error: {cross_err/cross_cnt:.4f}") |
| print(f"Time elapsed: {elapsed_time:.2f}s") |
| print("="*80) |
| |
| |
| metric_fp = os.path.join(RESULT_DIR, args.model_epoch.split("/")[0], f"table2_unified_{args.model_type}_sliding.txt") |
| os.makedirs(os.path.dirname(metric_fp), exist_ok=True) |
| |
| if not os.path.exists(metric_fp): |
| with open(metric_fp, "w") as f: |
| f.write("model_epoch,\tmodel_type,\twindow_size,\toverlap,\tinternal,\tcross\n") |
| |
| with open(metric_fp, "a") as f: |
| f.write( |
| f"{args.model_epoch},\t{args.model_type},\t{args.window_size},\t{args.overlap},\t" |
| f"{int_err/int_cnt:.5f},\t{cross_err/cross_cnt:.5f}\n" |
| ) |
| |
| print(f"\nResults saved to: {metric_fp}") |
|
|
|
|
| ''' |
| Usage examples: |
| |
| # RVQ model |
| python src/task/evaluation/table2_unified_sliding.py \ |
| --model_type rvq \ |
| --model_epoch same_rvq_lowerLR_512_wtextV3_nomask_newLoss_gps_t64_temporal_trans_trainFaceZMirror \ |
| --data_dir evaluation/motion/processed \ |
| --window_size 64 \ |
| --overlap 16 |
| |
| # VAE model |
| python src/task/evaluation/table2_unified_sliding.py \ |
| --model_type vae \ |
| --model_epoch same_vae_lowerLR_512_wtextv3_nomask_newLoss_gps_t64_temporal_trans_trainFaceZMirror \ |
| --data_dir evaluation/motion/processed \ |
| --window_size 64 \ |
| --overlap 16 |
| |
| # quick test (small number of samples) |
| python src/task/evaluation/table2_unified_sliding.py \ |
| --model_type vae \ |
| --model_epoch xxx \ |
| --data_dir evaluation/motion/processed \ |
| --num_samples 2 \ |
| --window_size 64 \ |
| --overlap 16 |
| |
| # different window size and overlap configurations |
| python src/task/evaluation/table2_unified_sliding.py \ |
| --model_type rvq \ |
| --model_epoch xxx \ |
| --data_dir evaluation/motion/processed \ |
| --window_size 128 \ |
| --overlap 32 |
| ''' |
|
|