Skip to content

Commit a6d5a0a

Browse files
committed
Add tests to python binding (#752)
Summary: Pull Request resolved: #752 Add python tests for marker sequence io in fbx and the unified save function. Reviewed By: jeongseok-meta Differential Revision: D85739418
1 parent 2ce2149 commit a6d5a0a

File tree

1 file changed

+231
-3
lines changed

1 file changed

+231
-3
lines changed

pymomentum/test/test_fbx_io.py

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,237 @@ def test_save_motions_with_joint_params(self) -> None:
8484

8585
def _verify_fbx(self, file_name: str) -> None:
8686
# Load FBX file
87-
l_character, motion, fps = pym_geometry.Character.load_fbx_with_motion(
88-
file_name
89-
)
87+
_, motion, fps = pym_geometry.Character.load_fbx_with_motion(file_name)
9088
self.assertEqual(1, len(motion))
9189
self.assertEqual(motion[0].shape, self.joint_params.shape)
9290
self.assertEqual(fps, 60)
91+
92+
def test_save_with_namespace(self) -> None:
93+
"""Test FBX save with namespace parameter."""
94+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
95+
offsets = np.zeros(self.joint_params.shape[1])
96+
# Save with namespace
97+
pym_geometry.Character.save_fbx(
98+
path=temp_file.name,
99+
character=self.character,
100+
motion=self.model_params.numpy(),
101+
offsets=offsets,
102+
fps=60,
103+
fbx_namespace="test_ns",
104+
)
105+
# Verify file can be loaded
106+
self._verify_fbx(temp_file.name)
107+
108+
def test_save_with_joint_params_and_namespace(self) -> None:
109+
"""Test FBX save with joint params and namespace parameter."""
110+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
111+
pym_geometry.Character.save_fbx_with_joint_params(
112+
path=temp_file.name,
113+
character=self.character,
114+
joint_params=self.joint_params.numpy(),
115+
fps=60,
116+
fbx_namespace="test_ns",
117+
)
118+
# Verify file can be loaded
119+
self._verify_fbx(temp_file.name)
120+
121+
def test_unified_save_fbx(self) -> None:
122+
"""Test unified save function with FBX extension."""
123+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
124+
offsets = np.zeros(self.joint_params.shape[1])
125+
# Use unified save function - should auto-detect FBX format
126+
pym_geometry.Character.save(
127+
path=temp_file.name,
128+
character=self.character,
129+
motion=self.model_params.numpy(),
130+
offsets=offsets,
131+
fps=60,
132+
)
133+
# Verify file can be loaded
134+
self._verify_fbx(temp_file.name)
135+
136+
def test_unified_save_glb(self) -> None:
137+
"""Test unified save function with GLB extension."""
138+
with tempfile.NamedTemporaryFile(suffix=".glb") as temp_file:
139+
offsets = np.zeros(self.joint_params.shape[1])
140+
# Use unified save function - should auto-detect GLTF format
141+
pym_geometry.Character.save(
142+
path=temp_file.name,
143+
character=self.character,
144+
motion=self.model_params.numpy(),
145+
offsets=offsets,
146+
fps=60,
147+
)
148+
# Verify file can be loaded
149+
loaded_char, loaded_motion, _loaded_offsets, loaded_fps = (
150+
pym_geometry.Character.load_gltf_with_motion(temp_file.name)
151+
)
152+
self.assertEqual(loaded_char.skeleton.size, self.character.skeleton.size)
153+
self.assertEqual(loaded_motion.shape, self.model_params.shape)
154+
self.assertEqual(loaded_fps, 60)
155+
156+
def test_unified_save_gltf(self) -> None:
157+
"""Test unified save function with GLTF extension."""
158+
with tempfile.NamedTemporaryFile(suffix=".gltf") as temp_file:
159+
offsets = np.zeros(self.joint_params.shape[1])
160+
# Use unified save function - should auto-detect GLTF format
161+
pym_geometry.Character.save(
162+
path=temp_file.name,
163+
character=self.character,
164+
motion=self.model_params.numpy(),
165+
offsets=offsets,
166+
fps=60,
167+
)
168+
# Verify file can be loaded
169+
loaded_char, loaded_motion, _loaded_offsets, loaded_fps = (
170+
pym_geometry.Character.load_gltf_with_motion(temp_file.name)
171+
)
172+
self.assertEqual(loaded_char.skeleton.size, self.character.skeleton.size)
173+
self.assertEqual(loaded_motion.shape, self.model_params.shape)
174+
self.assertEqual(loaded_fps, 60)
175+
176+
def test_marker_sequence_fbx_roundtrip(self) -> None:
177+
"""Test saving and loading marker sequences with FBX."""
178+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
179+
# Create test marker sequence
180+
nFrames = 5
181+
markers_per_frame = []
182+
for frame in range(nFrames):
183+
frame_markers = []
184+
for i in range(3):
185+
marker = pym_geometry.Marker(
186+
name=f"marker_{i}",
187+
pos=np.array(
188+
[float(frame + i), float(i), float(frame)], dtype=np.float32
189+
),
190+
occluded=(frame % 2 == 0 and i == 2),
191+
)
192+
frame_markers.append(marker)
193+
markers_per_frame.append(frame_markers)
194+
195+
# Save with unified function
196+
pym_geometry.Character.save(
197+
path=temp_file.name,
198+
character=self.character,
199+
fps=60,
200+
markers=markers_per_frame,
201+
)
202+
203+
# Load markers using load_markers function
204+
marker_sequences = pym_geometry.load_markers(temp_file.name)
205+
self.assertEqual(len(marker_sequences), 1)
206+
207+
loaded_sequence = marker_sequences[0]
208+
self.assertEqual(len(loaded_sequence.frames), nFrames)
209+
self.assertEqual(loaded_sequence.fps, 60.0)
210+
211+
# Verify marker data
212+
for frame_idx, frame in enumerate(loaded_sequence.frames):
213+
# Check marker count (marker 2 is occluded on even frames)
214+
if frame_idx % 2 == 0:
215+
# marker_2 should be occluded
216+
visible_markers = [m for m in frame.markers if not m.occluded]
217+
self.assertEqual(len(visible_markers), 2)
218+
else:
219+
# All markers visible
220+
visible_markers = [m for m in frame.markers if not m.occluded]
221+
self.assertEqual(len(visible_markers), 3)
222+
223+
def test_marker_sequence_fbx_with_motion(self) -> None:
224+
"""Test saving markers and motion together in FBX."""
225+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
226+
# Create test marker sequence
227+
nFrames = 3
228+
markers_per_frame = []
229+
for frame in range(nFrames):
230+
frame_markers = []
231+
for i in range(2):
232+
marker = pym_geometry.Marker(
233+
name=f"marker_{i}",
234+
pos=np.array([float(i), float(frame), 0.0], dtype=np.float32),
235+
occluded=False,
236+
)
237+
frame_markers.append(marker)
238+
markers_per_frame.append(frame_markers)
239+
240+
# Save with both motion and markers
241+
offsets = np.zeros(self.joint_params.shape[1])
242+
pym_geometry.Character.save(
243+
path=temp_file.name,
244+
character=self.character,
245+
motion=self.model_params[:nFrames].numpy(),
246+
offsets=offsets,
247+
fps=60,
248+
markers=markers_per_frame,
249+
)
250+
251+
# Load and verify markers
252+
marker_sequences = pym_geometry.load_markers(temp_file.name)
253+
self.assertEqual(len(marker_sequences), 1)
254+
self.assertEqual(len(marker_sequences[0].frames), nFrames)
255+
256+
# Load and verify motion
257+
_loaded_char, motion, _loaded_fps = (
258+
pym_geometry.Character.load_fbx_with_motion(temp_file.name)
259+
)
260+
self.assertEqual(len(motion), 1)
261+
self.assertEqual(motion[0].shape[0], nFrames)
262+
263+
def test_marker_sequence_sparse_keyframes(self) -> None:
264+
"""Test that marker sequences support sparse keyframes (not all frames have markers)."""
265+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
266+
# Create sparse marker sequence - only keyframe at frame 0 and 2
267+
nFrames = 3
268+
markers_per_frame = []
269+
270+
# Frame 0: has markers
271+
frame_markers_0 = [
272+
pym_geometry.Marker(
273+
name="marker_0",
274+
pos=np.array([0.0, 0.0, 0.0], dtype=np.float32),
275+
occluded=False,
276+
)
277+
]
278+
markers_per_frame.append(frame_markers_0)
279+
280+
# Frame 1: empty - no keyframe
281+
markers_per_frame.append([])
282+
283+
# Frame 2: has markers
284+
frame_markers_2 = [
285+
pym_geometry.Marker(
286+
name="marker_0",
287+
pos=np.array([2.0, 2.0, 2.0], dtype=np.float32),
288+
occluded=False,
289+
)
290+
]
291+
markers_per_frame.append(frame_markers_2)
292+
293+
# Save
294+
pym_geometry.Character.save(
295+
path=temp_file.name,
296+
character=self.character,
297+
fps=60,
298+
markers=markers_per_frame,
299+
)
300+
301+
# Load markers
302+
marker_sequences = pym_geometry.load_markers(temp_file.name)
303+
self.assertEqual(len(marker_sequences), 1)
304+
305+
loaded_sequence = marker_sequences[0]
306+
# Should have 3 frames total (sparse support)
307+
self.assertEqual(len(loaded_sequence.frames), nFrames)
308+
309+
def test_load_markers_empty_file(self) -> None:
310+
"""Test loading markers from a file without markers."""
311+
with tempfile.NamedTemporaryFile(suffix=".fbx") as temp_file:
312+
# Save character without markers
313+
pym_geometry.Character.save(
314+
path=temp_file.name, character=self.character, fps=60
315+
)
316+
317+
# Load markers - should return empty sequence
318+
marker_sequences = pym_geometry.load_markers(temp_file.name)
319+
self.assertEqual(len(marker_sequences), 1)
320+
self.assertEqual(len(marker_sequences[0].frames), 0)

0 commit comments

Comments
 (0)