SATA / src /sata /conversions /same2bvh.py
zzysteve
Initial commit
5221c8c
Raw
History Blame Contribute Delete
19.5 kB
"""
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)
# Extract skeleton data
lo = torch.from_numpy(data['lo']).float() # [nJ, 3]
go = torch.from_numpy(data['go']).float() # [nJ, 3]
qb = torch.from_numpy(data['qb']).bool() # [nJ]
edges = data['edges'] # [nE, 4]
# Build edge_index [2, nE]
edge_index = torch.from_numpy(edges[:, :2].T).long()
edge_feature = torch.from_numpy(edges[:, 2:]).long() # [nE, 2] (depth, reverse_depth)
# Extract pose data
q = torch.from_numpy(data['q']).float() # [nF, nJ, 6]
p = torch.from_numpy(data['p']).float() # [nF, nJ, 3]
qv = torch.from_numpy(data['qv']).float() # [nF, nJ, 6]
pv = torch.from_numpy(data['pv']).float() # [nF, nJ, 3]
if 'pprev' in data:
pprev = torch.from_numpy(data['pprev']).float() # [nF, nJ, 3]
pprev_flat = pprev.reshape(-1, pprev.shape[-1])
else:
pprev = pprev_flat = None
c = torch.from_numpy(data['c']).float() # [nF, nJ, 1]
r = torch.from_numpy(data['r']).float() # [nF, 4]
nF, nJ = q.shape[0], q.shape[1]
_log(f" Loaded {nF} frames, {nJ} joints", verbose)
# Flatten temporal dimension: [nF, nJ, D] -> [nF*nJ, D]
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])
# Create SkelData - check if text features exist
if 'tf' in data:
tf = torch.from_numpy(data['tf']).float() # [nJ, 768]
elif not load_tf:
# _log(f" Warning: 'tf' not found in {npz_path}, using zeros.", verbose)
tf = torch.zeros((nJ, 768), dtype=torch.float32)
else:
# load tf from joint_text_features
# Replace only the last 'processed' directory with 'joint_text_features'
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
)
# Create SkelPoseGraph
same_graph = SkelPoseGraph(skel_data, pose_data)
same_graph.tf = tf # Add text features directly to graph
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]
# Reshape flattened data back to [nF, nJ, D]
q = same_graph.q.reshape(num_frames, nJ, -1) # [nF, nJ, 6]
c = same_graph.c.reshape(num_frames, nJ, -1) # [nF, nJ, 1]
r = same_graph.r_nopad # [nF, 4]
# Convert 6D rotation to rotation matrix
qR = cdn(tensor_q2qR(q)) # [nF, nJ, 3, 3]
# Accumulate root motion
r_expanded = r.unsqueeze(1) # [nF, 1, 4]
ra_T = cdn(accum_root(r_expanded, num_frames, apply_height=True)) # [nF, 1, 4, 4]
ra_T = ra_T.squeeze(1) # [nF, 4, 4]
# Create skeleton from graph
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)
# Build skeleton hierarchy
for j_idx in range(nJ):
for pid, jid in edge_index.transpose(1, 0):
if pid == jid: # Skip self-loop (root dummy edge)
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
# Auto-detect contact points if needed
if contact_cleanup and cids is None:
cids = list(find_feet(same_graph))
# Create motion
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
)
# Set fps
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)
"""
# Create output directory if it doesn't exist
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)
# Save using fairmotion's bvh.save()
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)
# Step 1: Load SAME data
same_graph, num_frames = load_same_from_npz(npz_path, verbose=verbose)
# Step 2: Convert to Motion (using direct method)
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
)
# Step 3: Save as BVH
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()
# Find all .npz files
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
}
# Create output directory
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)
# Statistics
success_count = 0
failed_count = 0
failed_files = []
# Process each file
for idx, npz_file in enumerate(npz_files, 1):
try:
# Compute relative path and output path
relative_path = npz_file.relative_to(input_path)
output_file = output_path / relative_path.with_suffix(".bvh")
# Create output subdirectory
output_file.parent.mkdir(parents=True, exist_ok=True)
# Print progress
_log(f"\n[{idx}/{len(npz_files)}] Converting: {relative_path}", verbose)
# Convert
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)
# Summary
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
}
# Example usage
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
"""
)
# Input/output arguments
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")
# Conversion options
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()
# Batch conversion mode
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:
# Single file conversion
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
)