Natsha commited on
Commit
5e3f217
·
1 Parent(s): 7dc402c

Removed old data extraction methods and documented all the new functions.

Browse files
Files changed (1) hide show
  1. 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 make_ghost_markers(missing: int) -> np.array:
37
  """
38
- Creates a np array containing enough rows to fill a point cloud up to 1000 points.
39
- Ghost markers are always unlabeled markers, therefore their actor and marker class is 0.
40
- :param missing: `int` amount of missing rows in the cloud that need to be filled with this function.
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
- # Actor and marker classes enter as shape (x, 1000), so use np.expand_dims to create a new dimension at the end.
78
- # Return the concatenated array of shape (x, 1000, 14), which matches the original timeline dense cloud before
 
 
 
 
 
 
 
 
 
 
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: sorted `np.array` point cloud.
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 True
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, r: List[int], c, incl_keyed: int = 1) \
298
- -> List[List[float]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(r=r)
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 = False) -> None:
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
- r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
 
 
 
 
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 print_valid_frames_stats_for_actor(self, actor: int = 0):
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
- self._check_valid_frames(actor)
809
- return self.valid_frames[actor]
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
- # Finally, add 0.5 to the x and z to make the pose stand in the center of the normalized volume.
935
- w[..., start + 0] = np.clip(w[..., start + 0], -0.5, 0.5) + 0.5
 
 
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) + 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
- if output_file is None:
1106
- df = pd.DataFrame(self.extract_training_translations(r))
1107
- return df.to_csv(index=False).encode('utf-8')
1108
-
1109
- elif output_file.suffix == '.npy':
1110
- array_4d = self.extract_training_translations(r)
1111
- np.save(str(output_file), array_4d)
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 .csv or .npy')
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)