| """ |
| SAME to BVH Conversion Module |
| |
| This module provides functionality to convert SAME format data (stored in .npz files) |
| to BVH format through the following pipeline: |
| SAME (.npz) -> SkelPoseGraph -> Fairmotion Motion -> BVH (.bvh) |
| |
| The conversion process: |
| 1. Load SAME data from .npz file |
| 2. Convert to SkelPoseGraph format (internal graph representation) |
| 3. Convert SkelPoseGraph to Fairmotion Motion object |
| 4. Save Motion object as BVH file using fairmotion's bvh.save() |
| """ |
|
|
| import os |
| import numpy as np |
| import torch |
| from fairmotion.data import bvh |
| from fairmotion.core import motion as motion_class |
|
|
| from sata.mydataset import SkelData, PoseData |
| from sata.skel_pose_graph import SkelPoseGraph |
|
|
|
|
| def _log(message, verbose=True): |
| """Helper function to control verbose output""" |
| if verbose: |
| print(message) |
|
|
|
|
| def load_same_from_npz(npz_path, verbose=True, load_tf=True): |
| """ |
| Load SAME format data from .npz file |
| |
| Args: |
| npz_path: str, path to the .npz file containing SAME format data |
| |
| Returns: |
| tuple: (SkelPoseGraph, num_frames) |
| - SkelPoseGraph: The loaded SAME data as a graph object |
| - num_frames: int, number of frames in the motion |
| |
| The .npz file should contain the following arrays: |
| - lo: local offsets [nJ, 3] |
| - go: global offsets [nJ, 3] |
| - qb: quaternion boolean [nJ] |
| - edges: edge indices [nE, 4] (parent, child, depth, reverse_depth) |
| - q: joint rotations [nF, nJ, 6] (rotation 6D representation) |
| - p: joint positions [nF, nJ, 3] |
| - qv: joint rotation velocities [nF, nJ, 6] |
| - pv: joint position velocities [nF, nJ, 3] |
| - pprev: previous joint positions [nF, nJ, 3] |
| - c: contact labels [nF, nJ, 1] |
| - r: root motion [nF, 4] (theta, dx, dz, height) |
| """ |
| if not os.path.exists(npz_path): |
| raise FileNotFoundError(f"NPZ file not found: {npz_path}") |
| |
| _log(f"Loading SAME data from: {npz_path}", verbose) |
| data = np.load(npz_path) |
| |
| |
| lo = torch.from_numpy(data['lo']).float() |
| go = torch.from_numpy(data['go']).float() |
| qb = torch.from_numpy(data['qb']).bool() |
| edges = data['edges'] |
| |
| |
| edge_index = torch.from_numpy(edges[:, :2].T).long() |
| edge_feature = torch.from_numpy(edges[:, 2:]).long() |
| |
| |
| q = torch.from_numpy(data['q']).float() |
| p = torch.from_numpy(data['p']).float() |
| qv = torch.from_numpy(data['qv']).float() |
| pv = torch.from_numpy(data['pv']).float() |
| if 'pprev' in data: |
| pprev = torch.from_numpy(data['pprev']).float() |
| pprev_flat = pprev.reshape(-1, pprev.shape[-1]) |
| else: |
| pprev = pprev_flat = None |
| c = torch.from_numpy(data['c']).float() |
| r = torch.from_numpy(data['r']).float() |
| |
| nF, nJ = q.shape[0], q.shape[1] |
| _log(f" Loaded {nF} frames, {nJ} joints", verbose) |
| |
| |
| q_flat = q.reshape(-1, q.shape[-1]) |
| p_flat = p.reshape(-1, p.shape[-1]) |
| qv_flat = qv.reshape(-1, qv.shape[-1]) |
| pv_flat = pv.reshape(-1, pv.shape[-1]) |
| c_flat = c.reshape(-1, c.shape[-1]) |
| |
| |
| if 'tf' in data: |
| tf = torch.from_numpy(data['tf']).float() |
| elif not load_tf: |
| |
| tf = torch.zeros((nJ, 768), dtype=torch.float32) |
| else: |
| |
| |
| tf_path = npz_path.rsplit('/processed/', 1)[0] + '/joint_text_features/' + npz_path.rsplit('/', 1)[1] |
| tf = torch.from_numpy(np.load(tf_path)['tf']) |
| |
| skel_data = SkelData( |
| lo=lo, |
| go=go, |
| qb=qb, |
| edge_index=edge_index, |
| edge_feature=edge_feature, |
| tf = tf |
| ) |
| |
| pose_data = PoseData( |
| q=q_flat, |
| p=p_flat, |
| qv=qv_flat, |
| pv=pv_flat, |
| pprev=pprev_flat, |
| c=c_flat, |
| r=r |
| ) |
| |
| |
| same_graph = SkelPoseGraph(skel_data, pose_data) |
| same_graph.tf = tf |
| |
| return same_graph, nF |
|
|
|
|
| def same_graph_to_motion_direct(same_graph, num_frames, first_frame_zero=False, |
| contact_cleanup=False, cids=None, fps=30, verbose=True): |
| """ |
| Directly convert single SAME graph to Fairmotion Motion without batch mechanism. |
| This is a simplified version that avoids the complexity of batch processing. |
| |
| Args: |
| same_graph: SkelPoseGraph, the SAME format graph data (single motion) |
| num_frames: int, number of frames in the motion sequence |
| first_frame_zero: bool, whether to zero out the first frame root position |
| contact_cleanup: bool, whether to apply contact cleanup |
| cids: list, contact joint IDs for cleanup (optional, will auto-detect feet) |
| fps: int, frames per second for the motion (default: 30) |
| |
| Returns: |
| motion: fairmotion.core.Motion object |
| contact: contact information (if contact_cleanup is True) |
| """ |
| from sata.utils.motion_utils import make_motion |
| from sata.utils.tensor_utils import cdn, tensor_q2qR |
| from sata.mymodel import accum_root |
| from fairmotion.ops import conversions |
| from sata.skel_pose_graph import find_feet |
| |
| _log(f"Converting SAME graph to Motion (direct method)...", verbose) |
| |
| nJ = same_graph.lo.shape[0] |
| |
| |
| q = same_graph.q.reshape(num_frames, nJ, -1) |
| c = same_graph.c.reshape(num_frames, nJ, -1) |
| r = same_graph.r_nopad |
| |
| |
| qR = cdn(tensor_q2qR(q)) |
| |
| |
| r_expanded = r.unsqueeze(1) |
| ra_T = cdn(accum_root(r_expanded, num_frames, apply_height=True)) |
| ra_T = ra_T.squeeze(1) |
| |
| |
| lo = same_graph.lo |
| qb = same_graph.qb |
| edge_index = same_graph.edge_index |
| |
| skel = motion_class.Skeleton() |
| root_joint = motion_class.Joint(dof=6) |
| skel.add_joint(root_joint, None) |
| |
| |
| for j_idx in range(nJ): |
| for pid, jid in edge_index.transpose(1, 0): |
| if pid == jid: |
| continue |
| if jid == j_idx: |
| dof = 3 if qb[jid] else 0 |
| new_joint = motion_class.Joint( |
| dof=dof, |
| xform_from_parent_joint=conversions.p2T(cdn(lo[jid])) |
| ) |
| new_joint.set_parent_joint(skel.joints[pid]) |
| skel.add_joint(new_joint, skel.joints[pid]) |
| break |
| |
| |
| if contact_cleanup and cids is None: |
| cids = list(find_feet(same_graph)) |
| |
| |
| motion, contact = make_motion( |
| skel=skel, |
| qR=qR, |
| ra_T=ra_T, |
| c=cdn(c), |
| first_frame_zero=first_frame_zero, |
| contact_cleanup=contact_cleanup, |
| cid=cids |
| ) |
| |
| |
| motion.set_fps(fps) |
| |
| _log(f" ✓ Motion: {motion.num_frames()} frames, {motion.skel.num_joints()} joints, {motion.fps} fps", verbose) |
| |
| return motion, contact |
|
|
|
|
| def save_motion_as_bvh(motion, output_path, rot_order="XYZ", scale=1.0, |
| ee_as_joint=True, verbose=True): |
| """ |
| Save Fairmotion Motion object as BVH file |
| |
| Args: |
| motion: fairmotion.core.Motion object |
| output_path: str, path to save the BVH file |
| rot_order: str, rotation order for BVH (default: "XYZ") |
| scale: float, scale factor for the motion (default: 1.0) |
| ee_as_joint: bool, whether to treat end effectors as joints (default: True) |
| verbose: bool, whether to print progress (default: True) |
| """ |
| |
| output_dir = os.path.dirname(output_path) |
| if output_dir and not os.path.exists(output_dir): |
| os.makedirs(output_dir) |
| _log(f"Created output directory: {output_dir}", verbose) |
| |
| |
| bvh.save( |
| motion=motion, |
| filename=output_path, |
| scale=scale, |
| rot_order=rot_order, |
| verbose=verbose, |
| ee_as_joint=ee_as_joint |
| ) |
| |
| _log(f"✓ BVH file saved to: {output_path}", verbose) |
|
|
|
|
| def same_npz_to_bvh(npz_path, output_path, rot_order="XYZ", |
| scale=1.0, first_frame_zero=False, contact_cleanup=False, |
| cids=None, ee_as_joint=True, verbose=True, fps=30): |
| """ |
| Complete pipeline: Convert SAME .npz file to BVH file |
| |
| This is the main function that combines all steps: |
| 1. Load SAME data from .npz |
| 2. Convert to Motion object using direct method |
| 3. Save as BVH file |
| |
| Args: |
| npz_path: str, path to input .npz file |
| output_path: str, path to output .bvh file |
| rot_order: str, rotation order for BVH (default: "XYZ") |
| scale: float, scale factor (default: 1.0) |
| first_frame_zero: bool, zero out first frame root (default: False) |
| contact_cleanup: bool, apply contact cleanup (default: False) |
| cids: list, contact joint IDs (optional, auto-detected if not provided) |
| ee_as_joint: bool, treat end effectors as joints (default: True) |
| verbose: bool, print progress (default: True) |
| fps: int, frames per second for the motion (default: 30) |
| |
| Returns: |
| motion: fairmotion.core.Motion object that was saved |
| |
| Example: |
| >>> from sata.conversions.same2bvh import same_npz_to_bvh |
| >>> motion = same_npz_to_bvh( |
| ... npz_path="data/motion_001.npz", |
| ... output_path="output/motion_001.bvh" |
| ... ) |
| """ |
| _log("="*60, verbose) |
| _log("SAME to BVH Conversion Pipeline", verbose) |
| _log("="*60, verbose) |
| |
| |
| same_graph, num_frames = load_same_from_npz(npz_path, verbose=verbose) |
| |
| |
| motion, contact = same_graph_to_motion_direct( |
| same_graph=same_graph, |
| num_frames=num_frames, |
| first_frame_zero=first_frame_zero, |
| contact_cleanup=contact_cleanup, |
| cids=cids, |
| fps=fps, |
| verbose=verbose |
| ) |
| |
| |
| save_motion_as_bvh( |
| motion=motion, |
| output_path=output_path, |
| rot_order=rot_order, |
| scale=scale, |
| ee_as_joint=ee_as_joint, |
| verbose=verbose |
| ) |
| |
| _log("="*60, verbose) |
| _log("Conversion completed successfully!", verbose) |
| _log("="*60, verbose) |
| |
| return motion |
|
|
|
|
| def batch_convert_directory(input_dir, output_dir, rot_order="XYZ", |
| scale=1.0, first_frame_zero=False, contact_cleanup=False, |
| cids=None, ee_as_joint=True, verbose=True, fps=30): |
| """ |
| Batch convert all .npz files in a directory to BVH format |
| |
| This function recursively finds all .npz files in input_dir and converts |
| them to BVH format, preserving the directory structure in output_dir. |
| |
| Args: |
| input_dir: str, path to directory containing .npz files |
| output_dir: str, path to directory where .bvh files will be saved |
| rot_order: str, rotation order for BVH (default: "XYZ") |
| scale: float, scale factor (default: 1.0) |
| first_frame_zero: bool, zero out first frame root (default: False) |
| contact_cleanup: bool, apply contact cleanup (default: False) |
| cids: list, contact joint IDs (optional) |
| ee_as_joint: bool, treat end effectors as joints (default: True) |
| verbose: bool, print progress (default: True) |
| fps: int, frames per second for the motion (default: 30) |
| |
| Returns: |
| dict: Statistics dictionary with keys: |
| - 'total': total number of files processed |
| - 'success': number of successful conversions |
| - 'failed': number of failed conversions |
| - 'failed_files': list of failed file paths |
| - 'elapsed_time': elapsed time in seconds |
| |
| Example: |
| >>> from sata.conversions.same2bvh import batch_convert_directory |
| >>> stats = batch_convert_directory( |
| ... input_dir="data/same_motions", |
| ... output_dir="output/bvh_motions", |
| ... fps=60 |
| ... ) |
| >>> print(f"Converted {stats['success']}/{stats['total']} files") |
| """ |
| import glob |
| import time |
| from pathlib import Path |
| |
| start_time = time.time() |
| |
| |
| input_path = Path(input_dir) |
| if not input_path.exists(): |
| raise FileNotFoundError(f"Input directory not found: {input_dir}") |
| |
| npz_files = sorted(input_path.rglob("*.npz")) |
| |
| if not npz_files: |
| _log(f"No .npz files found in {input_dir}", verbose) |
| return { |
| 'total': 0, |
| 'success': 0, |
| 'failed': 0, |
| 'failed_files': [], |
| 'elapsed_time': 0 |
| } |
| |
| |
| output_path = Path(output_dir) |
| output_path.mkdir(parents=True, exist_ok=True) |
| |
| _log("="*60, verbose) |
| _log(f"Batch Converting {len(npz_files)} .npz files", verbose) |
| _log("="*60, verbose) |
| _log(f"Input: {input_dir}", verbose) |
| _log(f"Output: {output_dir}", verbose) |
| _log("="*60, verbose) |
| |
| |
| success_count = 0 |
| failed_count = 0 |
| failed_files = [] |
| |
| |
| for idx, npz_file in enumerate(npz_files, 1): |
| try: |
| |
| relative_path = npz_file.relative_to(input_path) |
| output_file = output_path / relative_path.with_suffix(".bvh") |
| |
| |
| output_file.parent.mkdir(parents=True, exist_ok=True) |
| |
| |
| _log(f"\n[{idx}/{len(npz_files)}] Converting: {relative_path}", verbose) |
| |
| |
| same_npz_to_bvh( |
| npz_path=str(npz_file), |
| output_path=str(output_file), |
| rot_order=rot_order, |
| scale=scale, |
| first_frame_zero=first_frame_zero, |
| contact_cleanup=contact_cleanup, |
| cids=cids, |
| ee_as_joint=ee_as_joint, |
| verbose=verbose, |
| fps=fps |
| ) |
| |
| success_count += 1 |
| _log(f"✓ Saved to: {output_file}", verbose) |
| |
| except Exception as e: |
| failed_count += 1 |
| failed_files.append(str(relative_path)) |
| _log(f"✗ Failed to convert {relative_path}", verbose) |
| _log(f" Error: {str(e)}", verbose) |
| |
| |
| elapsed_time = time.time() - start_time |
| minutes = int(elapsed_time) // 60 |
| seconds = int(elapsed_time) % 60 |
| |
| _log("\n" + "="*60, verbose) |
| _log("Batch Conversion Summary", verbose) |
| _log("="*60, verbose) |
| _log(f"Total: {len(npz_files)} files", verbose) |
| _log(f"Success: {success_count} files", verbose) |
| _log(f"Failed: {failed_count} files", verbose) |
| _log(f"Time: {minutes}m {seconds}s", verbose) |
| |
| if failed_files: |
| _log(f"\nFailed files:", verbose) |
| for f in failed_files: |
| _log(f" - {f}", verbose) |
| |
| _log("="*60, verbose) |
| |
| return { |
| 'total': len(npz_files), |
| 'success': success_count, |
| 'failed': failed_count, |
| 'failed_files': failed_files, |
| 'elapsed_time': elapsed_time |
| } |
|
|
|
|
| |
| if __name__ == "__main__": |
| import argparse |
| |
| parser = argparse.ArgumentParser( |
| description="Convert SAME format (.npz) to BVH format (.bvh)", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Convert single file |
| python same2bvh.py --input data/motion.npz --output result/motion.bvh |
| |
| # With custom rotation order and scale |
| python same2bvh.py --input data/motion.npz --output result/motion.bvh \\ |
| --rot_order ZXY --scale 0.01 |
| |
| # With contact cleanup |
| python same2bvh.py --input data/motion.npz --output result/motion.bvh \\ |
| --contact_cleanup --first_frame_zero |
| |
| # Batch convert entire directory |
| python same2bvh.py --input data/same_motions --output result/bvh_motions --batch |
| |
| # Batch convert with custom parameters |
| python same2bvh.py --input data/ --output output/ --batch \\ |
| --fps 60 --rot_order ZXY --contact_cleanup |
| """ |
| ) |
| |
| |
| parser.add_argument("--input", type=str, required=True, help="Input .npz file or directory path") |
| parser.add_argument("--output", type=str, required=True, help="Output .bvh file or directory path") |
| |
| |
| parser.add_argument("--rot_order", type=str, default="XYZ", |
| choices=["XYZ", "ZXY", "ZYX"], |
| help="Rotation order for BVH (default: XYZ)") |
| parser.add_argument("--scale", type=float, default=1.0, |
| help="Scale factor for the motion (default: 1.0)") |
| parser.add_argument("--fps", type=int, default=20, |
| help="Frames per second for the motion (default: 20)") |
| parser.add_argument("--first_frame_zero", action="store_true", |
| help="Zero out the first frame root position") |
| parser.add_argument("--contact_cleanup", action="store_true", |
| help="Apply contact cleanup to the motion") |
| parser.add_argument("--no_ee_as_joint", action="store_true", |
| help="Don't treat end effectors as joints") |
| parser.add_argument("--quiet", action="store_true", |
| help="Suppress verbose output") |
| parser.add_argument("--batch", action="store_true", |
| help="Batch convert all .npz files in input directory") |
| |
| args = parser.parse_args() |
| |
| |
| if args.batch: |
| batch_convert_directory( |
| input_dir=args.input, |
| output_dir=args.output, |
| rot_order=args.rot_order, |
| scale=args.scale, |
| fps=args.fps, |
| first_frame_zero=args.first_frame_zero, |
| contact_cleanup=args.contact_cleanup, |
| ee_as_joint=not args.no_ee_as_joint, |
| verbose=not args.quiet |
| ) |
| else: |
| |
| same_npz_to_bvh( |
| npz_path=args.input, |
| output_path=args.output, |
| rot_order=args.rot_order, |
| scale=args.scale, |
| fps=args.fps, |
| first_frame_zero=args.first_frame_zero, |
| contact_cleanup=args.contact_cleanup, |
| ee_as_joint=not args.no_ee_as_joint, |
| verbose=not args.quiet |
| ) |
|
|