Moved the core functionality of FBXContainer to a new class called FBXContainerBase.
Browse filesextract_inf_translations() will now replace the transforms of nodes without a keyframe with np.inf upon extraction.
This is so when we load the test data, we can easily replace this data with random data to serve as unlabeled markers.
- fbx_handler.py +273 -230
fbx_handler.py
CHANGED
@@ -5,7 +5,6 @@ from typing import List, Union, Tuple
|
|
5 |
import h5py
|
6 |
|
7 |
# Import util libs.
|
8 |
-
import contextlib
|
9 |
import fbx
|
10 |
import itertools
|
11 |
|
@@ -371,69 +370,32 @@ def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode,
|
|
371 |
]
|
372 |
|
373 |
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
|
|
|
|
|
|
382 |
"""
|
383 |
Class that stores references to important nodes in an FBX file.
|
384 |
Offers utility functions to quickly load animation data.
|
385 |
:param fbx_file: `Path` to the file to load.
|
386 |
-
:param volume_dims: `tuple` of `float` that represent the dimensions of the capture volume in meters.
|
387 |
-
:param max_actors: `int` maximum amount of actors to expect in a point cloud.
|
388 |
-
:param pc_size: `int` amount of points in a point cloud.
|
389 |
-
:param debug: If higher than -1, will print out debugging statements.
|
390 |
-
:param save_init: If the file is guaranteed to have all data, set to True to automatically call self.init().
|
391 |
-
:param r: Optional frame range that will be passed to init_transforms.
|
392 |
-
:param mode: `str` to indicate whether to store world transforms for inference only. Default 'train'.
|
393 |
"""
|
394 |
-
|
395 |
-
raise ValueError('Point cloud size must be large enough to contain the maximum amount of actors * 73'
|
396 |
-
f' markers: {pc_size}/{max_actors * 73}.')
|
397 |
-
|
398 |
self.debug = debug
|
399 |
-
# Python ENUM of the C++ time modes.
|
400 |
-
self.time_modes = globals.get_time_modes()
|
401 |
-
# Ordered list of marker names. Note: rearrange this in globals.py.
|
402 |
-
self.marker_names = globals.get_marker_names()
|
403 |
|
404 |
# Initiate empty lists to store references to nodes.
|
405 |
-
self.
|
406 |
-
self.
|
407 |
# Store names of the actors (all parent nodes that have the first 4 markers as children).
|
408 |
-
self.
|
409 |
-
|
410 |
-
self.labeled_world_transforms = None
|
411 |
-
self.unlabeled_world_transforms = None
|
412 |
-
|
413 |
-
# Split the dimensions tuple into its axes for easier access.
|
414 |
-
self.vol_x = volume_dims[0]
|
415 |
-
self.vol_y = volume_dims[1]
|
416 |
-
self.vol_z = volume_dims[2]
|
417 |
-
self.hvol_x = volume_dims[0] / 2
|
418 |
-
self.hvol_y = volume_dims[1] / 2
|
419 |
-
self.hvol_z = volume_dims[2] / 2
|
420 |
-
|
421 |
-
self.scale = scale
|
422 |
-
|
423 |
-
self.max_actors = max_actors
|
424 |
-
# Maximum point cloud size = 73 * max_actors + unlabeled markers.
|
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
|
437 |
"""
|
438 |
Stores scene, root, and time_mode properties.
|
439 |
Destroys the importer to remove the reference to the loaded file.
|
@@ -456,7 +418,7 @@ class FBXContainer:
|
|
456 |
# This will allow us to delete the uploaded file.
|
457 |
importer.Destroy()
|
458 |
|
459 |
-
def
|
460 |
"""
|
461 |
Stores the anim_stack, num_frames, start_frame, end_frame properties.
|
462 |
"""
|
@@ -474,12 +436,11 @@ class FBXContainer:
|
|
474 |
self.start_frame = local_time_span.GetStart().GetFrameCount()
|
475 |
self.end_frame = local_time_span.GetStop().GetFrameCount()
|
476 |
|
477 |
-
def
|
478 |
"""
|
479 |
Goes through all root children (generation 1).
|
480 |
If a child has 4 markers as children, it is considered an actor (Shogun subject) and appended to actors
|
481 |
and actor_names list properties.
|
482 |
-
Also initializes an empty valid_frames list for each found actor.
|
483 |
"""
|
484 |
ts = fbx.FbxTime()
|
485 |
ts.SetFrame(self.start_frame)
|
@@ -487,46 +448,234 @@ class FBXContainer:
|
|
487 |
te = fbx.FbxTime()
|
488 |
te.SetFrame(self.end_frame)
|
489 |
|
490 |
-
names_to_look_for = list(self.marker_names[:4])
|
491 |
# Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
|
492 |
gen1_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
|
493 |
for gen1_node in gen1_nodes:
|
494 |
gen2_nodes = [gen1_node.GetChild(i) for i in
|
495 |
range(gen1_node.GetChildCount())] # Actor nodes (/Mimi/Hips, /Mimi/ARIEL, etc)
|
496 |
|
497 |
-
# If the
|
498 |
-
|
499 |
-
|
500 |
-
self.
|
|
|
501 |
|
502 |
-
if len(self.
|
503 |
-
raise ValueError('No
|
504 |
-
'if it has the following
|
505 |
', '.join(names_to_look_for) + '.')
|
506 |
|
507 |
-
self.
|
508 |
-
self.valid_frames = [[] for _ in range(self.actor_count)]
|
509 |
|
510 |
-
def
|
511 |
"""
|
512 |
-
Goes through all
|
513 |
"""
|
514 |
-
for
|
515 |
-
|
516 |
-
for
|
517 |
-
for
|
518 |
-
child =
|
519 |
# Child name might have namespaces in it like this: Vera:ARIEL
|
520 |
# We want to match only on the actual name, so ignore namespaces.
|
521 |
-
if match_name(child,
|
522 |
-
|
|
|
|
|
|
|
523 |
|
524 |
-
|
525 |
-
raise ValueError(f'{actor_node.GetName()} does not have all markers.')
|
526 |
|
527 |
-
|
|
|
|
|
528 |
|
529 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
530 |
"""
|
531 |
Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
|
532 |
"""
|
@@ -538,9 +687,6 @@ class FBXContainer:
|
|
538 |
self.unlabeled_markers = [gen1_node.GetChild(um) for um in range(gen1_node.GetChildCount())]
|
539 |
return
|
540 |
|
541 |
-
if not ignore_missing:
|
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.
|
@@ -562,11 +708,11 @@ class FBXContainer:
|
|
562 |
labeled_data = []
|
563 |
|
564 |
# Iterate through all actors.
|
565 |
-
for actor_idx in range(self.
|
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.
|
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)
|
@@ -586,11 +732,13 @@ class FBXContainer:
|
|
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
|
|
|
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)
|
@@ -602,7 +750,7 @@ class FBXContainer:
|
|
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=
|
606 |
# Add the result to marker_data.
|
607 |
unlabeled_data.append(marker_data)
|
608 |
self._print(f'Unlabeled marker {ulm.GetName()} done', 1)
|
@@ -616,71 +764,16 @@ class FBXContainer:
|
|
616 |
# Returns shape (n_frames, n_unlabeled_markers, 14).
|
617 |
return self.unlabeled_world_transforms
|
618 |
|
619 |
-
def init(self
|
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)
|
628 |
-
self.__init_markers(ignore_missing=ignore_missing_labeled)
|
629 |
-
self.__init_unlabeled_markers(ignore_missing=ignore_missing_unlabeled)
|
630 |
-
self._print('Init done', 0)
|
631 |
-
|
632 |
-
def _print(self, txt: str, lvl: int = 0) -> None:
|
633 |
-
if lvl <= self.debug:
|
634 |
-
print(txt)
|
635 |
-
|
636 |
-
def _check_actor(self, actor: int = 0):
|
637 |
-
"""
|
638 |
-
Safety check to see if the actor `int` is a valid number (to avoid out of range errors).
|
639 |
-
:param actor: `int` actor index, which should be between 0-max_actors.
|
640 |
-
"""
|
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:
|
647 |
-
`list(range(self.num_frames))`
|
648 |
-
If the animation does not start at frame 0, this will return a list that has the correct frames.
|
649 |
-
:return: List of `int` frame numbers that are between the start and end frame of the animation.
|
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 |
-
|
660 |
-
|
661 |
-
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
# If the requested frame range is longer than the total frames, limit the range.
|
666 |
-
if r[1] - r[0] > self.num_frames:
|
667 |
-
r = list(range(r[0], r[0] + self.num_frames))
|
668 |
-
else:
|
669 |
-
r = list(range(r[0], r[1]))
|
670 |
-
|
671 |
-
# A tuple of 3 indicates a frame range with step.
|
672 |
-
elif isinstance(r, tuple) and len(r) == 3:
|
673 |
-
# If the requested frame range is longer than the total frames, limit the range.
|
674 |
-
if r[1] - r[0] > self.num_frames:
|
675 |
-
r = list(range(r[0], r[0] + self.num_frames, r[2]))
|
676 |
-
else:
|
677 |
-
r = list(range(r[0], r[1], r[2]))
|
678 |
-
|
679 |
-
# If r is None, return the default frame range.
|
680 |
-
else:
|
681 |
-
r = self.get_frame_range()
|
682 |
-
|
683 |
-
return r
|
684 |
|
685 |
def columns_from_joints(self) -> List[str]:
|
686 |
"""
|
@@ -688,58 +781,11 @@ class FBXContainer:
|
|
688 |
:return: List of column names, in the form of [node1_tx, node1_ty, node1_tz, node2_tx, node2_ty, node2_tz..].
|
689 |
"""
|
690 |
columns = []
|
691 |
-
for name in self.
|
692 |
columns += [f'{name}x', f'{name}y', f'{name}z']
|
693 |
|
694 |
return columns
|
695 |
|
696 |
-
def get_marker_by_name(self, actor: int, name: str):
|
697 |
-
"""
|
698 |
-
Returns the reference to the actor's marker.
|
699 |
-
:param actor: `int` actor index.
|
700 |
-
:param name: `str` marker name.
|
701 |
-
:return: `fbx.FbxNode` reference.
|
702 |
-
"""
|
703 |
-
self._check_actor(actor)
|
704 |
-
return self.markers[actor][name]
|
705 |
-
|
706 |
-
def get_parent_node_by_name(self, parent_name: str, ignore_namespace: bool = True) -> Union[fbx.FbxNode, None]:
|
707 |
-
"""
|
708 |
-
Utility function to get a parent node reference by name.
|
709 |
-
:param parent_name: `str` name that will be looked for in the node name.
|
710 |
-
:param ignore_namespace: `bool` Whether to ignore namespaces in a node's name.
|
711 |
-
:return:
|
712 |
-
"""
|
713 |
-
# Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
|
714 |
-
parent_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
|
715 |
-
|
716 |
-
return next(
|
717 |
-
(
|
718 |
-
parent_node
|
719 |
-
for parent_node in parent_nodes
|
720 |
-
if match_name(parent_node, parent_name, ignore_namespace=ignore_namespace)
|
721 |
-
),
|
722 |
-
None,
|
723 |
-
)
|
724 |
-
|
725 |
-
def get_node_by_path(self, path: str) -> fbx.FbxNode:
|
726 |
-
"""
|
727 |
-
Utility function to retrieve a node reference from a path like /Actor1/Hips/UpperLeg_l.
|
728 |
-
:param path: `str` path with forward slashes to follow.
|
729 |
-
:return: `fbx.FbxNode` reference to that node.
|
730 |
-
"""
|
731 |
-
# Split the path into node names.
|
732 |
-
node_names = [x for x in path.split('/') if x]
|
733 |
-
# Start the list of node references with the parent node that has its own function.
|
734 |
-
nodes = [self.get_parent_node_by_name(node_names[0], False)]
|
735 |
-
# Extend the list with each following child node of the previous parent.
|
736 |
-
nodes.extend(
|
737 |
-
get_child_node_by_name(nodes[idx], node_name)
|
738 |
-
for idx, node_name in enumerate(node_names[1:])
|
739 |
-
)
|
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.
|
@@ -767,9 +813,11 @@ class FBXContainer:
|
|
767 |
|
768 |
# Returns (n_frames, n_actors, 73, 15).
|
769 |
l_shape = self.labeled_world_transforms.shape
|
|
|
770 |
# Flatten the array, so we get a list of frames.
|
771 |
# Reshape to (n_frames * n_actors, 73, 15).
|
772 |
flattened = self.labeled_world_transforms.reshape(-1, l_shape[2], l_shape[3])
|
|
|
773 |
# Isolates the poses with all keyframes present by checking the last elements.
|
774 |
# Start with the mask.
|
775 |
# Returns shape of (n_frames * n_actors, 73, 15).
|
@@ -777,6 +825,7 @@ class FBXContainer:
|
|
777 |
# We only need a filter for the first dimension, so use .all to check if all markers
|
778 |
# have a keyframe. This results in shape (n_frames * n_actors,).
|
779 |
mask = mask.all(axis=1)
|
|
|
780 |
# Now isolate the right frames with the mask and remove the last element of the last dimension,
|
781 |
# because it won't be useful anymore.
|
782 |
# Also remove any frames that cross the limits of the volume.
|
@@ -802,16 +851,29 @@ class FBXContainer:
|
|
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=
|
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,
|
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,
|
814 |
-
flat_labeled = self.labeled_world_transforms.reshape(ls[0], -1, ls[-1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
815 |
|
816 |
if merged:
|
817 |
return utils.merge_labeled_and_unlabeled_data(labeled=flat_labeled,
|
@@ -832,9 +894,9 @@ class FBXContainer:
|
|
832 |
|
833 |
# First multiply by self.scale, which turns centimeters to meters.
|
834 |
# Then divide by volume dimensions, to normalize to the total area of the capture volume.
|
835 |
-
w[..., start + 0] = w[..., start + 0] * self.scale / self.
|
836 |
-
w[..., start + 1] = w[..., start + 1] * self.scale / self.
|
837 |
-
w[..., start + 2] = w[..., start + 2] * self.scale / self.
|
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.
|
@@ -862,21 +924,21 @@ class FBXContainer:
|
|
862 |
# Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
|
863 |
return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
|
864 |
|
865 |
-
def
|
866 |
"""
|
867 |
Returns the actor name based on the class value.
|
868 |
:param c: `float` actor class index.
|
869 |
:return: `str` actor name.
|
870 |
"""
|
871 |
-
return 'UNLABELED' if int(c) == 0 else self.
|
872 |
|
873 |
-
def
|
874 |
"""
|
875 |
Returns the marker name based on the class value.
|
876 |
:param c: `float` marker class index.
|
877 |
:return: `str` marker name.
|
878 |
"""
|
879 |
-
return 'UNLABELED' if int(c) == 0 else self.
|
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]:
|
@@ -957,25 +1019,6 @@ class FBXContainer:
|
|
957 |
|
958 |
return True
|
959 |
|
960 |
-
def remove_node(self, node: fbx.FbxNode, recursive: bool = False) -> bool:
|
961 |
-
"""
|
962 |
-
Removes a node by reference from the scene.
|
963 |
-
:param node: `fbx.FbxNode` to remove.
|
964 |
-
:param recursive: `bool` Apply deletion recursively.
|
965 |
-
:return: True if success.
|
966 |
-
"""
|
967 |
-
if recursive:
|
968 |
-
children = [node.GetChild(c) for c in range(node.GetChildCount())]
|
969 |
-
for child in children:
|
970 |
-
self.remove_node(child, True)
|
971 |
-
# Disconnect the marker node from its parent
|
972 |
-
node.GetParent().RemoveChild(node)
|
973 |
-
|
974 |
-
# Remove the marker node from the scene
|
975 |
-
self.scene.RemoveNode(node)
|
976 |
-
|
977 |
-
return True
|
978 |
-
|
979 |
def remove_unlabeled_markers(self) -> None:
|
980 |
"""
|
981 |
Uses self.remove_node() to delete all unlabeled markers from the scene.
|
@@ -1091,7 +1134,7 @@ class FBXContainer:
|
|
1091 |
:param actor: `int` actor index to apply to. Index starts at 0.
|
1092 |
:param actor_keys: `dict` with all marker keys for this actor.
|
1093 |
"""
|
1094 |
-
for marker_class, (marker_name, marker) in enumerate(self.
|
1095 |
marker_keys = actor_keys.get(marker_class)
|
1096 |
if marker_keys:
|
1097 |
self._print(f'Replacing keys for {marker_name}', 1)
|
@@ -1102,7 +1145,7 @@ class FBXContainer:
|
|
1102 |
For all actors, uses self.replace_keyframes_per_actor() to set keyframes on each actor's marker nodes.
|
1103 |
:param key_dict: `dict` with all actor keyframes.
|
1104 |
"""
|
1105 |
-
for actor_idx in range(self.
|
1106 |
actor_dict = key_dict.get(actor_idx + 1)
|
1107 |
if actor_dict:
|
1108 |
self._print(f'Replacing keys for actor {actor_idx}', 1)
|
|
|
5 |
import h5py
|
6 |
|
7 |
# Import util libs.
|
|
|
8 |
import fbx
|
9 |
import itertools
|
10 |
|
|
|
370 |
]
|
371 |
|
372 |
|
373 |
+
def get_children_of_parent(parent: fbx.FbxNode) -> List[fbx.FbxNode]:
|
374 |
+
"""
|
375 |
+
Returns a list of all the children of the given parent.
|
376 |
+
:param parent: `fbx.FbxNode` to get children from.
|
377 |
+
:return: List of `fbx.FbxNode` children.
|
378 |
+
"""
|
379 |
+
return [parent.GetChild(i) for i in range(parent.GetChildCount())]
|
380 |
+
|
381 |
+
|
382 |
+
class FBXContainerBase:
|
383 |
+
def __init__(self, fbx_file: Path, debug: int = -1) -> None:
|
384 |
"""
|
385 |
Class that stores references to important nodes in an FBX file.
|
386 |
Offers utility functions to quickly load animation data.
|
387 |
:param fbx_file: `Path` to the file to load.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
"""
|
389 |
+
self.input_fbx = fbx_file
|
|
|
|
|
|
|
390 |
self.debug = debug
|
|
|
|
|
|
|
|
|
391 |
|
392 |
# Initiate empty lists to store references to nodes.
|
393 |
+
self.parents = []
|
394 |
+
self.children = []
|
395 |
# Store names of the actors (all parent nodes that have the first 4 markers as children).
|
396 |
+
self.parent_names = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
+
def _init_scene(self) -> None:
|
399 |
"""
|
400 |
Stores scene, root, and time_mode properties.
|
401 |
Destroys the importer to remove the reference to the loaded file.
|
|
|
418 |
# This will allow us to delete the uploaded file.
|
419 |
importer.Destroy()
|
420 |
|
421 |
+
def _init_anim(self) -> None:
|
422 |
"""
|
423 |
Stores the anim_stack, num_frames, start_frame, end_frame properties.
|
424 |
"""
|
|
|
436 |
self.start_frame = local_time_span.GetStart().GetFrameCount()
|
437 |
self.end_frame = local_time_span.GetStop().GetFrameCount()
|
438 |
|
439 |
+
def _init_parents(self, names_to_look_for: List[str]) -> None:
|
440 |
"""
|
441 |
Goes through all root children (generation 1).
|
442 |
If a child has 4 markers as children, it is considered an actor (Shogun subject) and appended to actors
|
443 |
and actor_names list properties.
|
|
|
444 |
"""
|
445 |
ts = fbx.FbxTime()
|
446 |
ts.SetFrame(self.start_frame)
|
|
|
448 |
te = fbx.FbxTime()
|
449 |
te.SetFrame(self.end_frame)
|
450 |
|
|
|
451 |
# Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
|
452 |
gen1_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
|
453 |
for gen1_node in gen1_nodes:
|
454 |
gen2_nodes = [gen1_node.GetChild(i) for i in
|
455 |
range(gen1_node.GetChildCount())] # Actor nodes (/Mimi/Hips, /Mimi/ARIEL, etc)
|
456 |
|
457 |
+
# If the list of names_to_look_for are children of this parent, it must be a parent.
|
458 |
+
gen2_names = [node.GetName().split(':')[-1] for node in gen2_nodes]
|
459 |
+
if all(name in gen2_names for name in names_to_look_for):
|
460 |
+
self.parent_names.append(gen1_node.GetName())
|
461 |
+
self.parents.append(gen1_node)
|
462 |
|
463 |
+
if len(self.parents) == 0:
|
464 |
+
raise ValueError('No parents found. A node is considered a parent ' +
|
465 |
+
'if it has the following child nodes: ' +
|
466 |
', '.join(names_to_look_for) + '.')
|
467 |
|
468 |
+
self.parent_count = len(self.parents)
|
|
|
469 |
|
470 |
+
def _init_children(self, child_names: List[str]) -> None:
|
471 |
"""
|
472 |
+
Goes through all parent nodes and stores references to its child nodes.
|
473 |
"""
|
474 |
+
for parent_node in self.parents:
|
475 |
+
family = {}
|
476 |
+
for child_name in child_names:
|
477 |
+
for parent_idx in range(parent_node.GetChildCount()):
|
478 |
+
child = parent_node.GetChild(parent_idx)
|
479 |
# Child name might have namespaces in it like this: Vera:ARIEL
|
480 |
# We want to match only on the actual name, so ignore namespaces.
|
481 |
+
if match_name(child, child_name, ignore_namespace=True):
|
482 |
+
family[child_name] = child
|
483 |
+
|
484 |
+
if len(family) != len(child_names):
|
485 |
+
raise ValueError(f'{parent_node.GetName()} does not have all children.')
|
486 |
|
487 |
+
self.children.append(family)
|
|
|
488 |
|
489 |
+
def _print(self, txt: str, lvl: int = 0) -> None:
|
490 |
+
if lvl <= self.debug:
|
491 |
+
print(txt)
|
492 |
|
493 |
+
def get_frame_range(self) -> List[int]:
|
494 |
+
"""
|
495 |
+
Replacement and improvement for:
|
496 |
+
`list(range(self.num_frames))`
|
497 |
+
If the animation does not start at frame 0, this will return a list that has the correct frames.
|
498 |
+
:return: List of `int` frame numbers that are between the start and end frame of the animation.
|
499 |
+
"""
|
500 |
+
return list(range(self.start_frame, self.end_frame))
|
501 |
+
|
502 |
+
def _check_parent(self, parent: int = 0):
|
503 |
+
"""
|
504 |
+
Safety check to see if the actor `int` is a valid number (to avoid out of range errors).
|
505 |
+
:param parent: `int` actor index, which should be between 0-max_actors.
|
506 |
+
"""
|
507 |
+
if not 0 <= parent <= self.parent_count:
|
508 |
+
raise ValueError(f'Actor index must be between 0 and {self.parent_count - 1} ({parent}).')
|
509 |
+
|
510 |
+
def convert_r(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> List[int]:
|
511 |
+
"""
|
512 |
+
Converts the value of r to a list of frame numbers, depending on what r is.
|
513 |
+
:param r: Custom frame range to use.
|
514 |
+
:return: List of `int` frame numbers (doesn't have to start at 0).
|
515 |
+
"""
|
516 |
+
# If r is one int, use 0 as start frame. If r is higher than the total frames, limit the range.
|
517 |
+
if isinstance(r, int):
|
518 |
+
r = list(range(self.num_frames)) if r > self.num_frames else list(range(r))
|
519 |
+
|
520 |
+
# A tuple of 2 indicates a frame range without step.
|
521 |
+
elif isinstance(r, tuple) and len(r) == 2:
|
522 |
+
# If the requested frame range is longer than the total frames, limit the range.
|
523 |
+
if r[1] - r[0] > self.num_frames:
|
524 |
+
r = list(range(r[0], r[0] + self.num_frames))
|
525 |
+
else:
|
526 |
+
r = list(range(r[0], r[1]))
|
527 |
+
|
528 |
+
# A tuple of 3 indicates a frame range with step.
|
529 |
+
elif isinstance(r, tuple) and len(r) == 3:
|
530 |
+
# If the requested frame range is longer than the total frames, limit the range.
|
531 |
+
if r[1] - r[0] > self.num_frames:
|
532 |
+
r = list(range(r[0], r[0] + self.num_frames, r[2]))
|
533 |
+
else:
|
534 |
+
r = list(range(r[0], r[1], r[2]))
|
535 |
+
|
536 |
+
# If r is None, return the default frame range.
|
537 |
+
else:
|
538 |
+
r = self.get_frame_range()
|
539 |
+
|
540 |
+
return r
|
541 |
+
|
542 |
+
def get_parent_node_by_name(self, parent_name: str, ignore_namespace: bool = True) -> Union[fbx.FbxNode, None]:
|
543 |
+
"""
|
544 |
+
Utility function to get a parent node reference by name.
|
545 |
+
:param parent_name: `str` name that will be looked for in the node name.
|
546 |
+
:param ignore_namespace: `bool` Whether to ignore namespaces in a node's name.
|
547 |
+
:return:
|
548 |
+
"""
|
549 |
+
# Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
|
550 |
+
parent_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
|
551 |
+
|
552 |
+
return next(
|
553 |
+
(
|
554 |
+
parent_node
|
555 |
+
for parent_node in parent_nodes
|
556 |
+
if match_name(parent_node, parent_name, ignore_namespace=ignore_namespace)
|
557 |
+
),
|
558 |
+
None,
|
559 |
+
)
|
560 |
+
|
561 |
+
def get_child_by_name(self, parent: int, name: str):
|
562 |
+
"""
|
563 |
+
Returns the reference to the parent's direct child.
|
564 |
+
:param parent: `int` parent index.
|
565 |
+
:param name: `str` child name.
|
566 |
+
:return: `fbx.FbxNode` reference.
|
567 |
+
"""
|
568 |
+
self._check_parent(parent)
|
569 |
+
return self.children[parent][name]
|
570 |
+
|
571 |
+
def get_node_by_path(self, path: str) -> fbx.FbxNode:
|
572 |
+
"""
|
573 |
+
Utility function to retrieve a node reference from a path like /Actor1/Hips/UpperLeg_l.
|
574 |
+
:param path: `str` path with forward slashes to follow.
|
575 |
+
:return: `fbx.FbxNode` reference to that node.
|
576 |
+
"""
|
577 |
+
# Split the path into node names.
|
578 |
+
node_names = [x for x in path.split('/') if x]
|
579 |
+
# Start the list of node references with the parent node that has its own function.
|
580 |
+
nodes = [self.get_parent_node_by_name(node_names[0], False)]
|
581 |
+
# Extend the list with each following child node of the previous parent.
|
582 |
+
nodes.extend(
|
583 |
+
get_child_node_by_name(nodes[idx], node_name)
|
584 |
+
for idx, node_name in enumerate(node_names[1:])
|
585 |
+
)
|
586 |
+
# Return the last node in the chain, which will be the node we were looking for.
|
587 |
+
return nodes[-1]
|
588 |
+
|
589 |
+
def get_hierarchy(self, start: fbx.FbxNode, hierarchy: List = None) -> List[fbx.FbxNode]:
|
590 |
+
"""
|
591 |
+
Returns the hierarchy under the start node.
|
592 |
+
:param hierarchy: Hierarchical `List` of `fbx.FbxNode` references.
|
593 |
+
:param start: `fbx.FbxNode` that will be used as starting point for finding the leaf nodes.
|
594 |
+
:return:
|
595 |
+
"""
|
596 |
+
if hierarchy is None:
|
597 |
+
hierarchy = []
|
598 |
+
hierarchy.append(start)
|
599 |
+
children = get_children_of_parent(start)
|
600 |
+
for child in children:
|
601 |
+
self.get_hierarchy(child, hierarchy)
|
602 |
+
|
603 |
+
return hierarchy
|
604 |
+
|
605 |
+
def remove_node(self, node: fbx.FbxNode, recursive: bool = False) -> bool:
|
606 |
+
"""
|
607 |
+
Removes a node by reference from the scene.
|
608 |
+
:param node: `fbx.FbxNode` to remove.
|
609 |
+
:param recursive: `bool` Apply deletion recursively.
|
610 |
+
:return: True if success.
|
611 |
+
"""
|
612 |
+
if recursive:
|
613 |
+
children = [node.GetChild(c) for c in range(node.GetChildCount())]
|
614 |
+
for child in children:
|
615 |
+
self.remove_node(child, True)
|
616 |
+
# Disconnect the marker node from its parent
|
617 |
+
node.GetParent().RemoveChild(node)
|
618 |
+
|
619 |
+
# Remove the marker node from the scene
|
620 |
+
self.scene.RemoveNode(node)
|
621 |
+
|
622 |
+
return True
|
623 |
+
|
624 |
+
|
625 |
+
class FBXContainer(FBXContainerBase):
|
626 |
+
def __init__(self, fbx_file: Path,
|
627 |
+
volume_dims: Tuple[float] = (10., 10., 10.),
|
628 |
+
max_actors: int = 8,
|
629 |
+
pc_size: int = 1024,
|
630 |
+
scale: float = 0.01,
|
631 |
+
debug: int = -1):
|
632 |
+
"""
|
633 |
+
Class that stores references to important nodes in an FBX file.
|
634 |
+
Offers utility functions to quickly load animation data.
|
635 |
+
:param fbx_file: `Path` to the file to load.
|
636 |
+
:param volume_dims: `tuple` of `float` that represent the dimensions of the capture volume in meters.
|
637 |
+
:param max_actors: `int` maximum amount of actors to expect in a point cloud.
|
638 |
+
:param pc_size: `int` amount of points in a point cloud.
|
639 |
+
:param debug: If higher than -1, will print out debugging statements.
|
640 |
+
"""
|
641 |
+
super().__init__(fbx_file=fbx_file, debug=debug)
|
642 |
+
|
643 |
+
if pc_size < max_actors * 73:
|
644 |
+
raise ValueError('Point cloud size must be large enough to contain the maximum amount of actors * 73'
|
645 |
+
f' markers: {pc_size}/{max_actors * 73}.')
|
646 |
+
|
647 |
+
self.debug = debug
|
648 |
+
# Python ENUM of the C++ time modes.
|
649 |
+
self.time_modes = globals.get_time_modes()
|
650 |
+
# Ordered list of marker names. Note: rearrange this in globals.py.
|
651 |
+
self.child_names = globals.get_marker_names()
|
652 |
+
|
653 |
+
# Initiate empty lists to store references to nodes.
|
654 |
+
self.children = []
|
655 |
+
|
656 |
+
self.labeled_world_transforms = None
|
657 |
+
self.unlabeled_world_transforms = None
|
658 |
+
|
659 |
+
# Split the dimensions tuple into its axes for easier access.
|
660 |
+
self.vol_x = volume_dims[0]
|
661 |
+
self.vol_y = volume_dims[1]
|
662 |
+
self.vol_z = volume_dims[2]
|
663 |
+
self.hvol_x = volume_dims[0] / 2
|
664 |
+
self.hvol_y = volume_dims[1] / 2
|
665 |
+
self.hvol_z = volume_dims[2] / 2
|
666 |
+
|
667 |
+
self.scale = scale
|
668 |
+
|
669 |
+
self.max_actors = max_actors
|
670 |
+
# Maximum point cloud size = 73 * max_actors + unlabeled markers.
|
671 |
+
self.pc_size = pc_size
|
672 |
+
|
673 |
+
self.output_fbx = utils.append_suffix_to_file(fbx_file, '_INF')
|
674 |
+
self.valid_frames = []
|
675 |
+
|
676 |
+
self.init()
|
677 |
+
|
678 |
+
def __init_unlabeled_markers(self) -> None:
|
679 |
"""
|
680 |
Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
|
681 |
"""
|
|
|
687 |
self.unlabeled_markers = [gen1_node.GetChild(um) for um in range(gen1_node.GetChildCount())]
|
688 |
return
|
689 |
|
|
|
|
|
|
|
690 |
def init_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
|
691 |
"""
|
692 |
Calls the init functions for the labeled and unlabeled world transforms.
|
|
|
708 |
labeled_data = []
|
709 |
|
710 |
# Iterate through all actors.
|
711 |
+
for actor_idx in range(self.parent_count):
|
712 |
# Initialize an empty list to store the results for this actor in.
|
713 |
actor_data = []
|
714 |
# Iterate through all markers for this actor.
|
715 |
+
for marker_idx, (n, m) in enumerate(self.children[actor_idx].items()):
|
716 |
# Get this marker's local translation animation curve.
|
717 |
# This requires the animation layer, so we can't do it within the function itself.
|
718 |
curve = m.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
|
|
732 |
self.labeled_world_transforms = np.transpose(wide_layout, axes=(3, 0, 1, 2))
|
733 |
return self.labeled_world_transforms
|
734 |
|
735 |
+
def init_unlabeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
|
736 |
+
incl_keyed: int = 0) -> np.array:
|
737 |
"""
|
738 |
For all unlabeled markers, stores a list for each element in the world transform for each frame
|
739 |
in r. This can later be used to recreate the world transform matrix.
|
740 |
:param r: Custom frame range to use.
|
741 |
+
:param incl_keyed: `bool` whether to check if the marker was keyed at the frame.
|
742 |
:return: `np.array` of shape (n_frames, n_unlabeled_markers, 14).
|
743 |
"""
|
744 |
r = self.convert_r(r)
|
|
|
750 |
# This requires the animation layer, so we can't do it within the function itself.
|
751 |
curve = ulm.LclTranslation.GetCurve(self.anim_layer, 'X', True)
|
752 |
# Get a list of each world transform element for all frames.
|
753 |
+
marker_data = get_world_transforms(0, 0, ulm, r, curve, incl_keyed=incl_keyed)
|
754 |
# Add the result to marker_data.
|
755 |
unlabeled_data.append(marker_data)
|
756 |
self._print(f'Unlabeled marker {ulm.GetName()} done', 1)
|
|
|
764 |
# Returns shape (n_frames, n_unlabeled_markers, 14).
|
765 |
return self.unlabeled_world_transforms
|
766 |
|
767 |
+
def init(self) -> None:
|
768 |
"""
|
769 |
Initializes the scene.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
770 |
"""
|
771 |
+
self._init_scene()
|
772 |
+
self._init_anim()
|
773 |
+
self._init_parents(list(self.child_names[:4]))
|
774 |
+
self._init_children(self.child_names)
|
775 |
+
self.__init_unlabeled_markers()
|
776 |
+
self._print('Init done', 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
777 |
|
778 |
def columns_from_joints(self) -> List[str]:
|
779 |
"""
|
|
|
781 |
:return: List of column names, in the form of [node1_tx, node1_ty, node1_tz, node2_tx, node2_ty, node2_tz..].
|
782 |
"""
|
783 |
columns = []
|
784 |
+
for name in self.child_names:
|
785 |
columns += [f'{name}x', f'{name}y', f'{name}z']
|
786 |
|
787 |
return columns
|
788 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
789 |
def remove_clipping_poses(self, arr: np.array) -> np.array:
|
790 |
"""
|
791 |
Checks for each axis if it does not cross the volume limits. Returns an array without clipping poses.
|
|
|
813 |
|
814 |
# Returns (n_frames, n_actors, 73, 15).
|
815 |
l_shape = self.labeled_world_transforms.shape
|
816 |
+
|
817 |
# Flatten the array, so we get a list of frames.
|
818 |
# Reshape to (n_frames * n_actors, 73, 15).
|
819 |
flattened = self.labeled_world_transforms.reshape(-1, l_shape[2], l_shape[3])
|
820 |
+
|
821 |
# Isolates the poses with all keyframes present by checking the last elements.
|
822 |
# Start with the mask.
|
823 |
# Returns shape of (n_frames * n_actors, 73, 15).
|
|
|
825 |
# We only need a filter for the first dimension, so use .all to check if all markers
|
826 |
# have a keyframe. This results in shape (n_frames * n_actors,).
|
827 |
mask = mask.all(axis=1)
|
828 |
+
|
829 |
# Now isolate the right frames with the mask and remove the last element of the last dimension,
|
830 |
# because it won't be useful anymore.
|
831 |
# Also remove any frames that cross the limits of the volume.
|
|
|
851 |
# If either of the arrays is None, we can initialize them with r.
|
852 |
if self.labeled_world_transforms is None:
|
853 |
# For inference, we don't need keyed frames, so incl_keyed is False.
|
854 |
+
self.init_labeled_world_transforms(r=r, incl_keyed=1)
|
855 |
if self.unlabeled_world_transforms is None:
|
856 |
# Note: Unlabeled data is already flattened.
|
857 |
+
self.init_unlabeled_world_transforms(r=r, incl_keyed=1)
|
858 |
|
859 |
+
# Returns (n_frames, n_actors, 73, 15).
|
860 |
ls = self.labeled_world_transforms.shape
|
861 |
# Flatten the array, so we get a list of frames.
|
862 |
+
# Returns shape (n_frames, 73 * n_actors, 15).
|
863 |
+
flat_labeled = self.labeled_world_transforms.reshape(ls[0], -1, ls[-1])
|
864 |
+
|
865 |
+
# Find all labeled markers that have their keyed value set to 0 (which means they had no keyframe on tx),
|
866 |
+
# and set their transforms to np.inf.
|
867 |
+
mask = flat_labeled[..., -1] == 0
|
868 |
+
flat_labeled[mask, 2:] = np.inf
|
869 |
+
|
870 |
+
# Do the same for the unlabeled markers.
|
871 |
+
mask = self.unlabeled_world_transforms[..., -1] == 0
|
872 |
+
self.unlabeled_world_transforms[mask, 2:] = np.inf
|
873 |
+
|
874 |
+
# Remove the last element of the last dimension of both arrays, because it won't be useful anymore.
|
875 |
+
flat_labeled = flat_labeled[..., :-1]
|
876 |
+
self.unlabeled_world_transforms = self.unlabeled_world_transforms[..., :-1]
|
877 |
|
878 |
if merged:
|
879 |
return utils.merge_labeled_and_unlabeled_data(labeled=flat_labeled,
|
|
|
894 |
|
895 |
# First multiply by self.scale, which turns centimeters to meters.
|
896 |
# Then divide by volume dimensions, to normalize to the total area of the capture volume.
|
897 |
+
w[..., start + 0] = w[..., start + 0] * self.scale / self.vol_x
|
898 |
+
w[..., start + 1] = w[..., start + 1] * self.scale / self.vol_y
|
899 |
+
w[..., start + 2] = w[..., start + 2] * self.scale / self.vol_z
|
900 |
|
901 |
# Then move the x and z to the center of the volume. Y doesn't need to be done because pose needs to stand
|
902 |
# on the floor.
|
|
|
924 |
# Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
|
925 |
return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
|
926 |
|
927 |
+
def convert_class_to_parent(self, c: float = 0):
|
928 |
"""
|
929 |
Returns the actor name based on the class value.
|
930 |
:param c: `float` actor class index.
|
931 |
:return: `str` actor name.
|
932 |
"""
|
933 |
+
return 'UNLABELED' if int(c) == 0 else self.parent_names[int(c) - 1]
|
934 |
|
935 |
+
def convert_class_to_child(self, c: float = 0):
|
936 |
"""
|
937 |
Returns the marker name based on the class value.
|
938 |
:param c: `float` marker class index.
|
939 |
:return: `str` marker name.
|
940 |
"""
|
941 |
+
return 'UNLABELED' if int(c) == 0 else self.child_names[int(c) - 1]
|
942 |
|
943 |
def export_train_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) \
|
944 |
-> Union[bytes, pd.DataFrame, np.array]:
|
|
|
1019 |
|
1020 |
return True
|
1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1022 |
def remove_unlabeled_markers(self) -> None:
|
1023 |
"""
|
1024 |
Uses self.remove_node() to delete all unlabeled markers from the scene.
|
|
|
1134 |
:param actor: `int` actor index to apply to. Index starts at 0.
|
1135 |
:param actor_keys: `dict` with all marker keys for this actor.
|
1136 |
"""
|
1137 |
+
for marker_class, (marker_name, marker) in enumerate(self.children[actor].items(), start=1):
|
1138 |
marker_keys = actor_keys.get(marker_class)
|
1139 |
if marker_keys:
|
1140 |
self._print(f'Replacing keys for {marker_name}', 1)
|
|
|
1145 |
For all actors, uses self.replace_keyframes_per_actor() to set keyframes on each actor's marker nodes.
|
1146 |
:param key_dict: `dict` with all actor keyframes.
|
1147 |
"""
|
1148 |
+
for actor_idx in range(self.parent_count):
|
1149 |
actor_dict = key_dict.get(actor_idx + 1)
|
1150 |
if actor_dict:
|
1151 |
self._print(f'Replacing keys for actor {actor_idx}', 1)
|