Natsha commited on
Commit
74bcef0
·
1 Parent(s): 5e3f217

Moved the core functionality of FBXContainer to a new class called FBXContainerBase.

Browse files

extract_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.

Files changed (1) hide show
  1. 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
- class FBXContainer:
375
- def __init__(self, fbx_file: Path,
376
- volume_dims: Tuple[float] = (10., 10., 10.),
377
- max_actors: int = 8,
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.
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
- if pc_size < max_actors * 73:
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.markers = []
406
- self.actors = []
407
  # Store names of the actors (all parent nodes that have the first 4 markers as children).
408
- self.actor_names = []
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 __init_scene(self) -> None:
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 __init_anim(self) -> None:
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 __init_actors(self, ignore_missing: bool = False) -> None:
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 first 3 marker names are children of this parent, it must be an actor.
498
- if all(name in [node.GetName().split(':')[-1] for node in gen2_nodes] for name in names_to_look_for):
499
- self.actor_names.append(gen1_node.GetName())
500
- self.actors.append(gen1_node)
 
501
 
502
- if len(self.actors) == 0 and not ignore_missing:
503
- raise ValueError('No actors/subjects found. A node is considered an actor ' +
504
- 'if it has the following children nodes: ' +
505
  ', '.join(names_to_look_for) + '.')
506
 
507
- self.actor_count = len(self.actors)
508
- self.valid_frames = [[] for _ in range(self.actor_count)]
509
 
510
- def __init_markers(self, ignore_missing: bool = False) -> None:
511
  """
512
- Goes through all actor nodes and stores references to its marker nodes.
513
  """
514
- for actor_node in self.actors:
515
- actor_markers = {}
516
- for marker_name in self.marker_names:
517
- for actor_idx in range(actor_node.GetChildCount()):
518
- child = actor_node.GetChild(actor_idx)
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, marker_name, ignore_namespace=True):
522
- actor_markers[marker_name] = child
 
 
 
523
 
524
- if len(actor_markers) != len(self.marker_names) and not ignore_missing:
525
- raise ValueError(f'{actor_node.GetName()} does not have all markers.')
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
  """
@@ -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.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)
@@ -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) -> 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)
@@ -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=0)
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, 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)
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
- # 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))
662
-
663
- # A tuple of 2 indicates a frame range without step.
664
- elif isinstance(r, tuple) and len(r) == 2:
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.marker_names:
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=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
 
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.hvol_x
836
- w[..., start + 1] = w[..., start + 1] * self.scale / self.hvol_y
837
- w[..., start + 2] = w[..., start + 2] * self.scale / self.hvol_z
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 convert_class_to_actor(self, c: float = 0):
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.actor_names[int(c) - 1]
872
 
873
- def convert_class_to_marker(self, c: float = 0):
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.marker_names[int(c) - 1]
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.markers[actor].items(), start=1):
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.actor_count):
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)