Sam / convertNPZtoBVH /conver_bvh.py
Amanpreet
fixed
82d4b57
import os
import numpy as np
from scipy.spatial.transform import Rotation
from collections import deque
import sys
import argparse
# Custom print function to show only progress
def log_progress(message):
if message.startswith("PROGRESS:"):
print(message)
def parse_obj(filename):
vertices = []
lines = []
try:
with open(filename, 'r') as f:
for line in f:
if line.startswith('v '):
parts = line.split()
vertices.append([float(parts[1]), float(parts[2]), float(parts[3])])
elif line.startswith('l '):
parts = line.split()
lines.append([int(parts[1]) - 1, int(parts[2]) - 1])
return np.array(vertices), lines
except Exception as e:
raise ValueError(f"Error parsing OBJ file {filename}: {str(e)}")
def build_hierarchy(lines, root=0):
num_joints = max(max(line) for line in lines) + 1
adj = [[] for _ in range(num_joints)]
for a, b in lines:
adj[a].append(b)
adj[b].append(a)
parent = [-1] * num_joints
queue = deque([root])
visited = [False] * num_joints
visited[root] = True
while queue:
p = queue.popleft()
for c in adj[p]:
if not visited[c]:
parent[c] = p
queue.append(c)
visited[c] = True
if not all(visited):
raise ValueError("The skeleton has disconnected components.")
children = [[] for _ in range(num_joints)]
for c in range(num_joints):
if parent[c] != -1:
children[parent[c]].append(c)
return parent, children
def compute_offsets(vertices_ref, parent):
num_joints = len(vertices_ref)
scale_factor = 0.05
offsets = np.zeros((num_joints, 3))
for j in range(num_joints):
if parent[j] != -1:
offsets[j] = (vertices_ref[j] - vertices_ref[parent[j]])*scale_factor
return offsets
def compute_R_world(joint, vertices_ref, vertices_cur, children):
if not children[joint]:
return np.eye(3)
elif len(children[joint]) == 1:
c = children[joint][0]
V_ref = vertices_ref[c] - vertices_ref[joint]
V_cur = vertices_cur[c] - vertices_cur[joint]
norm_ref = np.linalg.norm(V_ref)
norm_cur = np.linalg.norm(V_cur)
if norm_ref < 1e-6 or norm_cur < 1e-6:
return np.eye(3)
V_ref_norm = V_ref / norm_ref
V_cur_norm = V_cur / norm_cur
cos_theta = np.clip(np.dot(V_ref_norm, V_cur_norm), -1.0, 1.0)
if cos_theta > 0.99999:
return np.eye(3)
axis = np.cross(V_ref_norm, V_cur_norm)
axis_norm = np.linalg.norm(axis)
if axis_norm < 1e-6:
return np.eye(3)
axis = axis / axis_norm
angle = np.arccos(cos_theta)
return Rotation.from_rotvec(axis * angle).as_matrix()
else:
A = np.column_stack([vertices_ref[c] - vertices_ref[joint] for c in children[joint]])
B = np.column_stack([vertices_cur[c] - vertices_cur[joint] for c in children[joint]])
M = B @ A.T
U, _, Vh = np.linalg.svd(M)
R = U @ Vh
if np.linalg.det(R) < 0:
Vh[-1, :] *= -1
R = U @ Vh
return R
def calculate_motion_velocity(rotations):
n_frames = len(rotations)
if n_frames <= 1:
return np.zeros(n_frames)
velocities = np.zeros(n_frames)
for i in range(1, n_frames):
prev_quat = rotations[i-1].as_quat()
curr_quat = rotations[i].as_quat()
if np.dot(prev_quat, curr_quat) < 0:
curr_quat = -curr_quat
diff = Rotation.from_quat(prev_quat).inv() * Rotation.from_quat(curr_quat)
velocities[i] = np.linalg.norm(diff.as_rotvec())
if n_frames > 1:
velocities[0] = velocities[1]
return velocities
def adaptive_smooth_rotations(rotations, window_size=7, velocity_threshold=0.03):
n_frames = len(rotations)
if n_frames <= 1:
return rotations
velocities = calculate_motion_velocity(rotations)
if np.max(velocities) > 0:
velocities = velocities / np.max(velocities)
smoothed = []
half_window = window_size // 2
for i in range(n_frames):
start_idx = max(0, i - half_window)
end_idx = min(n_frames - 1, i + half_window)
window_rots = rotations[start_idx:end_idx + 1]
velocity_factor = min(1.0, velocities[i] / velocity_threshold)
sigma = 0.5 + 1.5 * velocity_factor
dist = np.linspace(-1, 1, len(window_rots))
weights = np.exp(-sigma * np.square(dist))
weights = weights / np.sum(weights)
quats = [r.as_quat() for r in window_rots]
for j in range(1, len(quats)):
if np.dot(quats[0], quats[j]) < 0:
quats[j] = -quats[j]
result_quat = np.zeros(4)
for j in range(len(quats)):
result_quat += weights[j] * quats[j]
result_quat = result_quat / np.linalg.norm(result_quat)
smoothed.append(Rotation.from_quat(result_quat))
return smoothed
def adaptive_smooth_positions(positions, window_size=7, velocity_threshold=0.03):
n_frames = len(positions)
if n_frames <= 1:
return positions
positions = np.array(positions)
smoothed = np.zeros_like(positions)
half_window = window_size // 2
velocities = np.zeros(n_frames)
for i in range(1, n_frames):
velocities[i] = np.linalg.norm(positions[i] - positions[i-1])
velocities[0] = velocities[1]
if np.max(velocities) > 0:
velocities = velocities / np.max(velocities)
for i in range(n_frames):
start_idx = max(0, i - half_window)
end_idx = min(n_frames - 1, i + half_window)
window_pos = positions[start_idx:end_idx + 1]
velocity_factor = min(1.0, velocities[i] / velocity_threshold)
sigma = 0.5 + 1.5 * velocity_factor
dist = np.linspace(-1, 1, len(window_pos))
weights = np.exp(-sigma * np.square(dist))
weights = weights / np.sum(weights)
smoothed[i] = np.sum(window_pos * weights[:, np.newaxis], axis=0)
return smoothed
def detect_arm_joints(children, num_joints):
return [j for j in range(num_joints) if len(children[j]) == 1]
def main(output_dir, smoothing_window=8, velocity_threshold=0.04, joint_constraint=True):
folder = os.path.join(output_dir, 'obj_sequence')
try:
obj_files = sorted([f for f in os.listdir(folder) if f.endswith('.obj')])
except Exception as e:
sys.exit(f"Error accessing folder {folder}: {e}")
if not obj_files:
sys.exit("No OBJ files found.")
vertices_ref, lines = parse_obj(os.path.join(folder, obj_files[0]))
num_joints = len(vertices_ref)
parent, children = build_hierarchy(lines)
offsets = compute_offsets(vertices_ref, parent)
root = 0
hierarchy_order = []
def dfs(joint):
hierarchy_order.append(joint)
for child in children[joint]:
dfs(child)
dfs(root)
arm_joints = detect_arm_joints(children, num_joints)
all_root_positions = []
all_positions = [[] for _ in range(num_joints)]
all_rotations = [[] for _ in range(num_joints)]
total_files = len(obj_files)
for i in range(total_files):
obj_file = obj_files[i]
vertices_cur = parse_obj(os.path.join(folder, obj_file))[0]
R_world = [compute_R_world(j, vertices_ref, vertices_cur, children) for j in range(num_joints)]
R_local = [R_world[j] if parent[j] == -1 else R_world[parent[j]].T @ R_world[j] for j in range(num_joints)]
rotations = [Rotation.from_matrix(R) for R in R_local]
all_root_positions.append(vertices_cur[root])
for j in range(num_joints):
all_positions[j].append(vertices_cur[j])
all_rotations[j].append(rotations[j])
# First half of progress (0-50%)
progress = (i / total_files) * 50
log_progress(f"PROGRESS:{progress:.2f}")
smoothed_root_positions = adaptive_smooth_positions(all_root_positions, smoothing_window, velocity_threshold)
smoothed_positions = [adaptive_smooth_positions(np.array(pos), smoothing_window, velocity_threshold) for pos in all_positions]
smoothed_rotations = [adaptive_smooth_rotations(rot, smoothing_window, velocity_threshold) for rot in all_rotations]
# Enforce bone lengths (no lengthening restraint)
for i in range(total_files):
# Start with root position
smoothed_positions[root][i] = smoothed_root_positions[i]
# Adjust each child joint to maintain bone length
for j in range(num_joints):
if parent[j] != -1: # Skip root
parent_pos = smoothed_positions[parent[j]][i]
child_pos = smoothed_positions[j][i]
ref_offset = offsets[j] # Reference bone length vector
bone_length = np.linalg.norm(ref_offset)
if bone_length < 1e-6:
continue # Skip if bone length is near zero
current_vec = child_pos - parent_pos
current_length = np.linalg.norm(current_vec)
if current_length < 1e-6:
# If current length is near zero, use reference direction
smoothed_positions[j][i] = parent_pos + ref_offset
else:
# Scale the current vector to match reference bone length
corrected_vec = (current_vec / current_length) * bone_length
smoothed_positions[j][i] = parent_pos + corrected_vec
motion_data = []
joints_to_remove = {10, 13, 16, 6, 3}
for i in range(total_files):
root_pos = smoothed_root_positions[i]
if joint_constraint:
for j in range(num_joints):
euler = smoothed_rotations[j][i].as_euler('ZYX', degrees=True)
if j in arm_joints:
euler = np.clip(euler, -180, 180)
else:
euler = np.clip(euler, -150, 150)
smoothed_rotations[j][i] = Rotation.from_euler('ZYX', euler, degrees=True)
euler_angles = [smoothed_rotations[j][i].as_euler('ZYX', degrees=True) for j in range(num_joints)]
motion_line = list(root_pos) + list(euler_angles[root])
for j in hierarchy_order[1:]:
motion_line.extend(euler_angles[j])
motion_data.append(motion_line)
# Second half of progress (50-100%)
progress = 50 + (i / total_files) * 50
log_progress(f"PROGRESS:{progress:.2f}")
bvh_dir = os.path.join(output_dir, 'bvh')
os.makedirs(bvh_dir, exist_ok=True)
bvh_file = os.path.join(bvh_dir, 'output.bvh')
with open(bvh_file, 'w') as f:
f.write("HIERARCHY\n")
def write_hierarchy(joint, parent, f, indent=0):
if parent == -1:
f.write("ROOT Joint{}\n".format(joint))
else:
f.write(" " * indent + "JOINT Joint{}\n".format(joint))
f.write(" " * indent + "{\n")
f.write(" " * (indent + 1) + "OFFSET {:.6f} {:.6f} {:.6f}\n".format(*offsets[joint]))
if parent == -1:
f.write(" " * (indent + 1) + "CHANNELS 6 Xposition Yposition Zposition Zrotation Yrotation Xrotation\n")
else:
f.write(" " * (indent + 1) + "CHANNELS 3 Zrotation Yrotation Xrotation\n")
for child in children[joint]:
write_hierarchy(child, joint, f, indent + 1)
if not children[joint]:
f.write(" " * (indent + 1) + "End Site\n")
f.write(" " * (indent + 1) + "{\n")
f.write(" " * (indent + 2) + "OFFSET 0.000000 0.000000 0.000000\n")
f.write(" " * (indent + 1) + "}\n")
f.write(" " * indent + "}\n")
write_hierarchy(root, -1, f)
f.write("MOTION\n")
f.write("Frames: {}\n".format(len(motion_data)))
f.write("Frame Time: 0.033333\n")
for motion_line in motion_data:
f.write(" ".join("{:.6f}".format(x) for x in motion_line) + "\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser('Convert OBJ sequence to BVH with improved adaptive smoothing.')
parser.add_argument('--output-dir', type=str, default='../outputs/', help='Output directory containing obj_sequence')
parser.add_argument('--smoothing-window', type=int, default=7, help='Size of smoothing window')
parser.add_argument('--velocity-threshold', type=float, default=0.03, help='Velocity threshold for adaptive smoothing')
parser.add_argument('--disable-joint-constraints', action='store_false', dest='joint_constraint',
help='Disable joint constraints that prevent extreme rotations')
args = parser.parse_args()
main(args.output_dir, args.smoothing_window, args.velocity_threshold, args.joint_constraint)