Removed old data extraction methods and documented all the new functions.
Browse files- fbx_handler.py +197 -410
fbx_handler.py
CHANGED
@@ -33,37 +33,22 @@ def center_axis(a: Union[List[float], np.array]) -> np.array:
|
|
33 |
return a
|
34 |
|
35 |
|
36 |
-
def
|
37 |
"""
|
38 |
-
|
39 |
-
|
40 |
-
:
|
41 |
-
:return: multidimensional `np.array` with the shape: (missing, 5).
|
42 |
"""
|
43 |
-
return np.column_stack([
|
44 |
-
np.zeros((missing, 1), dtype=int), # 0
|
45 |
-
np.zeros((missing, 1), dtype=int), # 0
|
46 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
47 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
48 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
49 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
50 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
51 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
52 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
53 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
54 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
55 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
56 |
-
np.random.rand(missing, 1), # 0.0-1.0
|
57 |
-
np.random.rand(missing, 1) # 0.0-1.0
|
58 |
-
])
|
59 |
-
|
60 |
-
|
61 |
-
def append_zero(arr: np.ndarray) -> np.ndarray:
|
62 |
zeros = np.zeros((arr.shape[0], arr.shape[1], 1), dtype=float)
|
63 |
return np.concatenate((arr, zeros), axis=-1)
|
64 |
|
65 |
|
66 |
def append_one(arr: np.ndarray) -> np.ndarray:
|
|
|
|
|
|
|
|
|
|
|
67 |
ones = np.ones((arr.shape[0], arr.shape[1], 1), dtype=float)
|
68 |
return np.concatenate((arr, ones), axis=-1)
|
69 |
|
@@ -74,10 +59,19 @@ def merge_tdc(actor_classes: np.array,
|
|
74 |
rotation_vectors: np.array,
|
75 |
scale_vectors: np.array,
|
76 |
ordered: bool = True) -> np.array:
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
# splitting it into sub arrays.
|
80 |
-
|
81 |
tdc = np.concatenate((np.expand_dims(actor_classes, -1),
|
82 |
np.expand_dims(marker_classes, -1),
|
83 |
append_zero(translation_vectors),
|
@@ -111,7 +105,7 @@ def sort_cloud(cloud: np.array) -> np.array:
|
|
111 |
Convenience function to sort a timeline dense cloud by actor and marker classes.
|
112 |
Not required.
|
113 |
:param cloud: `np.array` point cloud to sort.
|
114 |
-
:return:
|
115 |
"""
|
116 |
# Extract the first two elements of the third dimension
|
117 |
actor_classes = cloud[:, :, 0]
|
@@ -131,7 +125,14 @@ def sort_cloud(cloud: np.array) -> np.array:
|
|
131 |
return sorted_tdc
|
132 |
|
133 |
|
134 |
-
def create_keyframe(anim_curve: fbx.FbxAnimCurve, frame: int, value: float):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
# Create an FbxTime object with the given frame number
|
136 |
t = fbx.FbxTime()
|
137 |
t.SetFrame(frame)
|
@@ -139,22 +140,43 @@ def create_keyframe(anim_curve: fbx.FbxAnimCurve, frame: int, value: float):
|
|
139 |
# Create a new keyframe with the specified value
|
140 |
key_index = anim_curve.KeyAdd(t)[0]
|
141 |
anim_curve.KeySetValue(key_index, value)
|
142 |
-
return
|
143 |
|
144 |
|
145 |
def get_child_node_by_name(parent_node: fbx.FbxNode, name: str, ignore_namespace: bool = False) \
|
146 |
-> Union[fbx.FbxNode, None]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
for c in range(parent_node.GetChildCount()):
|
|
|
148 |
child = parent_node.GetChild(c)
|
|
|
149 |
if match_name(child, name, ignore_namespace):
|
|
|
150 |
return child
|
|
|
151 |
return None
|
152 |
|
153 |
|
154 |
def match_name(node: fbx.FbxNode, name: str, ignore_namespace: bool = True) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
node_name = node.GetName()
|
|
|
156 |
if ignore_namespace:
|
157 |
node_name = node_name.split(':')[-1]
|
|
|
158 |
return node_name == name
|
159 |
|
160 |
|
@@ -233,36 +255,6 @@ def world_to_local_transform(node: fbx.FbxNode, world_transform: fbx.FbxAMatrix,
|
|
233 |
return [lcl.GetT()[t] for t in range(3)], [lcl.GetR()[r] for r in range(3)], [lcl.GetS()[s] for s in range(3)]
|
234 |
|
235 |
|
236 |
-
def get_world_transform(m: fbx.FbxNode, t: fbx.FbxTime, axes: str = 'trs') -> np.array:
|
237 |
-
"""
|
238 |
-
Evaluates the world translation of the given node at the given time,
|
239 |
-
scales it down by scale and turns it into a vector list.
|
240 |
-
:param m: `fbx.FbxNode` marker to evaluate the world translation of.
|
241 |
-
:param t: `fbx.FbxTime` time to evaluate at.
|
242 |
-
:param axes: `str` that contains types of info to include. Options are a combination of t, r, and s.
|
243 |
-
:return: Vector in the form: [tx, ty, etc..].
|
244 |
-
"""
|
245 |
-
matrix = m.EvaluateGlobalTransform(t)
|
246 |
-
|
247 |
-
# If axes is only the translation, we return a vector of (tx, ty, tz) only (useful for the training).
|
248 |
-
if axes == 't':
|
249 |
-
return np.array([matrix[i] for i in range(3)])
|
250 |
-
|
251 |
-
# Otherwise, we assemble the entire row depending on the axes.
|
252 |
-
world = []
|
253 |
-
if 't' in axes:
|
254 |
-
world += list(matrix.GetT())
|
255 |
-
world[3] = 0.0
|
256 |
-
if 'r' in axes:
|
257 |
-
world += list(matrix.GetR())
|
258 |
-
world[7] = 0.0
|
259 |
-
if 's' in axes:
|
260 |
-
world += list(matrix.GetS())
|
261 |
-
world[11] = 1.0
|
262 |
-
|
263 |
-
return np.array(world)
|
264 |
-
|
265 |
-
|
266 |
def isolate_actor_from_tdc(tdc: np.array, actor: int) -> np.array:
|
267 |
"""
|
268 |
Returns all markers of the given actor in the timeline dense cloud.
|
@@ -287,23 +279,56 @@ def split_tdc_into_actors(tdc: np.array) -> List[np.array]:
|
|
287 |
|
288 |
|
289 |
def get_keyed_frames_from_curve(curve: fbx.FbxAnimCurve, length: int = -1) -> List[fbx.FbxAnimCurveKey]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
290 |
frames = [curve.KeyGet(i).GetTime().GetFrameCount() for i in range(curve.KeyGetCount())]
|
|
|
291 |
dif = length - len(frames)
|
|
|
292 |
if dif > 0 and length != -1:
|
293 |
frames += [0.] * dif
|
|
|
294 |
return frames
|
295 |
|
296 |
|
297 |
-
def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode,
|
298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
299 |
zeros = [0.0 for _ in range(len(r))]
|
|
|
300 |
ones = [1.0 for _ in range(len(r))]
|
301 |
|
|
|
302 |
tx, ty, tz, rx, ry, rz, sx, sy, sz = [], [], [], [], [], [], [], [], []
|
|
|
303 |
actors = [actor_idx for _ in range(len(r))]
|
|
|
304 |
markers = [marker_idx for _ in range(len(r))]
|
|
|
305 |
t = fbx.FbxTime()
|
306 |
|
|
|
|
|
307 |
for f in r:
|
308 |
t.SetFrame(f)
|
309 |
wt = m.EvaluateGlobalTransform(t)
|
@@ -318,6 +343,7 @@ def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode, r: Lis
|
|
318 |
sy.append(wts[1])
|
319 |
sz.append(wts[2])
|
320 |
|
|
|
321 |
if not incl_keyed:
|
322 |
return [
|
323 |
actors,
|
@@ -327,9 +353,14 @@ def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode, r: Lis
|
|
327 |
sx, sy, sz, ones
|
328 |
]
|
329 |
|
|
|
|
|
|
|
330 |
keyed_frames = get_keyed_frames_from_curve(c)
|
|
|
331 |
keyed_bools = [1 if f in keyed_frames else 0 for f in r]
|
332 |
|
|
|
333 |
return [
|
334 |
actors,
|
335 |
markers,
|
@@ -347,9 +378,7 @@ class FBXContainer:
|
|
347 |
pc_size: int = 1024,
|
348 |
scale: float = 0.01,
|
349 |
debug: int = -1,
|
350 |
-
save_init: bool = True
|
351 |
-
r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
352 |
-
mode: str = 'train'):
|
353 |
"""
|
354 |
Class that stores references to important nodes in an FBX file.
|
355 |
Offers utility functions to quickly load animation data.
|
@@ -396,14 +425,13 @@ class FBXContainer:
|
|
396 |
self.pc_size = pc_size
|
397 |
|
398 |
self.input_fbx = fbx_file
|
399 |
-
# self.output_fbx = append_suffix_to_fbx(fbx_file, '_INF')
|
400 |
self.output_fbx = utils.append_suffix_to_file(fbx_file, '_INF')
|
401 |
self.valid_frames = []
|
402 |
|
403 |
# If we know that the input file has valid data,
|
404 |
# we can automatically call the init function and ignore missing data.
|
405 |
if save_init:
|
406 |
-
self.init(
|
407 |
|
408 |
def __init_scene(self) -> None:
|
409 |
"""
|
@@ -498,7 +526,7 @@ class FBXContainer:
|
|
498 |
|
499 |
self.markers.append(actor_markers)
|
500 |
|
501 |
-
def __init_unlabeled_markers(self, ignore_missing: bool =
|
502 |
"""
|
503 |
Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
|
504 |
"""
|
@@ -514,47 +542,86 @@ class FBXContainer:
|
|
514 |
raise ValueError('No unlabeled markers found.')
|
515 |
|
516 |
def init_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
|
517 |
-
|
|
|
|
|
|
|
518 |
self.init_labeled_world_transforms(r=r, incl_keyed=1)
|
519 |
self.init_unlabeled_world_transforms(r=r)
|
520 |
|
521 |
def init_labeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
522 |
-
incl_keyed: int = 1):
|
523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
524 |
r = self.convert_r(r)
|
525 |
labeled_data = []
|
526 |
|
|
|
527 |
for actor_idx in range(self.actor_count):
|
|
|
528 |
actor_data = []
|
|
|
529 |
for marker_idx, (n, m) in enumerate(self.markers[actor_idx].items()):
|
|
|
|
|
530 |
curve = m.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
|
|
531 |
marker_data = get_world_transforms(actor_idx + 1, marker_idx + 1, m, r, curve, incl_keyed)
|
|
|
532 |
actor_data.append(marker_data)
|
533 |
self._print(f'Actor {actor_idx} marker {marker_idx} done', 1)
|
|
|
534 |
labeled_data.append(actor_data)
|
535 |
|
|
|
|
|
536 |
wide_layout = np.array(labeled_data)
|
|
|
|
|
537 |
self.labeled_world_transforms = np.transpose(wide_layout, axes=(3, 0, 1, 2))
|
538 |
return self.labeled_world_transforms
|
539 |
|
540 |
def init_unlabeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
r = self.convert_r(r)
|
542 |
-
|
543 |
unlabeled_data = []
|
544 |
|
|
|
545 |
for ulm in self.unlabeled_markers:
|
|
|
|
|
546 |
curve = ulm.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
|
|
547 |
marker_data = get_world_transforms(0, 0, ulm, r, curve, incl_keyed=0)
|
|
|
548 |
unlabeled_data.append(marker_data)
|
549 |
self._print(f'Unlabeled marker {ulm.GetName()} done', 1)
|
550 |
|
|
|
|
|
551 |
wide_layout = np.array(unlabeled_data)
|
|
|
|
|
552 |
self.unlabeled_world_transforms = np.transpose(wide_layout, axes=(2, 0, 1))
|
553 |
# Returns shape (n_frames, n_unlabeled_markers, 14).
|
554 |
return self.unlabeled_world_transforms
|
555 |
|
556 |
-
def init(self, ignore_missing_labeled: bool = False, ignore_missing_unlabeled: bool = False
|
557 |
-
|
|
|
|
|
|
|
|
|
558 |
self.__init_scene()
|
559 |
self.__init_anim()
|
560 |
self.__init_actors(ignore_missing=ignore_missing_labeled)
|
@@ -574,120 +641,6 @@ class FBXContainer:
|
|
574 |
if not 0 <= actor <= self.actor_count:
|
575 |
raise ValueError(f'Actor index must be between 0 and {self.actor_count - 1} ({actor}).')
|
576 |
|
577 |
-
def _set_valid_frames_for_actor(self, actor: int = 0):
|
578 |
-
"""
|
579 |
-
Checks for each frame in the frame range, and for each marker, if there is a keyframe present
|
580 |
-
at that frame on LocalTranslation X.
|
581 |
-
If the keyframe is missing, removes that frame from the list of valid frames for that actor.
|
582 |
-
This eventually leaves a list of frames where each number is guaranteed to have a keyframe on all markers.
|
583 |
-
The list is appended to valid_frames, which can be indexed per actor.
|
584 |
-
Finally, stores a list of frames that is valid for all actors in common_frames.
|
585 |
-
:param actor: `int` index of the actor to find keyframes for.
|
586 |
-
"""
|
587 |
-
# Make sure the actor index is in range.
|
588 |
-
self._check_actor(actor)
|
589 |
-
|
590 |
-
frames = self.get_frame_range()
|
591 |
-
for n, marker in self.markers[actor].items():
|
592 |
-
# Get the animation curve for local translation x.
|
593 |
-
t_curve = marker.LclTranslation.GetCurve(self.anim_layer, 'X')
|
594 |
-
# If an actor was recorded but seems to have no animation curves, we set their valid frames to nothing.
|
595 |
-
# Then we return, because there is no point in further checking non-existent keyframes.
|
596 |
-
if t_curve is None:
|
597 |
-
self.valid_frames[actor] = []
|
598 |
-
self._print('Found no animation curve', 2)
|
599 |
-
return
|
600 |
-
|
601 |
-
# Get all keyframes on the animation curve and store their frame numbers.
|
602 |
-
self._print(f'Checking keyframes for {n}', 2)
|
603 |
-
keys = [t_curve.KeyGet(i).GetTime().GetFrameCount() for i in range(t_curve.KeyGetCount())]
|
604 |
-
# Check for each frame in frames if it is present in the list of keyframed frames.
|
605 |
-
for frame in frames:
|
606 |
-
if frame not in keys:
|
607 |
-
# If the frame is not present, that means there is no keyframe with that frame number,
|
608 |
-
# so we don't want to use that frame because it is invalid, so we remove it from the list.
|
609 |
-
with contextlib.suppress(ValueError):
|
610 |
-
frames.remove(frame)
|
611 |
-
|
612 |
-
self._print(f'Found {len(frames)}/{self.num_frames} valid frames for {self.actor_names[actor]}', 1)
|
613 |
-
self.valid_frames[actor] = frames
|
614 |
-
|
615 |
-
# Store all frame lists that have at least 1 frame.
|
616 |
-
other_lists = [r for r in self.valid_frames if r]
|
617 |
-
# Make one list that contains all shared frame numbers.
|
618 |
-
self.common_frames = [num for num in self.get_frame_range()
|
619 |
-
if all(num in other_list for other_list in other_lists)]
|
620 |
-
|
621 |
-
def _check_valid_frames(self, actor: int = 0):
|
622 |
-
"""
|
623 |
-
Safety check to see if the given actor has any valid frames stored.
|
624 |
-
If not, calls _set_valid_frames_for_actor() for that actor.
|
625 |
-
:param actor: `int` actor index.
|
626 |
-
"""
|
627 |
-
self._check_actor(actor)
|
628 |
-
|
629 |
-
if not len(self.valid_frames[actor]):
|
630 |
-
self._print(f'Getting missing valid frames for {self.actor_names[actor]}', 1)
|
631 |
-
self._set_valid_frames_for_actor(actor)
|
632 |
-
|
633 |
-
def get_transformed_axes(self, actor: int = 0, frame: int = 0) -> Tuple[np.array, np.array, np.array]:
|
634 |
-
"""
|
635 |
-
Evaluates all marker nodes for the given actor and modifies the resulting point cloud,
|
636 |
-
so it is centered and scaled properly for training.
|
637 |
-
:param actor: `int` actor index.
|
638 |
-
:param frame: `int` frame to evaluate the markers at.
|
639 |
-
:return: 1D list of `float` that contains the tx, ty and tz for each marker, in that order.
|
640 |
-
"""
|
641 |
-
# Set new frame to evaluate at.
|
642 |
-
time = fbx.FbxTime()
|
643 |
-
time.SetFrame(frame)
|
644 |
-
# Prepare arrays for each axis.
|
645 |
-
x, y, z = [], [], []
|
646 |
-
|
647 |
-
# For each marker, store the x, y and z global position.
|
648 |
-
for n, m in self.markers[actor].items():
|
649 |
-
t = m.EvaluateGlobalTransform(time).GetT()
|
650 |
-
x += [t[0] * self.scale]
|
651 |
-
y += [t[1] * self.scale]
|
652 |
-
z += [t[2] * self.scale]
|
653 |
-
|
654 |
-
# Move the point cloud to the center of the x and y axes. This will put the actor in the middle.
|
655 |
-
x = center_axis(x)
|
656 |
-
z = center_axis(z)
|
657 |
-
|
658 |
-
# Move the actor to the middle of the volume floor by adding volume_dim/2 to x and z.
|
659 |
-
x += self.vol_x / 2.
|
660 |
-
z += self.vol_z / 2.
|
661 |
-
|
662 |
-
# Squeeze the actor into the 1x1 plane for the neural network by dividing the axes.
|
663 |
-
x /= self.vol_x
|
664 |
-
z /= self.vol_z
|
665 |
-
y = np.array(y) / self.vol_y
|
666 |
-
|
667 |
-
# EXTRA: Add any extra modifications to the point cloud here.
|
668 |
-
|
669 |
-
return x, y, z
|
670 |
-
|
671 |
-
def get_transformed_pc(self, actor: int = 0, frame: int = 0, t: str = 'np') -> Union[np.array, List[float]]:
|
672 |
-
|
673 |
-
x, y, z = self.get_transformed_axes(actor, frame)
|
674 |
-
# If we need to return a numpy array, simply vstack the axes to get a shape of (3, 73).
|
675 |
-
# This is in preparation for PyTorch's CNN layers that use input shape (batch_size, C, H, W).
|
676 |
-
if t == 'np':
|
677 |
-
# Exports shape of (3, 9, 9).
|
678 |
-
# return make_pc_ghost_markers(np.vstack((x, y, z)))
|
679 |
-
# Exports shape of (1, 3, 73).
|
680 |
-
return np.vstack((x, y, z))[None, ...]
|
681 |
-
|
682 |
-
# Append all values to a new array, one axis at a time.
|
683 |
-
# This way it will match the column names order.
|
684 |
-
pose = []
|
685 |
-
for i in range(len(x)):
|
686 |
-
pose += [x[i]]
|
687 |
-
pose += [y[i]]
|
688 |
-
pose += [z[i]]
|
689 |
-
return pose
|
690 |
-
|
691 |
def get_frame_range(self) -> List[int]:
|
692 |
"""
|
693 |
Replacement and improvement for:
|
@@ -697,7 +650,12 @@ class FBXContainer:
|
|
697 |
"""
|
698 |
return list(range(self.start_frame, self.end_frame))
|
699 |
|
700 |
-
def convert_r(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None):
|
|
|
|
|
|
|
|
|
|
|
701 |
# If r is one int, use 0 as start frame. If r is higher than the total frames, limit the range.
|
702 |
if isinstance(r, int):
|
703 |
r = list(range(self.num_frames)) if r > self.num_frames else list(range(r))
|
@@ -782,82 +740,12 @@ class FBXContainer:
|
|
782 |
# Return the last node in the chain, which will be the node we were looking for.
|
783 |
return nodes[-1]
|
784 |
|
785 |
-
def
|
786 |
-
"""
|
787 |
-
Prints: actor name, total amount of frames in the animation, amount of valid frames for the given actor,
|
788 |
-
number of missing frames, and the ratio of valid/total frames.
|
789 |
-
:param actor: `int` actor index.
|
790 |
-
:return: Tuple of `str` actor name, `int` total frames, `int` amount of valid frames, `float` valid frame ratio.
|
791 |
-
"""
|
792 |
-
self._check_actor(actor)
|
793 |
-
self._check_valid_frames(actor)
|
794 |
-
|
795 |
-
len_valid = len(self.valid_frames[actor])
|
796 |
-
ratio = (len_valid / self.num_frames) * 100
|
797 |
-
print(f'Actor {self.actor_names[actor]}: Total: {self.num_frames}, valid: {len_valid}, missing: '
|
798 |
-
f'{self.num_frames - len_valid}, ratio: {ratio:.2f}% valid.')
|
799 |
-
|
800 |
-
return self.actor_names[actor], self.num_frames, len_valid, ratio
|
801 |
-
|
802 |
-
def get_valid_frames_for_actor(self, actor: int = 0) -> List[int]:
|
803 |
-
"""
|
804 |
-
Collects the valid frames for the given actor.
|
805 |
-
:param actor: `int` actor index.
|
806 |
-
:return: List of `int` frame numbers that have a keyframe on tx for all markers.
|
807 |
"""
|
808 |
-
|
809 |
-
|
810 |
-
|
811 |
-
def extract_valid_translations_per_actor(self, actor: int = 0, t: str = 'np'):
|
812 |
"""
|
813 |
-
Assembles the poses for the valid frames for the given actor as a 2D list where each row is a pose.
|
814 |
-
:param actor: `int` actor index.
|
815 |
-
:param t: If 'np', returns a (3, -1) `np.array`. Otherwise returns a list of floats.
|
816 |
-
:return: List of poses, where each pose is a list of `float` translations.
|
817 |
-
"""
|
818 |
-
# Ensure the actor index is within range.
|
819 |
-
self._check_actor(actor)
|
820 |
-
self._check_valid_frames(actor)
|
821 |
-
|
822 |
-
# Returns shape (n_valid_frames, 3, 73).
|
823 |
-
return np.vstack([self.get_transformed_pc(actor, frame) for frame in self.valid_frames[actor]])
|
824 |
-
|
825 |
-
# poses = []
|
826 |
-
# # Go through all valid frames for this actor.
|
827 |
-
# # Note that these frames can be different per actor.
|
828 |
-
# for frame in self.valid_frames[actor]:
|
829 |
-
# self._print(f' Extracting frame: {frame}', 1)
|
830 |
-
# # Get the centered point cloud as a 1D list.
|
831 |
-
# pose_at_frame = self.get_transformed_pc(actor, frame, t)
|
832 |
-
# poses.append(pose_at_frame)
|
833 |
-
#
|
834 |
-
# return np.array(poses) if t == 'np' else poses
|
835 |
-
|
836 |
-
def extract_all_valid_translations(self, t: str = 'np') -> Union[np.array, pd.DataFrame]:
|
837 |
-
"""
|
838 |
-
Convenience method that calls self.extract_valid_translations_per_actor() for all actors
|
839 |
-
and returns a `DataFrame` containing all poses after each other.
|
840 |
-
:param t: If 'np', returns a `np.array`. Otherwise, returns a DataFrame.
|
841 |
-
:return: `np.array` or `DataFrame` where each row is a pose.
|
842 |
-
"""
|
843 |
-
# Returns shape (n_total_valid_frames, 3, 73).
|
844 |
-
return np.vstack([self.extract_valid_translations_per_actor(i) for i in range(self.actor_count)])
|
845 |
-
# all_poses = []
|
846 |
-
# # For each actor, add their valid poses to all_poses.
|
847 |
-
# for i in range(self.actor_count):
|
848 |
-
# self._print(f'Extracting actor {self.actor_names[i]}', 0)
|
849 |
-
# all_poses.extend(self.extract_valid_translations_per_actor(i, t))
|
850 |
-
#
|
851 |
-
# self._print('Extracting finished')
|
852 |
-
# # Note that the column names are/must be in the same order as the markers.
|
853 |
-
# if t == 'np':
|
854 |
-
# # Shape: (n_poses, 3, 73).
|
855 |
-
# return np.array(all_poses)
|
856 |
-
# else:
|
857 |
-
# return pd.DataFrame(all_poses, columns=self.columns_from_joints())
|
858 |
-
|
859 |
-
def remove_clipping_poses(self, arr: np.array) -> np.array:
|
860 |
-
|
861 |
mask_x1 = (arr[:, :, 2] < self.hvol_x / self.scale).all(axis=1)
|
862 |
mask_x2 = (arr[:, :, 2] > -self.hvol_x / self.scale).all(axis=1)
|
863 |
mask_z1 = (arr[:, :, 4] < self.hvol_z / self.scale).all(axis=1)
|
@@ -867,6 +755,13 @@ class FBXContainer:
|
|
867 |
return arr[mask]
|
868 |
|
869 |
def extract_training_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
870 |
if self.labeled_world_transforms is None:
|
871 |
self.init_labeled_world_transforms(r=r, incl_keyed=1)
|
872 |
|
@@ -896,12 +791,25 @@ class FBXContainer:
|
|
896 |
|
897 |
def extract_inf_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
898 |
merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
899 |
if self.labeled_world_transforms is None:
|
|
|
900 |
self.init_labeled_world_transforms(r=r, incl_keyed=0)
|
901 |
if self.unlabeled_world_transforms is None:
|
|
|
902 |
self.init_unlabeled_world_transforms(r=r)
|
903 |
|
|
|
904 |
ls = self.labeled_world_transforms.shape
|
|
|
905 |
# Returns shape (n_frames, 73 * n_actors, 14).
|
906 |
flat_labeled = self.labeled_world_transforms.reshape(ls[0], -1, ls[-1])[..., :14]
|
907 |
|
@@ -918,7 +826,6 @@ class FBXContainer:
|
|
918 |
:param w: `np.array` that can either be a timeline dense cloud or translation vectors.
|
919 |
:return: Modified `np.array`.
|
920 |
"""
|
921 |
-
|
922 |
# If the last dimension has 3 elements, it is a translation vector of shape (tx, ty, tz).
|
923 |
# If it has 14 elements, it is a full marker row of shape (actor, marker, tx, ty, tz, tw, rx, ry, rz, tw, etc).
|
924 |
start = 0 if w.shape[-1] == 3 else 2
|
@@ -931,144 +838,15 @@ class FBXContainer:
|
|
931 |
|
932 |
# Then move the x and z to the center of the volume. Y doesn't need to be done because pose needs to stand
|
933 |
# on the floor.
|
934 |
-
#
|
935 |
-
|
|
|
|
|
936 |
w[..., start + 1] = np.clip(w[..., start + 1], -0.5, 0.5)
|
937 |
-
w[..., start + 2] = np.clip(w[..., start + 2], -0.5, 0.5)
|
938 |
|
939 |
return w
|
940 |
|
941 |
-
def is_kf_present(self, marker: fbx.FbxNode, time: fbx.FbxTime) -> bool:
|
942 |
-
"""
|
943 |
-
Returns True if a keyframe is found on the given node's local translation x animation curve.
|
944 |
-
Else returns False.
|
945 |
-
:param marker: `fbx.FbxNode` marker node to evaluate.
|
946 |
-
:param time: `fbx.FbxTime` time to evaluate at.
|
947 |
-
:return: True if a keyframe was found, False otherwise.
|
948 |
-
"""
|
949 |
-
curve = marker.LclTranslation.GetCurve(self.anim_layer, 'X')
|
950 |
-
return False if curve is None else curve.KeyFind(time) != -1
|
951 |
-
|
952 |
-
def get_sc(self, frame: int) -> np.array:
|
953 |
-
"""
|
954 |
-
For each actor at the given time, find all markers with keyframes and add their values to a point cloud.
|
955 |
-
:param frame: `fbx.FbxTime` time at which to evaluate the marker.
|
956 |
-
:return: sparse point cloud as `np.array`.
|
957 |
-
"""
|
958 |
-
time = fbx.FbxTime()
|
959 |
-
time.SetFrame(frame)
|
960 |
-
# Start with a cloud of unlabeled markers, which will use actor and marker class 0.
|
961 |
-
# It is important to start with these before the labeled markers,
|
962 |
-
# because by adding the labeled markers after (which use classes 1-74),
|
963 |
-
# we eventually return an array that doesn't need to be sorted anymore.
|
964 |
-
cloud = [
|
965 |
-
[0, 0, *get_world_transform(m, time)]
|
966 |
-
for m in self.unlabeled_markers
|
967 |
-
if self.is_kf_present(m, time)
|
968 |
-
]
|
969 |
-
|
970 |
-
# Iterate through all actors to get their markers' world translations and add them to the cloud list.
|
971 |
-
for actor_idx in range(self.actor_count):
|
972 |
-
cloud.extend(
|
973 |
-
# This actor's point cloud is made up of all markers that have a keyframe at the given time.
|
974 |
-
# For each marker, we create this row: [actor class (index+1), marker class (index+1), tx, ty, tz].
|
975 |
-
# We use index+1 because the unlabeled markers will use index 0 for both classes.
|
976 |
-
[actor_idx + 1, marker_class, *get_world_transform(m, time)]
|
977 |
-
for marker_class, (marker_name, m) in enumerate(
|
978 |
-
self.markers[actor_idx].items(), start=1
|
979 |
-
)
|
980 |
-
# Only add the marker if it has a keyframe. Missing keyframes on these markers are potentially
|
981 |
-
# among the keyframes on the unlabeled markers. The job of the labeler AI is to predict which
|
982 |
-
# point (unlabeled or labeled) is which marker.
|
983 |
-
if self.is_kf_present(m, time)
|
984 |
-
)
|
985 |
-
|
986 |
-
# If the data is extremely noisy, it might have only a few labeled markers and a lot of unlabeled markers.
|
987 |
-
# The returned point cloud is not allowed to be bigger than the maximum size (self.pc_size),
|
988 |
-
# so return the cloud as a np array that cuts off any excessive markers.
|
989 |
-
return np.array(cloud)[:self.pc_size]
|
990 |
-
|
991 |
-
def get_dc(self, frame: int = 0) -> np.array:
|
992 |
-
self._print(f'Getting sparse cloud for frame {frame}', 2)
|
993 |
-
cloud = self.get_sc(frame)
|
994 |
-
missing = self.pc_size - cloud.shape[0]
|
995 |
-
|
996 |
-
# Only bother creating ghost markers if there are any missing rows.
|
997 |
-
# If we need to add ghost markers, add them before the existing cloud,
|
998 |
-
# so that the cloud will remain a sorted array regarding the actor and marker classes.
|
999 |
-
if missing > 0:
|
1000 |
-
self._print('Making ghost markers', 2)
|
1001 |
-
ghost_cloud = make_ghost_markers(missing)
|
1002 |
-
cloud = np.vstack([ghost_cloud, cloud])
|
1003 |
-
|
1004 |
-
return cloud
|
1005 |
-
|
1006 |
-
def get_tsc(self) -> np.array:
|
1007 |
-
"""
|
1008 |
-
Convenience method that calls self.get_sparse_cloud() for all frames in the frame range
|
1009 |
-
and returns the combined result.
|
1010 |
-
:return: `np.array` that contains a sparse cloud for each frame in the frame range.
|
1011 |
-
"""
|
1012 |
-
return np.array([self.get_sc(f) for f in self.get_frame_range()])
|
1013 |
-
|
1014 |
-
def get_tdc(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
|
1015 |
-
"""
|
1016 |
-
For each frame in the frame range, collects the point cloud that is present in the file.
|
1017 |
-
Then it creates a ghost cloud of random markers that are treated as unlabeled markers,
|
1018 |
-
and adds them together to create a dense cloud whose shape is always (self.pc_size, 5).
|
1019 |
-
Optionally shuffles this dense cloud before adding it to the final list.
|
1020 |
-
:param r: tuple of `int` that indicates the frame range to get. Default is None,
|
1021 |
-
resulting in the animation frame range.
|
1022 |
-
:return: `np.array` that contains a dense point cloud for each frame,
|
1023 |
-
with a shape of (self.num_frames, self.pc_size, 5).
|
1024 |
-
"""
|
1025 |
-
|
1026 |
-
r = self.convert_r(r)
|
1027 |
-
|
1028 |
-
# results = utils.parallel_process(r, self.get_dc)
|
1029 |
-
|
1030 |
-
return np.array([self.get_dc(f) for f in r])
|
1031 |
-
|
1032 |
-
def modify_actor_pose(self, actor: np.array) -> np.array:
|
1033 |
-
# Scale to cm.
|
1034 |
-
actor[:, 2:5] *= self.scale
|
1035 |
-
# Move the point cloud to the center of the x and y axes. This will put the actor in the middle.
|
1036 |
-
for axis in range(2, 5):
|
1037 |
-
actor[:, axis] = center_axis(actor[:, axis])
|
1038 |
-
|
1039 |
-
# Move the actor to the middle of the volume floor by adding volume_dim/2 to x and z.
|
1040 |
-
actor[:, 2] += self.vol_x / 2.
|
1041 |
-
actor[:, 4] += self.vol_z / 2.
|
1042 |
-
|
1043 |
-
# Squeeze the actor into the 1x1 plane for the neural network by dividing the axes.
|
1044 |
-
actor[:, 2] /= self.vol_x
|
1045 |
-
actor[:, 3] /= self.vol_y
|
1046 |
-
actor[:, 4] /= self.vol_z
|
1047 |
-
|
1048 |
-
def split_tdc(self, cloud: np.array = None) \
|
1049 |
-
-> Tuple[np.array, np.array, np.array, np.array, np.array]:
|
1050 |
-
"""
|
1051 |
-
Splits a timeline dense cloud with shape (self.num_frames, self.pc_size, 5) into 3 different
|
1052 |
-
arrays:
|
1053 |
-
1. A `np.array` with the actor classes as shape (self.num_frames, self.pc_size, 1).
|
1054 |
-
2. A `np.array` with the marker classes as shape (self.num_frames, self.pc_size, 1).
|
1055 |
-
3. A `np.array` with the translation floats as shape (self.num_frames, self.pc_size, 4).
|
1056 |
-
4. A `np.array` with the rotation Euler angles as shape (self.num_frames, self.pc_size, 3).
|
1057 |
-
:param cloud: `np.array` of shape (self.num_frames, self.pc_size, 5) that contains a dense point cloud
|
1058 |
-
(self.pc_size, 5) per frame in the frame range.
|
1059 |
-
:return: Return tuple of `np.array` as (actor classes, marker classes, translation vectors).
|
1060 |
-
"""
|
1061 |
-
if cloud is None:
|
1062 |
-
cloud = self.extract_inf_translations()
|
1063 |
-
|
1064 |
-
if cloud.shape[1] != self.pc_size:
|
1065 |
-
raise ValueError(f"Dense cloud doesn't have enough points. {cloud.shape[1]}/{self.pc_size}.")
|
1066 |
-
if cloud.shape[2] < 14:
|
1067 |
-
raise ValueError(f"Dense cloud is missing columns: {cloud.shape[2]}.")
|
1068 |
-
|
1069 |
-
# Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
|
1070 |
-
return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
|
1071 |
-
|
1072 |
def get_split_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
1073 |
mode: str = 'train') -> Tuple[np.array, np.array, np.array, np.array, np.array]:
|
1074 |
"""
|
@@ -1102,17 +880,13 @@ class FBXContainer:
|
|
1102 |
|
1103 |
def export_train_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) \
|
1104 |
-> Union[bytes, pd.DataFrame, np.array]:
|
1105 |
-
|
1106 |
-
|
1107 |
-
|
1108 |
-
|
1109 |
-
|
1110 |
-
|
1111 |
-
|
1112 |
-
self._print(f'Exported train data to {output_file}', 0)
|
1113 |
-
return array_4d
|
1114 |
-
|
1115 |
-
elif output_file.suffix == '.h5':
|
1116 |
array_4d = self.extract_training_translations(r)
|
1117 |
with h5py.File(output_file, 'w') as h5f:
|
1118 |
h5f.create_dataset('array_data', data=array_4d, compression='gzip', compression_opts=9)
|
@@ -1120,10 +894,17 @@ class FBXContainer:
|
|
1120 |
return array_4d
|
1121 |
|
1122 |
else:
|
1123 |
-
raise ValueError('Invalid file extension. Must be .
|
1124 |
|
1125 |
def export_test_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
1126 |
merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1127 |
# Retrieve the clean world transforms.
|
1128 |
# If merged is True, this will be one array of shape (n_frames, pc_size, 14).
|
1129 |
# If merged is False, this will be two arrays, one of shape (n_frames, 73 * n_actors, 14),
|
@@ -1281,6 +1062,12 @@ class FBXContainer:
|
|
1281 |
self.set_default_lcl_scaling(marker, lcl_s)
|
1282 |
|
1283 |
def replace_keyframes_per_marker(self, marker: fbx.FbxNode, marker_keys: dict) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
1284 |
# Initialize empty variables for the local rotation and scaling.
|
1285 |
# These will be filled at the first keyframe
|
1286 |
self.set_default_lcl_transforms(marker, marker_keys)
|
|
|
33 |
return a
|
34 |
|
35 |
|
36 |
+
def append_zero(arr: np.ndarray) -> np.ndarray:
|
37 |
"""
|
38 |
+
Appends a zero array to the end of an array.
|
39 |
+
:param arr: `np.array` to fill.
|
40 |
+
:return: `np.array` with a zero array appended to the end.
|
|
|
41 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
zeros = np.zeros((arr.shape[0], arr.shape[1], 1), dtype=float)
|
43 |
return np.concatenate((arr, zeros), axis=-1)
|
44 |
|
45 |
|
46 |
def append_one(arr: np.ndarray) -> np.ndarray:
|
47 |
+
"""
|
48 |
+
Appends a one array to the end of an array.
|
49 |
+
:param arr: `np.array` to fill.
|
50 |
+
:return: `np.array` with a one array appended to the end.
|
51 |
+
"""
|
52 |
ones = np.ones((arr.shape[0], arr.shape[1], 1), dtype=float)
|
53 |
return np.concatenate((arr, ones), axis=-1)
|
54 |
|
|
|
59 |
rotation_vectors: np.array,
|
60 |
scale_vectors: np.array,
|
61 |
ordered: bool = True) -> np.array:
|
62 |
+
"""
|
63 |
+
Merges the given arrays into a single array of shape (n_frames, pc_size, 14).
|
64 |
+
:param actor_classes: `np.array` of actor class labels.
|
65 |
+
:param marker_classes: `np.array` of marker class labels.
|
66 |
+
:param translation_vectors: `np.array` of translation vectors (3 values each).
|
67 |
+
:param rotation_vectors: `np.array` of Euler rotation angles (3 values each).
|
68 |
+
:param scale_vectors: `np.array` of scale factors (3 values each).
|
69 |
+
:param ordered: Whether to sort the cloud by actor and marker classes.
|
70 |
+
:return: Merged `np.array` of shape (n_frames, pc_size, 14).
|
71 |
+
"""
|
72 |
+
# Actor and marker classes enter as shape (x, 1024), so use np.expand_dims to create a new dimension at the end.
|
73 |
+
# Return the concatenated array of shape (x, 1024, 14), which matches the original timeline dense cloud before
|
74 |
# splitting it into sub arrays.
|
|
|
75 |
tdc = np.concatenate((np.expand_dims(actor_classes, -1),
|
76 |
np.expand_dims(marker_classes, -1),
|
77 |
append_zero(translation_vectors),
|
|
|
105 |
Convenience function to sort a timeline dense cloud by actor and marker classes.
|
106 |
Not required.
|
107 |
:param cloud: `np.array` point cloud to sort.
|
108 |
+
:return: Sorted `np.array` point cloud.
|
109 |
"""
|
110 |
# Extract the first two elements of the third dimension
|
111 |
actor_classes = cloud[:, :, 0]
|
|
|
125 |
return sorted_tdc
|
126 |
|
127 |
|
128 |
+
def create_keyframe(anim_curve: fbx.FbxAnimCurve, frame: int, value: float) -> None:
|
129 |
+
"""
|
130 |
+
Creates a keyframe at the given frame number on the given animation curve.
|
131 |
+
:param anim_curve: `fbx.FbxAnimCurve` node to add the keyframe to.
|
132 |
+
:param frame: `int` frame number at which to add the keyframe.
|
133 |
+
:param value: `float` value that the keyframe will have.
|
134 |
+
:return: True
|
135 |
+
"""
|
136 |
# Create an FbxTime object with the given frame number
|
137 |
t = fbx.FbxTime()
|
138 |
t.SetFrame(frame)
|
|
|
140 |
# Create a new keyframe with the specified value
|
141 |
key_index = anim_curve.KeyAdd(t)[0]
|
142 |
anim_curve.KeySetValue(key_index, value)
|
143 |
+
return
|
144 |
|
145 |
|
146 |
def get_child_node_by_name(parent_node: fbx.FbxNode, name: str, ignore_namespace: bool = False) \
|
147 |
-> Union[fbx.FbxNode, None]:
|
148 |
+
"""
|
149 |
+
Gets the child node with the given name.
|
150 |
+
:param parent_node: `fbx.FbxNode` to get the child node from.
|
151 |
+
:param name: `str` name of the child node to get.
|
152 |
+
:param ignore_namespace: `bool` whether to ignore the namespace in the node name.
|
153 |
+
:return: `fbx.FbxNode` child node with the given name, if it exists, else None.
|
154 |
+
"""
|
155 |
+
# Loop through all child nodes of the parent node.
|
156 |
for c in range(parent_node.GetChildCount()):
|
157 |
+
# Get the child node.
|
158 |
child = parent_node.GetChild(c)
|
159 |
+
# Check if the name of the child node matches the given name.
|
160 |
if match_name(child, name, ignore_namespace):
|
161 |
+
# If it matches, return the child node.
|
162 |
return child
|
163 |
+
# If no child node matches the given name, return None.
|
164 |
return None
|
165 |
|
166 |
|
167 |
def match_name(node: fbx.FbxNode, name: str, ignore_namespace: bool = True) -> bool:
|
168 |
+
"""
|
169 |
+
Checks if the given node's name matches the given name.
|
170 |
+
:param node: `fbx.FbxNode` to check.
|
171 |
+
:param name: `str` name to match.
|
172 |
+
:param ignore_namespace: `bool` whether to ignore the namespace in the node name.
|
173 |
+
"""
|
174 |
+
# get the name of the node
|
175 |
node_name = node.GetName()
|
176 |
+
# if ignore_namespace is True, remove the namespace from the node name
|
177 |
if ignore_namespace:
|
178 |
node_name = node_name.split(':')[-1]
|
179 |
+
# return True if the node name matches the provided name, False otherwise
|
180 |
return node_name == name
|
181 |
|
182 |
|
|
|
255 |
return [lcl.GetT()[t] for t in range(3)], [lcl.GetR()[r] for r in range(3)], [lcl.GetS()[s] for s in range(3)]
|
256 |
|
257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
def isolate_actor_from_tdc(tdc: np.array, actor: int) -> np.array:
|
259 |
"""
|
260 |
Returns all markers of the given actor in the timeline dense cloud.
|
|
|
279 |
|
280 |
|
281 |
def get_keyed_frames_from_curve(curve: fbx.FbxAnimCurve, length: int = -1) -> List[fbx.FbxAnimCurveKey]:
|
282 |
+
"""
|
283 |
+
Returns a list of all the frames on the given curve.
|
284 |
+
:param curve: `fbx.FbxAnimCurve` to get frames from.
|
285 |
+
:param length: Desired amount of frame numbers to return. If this is more than there are keyframes on the curve,
|
286 |
+
it pads 0s to the end. Default -1, which will not add any 0s.
|
287 |
+
:return: List of all the frames on the given curve.
|
288 |
+
"""
|
289 |
+
# Get all the frames on the curve.
|
290 |
frames = [curve.KeyGet(i).GetTime().GetFrameCount() for i in range(curve.KeyGetCount())]
|
291 |
+
# Calculate the difference between desired length and actual length of frames.
|
292 |
dif = length - len(frames)
|
293 |
+
# If desired length is greater than actual length and length is not -1, add 0s to the end.
|
294 |
if dif > 0 and length != -1:
|
295 |
frames += [0.] * dif
|
296 |
+
# Return the list of all frames on the curve.
|
297 |
return frames
|
298 |
|
299 |
|
300 |
+
def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode,
|
301 |
+
r: List[int], c: fbx.FbxAnimCurve, incl_keyed: int = 1) -> List[List[float]]:
|
302 |
+
"""
|
303 |
+
For the given marker node, gets the world transform for each frame in r, and stores the translation, rotation
|
304 |
+
and scaling values as a list of lists. Stores the actor and marker classes at the start of this list of lists.
|
305 |
+
Optionally, if incl_keyed is 1, also stores the keyed frames as the last list.
|
306 |
+
Note: This function has to be passed the animation curve, because we need the animation layer to get the
|
307 |
+
animation curve. The animation layer is stored inside the FBXContainer class, to which we don't have access to here.
|
308 |
+
:param actor_idx: `int` actor class.
|
309 |
+
:param marker_idx: `int` marker class.
|
310 |
+
:param m: `fbx.FbxNode` to evaluate the world transform of at each frame.
|
311 |
+
:param r: `List[int]` list of frame numbers to evaluate the world transform at.
|
312 |
+
:param c: `fbx.FbxAnimCurve` node to read the keyframes from.
|
313 |
+
:param incl_keyed: `bool` whether to include if there was a key on a given frame or not. 0 if not.
|
314 |
+
:return:
|
315 |
+
"""
|
316 |
+
# Create a list of zeros with the same length as r.
|
317 |
zeros = [0.0 for _ in range(len(r))]
|
318 |
+
# Create a list of ones with the same length as r.
|
319 |
ones = [1.0 for _ in range(len(r))]
|
320 |
|
321 |
+
# Create empty lists for each transformation parameter.
|
322 |
tx, ty, tz, rx, ry, rz, sx, sy, sz = [], [], [], [], [], [], [], [], []
|
323 |
+
# Create a list of actor classes with the same length as r.
|
324 |
actors = [actor_idx for _ in range(len(r))]
|
325 |
+
# Create a list of marker classes with the same length as r.
|
326 |
markers = [marker_idx for _ in range(len(r))]
|
327 |
+
# Create a new FbxTime object without a frame set yet.
|
328 |
t = fbx.FbxTime()
|
329 |
|
330 |
+
# For each frame in the given frame range (which does not need to start at 0),
|
331 |
+
# evaluate the world transform at each frame and store the relevant items into their respective lists.
|
332 |
for f in r:
|
333 |
t.SetFrame(f)
|
334 |
wt = m.EvaluateGlobalTransform(t)
|
|
|
343 |
sy.append(wts[1])
|
344 |
sz.append(wts[2])
|
345 |
|
346 |
+
# If we don't need to include keyed frames, return the list of lists as is.
|
347 |
if not incl_keyed:
|
348 |
return [
|
349 |
actors,
|
|
|
353 |
sx, sy, sz, ones
|
354 |
]
|
355 |
|
356 |
+
# However, if we do need those keys, we first retrieve all the keyframed frame numbers from the curve.
|
357 |
+
# Note: We do this after returning the previous results, because the following lines are very slow
|
358 |
+
# and unnecessary for inference.
|
359 |
keyed_frames = get_keyed_frames_from_curve(c)
|
360 |
+
# Then we check if any of the frame numbers are in the keyed frames, which means it had a keyframe and should be 1.
|
361 |
keyed_bools = [1 if f in keyed_frames else 0 for f in r]
|
362 |
|
363 |
+
# Finally, return the complete lists of lists.
|
364 |
return [
|
365 |
actors,
|
366 |
markers,
|
|
|
378 |
pc_size: int = 1024,
|
379 |
scale: float = 0.01,
|
380 |
debug: int = -1,
|
381 |
+
save_init: bool = True):
|
|
|
|
|
382 |
"""
|
383 |
Class that stores references to important nodes in an FBX file.
|
384 |
Offers utility functions to quickly load animation data.
|
|
|
425 |
self.pc_size = pc_size
|
426 |
|
427 |
self.input_fbx = fbx_file
|
|
|
428 |
self.output_fbx = utils.append_suffix_to_file(fbx_file, '_INF')
|
429 |
self.valid_frames = []
|
430 |
|
431 |
# If we know that the input file has valid data,
|
432 |
# we can automatically call the init function and ignore missing data.
|
433 |
if save_init:
|
434 |
+
self.init()
|
435 |
|
436 |
def __init_scene(self) -> None:
|
437 |
"""
|
|
|
526 |
|
527 |
self.markers.append(actor_markers)
|
528 |
|
529 |
+
def __init_unlabeled_markers(self, ignore_missing: bool = True) -> None:
|
530 |
"""
|
531 |
Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
|
532 |
"""
|
|
|
542 |
raise ValueError('No unlabeled markers found.')
|
543 |
|
544 |
def init_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
|
545 |
+
"""
|
546 |
+
Calls the init functions for the labeled and unlabeled world transforms.
|
547 |
+
:param r: Custom frame range to extract.
|
548 |
+
"""
|
549 |
self.init_labeled_world_transforms(r=r, incl_keyed=1)
|
550 |
self.init_unlabeled_world_transforms(r=r)
|
551 |
|
552 |
def init_labeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
553 |
+
incl_keyed: int = 1) -> np.array:
|
554 |
+
"""
|
555 |
+
For each actor, for each marker, stores a list for each element in the world transform for each frame
|
556 |
+
in r. This can later be used to recreate the world transform matrix.
|
557 |
+
:param r: Custom frame range to use.
|
558 |
+
:param incl_keyed: `bool` whether to check if the marker was keyed at the frame.
|
559 |
+
:return: `np.array` of shape (n_frames, n_markers, 14).
|
560 |
+
"""
|
561 |
r = self.convert_r(r)
|
562 |
labeled_data = []
|
563 |
|
564 |
+
# Iterate through all actors.
|
565 |
for actor_idx in range(self.actor_count):
|
566 |
+
# Initialize an empty list to store the results for this actor in.
|
567 |
actor_data = []
|
568 |
+
# Iterate through all markers for this actor.
|
569 |
for marker_idx, (n, m) in enumerate(self.markers[actor_idx].items()):
|
570 |
+
# Get this marker's local translation animation curve.
|
571 |
+
# This requires the animation layer, so we can't do it within the function itself.
|
572 |
curve = m.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
573 |
+
# Get a list of each world transform element for all frames.
|
574 |
marker_data = get_world_transforms(actor_idx + 1, marker_idx + 1, m, r, curve, incl_keyed)
|
575 |
+
# Add the result to actor_data.
|
576 |
actor_data.append(marker_data)
|
577 |
self._print(f'Actor {actor_idx} marker {marker_idx} done', 1)
|
578 |
+
# Add all this actor_data to the global labeled_data.
|
579 |
labeled_data.append(actor_data)
|
580 |
|
581 |
+
# Convert the list to a np array. This will have all frames at the last dimension because of this order:
|
582 |
+
# Shape (n_actors, n_markers, 14/15, n_frames).
|
583 |
wide_layout = np.array(labeled_data)
|
584 |
+
# Transpose the array so that the first dimension is the frames.
|
585 |
+
# Shape (n_frames, n_actors, n_markers, 14).
|
586 |
self.labeled_world_transforms = np.transpose(wide_layout, axes=(3, 0, 1, 2))
|
587 |
return self.labeled_world_transforms
|
588 |
|
589 |
def init_unlabeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
|
590 |
+
"""
|
591 |
+
For all unlabeled markers, stores a list for each element in the world transform for each frame
|
592 |
+
in r. This can later be used to recreate the world transform matrix.
|
593 |
+
:param r: Custom frame range to use.
|
594 |
+
:return: `np.array` of shape (n_frames, n_unlabeled_markers, 14).
|
595 |
+
"""
|
596 |
r = self.convert_r(r)
|
|
|
597 |
unlabeled_data = []
|
598 |
|
599 |
+
# Iterate through all unlabeled markers.
|
600 |
for ulm in self.unlabeled_markers:
|
601 |
+
# Get this marker's local translation animation curve.
|
602 |
+
# This requires the animation layer, so we can't do it within the function itself.
|
603 |
curve = ulm.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
604 |
+
# Get a list of each world transform element for all frames.
|
605 |
marker_data = get_world_transforms(0, 0, ulm, r, curve, incl_keyed=0)
|
606 |
+
# Add the result to marker_data.
|
607 |
unlabeled_data.append(marker_data)
|
608 |
self._print(f'Unlabeled marker {ulm.GetName()} done', 1)
|
609 |
|
610 |
+
# Convert the list to a np array. This will have all frames at the last dimension because of this order:
|
611 |
+
# Shape (n_unlabeled_markers, 14/15, n_frames).
|
612 |
wide_layout = np.array(unlabeled_data)
|
613 |
+
# Transpose the array so that the first dimension is the frames.
|
614 |
+
# Shape (n_frames, n_unlabeled_markers, 14).
|
615 |
self.unlabeled_world_transforms = np.transpose(wide_layout, axes=(2, 0, 1))
|
616 |
# Returns shape (n_frames, n_unlabeled_markers, 14).
|
617 |
return self.unlabeled_world_transforms
|
618 |
|
619 |
+
def init(self, ignore_missing_labeled: bool = False, ignore_missing_unlabeled: bool = False) -> None:
|
620 |
+
"""
|
621 |
+
Initializes the scene.
|
622 |
+
:param ignore_missing_labeled: `bool` whether to ignore errors for missing labeled markers.
|
623 |
+
:param ignore_missing_unlabeled: `bool` whether to ignore errors for missing unlabeled markers.
|
624 |
+
"""
|
625 |
self.__init_scene()
|
626 |
self.__init_anim()
|
627 |
self.__init_actors(ignore_missing=ignore_missing_labeled)
|
|
|
641 |
if not 0 <= actor <= self.actor_count:
|
642 |
raise ValueError(f'Actor index must be between 0 and {self.actor_count - 1} ({actor}).')
|
643 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
def get_frame_range(self) -> List[int]:
|
645 |
"""
|
646 |
Replacement and improvement for:
|
|
|
650 |
"""
|
651 |
return list(range(self.start_frame, self.end_frame))
|
652 |
|
653 |
+
def convert_r(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> List[int]:
|
654 |
+
"""
|
655 |
+
Converts the value of r to a list of frame numbers, depending on what r is.
|
656 |
+
:param r: Custom frame range to use.
|
657 |
+
:return: List of `int` frame numbers (doesn't have to start at 0).
|
658 |
+
"""
|
659 |
# If r is one int, use 0 as start frame. If r is higher than the total frames, limit the range.
|
660 |
if isinstance(r, int):
|
661 |
r = list(range(self.num_frames)) if r > self.num_frames else list(range(r))
|
|
|
740 |
# Return the last node in the chain, which will be the node we were looking for.
|
741 |
return nodes[-1]
|
742 |
|
743 |
+
def remove_clipping_poses(self, arr: np.array) -> np.array:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
744 |
"""
|
745 |
+
Checks for each axis if it does not cross the volume limits. Returns an array without clipping poses.
|
746 |
+
:param arr: `np.array` to filter.
|
747 |
+
:return: Filtered `np.array` that only has non-clipping poses.
|
|
|
748 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
749 |
mask_x1 = (arr[:, :, 2] < self.hvol_x / self.scale).all(axis=1)
|
750 |
mask_x2 = (arr[:, :, 2] > -self.hvol_x / self.scale).all(axis=1)
|
751 |
mask_z1 = (arr[:, :, 4] < self.hvol_z / self.scale).all(axis=1)
|
|
|
755 |
return arr[mask]
|
756 |
|
757 |
def extract_training_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
|
758 |
+
"""
|
759 |
+
Manipulates the existing labeled world transform array into one that is suitable for training.
|
760 |
+
It does this through flattening the array to shape (n_frames, n_actors * 73, 15), then removing
|
761 |
+
all clipping frames and finally transforms the frames to the right location and scale.
|
762 |
+
:param r: Custom frame range to use if the labeled transforms are not stored yet.
|
763 |
+
:return: Transformed labeled world transforms.
|
764 |
+
"""
|
765 |
if self.labeled_world_transforms is None:
|
766 |
self.init_labeled_world_transforms(r=r, incl_keyed=1)
|
767 |
|
|
|
791 |
|
792 |
def extract_inf_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
793 |
merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
|
794 |
+
"""
|
795 |
+
Manipulates the existing (un)labeled world transform arrays into arrays that are suitable for inference.
|
796 |
+
It does this through flattening the labeled world transforms to shape (n_frames, n_actors * 73, 14).
|
797 |
+
If merged is True, merges the unlabeled data with the labeled data.
|
798 |
+
:param r: Custom frame range to use if the transforms were not extracted yet.
|
799 |
+
:param merged: `bool` whether to merge both arrays into one or return separate arrays.
|
800 |
+
:return: If merged, returns one `np.array`, else flattened labeled `np.array` and unlabeled `np.array`.
|
801 |
+
"""
|
802 |
+
# If either of the arrays is None, we can initialize them with r.
|
803 |
if self.labeled_world_transforms is None:
|
804 |
+
# For inference, we don't need keyed frames, so incl_keyed is False.
|
805 |
self.init_labeled_world_transforms(r=r, incl_keyed=0)
|
806 |
if self.unlabeled_world_transforms is None:
|
807 |
+
# Note: Unlabeled data is already flattened.
|
808 |
self.init_unlabeled_world_transforms(r=r)
|
809 |
|
810 |
+
# Returns (n_frames, n_actors, 73, 14).
|
811 |
ls = self.labeled_world_transforms.shape
|
812 |
+
# Flatten the array, so we get a list of frames.
|
813 |
# Returns shape (n_frames, 73 * n_actors, 14).
|
814 |
flat_labeled = self.labeled_world_transforms.reshape(ls[0], -1, ls[-1])[..., :14]
|
815 |
|
|
|
826 |
:param w: `np.array` that can either be a timeline dense cloud or translation vectors.
|
827 |
:return: Modified `np.array`.
|
828 |
"""
|
|
|
829 |
# If the last dimension has 3 elements, it is a translation vector of shape (tx, ty, tz).
|
830 |
# If it has 14 elements, it is a full marker row of shape (actor, marker, tx, ty, tz, tw, rx, ry, rz, tw, etc).
|
831 |
start = 0 if w.shape[-1] == 3 else 2
|
|
|
838 |
|
839 |
# Then move the x and z to the center of the volume. Y doesn't need to be done because pose needs to stand
|
840 |
# on the floor.
|
841 |
+
# We do not add 0.5 here to move the pose to the middle of the capture space,
|
842 |
+
# because in the Dataset we still need to randomly rotate it in world space.
|
843 |
+
# So we keep it centered here.
|
844 |
+
w[..., start + 0] = np.clip(w[..., start + 0], -0.5, 0.5)
|
845 |
w[..., start + 1] = np.clip(w[..., start + 1], -0.5, 0.5)
|
846 |
+
w[..., start + 2] = np.clip(w[..., start + 2], -0.5, 0.5)
|
847 |
|
848 |
return w
|
849 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
850 |
def get_split_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
851 |
mode: str = 'train') -> Tuple[np.array, np.array, np.array, np.array, np.array]:
|
852 |
"""
|
|
|
880 |
|
881 |
def export_train_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) \
|
882 |
-> Union[bytes, pd.DataFrame, np.array]:
|
883 |
+
"""
|
884 |
+
Exports train data to an HDF5 file.
|
885 |
+
:param output_file: `Path` to the file.
|
886 |
+
:param r: Custom frame range to use.
|
887 |
+
:return: `np.array` of shape (n_poses, 73, 14) of train data.
|
888 |
+
"""
|
889 |
+
if output_file.suffix == '.h5':
|
|
|
|
|
|
|
|
|
890 |
array_4d = self.extract_training_translations(r)
|
891 |
with h5py.File(output_file, 'w') as h5f:
|
892 |
h5f.create_dataset('array_data', data=array_4d, compression='gzip', compression_opts=9)
|
|
|
894 |
return array_4d
|
895 |
|
896 |
else:
|
897 |
+
raise ValueError('Invalid file extension. Must be .h5')
|
898 |
|
899 |
def export_test_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
900 |
merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
|
901 |
+
"""
|
902 |
+
Exports test data to an HDF5 file.
|
903 |
+
:param output_file: `Path` to the file.
|
904 |
+
:param r: Custom frame range to use.
|
905 |
+
:param merged: `bool` whether to merge the test data or output an unlabeled dataset and labeled dataset.
|
906 |
+
:return: `np.array` of the test data.
|
907 |
+
"""
|
908 |
# Retrieve the clean world transforms.
|
909 |
# If merged is True, this will be one array of shape (n_frames, pc_size, 14).
|
910 |
# If merged is False, this will be two arrays, one of shape (n_frames, 73 * n_actors, 14),
|
|
|
1062 |
self.set_default_lcl_scaling(marker, lcl_s)
|
1063 |
|
1064 |
def replace_keyframes_per_marker(self, marker: fbx.FbxNode, marker_keys: dict) -> None:
|
1065 |
+
"""
|
1066 |
+
For the given marker, creates new keyframes on its local translation animation curves,
|
1067 |
+
and sets the default local rotation values.
|
1068 |
+
:param marker: `fbx.FbxNode` to set the default values on.
|
1069 |
+
:param marker_keys: `dict` of keys that contain all info needed to create world transform matrices.
|
1070 |
+
"""
|
1071 |
# Initialize empty variables for the local rotation and scaling.
|
1072 |
# These will be filled at the first keyframe
|
1073 |
self.set_default_lcl_transforms(marker, marker_keys)
|