Natsha commited on
Commit
e269a6f
·
1 Parent(s): a0a49ab

Changed the way transforms are extracted and added functions to export them to HDF5 (.h5) files.

Browse files
Files changed (7) hide show
  1. app.py +2 -2
  2. fbx_handler.md +16 -7
  3. fbx_handler.py +396 -145
  4. labeler/data_setup.py +161 -0
  5. preprocess_files.py +71 -0
  6. requirements.txt +3 -1
  7. utils.py +104 -0
app.py CHANGED
@@ -10,9 +10,9 @@ import streamlit as st
10
  import fbx_handler
11
 
12
 
13
- def process_file(file: Path) -> bytes:
14
  fbx_content = fbx_handler.FBXContainer(file)
15
- return fbx_content.export(t='string')
16
 
17
 
18
  # Initialize session state variables if they don't exist
 
10
  import fbx_handler
11
 
12
 
13
+ def process_file(file: Path) -> int:
14
  fbx_content = fbx_handler.FBXContainer(file)
15
+ return 1
16
 
17
 
18
  # Initialize session state variables if they don't exist
fbx_handler.md CHANGED
@@ -8,26 +8,35 @@ input_file = Path('/path/to/file.fbx')
8
  container = FBXContainer(input_file)
9
  ```
10
 
 
 
 
 
 
 
 
11
  ## Training workflow:
12
  ```python
13
- # Get dataframe with all valid translation numbers.
14
- df = container.extract_all_valid_translations()
 
 
15
  # Convert to dataset...
16
  ...
17
  ```
18
 
19
  ## Testing workflow:
20
  ```python
21
- # Get timeline dense cloud.
22
- tdc = container.get_tdc() # wrap in shuffle_tdc() to shuffle nodes.
23
- # Split array into subarrays.
24
- actors_test, markers_test, t_test, r_test, s_test = container.split_tdc(tdc)
25
  # Predict the new actors and classes...
26
  actors_pred, markers_pred = Labeler(container.transform_translations(t_test))
27
  # Merge the new labels with their original translations.
28
  merged = merge_tdc(actors_pred, markers_pred, t_test, r_test, s_test)
29
  # Convert the full cloud into a dict structured for easy keyframes.
30
- new_dict = tsc_to_dict(merged)
31
  # Replace the old translation keyframes with the new values.
32
  container.replace_keyframes_for_all_actors(new_dict)
33
  # Export file.
 
8
  container = FBXContainer(input_file)
9
  ```
10
 
11
+ ## Preprocess data:
12
+ ```python
13
+ container.init_world_transforms(r=...)
14
+ train_raw_data = container.extract_training_translations()
15
+ test_raw_data = container.extract_inf_translations()
16
+ ```
17
+
18
  ## Training workflow:
19
  ```python
20
+ # Load file.
21
+ container = FBXContainer(input_file)
22
+ # Get np.array with all valid translation numbers.
23
+ actors_train, markers_train, t_test, _, _ = container.get_split_transforms(mode='train')
24
  # Convert to dataset...
25
  ...
26
  ```
27
 
28
  ## Testing workflow:
29
  ```python
30
+ # Load file.
31
+ container = FBXContainer(input_file)
32
+ # Get splitted original data (no transforms applied).
33
+ actors_test, markers_test, t_test, r_test_, s_test = container.get_split_transforms(mode='test')
34
  # Predict the new actors and classes...
35
  actors_pred, markers_pred = Labeler(container.transform_translations(t_test))
36
  # Merge the new labels with their original translations.
37
  merged = merge_tdc(actors_pred, markers_pred, t_test, r_test, s_test)
38
  # Convert the full cloud into a dict structured for easy keyframes.
39
+ new_dict = array_to_dict(merged)
40
  # Replace the old translation keyframes with the new values.
41
  container.replace_keyframes_for_all_actors(new_dict)
42
  # Export file.
fbx_handler.py CHANGED
@@ -1,10 +1,8 @@
1
- # Import core libs.
2
- from pprint import pprint
3
-
4
  import pandas as pd
5
  import numpy as np
6
  from pathlib import Path
7
  from typing import List, Union, Tuple
 
8
 
9
  # Import util libs.
10
  import contextlib
@@ -13,16 +11,18 @@ import itertools
13
 
14
  # Import custom data.
15
  import globals
 
16
 
17
 
18
- def center_axis(a: List[float]) -> np.array:
19
  """
20
  Centers a list of floats.
21
  :param a: List of floats to center.
22
  :return: The centered list as a `np.array`.
23
  """
24
  # Turn list into np array for optimized math.
25
- a = np.array(a)
 
26
 
27
  # Find the centroid by subtracting the lowest value from the highest value.
28
  _min = np.min(a)
@@ -58,17 +58,6 @@ def make_ghost_markers(missing: int) -> np.array:
58
  ])
59
 
60
 
61
- def append_suffix(file_path: Path, suffix: str = '_INF'):
62
- """
63
- Adds a suffix to the given file path.
64
- :param file_path: `Path` object to the original file.
65
- :param suffix: `str` suffix to add to the end of the original file name.
66
- :return: Updated `Path`.
67
- """
68
- new_file_name = file_path.stem + suffix + file_path.suffix
69
- return file_path.with_name(new_file_name)
70
-
71
-
72
  def append_zero(arr: np.ndarray) -> np.ndarray:
73
  zeros = np.zeros((arr.shape[0], arr.shape[1], 1), dtype=float)
74
  return np.concatenate((arr, zeros), axis=-1)
@@ -85,8 +74,8 @@ def merge_tdc(actor_classes: np.array,
85
  rotation_vectors: np.array,
86
  scale_vectors: np.array,
87
  ordered: bool = True) -> np.array:
88
- # Actor and marker classes enter as shape (x, 1000), so use np.expand_dims to create an extra dimension at the end.
89
- # Return the concatenated array of shape (x, 1000, 5), which matches the original timeline dense cloud before
90
  # splitting it into sub arrays.
91
 
92
  tdc = np.concatenate((np.expand_dims(actor_classes, -1),
@@ -111,7 +100,7 @@ def shuffle_tdc(tdc: np.array) -> np.array:
111
  if tdc.ndim != 3:
112
  raise ValueError(f'Array does not have 3 dimensions: {tdc.ndim}/3.')
113
 
114
- # Shuffle the node rows.
115
  for i in range(tdc.shape[0]):
116
  np.random.shuffle(tdc[i])
117
  return tdc
@@ -142,10 +131,6 @@ def sort_cloud(cloud: np.array) -> np.array:
142
  return sorted_tdc
143
 
144
 
145
- def isolate_labeled_markers_from_tdc(tdc: np.array) -> np.array:
146
- return np.stack([tdc[i, tdc[i, :, 0] > 0.] for i in range(tdc.shape[0])], axis=0)
147
-
148
-
149
  def create_keyframe(anim_curve: fbx.FbxAnimCurve, frame: int, value: float):
150
  # Create an FbxTime object with the given frame number
151
  t = fbx.FbxTime()
@@ -173,7 +158,7 @@ def match_name(node: fbx.FbxNode, name: str, ignore_namespace: bool = True) -> b
173
  return node_name == name
174
 
175
 
176
- def tsc_to_dict(tsc: np.array, start_frame: int = 0) -> dict:
177
  """
178
  Converts an `np.array` timeline sparse cloud to a dictionary structured for keyframed animation.
179
  :param tsc: `np.array` timeline sparse cloud to process.
@@ -248,16 +233,16 @@ def world_to_local_transform(node: fbx.FbxNode, world_transform: fbx.FbxAMatrix,
248
  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)]
249
 
250
 
251
- def get_world_transform(m: fbx.FbxNode, time: fbx.FbxTime, axes: str = 'trs') -> np.array:
252
  """
253
  Evaluates the world translation of the given node at the given time,
254
  scales it down by scale and turns it into a vector list.
255
  :param m: `fbx.FbxNode` marker to evaluate the world translation of.
256
- :param time: `fbx.FbxTime` time to evaluate at.
257
  :param axes: `str` that contains types of info to include. Options are a combination of t, r, and s.
258
  :return: Vector in the form: [tx, ty, etc..].
259
  """
260
- matrix = m.EvaluateGlobalTransform(time)
261
 
262
  # If axes is only the translation, we return a vector of (tx, ty, tz) only (useful for the training).
263
  if axes == 't':
@@ -301,12 +286,70 @@ def split_tdc_into_actors(tdc: np.array) -> List[np.array]:
301
  return [isolate_actor_from_tdc(tdc, i) for i in range(1, actor_count + 1)]
302
 
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  class FBXContainer:
305
  def __init__(self, fbx_file: Path,
306
  volume_dims: Tuple[float] = (10., 4., 10.),
307
- max_actors: int = 10,
308
- pc_size: int = 1000,
309
- scale: float = 0.01):
 
 
 
 
310
  """
311
  Class that stores references to important nodes in an FBX file.
312
  Offers utility functions to quickly load animation data.
@@ -314,11 +357,16 @@ class FBXContainer:
314
  :param volume_dims: `tuple` of `float` that represent the dimensions of the capture volume in meters.
315
  :param max_actors: `int` maximum amount of actors to expect in a point cloud.
316
  :param pc_size: `int` amount of points in a point cloud.
 
 
 
 
317
  """
318
  if pc_size < max_actors * 73:
319
  raise ValueError('Point cloud size must be large enough to contain the maximum amount of actors * 73'
320
  f' markers: {pc_size}/{max_actors * 73}.')
321
 
 
322
  # Python ENUM of the C++ time modes.
323
  self.time_modes = globals.get_time_modes()
324
  # Ordered list of marker names. Note: rearrange this in globals.py.
@@ -330,6 +378,9 @@ class FBXContainer:
330
  # Store names of the actors (all parent nodes that have the first 4 markers as children).
331
  self.actor_names = []
332
 
 
 
 
333
  # Split the dimensions tuple into its axes for easier access.
334
  self.vol_x = volume_dims[0]
335
  self.vol_y = volume_dims[1]
@@ -342,16 +393,16 @@ class FBXContainer:
342
  self.pc_size = pc_size
343
 
344
  self.input_fbx = fbx_file
345
- self.output_fbx = append_suffix(fbx_file, '_INF')
 
346
  self.valid_frames = []
347
 
348
- self.__init_scene()
349
- self.__init_anim()
350
- self.__init_actors()
351
- self.__init_markers()
352
- self.__init_unlabeled_markers()
353
 
354
- def __init_scene(self):
355
  """
356
  Stores scene, root, and time_mode properties.
357
  Destroys the importer to remove the reference to the loaded file.
@@ -365,6 +416,8 @@ class FBXContainer:
365
  self.scene = fbx.FbxScene.Create(self.manager, '')
366
  importer.Import(self.scene)
367
  self.root = self.scene.GetRootNode()
 
 
368
  self.time_mode = self.scene.GetGlobalSettings().GetTimeMode()
369
  fbx.FbxTime.SetGlobalTimeMode(self.time_mode)
370
 
@@ -372,27 +425,38 @@ class FBXContainer:
372
  # This will allow us to delete the uploaded file.
373
  importer.Destroy()
374
 
375
- def __init_anim(self):
376
  """
377
  Stores the anim_stack, num_frames, start_frame, end_frame properties.
378
  """
379
  # Get the animation stack and layer.
380
  anim_stack = self.scene.GetCurrentAnimationStack()
381
  self.anim_layer = anim_stack.GetSrcObject(fbx.FbxCriteria.ObjectType(fbx.FbxAnimLayer.ClassId), 0)
 
 
382
 
383
  # Find the total number of frames to expect from the local time span.
384
  local_time_span = anim_stack.GetLocalTimeSpan()
385
  self.num_frames = int(local_time_span.GetDuration().GetFrameCount())
 
 
386
  self.start_frame = local_time_span.GetStart().GetFrameCount()
387
  self.end_frame = local_time_span.GetStop().GetFrameCount()
388
 
389
- def __init_actors(self):
390
  """
391
  Goes through all root children (generation 1).
392
  If a child has 4 markers as children, it is considered an actor (Shogun subject) and appended to actors
393
  and actor_names list properties.
394
  Also initializes an empty valid_frames list for each found actor.
395
  """
 
 
 
 
 
 
 
396
  # Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
397
  gen1_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
398
  for gen1_node in gen1_nodes:
@@ -400,14 +464,19 @@ class FBXContainer:
400
  range(gen1_node.GetChildCount())] # Actor nodes (/Mimi/Hips, /Mimi/ARIEL, etc)
401
 
402
  # If the first 3 marker names are children of this parent, it must be an actor.
403
- if all(name in [node.GetName().split(':')[-1] for node in gen2_nodes] for name in self.marker_names[:4]):
404
  self.actor_names.append(gen1_node.GetName())
405
  self.actors.append(gen1_node)
406
 
 
 
 
 
 
407
  self.actor_count = len(self.actors)
408
  self.valid_frames = [[] for _ in range(self.actor_count)]
409
 
410
- def __init_markers(self):
411
  """
412
  Goes through all actor nodes and stores references to its marker nodes.
413
  """
@@ -421,11 +490,12 @@ class FBXContainer:
421
  if match_name(child, marker_name, ignore_namespace=True):
422
  actor_markers[marker_name] = child
423
 
424
- assert len(actor_markers) == len(self.marker_names), f'{actor_node.GetName()} does not have all markers.'
 
425
 
426
  self.markers.append(actor_markers)
427
 
428
- def __init_unlabeled_markers(self):
429
  """
430
  Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
431
  """
@@ -437,13 +507,69 @@ class FBXContainer:
437
  self.unlabeled_markers = [gen1_node.GetChild(um) for um in range(gen1_node.GetChildCount())]
438
  return
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  def _check_actor(self, actor: int = 0):
441
  """
442
  Safety check to see if the actor `int` is a valid number (to avoid out of range errors).
443
  :param actor: `int` actor index, which should be between 0-max_actors.
444
  """
445
- assert 0 <= actor <= self.actor_count, f'Actor index must be between 0 and {self.actor_count - 1}. ' \
446
- f'It is {actor}.'
447
 
448
  def _set_valid_frames_for_actor(self, actor: int = 0):
449
  """
@@ -459,16 +585,18 @@ class FBXContainer:
459
  self._check_actor(actor)
460
 
461
  frames = self.get_frame_range()
462
- for _, marker in self.markers[actor].items():
463
  # Get the animation curve for local translation x.
464
  t_curve = marker.LclTranslation.GetCurve(self.anim_layer, 'X')
465
  # If an actor was recorded but seems to have no animation curves, we set their valid frames to nothing.
466
  # Then we return, because there is no point in further checking non-existent keyframes.
467
  if t_curve is None:
468
  self.valid_frames[actor] = []
 
469
  return
470
 
471
  # Get all keyframes on the animation curve and store their frame numbers.
 
472
  keys = [t_curve.KeyGet(i).GetTime().GetFrameCount() for i in range(t_curve.KeyGetCount())]
473
  # Check for each frame in frames if it is present in the list of keyframed frames.
474
  for frame in frames:
@@ -478,6 +606,7 @@ class FBXContainer:
478
  with contextlib.suppress(ValueError):
479
  frames.remove(frame)
480
 
 
481
  self.valid_frames[actor] = frames
482
 
483
  # Store all frame lists that have at least 1 frame.
@@ -486,13 +615,6 @@ class FBXContainer:
486
  self.common_frames = [num for num in self.get_frame_range()
487
  if all(num in other_list for other_list in other_lists)]
488
 
489
- def set_valid_frames(self):
490
- """
491
- For each actor, calls _set_valid_frames_for_actor().
492
- """
493
- for i in range(self.actor_count):
494
- self._set_valid_frames_for_actor(i)
495
-
496
  def _check_valid_frames(self, actor: int = 0):
497
  """
498
  Safety check to see if the given actor has any valid frames stored.
@@ -502,9 +624,10 @@ class FBXContainer:
502
  self._check_actor(actor)
503
 
504
  if not len(self.valid_frames[actor]):
 
505
  self._set_valid_frames_for_actor(actor)
506
 
507
- def get_transformed_pc(self, actor: int = 0, frame: int = 0) -> List[float]:
508
  """
509
  Evaluates all marker nodes for the given actor and modifies the resulting point cloud,
510
  so it is centered and scaled properly for training.
@@ -538,7 +661,20 @@ class FBXContainer:
538
  z /= self.vol_z
539
  y = np.array(y) / self.vol_y
540
 
541
- # TODO: Optional: Add any extra modifications to the point cloud here.
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
543
  # Append all values to a new array, one axis at a time.
544
  # This way it will match the column names order.
@@ -549,16 +685,6 @@ class FBXContainer:
549
  pose += [z[i]]
550
  return pose
551
 
552
- def extract_scaled_translation(self, m: fbx.FbxNode, time: fbx.FbxTime) -> List[float]:
553
- """
554
- Evaluates a node's world translation at the given time and scales the vector down by a factor of self.scale.
555
- :param m: `fbx.FbxNode` node that needs to be evaluated.
556
- :param time: `fbx.FbxTime` at which frame/time the node needs to be evaluated.
557
- :return: Translation vector as a list of floats.
558
- """
559
- t = m.EvaluateGlobalTransform(time).GetT()
560
- return [t[i] * self.scale for i in range(3)]
561
-
562
  def get_frame_range(self) -> List[int]:
563
  """
564
  Replacement and improvement for:
@@ -568,6 +694,33 @@ class FBXContainer:
568
  """
569
  return list(range(self.start_frame, self.end_frame))
570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  def columns_from_joints(self) -> List[str]:
572
  """
573
  Generates a list of column names based on the (order of the) marker names.
@@ -652,40 +805,97 @@ class FBXContainer:
652
  self._check_valid_frames(actor)
653
  return self.valid_frames[actor]
654
 
655
- def extract_valid_translations_per_actor(self, actor: int = 0) -> List[List[float]]:
656
  """
657
  Assembles the poses for the valid frames for the given actor as a 2D list where each row is a pose.
658
  :param actor: `int` actor index.
 
659
  :return: List of poses, where each pose is a list of `float` translations.
660
  """
661
  # Ensure the actor index is within range.
662
  self._check_actor(actor)
 
663
 
664
- poses = []
665
- # Go through all valid frames for this actor.
666
- # Note that these frames can be different per actor.
667
- for frame in self.valid_frames[actor]:
668
- # Get the centered point cloud as a 1D list.
669
- pose_at_frame = self.get_transformed_pc(actor, frame)
670
- poses.append(pose_at_frame)
671
 
672
- return poses
 
 
 
 
 
 
 
 
 
673
 
674
- def extract_all_valid_translations(self) -> pd.DataFrame:
675
  """
676
  Convenience method that calls self.extract_valid_translations_per_actor() for all actors
677
  and returns a `DataFrame` containing all poses after each other.
678
- :return: `DataFrame` where each row is a pose.
679
- """
680
- # Note that the column names are/must be in the same order as the markers.
681
- columns = self.columns_from_joints()
682
-
683
- all_poses = []
684
- # For each actor, add their valid poses to all_poses.
685
- for i in range(self.actor_count):
686
- all_poses.extend(self.extract_valid_translations_per_actor(i))
687
-
688
- return pd.DataFrame(all_poses, columns=columns)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
690
  def transform_translations(self, w: np.array) -> np.array:
691
  """
@@ -697,7 +907,7 @@ class FBXContainer:
697
  raise ValueError(f'Array does not have 3 dimensions: {w.ndim}/3.')
698
 
699
  # If the last dimension has 3 elements, it is a translation vector of shape (tx, ty, tz).
700
- # If it has 14 elements, it is a full marker row of shape (actor, marker, tx, ty, tz, rx, ry, rz, etc).
701
  start = 0 if w.shape[-1] == 3 else 2
702
 
703
  # First multiply by self.scale, which turns meters to centimeters.
@@ -761,6 +971,21 @@ class FBXContainer:
761
  # so return the cloud as a np array that cuts off any excessive markers.
762
  return np.array(cloud)[:self.pc_size]
763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  def get_tsc(self) -> np.array:
765
  """
766
  Convenience method that calls self.get_sparse_cloud() for all frames in the frame range
@@ -769,7 +994,7 @@ class FBXContainer:
769
  """
770
  return np.array([self.get_sc(f) for f in self.get_frame_range()])
771
 
772
- def get_tdc(self, r: Union[int, Tuple[int, int]] = None) -> np.array:
773
  """
774
  For each frame in the frame range, collects the point cloud that is present in the file.
775
  Then it creates a ghost cloud of random markers that are treated as unlabeled markers,
@@ -781,32 +1006,27 @@ class FBXContainer:
781
  with a shape of (self.num_frames, self.pc_size, 5).
782
  """
783
 
784
- clouds = []
785
 
786
- # If r is one int, use 0 as start frame.
787
- if isinstance(r, int):
788
- r = list(range(r))
789
- # If r is two ints, use that as specific frame range.
790
- elif isinstance(r, tuple) and len(r) >= 2:
791
- r = list(range(r[0], r[1]))
792
- # If r is empty, use the animation frame range.
793
- else:
794
- r = self.get_frame_range()
795
 
796
- for frame in r:
797
- cloud = self.get_sc(frame)
798
- missing = self.pc_size - cloud.shape[0]
799
 
800
- # Only bother creating ghost markers if there are any missing rows.
801
- # If we need to add ghost markers, add them before the existing cloud,
802
- # so that the cloud will remain a sorted array regarding the actor and marker classes.
803
- if missing > 0:
804
- ghost_cloud = make_ghost_markers(missing)
805
- cloud = np.vstack([ghost_cloud, cloud])
806
 
807
- clouds.append(cloud)
 
 
808
 
809
- return np.array(clouds)
 
 
 
810
 
811
  def split_tdc(self, cloud: np.array = None) \
812
  -> Tuple[np.array, np.array, np.array, np.array, np.array]:
@@ -822,13 +1042,28 @@ class FBXContainer:
822
  :return: Return tuple of `np.array` as (actor classes, marker classes, translation vectors).
823
  """
824
  if cloud is None:
825
- cloud = self.get_tdc()
 
 
 
 
 
826
 
827
- if cloud.shape[1] != 1000:
828
- raise ValueError(f"Dense cloud doesn't have enough points. {cloud.shape[1]}/1000.")
829
- if cloud.shape[2] != 14:
830
- raise ValueError(f"Dense cloud is missing columns: {cloud.shape[2]}/14.")
831
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  # Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
833
  return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
834
 
@@ -848,21 +1083,50 @@ class FBXContainer:
848
  """
849
  return 'UNLABELED' if int(c) == 0 else self.marker_names[int(c) - 1]
850
 
851
- def export(self, t: str = 'csv', output_file: Path = None) -> Union[bytes, Path]:
852
- # Get the dataframe with all animation data.
853
- df = self.extract_all_valid_translations()
854
-
855
- if t == 'string':
856
  return df.to_csv(index=False).encode('utf-8')
857
 
858
- if output_file is None:
859
- output_file = self.input_fbx.with_suffix('.csv')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
 
861
- if output_file.suffix != '.csv':
862
- raise ValueError(f'{output_file} needs to be a .csv file.')
 
 
 
863
 
864
- df.to_csv(output_file, index=False)
865
- return output_file
 
 
 
 
 
 
866
 
867
  def export_fbx(self, output_file: Path = None) -> bool:
868
  """
@@ -879,18 +1143,20 @@ class FBXContainer:
879
  # Initialize the exporter with the output file path
880
  result = exporter.Initialize(str(output_file))
881
  if not result:
882
- print(f"Failed to initialize the exporter for file '{output_file}'.")
883
  return False
884
 
885
  # Export the scene
886
  result = exporter.Export(self.scene)
887
  if not result:
888
- print(f"Failed to export the scene to file '{output_file}'.")
889
  return False
890
 
891
  # Clean up the manager and exporter
892
  exporter.Destroy()
893
 
 
 
894
  return True
895
 
896
  def remove_node(self, node: fbx.FbxNode, recursive: bool = False) -> bool:
@@ -1024,6 +1290,7 @@ class FBXContainer:
1024
  for marker_class, (marker_name, marker) in enumerate(self.markers[actor].items(), start=1):
1025
  marker_keys = actor_keys.get(marker_class)
1026
  if marker_keys:
 
1027
  self.replace_keyframes_per_marker(marker, marker_keys)
1028
 
1029
  def replace_keyframes_for_all_actors(self, key_dict: dict) -> None:
@@ -1032,23 +1299,7 @@ class FBXContainer:
1032
  :param key_dict: `dict` with all actor keyframes.
1033
  """
1034
  for actor_idx in range(self.actor_count):
1035
- actor_dict = key_dict.get(actor_idx+1)
1036
  if actor_dict:
 
1037
  self.replace_keyframes_per_actor(actor_idx, actor_dict)
1038
-
1039
-
1040
- # d = FBXContainer(Path('G:/Firestorm/mocap-ai/data/fbx/dowg/TAKE_01+1_ALL_001.fbx'))
1041
- # og_cloud = d.get_tdc()
1042
- # # print(og_cloud[0, -10:, 2:5])
1043
- # di = tsc_to_dict(og_cloud)
1044
- # d.replace_keyframes_for_all_actors(di)
1045
- # # new_cloud = d.get_tdc(r=100)
1046
- # # print(new_cloud[0, -10:, 2:5])
1047
- # # actors_train, markers_train, t_train, r_train, s_train = d.split_tdc(cloud)
1048
- # # # t_train_transformed = d.transform_translations(t_train)
1049
- # # # splits = d.split_tdc(apply_transform=False)
1050
- # # merged = merge_tdc(actors_train, markers_train, t_train, r_train, s_train)
1051
- # # pc_dict = tsc_to_dict(merged, d.start_frame)
1052
- # # d.replace_keyframes_for_all_actors(pc_dict)
1053
- # # # d.cleanup()
1054
- # d.export_fbx(Path('G:/Firestorm/mocap-ai/data/fbx/export/TAKE_01+1_ALL_001.fbx'))
 
 
 
 
1
  import pandas as pd
2
  import numpy as np
3
  from pathlib import Path
4
  from typing import List, Union, Tuple
5
+ import h5py
6
 
7
  # Import util libs.
8
  import contextlib
 
11
 
12
  # Import custom data.
13
  import globals
14
+ import utils
15
 
16
 
17
+ def center_axis(a: Union[List[float], np.array]) -> np.array:
18
  """
19
  Centers a list of floats.
20
  :param a: List of floats to center.
21
  :return: The centered list as a `np.array`.
22
  """
23
  # Turn list into np array for optimized math.
24
+ if not isinstance(a, np.ndarray):
25
+ a = np.array(a)
26
 
27
  # Find the centroid by subtracting the lowest value from the highest value.
28
  _min = np.min(a)
 
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)
 
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),
 
100
  if tdc.ndim != 3:
101
  raise ValueError(f'Array does not have 3 dimensions: {tdc.ndim}/3.')
102
 
103
+ # Shuffle the frames.
104
  for i in range(tdc.shape[0]):
105
  np.random.shuffle(tdc[i])
106
  return tdc
 
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()
 
158
  return node_name == name
159
 
160
 
161
+ def array_to_dict(tsc: np.array, start_frame: int = 0) -> dict:
162
  """
163
  Converts an `np.array` timeline sparse cloud to a dictionary structured for keyframed animation.
164
  :param tsc: `np.array` timeline sparse cloud to process.
 
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':
 
286
  return [isolate_actor_from_tdc(tdc, i) for i in range(1, actor_count + 1)]
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)
310
+ wtt, wtr, wts = wt.GetT(), wt.GetR(), wt.GetS()
311
+ tx.append(wtt[0])
312
+ ty.append(wtt[1])
313
+ tz.append(wtt[2])
314
+ rx.append(wtr[0])
315
+ ry.append(wtr[1])
316
+ rz.append(wtr[2])
317
+ sx.append(wts[0])
318
+ sy.append(wts[1])
319
+ sz.append(wts[2])
320
+
321
+ if not incl_keyed:
322
+ return [
323
+ actors,
324
+ markers,
325
+ tx, ty, tz, zeros,
326
+ rx, ry, rz, zeros,
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,
336
+ tx, ty, tz, zeros,
337
+ rx, ry, rz, zeros,
338
+ sx, sy, sz, ones,
339
+ keyed_bools
340
+ ]
341
+
342
+
343
  class FBXContainer:
344
  def __init__(self, fbx_file: Path,
345
  volume_dims: Tuple[float] = (10., 4., 10.),
346
+ max_actors: int = 8,
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.
 
357
  :param volume_dims: `tuple` of `float` that represent the dimensions of the capture volume in meters.
358
  :param max_actors: `int` maximum amount of actors to expect in a point cloud.
359
  :param pc_size: `int` amount of points in a point cloud.
360
+ :param debug: If higher than -1, will print out debugging statements.
361
+ :param save_init: If the file is guaranteed to have all data, set to True to automatically call self.init().
362
+ :param r: Optional frame range that will be passed to init_transforms.
363
+ :param mode: `str` to indicate whether to store world transforms for inference only. Default 'train'.
364
  """
365
  if pc_size < max_actors * 73:
366
  raise ValueError('Point cloud size must be large enough to contain the maximum amount of actors * 73'
367
  f' markers: {pc_size}/{max_actors * 73}.')
368
 
369
+ self.debug = debug
370
  # Python ENUM of the C++ time modes.
371
  self.time_modes = globals.get_time_modes()
372
  # Ordered list of marker names. Note: rearrange this in globals.py.
 
378
  # Store names of the actors (all parent nodes that have the first 4 markers as children).
379
  self.actor_names = []
380
 
381
+ self.labeled_world_transforms = None
382
+ self.unlabeled_world_transforms = None
383
+
384
  # Split the dimensions tuple into its axes for easier access.
385
  self.vol_x = volume_dims[0]
386
  self.vol_y = volume_dims[1]
 
393
  self.pc_size = pc_size
394
 
395
  self.input_fbx = fbx_file
396
+ # self.output_fbx = append_suffix_to_fbx(fbx_file, '_INF')
397
+ self.output_fbx = utils.append_suffix_to_file(fbx_file, '_INF')
398
  self.valid_frames = []
399
 
400
+ # If we know that the input file has valid data,
401
+ # we can automatically call the init function and ignore missing data.
402
+ if save_init:
403
+ self.init(r=r)
 
404
 
405
+ def __init_scene(self) -> None:
406
  """
407
  Stores scene, root, and time_mode properties.
408
  Destroys the importer to remove the reference to the loaded file.
 
416
  self.scene = fbx.FbxScene.Create(self.manager, '')
417
  importer.Import(self.scene)
418
  self.root = self.scene.GetRootNode()
419
+ if self.root is None:
420
+ raise ValueError('No root node found.')
421
  self.time_mode = self.scene.GetGlobalSettings().GetTimeMode()
422
  fbx.FbxTime.SetGlobalTimeMode(self.time_mode)
423
 
 
425
  # This will allow us to delete the uploaded file.
426
  importer.Destroy()
427
 
428
+ def __init_anim(self) -> None:
429
  """
430
  Stores the anim_stack, num_frames, start_frame, end_frame properties.
431
  """
432
  # Get the animation stack and layer.
433
  anim_stack = self.scene.GetCurrentAnimationStack()
434
  self.anim_layer = anim_stack.GetSrcObject(fbx.FbxCriteria.ObjectType(fbx.FbxAnimLayer.ClassId), 0)
435
+ if self.anim_layer is None:
436
+ raise ValueError('No animation layer found.')
437
 
438
  # Find the total number of frames to expect from the local time span.
439
  local_time_span = anim_stack.GetLocalTimeSpan()
440
  self.num_frames = int(local_time_span.GetDuration().GetFrameCount())
441
+ if self.num_frames == 0:
442
+ raise ValueError('Number of animated frames is 0.')
443
  self.start_frame = local_time_span.GetStart().GetFrameCount()
444
  self.end_frame = local_time_span.GetStop().GetFrameCount()
445
 
446
+ def __init_actors(self, ignore_missing: bool = False) -> None:
447
  """
448
  Goes through all root children (generation 1).
449
  If a child has 4 markers as children, it is considered an actor (Shogun subject) and appended to actors
450
  and actor_names list properties.
451
  Also initializes an empty valid_frames list for each found actor.
452
  """
453
+ ts = fbx.FbxTime()
454
+ ts.SetFrame(self.start_frame)
455
+
456
+ te = fbx.FbxTime()
457
+ te.SetFrame(self.end_frame)
458
+
459
+ names_to_look_for = list(self.marker_names[:4])
460
  # Find all parent nodes (/System, /Unlabeled_Markers, /Actor1, etc).
461
  gen1_nodes = [self.root.GetChild(i) for i in range(self.root.GetChildCount())]
462
  for gen1_node in gen1_nodes:
 
464
  range(gen1_node.GetChildCount())] # Actor nodes (/Mimi/Hips, /Mimi/ARIEL, etc)
465
 
466
  # If the first 3 marker names are children of this parent, it must be an actor.
467
+ if all(name in [node.GetName().split(':')[-1] for node in gen2_nodes] for name in names_to_look_for):
468
  self.actor_names.append(gen1_node.GetName())
469
  self.actors.append(gen1_node)
470
 
471
+ if len(self.actors) == 0 and not ignore_missing:
472
+ raise ValueError('No actors/subjects found. A node is considered an actor ' +
473
+ 'if it has the following children nodes: ' +
474
+ ', '.join(names_to_look_for) + '.')
475
+
476
  self.actor_count = len(self.actors)
477
  self.valid_frames = [[] for _ in range(self.actor_count)]
478
 
479
+ def __init_markers(self, ignore_missing: bool = False) -> None:
480
  """
481
  Goes through all actor nodes and stores references to its marker nodes.
482
  """
 
490
  if match_name(child, marker_name, ignore_namespace=True):
491
  actor_markers[marker_name] = child
492
 
493
+ if len(actor_markers) != len(self.marker_names) and not ignore_missing:
494
+ raise ValueError(f'{actor_node.GetName()} does not have all markers.')
495
 
496
  self.markers.append(actor_markers)
497
 
498
+ def __init_unlabeled_markers(self, ignore_missing: bool = False) -> None:
499
  """
500
  Looks for the Unlabeled_Markers parent node under the root and stores references to all unlabeled marker nodes.
501
  """
 
507
  self.unlabeled_markers = [gen1_node.GetChild(um) for um in range(gen1_node.GetChildCount())]
508
  return
509
 
510
+ if not ignore_missing:
511
+ raise ValueError('No unlabeled markers found.')
512
+
513
+ def init_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
514
+
515
+ self.init_labeled_world_transforms(r=r, incl_keyed=1)
516
+ self.init_unlabeled_world_transforms(r=r)
517
+
518
+ def init_labeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
519
+ incl_keyed: int = 1):
520
+
521
+ r = self.convert_r(r)
522
+ labeled_data = []
523
+
524
+ for actor_idx in range(self.actor_count):
525
+ actor_data = []
526
+ for marker_idx, (n, m) in enumerate(self.markers[actor_idx].items()):
527
+ curve = m.LclTranslation.GetCurve(self.anim_layer, 'X', True)
528
+ marker_data = get_world_transforms(actor_idx + 1, marker_idx + 1, m, r, curve, incl_keyed)
529
+ actor_data.append(marker_data)
530
+ self._print(f'Actor {actor_idx} marker {marker_idx} done', 1)
531
+ labeled_data.append(actor_data)
532
+
533
+ wide_layout = np.array(labeled_data)
534
+ self.labeled_world_transforms = np.transpose(wide_layout, axes=(3, 0, 1, 2))
535
+ return self.labeled_world_transforms
536
+
537
+ def init_unlabeled_world_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
538
+ r = self.convert_r(r)
539
+
540
+ unlabeled_data = []
541
+
542
+ for ulm in self.unlabeled_markers:
543
+ curve = ulm.LclTranslation.GetCurve(self.anim_layer, 'X', True)
544
+ marker_data = get_world_transforms(0, 0, ulm, r, curve, incl_keyed=0)
545
+ unlabeled_data.append(marker_data)
546
+ self._print(f'Unlabeled marker {ulm.GetName()} done', 1)
547
+
548
+ wide_layout = np.array(unlabeled_data)
549
+ self.unlabeled_world_transforms = np.transpose(wide_layout, axes=(2, 0, 1))
550
+ # Returns shape (n_frames, n_unlabeled_markers, 14).
551
+ return self.unlabeled_world_transforms
552
+
553
+ def init(self, ignore_missing_labeled: bool = False, ignore_missing_unlabeled: bool = False,
554
+ r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> None:
555
+ self.__init_scene()
556
+ self.__init_anim()
557
+ self.__init_actors(ignore_missing=ignore_missing_labeled)
558
+ self.__init_markers(ignore_missing=ignore_missing_labeled)
559
+ self.__init_unlabeled_markers(ignore_missing=ignore_missing_unlabeled)
560
+ self._print('Init done', 0)
561
+
562
+ def _print(self, txt: str, lvl: int = 0) -> None:
563
+ if lvl <= self.debug:
564
+ print(txt)
565
+
566
  def _check_actor(self, actor: int = 0):
567
  """
568
  Safety check to see if the actor `int` is a valid number (to avoid out of range errors).
569
  :param actor: `int` actor index, which should be between 0-max_actors.
570
  """
571
+ if not 0 <= actor <= self.actor_count:
572
+ raise ValueError(f'Actor index must be between 0 and {self.actor_count - 1} ({actor}).')
573
 
574
  def _set_valid_frames_for_actor(self, actor: int = 0):
575
  """
 
585
  self._check_actor(actor)
586
 
587
  frames = self.get_frame_range()
588
+ for n, marker in self.markers[actor].items():
589
  # Get the animation curve for local translation x.
590
  t_curve = marker.LclTranslation.GetCurve(self.anim_layer, 'X')
591
  # If an actor was recorded but seems to have no animation curves, we set their valid frames to nothing.
592
  # Then we return, because there is no point in further checking non-existent keyframes.
593
  if t_curve is None:
594
  self.valid_frames[actor] = []
595
+ self._print('Found no animation curve', 2)
596
  return
597
 
598
  # Get all keyframes on the animation curve and store their frame numbers.
599
+ self._print(f'Checking keyframes for {n}', 2)
600
  keys = [t_curve.KeyGet(i).GetTime().GetFrameCount() for i in range(t_curve.KeyGetCount())]
601
  # Check for each frame in frames if it is present in the list of keyframed frames.
602
  for frame in frames:
 
606
  with contextlib.suppress(ValueError):
607
  frames.remove(frame)
608
 
609
+ self._print(f'Found {len(frames)}/{self.num_frames} valid frames for {self.actor_names[actor]}', 1)
610
  self.valid_frames[actor] = frames
611
 
612
  # Store all frame lists that have at least 1 frame.
 
615
  self.common_frames = [num for num in self.get_frame_range()
616
  if all(num in other_list for other_list in other_lists)]
617
 
 
 
 
 
 
 
 
618
  def _check_valid_frames(self, actor: int = 0):
619
  """
620
  Safety check to see if the given actor has any valid frames stored.
 
624
  self._check_actor(actor)
625
 
626
  if not len(self.valid_frames[actor]):
627
+ self._print(f'Getting missing valid frames for {self.actor_names[actor]}', 1)
628
  self._set_valid_frames_for_actor(actor)
629
 
630
+ def get_transformed_axes(self, actor: int = 0, frame: int = 0) -> Tuple[np.array, np.array, np.array]:
631
  """
632
  Evaluates all marker nodes for the given actor and modifies the resulting point cloud,
633
  so it is centered and scaled properly for training.
 
661
  z /= self.vol_z
662
  y = np.array(y) / self.vol_y
663
 
664
+ # EXTRA: Add any extra modifications to the point cloud here.
665
+
666
+ return x, y, z
667
+
668
+ def get_transformed_pc(self, actor: int = 0, frame: int = 0, t: str = 'np') -> Union[np.array, List[float]]:
669
+
670
+ x, y, z = self.get_transformed_axes(actor, frame)
671
+ # If we need to return a numpy array, simply vstack the axes to get a shape of (3, 73).
672
+ # This is in preparation for PyTorch's CNN layers that use input shape (batch_size, C, H, W).
673
+ if t == 'np':
674
+ # Exports shape of (3, 9, 9).
675
+ # return make_pc_ghost_markers(np.vstack((x, y, z)))
676
+ # Exports shape of (1, 3, 73).
677
+ return np.vstack((x, y, z))[None, ...]
678
 
679
  # Append all values to a new array, one axis at a time.
680
  # This way it will match the column names order.
 
685
  pose += [z[i]]
686
  return pose
687
 
 
 
 
 
 
 
 
 
 
 
688
  def get_frame_range(self) -> List[int]:
689
  """
690
  Replacement and improvement for:
 
694
  """
695
  return list(range(self.start_frame, self.end_frame))
696
 
697
+ def convert_r(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None):
698
+ # If r is one int, use 0 as start frame. If r is higher than the total frames, limit the range.
699
+ if isinstance(r, int):
700
+ r = list(range(self.num_frames)) if r > self.num_frames else list(range(r))
701
+
702
+ # A tuple of 2 indicates a frame range without step.
703
+ elif isinstance(r, tuple) and len(r) == 2:
704
+ # If the requested frame range is longer than the total frames, limit the range.
705
+ if r[1] - r[0] > self.num_frames:
706
+ r = list(range(r[0], r[0] + self.num_frames))
707
+ else:
708
+ r = list(range(r[0], r[1]))
709
+
710
+ # A tuple of 3 indicates a frame range with step.
711
+ elif isinstance(r, tuple) and len(r) == 3:
712
+ # If the requested frame range is longer than the total frames, limit the range.
713
+ if r[1] - r[0] > self.num_frames:
714
+ r = list(range(r[0], r[0] + self.num_frames, r[2]))
715
+ else:
716
+ r = list(range(r[0], r[1], r[2]))
717
+
718
+ # If r is None, return the default frame range.
719
+ else:
720
+ r = self.get_frame_range()
721
+
722
+ return r
723
+
724
  def columns_from_joints(self) -> List[str]:
725
  """
726
  Generates a list of column names based on the (order of the) marker names.
 
805
  self._check_valid_frames(actor)
806
  return self.valid_frames[actor]
807
 
808
+ def extract_valid_translations_per_actor(self, actor: int = 0, t: str = 'np'):
809
  """
810
  Assembles the poses for the valid frames for the given actor as a 2D list where each row is a pose.
811
  :param actor: `int` actor index.
812
+ :param t: If 'np', returns a (3, -1) `np.array`. Otherwise returns a list of floats.
813
  :return: List of poses, where each pose is a list of `float` translations.
814
  """
815
  # Ensure the actor index is within range.
816
  self._check_actor(actor)
817
+ self._check_valid_frames(actor)
818
 
819
+ # Returns shape (n_valid_frames, 3, 73).
820
+ return np.vstack([self.get_transformed_pc(actor, frame) for frame in self.valid_frames[actor]])
 
 
 
 
 
821
 
822
+ # poses = []
823
+ # # Go through all valid frames for this actor.
824
+ # # Note that these frames can be different per actor.
825
+ # for frame in self.valid_frames[actor]:
826
+ # self._print(f' Extracting frame: {frame}', 1)
827
+ # # Get the centered point cloud as a 1D list.
828
+ # pose_at_frame = self.get_transformed_pc(actor, frame, t)
829
+ # poses.append(pose_at_frame)
830
+ #
831
+ # return np.array(poses) if t == 'np' else poses
832
 
833
+ def extract_all_valid_translations(self, t: str = 'np') -> Union[np.array, pd.DataFrame]:
834
  """
835
  Convenience method that calls self.extract_valid_translations_per_actor() for all actors
836
  and returns a `DataFrame` containing all poses after each other.
837
+ :param t: If 'np', returns a `np.array`. Otherwise, returns a DataFrame.
838
+ :return: `np.array` or `DataFrame` where each row is a pose.
839
+ """
840
+ # Returns shape (n_total_valid_frames, 3, 73).
841
+ return np.vstack([self.extract_valid_translations_per_actor(i) for i in range(self.actor_count)])
842
+ # all_poses = []
843
+ # # For each actor, add their valid poses to all_poses.
844
+ # for i in range(self.actor_count):
845
+ # self._print(f'Extracting actor {self.actor_names[i]}', 0)
846
+ # all_poses.extend(self.extract_valid_translations_per_actor(i, t))
847
+ #
848
+ # self._print('Extracting finished')
849
+ # # Note that the column names are/must be in the same order as the markers.
850
+ # if t == 'np':
851
+ # # Shape: (n_poses, 3, 73).
852
+ # return np.array(all_poses)
853
+ # else:
854
+ # return pd.DataFrame(all_poses, columns=self.columns_from_joints())
855
+
856
+ def extract_training_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
857
+ if self.labeled_world_transforms is None:
858
+ self.init_labeled_world_transforms(r=r, incl_keyed=1)
859
+
860
+ # Returns (n_frames, n_actors, 73, 15).
861
+ l_shape = self.labeled_world_transforms.shape
862
+ # Flatten the array, so we get a list of frames.
863
+ # Reshape to (n_frames * n_actors, 73, 15).
864
+ flattened = self.labeled_world_transforms.reshape(-1, l_shape[2], l_shape[3])
865
+ # Isolates the poses with all keyframes present by checking the last elements.
866
+ # Start with the mask.
867
+ # Returns shape of (n_frames * n_actors, 73).
868
+ mask = (flattened[..., -1] == 1)
869
+ # We only need a filter for the first dimension, so use .all to check if all markers
870
+ # have a keyframe. This results in shape (n_frames * n_actors,).
871
+ mask = mask.all(axis=1)
872
+ # Now isolate the right frames with the mask and remove the last element of the last dimension,
873
+ # because it won't be useful anymore.
874
+ valid_poses = flattened[mask][..., :-1]
875
+
876
+ # Now we need to center the tx and tz axes.
877
+ for valid_pose in valid_poses:
878
+ for axis in [2, 4]:
879
+ valid_pose[:, axis] = center_axis(valid_pose[:, axis])
880
+ return self.transform_translations(valid_poses)
881
+
882
+ def extract_inf_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
883
+ merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
884
+ if self.labeled_world_transforms is None:
885
+ self.init_labeled_world_transforms(r=r, incl_keyed=0)
886
+ if self.unlabeled_world_transforms is None:
887
+ self.init_unlabeled_world_transforms(r=r)
888
+
889
+ ls = self.labeled_world_transforms.shape
890
+ # Returns shape (n_frames, 73 * n_actors, 14).
891
+ flat_labeled = self.labeled_world_transforms.reshape(ls[0], -1, ls[-1])[..., :14]
892
+
893
+ if merged:
894
+ return utils.merge_labeled_and_unlabeled_data(labeled=flat_labeled,
895
+ unlabeled=self.unlabeled_world_transforms,
896
+ pc_size=self.pc_size)
897
+ else:
898
+ return flat_labeled, self.unlabeled_world_transforms
899
 
900
  def transform_translations(self, w: np.array) -> np.array:
901
  """
 
907
  raise ValueError(f'Array does not have 3 dimensions: {w.ndim}/3.')
908
 
909
  # If the last dimension has 3 elements, it is a translation vector of shape (tx, ty, tz).
910
+ # If it has 14 elements, it is a full marker row of shape (actor, marker, tx, ty, tz, tw, rx, ry, rz, tw, etc).
911
  start = 0 if w.shape[-1] == 3 else 2
912
 
913
  # First multiply by self.scale, which turns meters to centimeters.
 
971
  # so return the cloud as a np array that cuts off any excessive markers.
972
  return np.array(cloud)[:self.pc_size]
973
 
974
+ def get_dc(self, frame: int = 0) -> np.array:
975
+ self._print(f'Getting sparse cloud for frame {frame}', 2)
976
+ cloud = self.get_sc(frame)
977
+ missing = self.pc_size - cloud.shape[0]
978
+
979
+ # Only bother creating ghost markers if there are any missing rows.
980
+ # If we need to add ghost markers, add them before the existing cloud,
981
+ # so that the cloud will remain a sorted array regarding the actor and marker classes.
982
+ if missing > 0:
983
+ self._print('Making ghost markers', 2)
984
+ ghost_cloud = make_ghost_markers(missing)
985
+ cloud = np.vstack([ghost_cloud, cloud])
986
+
987
+ return cloud
988
+
989
  def get_tsc(self) -> np.array:
990
  """
991
  Convenience method that calls self.get_sparse_cloud() for all frames in the frame range
 
994
  """
995
  return np.array([self.get_sc(f) for f in self.get_frame_range()])
996
 
997
+ def get_tdc(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) -> np.array:
998
  """
999
  For each frame in the frame range, collects the point cloud that is present in the file.
1000
  Then it creates a ghost cloud of random markers that are treated as unlabeled markers,
 
1006
  with a shape of (self.num_frames, self.pc_size, 5).
1007
  """
1008
 
1009
+ r = self.convert_r(r)
1010
 
1011
+ # results = utils.parallel_process(r, self.get_dc)
 
 
 
 
 
 
 
 
1012
 
1013
+ return np.array([self.get_dc(f) for f in r])
 
 
1014
 
1015
+ def modify_actor_pose(self, actor: np.array) -> np.array:
1016
+ # Scale to cm.
1017
+ actor[:, 2:5] *= self.scale
1018
+ # Move the point cloud to the center of the x and y axes. This will put the actor in the middle.
1019
+ for axis in range(2, 5):
1020
+ actor[:, axis] = center_axis(actor[:, axis])
1021
 
1022
+ # Move the actor to the middle of the volume floor by adding volume_dim/2 to x and z.
1023
+ actor[:, 2] += self.vol_x / 2.
1024
+ actor[:, 4] += self.vol_z / 2.
1025
 
1026
+ # Squeeze the actor into the 1x1 plane for the neural network by dividing the axes.
1027
+ actor[:, 2] /= self.vol_x
1028
+ actor[:, 3] /= self.vol_y
1029
+ actor[:, 4] /= self.vol_z
1030
 
1031
  def split_tdc(self, cloud: np.array = None) \
1032
  -> Tuple[np.array, np.array, np.array, np.array, np.array]:
 
1042
  :return: Return tuple of `np.array` as (actor classes, marker classes, translation vectors).
1043
  """
1044
  if cloud is None:
1045
+ cloud = self.extract_inf_translations()
1046
+
1047
+ if cloud.shape[1] != self.pc_size:
1048
+ raise ValueError(f"Dense cloud doesn't have enough points. {cloud.shape[1]}/{self.pc_size}.")
1049
+ if cloud.shape[2] < 14:
1050
+ raise ValueError(f"Dense cloud is missing columns: {cloud.shape[2]}.")
1051
 
1052
+ # Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
1053
+ return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
 
 
1054
 
1055
+ def get_split_transforms(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
1056
+ mode: str = 'train') -> Tuple[np.array, np.array, np.array, np.array, np.array]:
1057
+ """
1058
+ Splits a timeline dense cloud with shape (self.num_frames, self.pc_size, 5) into 3 different
1059
+ arrays:
1060
+ 1. A `np.array` with the actor classes as shape (self.num_frames, self.pc_size, 1).
1061
+ 2. A `np.array` with the marker classes as shape (self.num_frames, self.pc_size, 1).
1062
+ 3. A `np.array` with the translation floats as shape (self.num_frames, self.pc_size, 4).
1063
+ 4. A `np.array` with the rotation Euler angles as shape (self.num_frames, self.pc_size, 3).
1064
+ :return: Return tuple of `np.array` as (actor classes, marker classes, translation vectors).
1065
+ """
1066
+ cloud = self.extract_training_translations(r) if mode == 'train' else self.extract_inf_translations(r)
1067
  # Return np arrays as (actor classes, marker classes, translation vectors, rotation vectors, scale vectors).
1068
  return cloud[:, :, 0], cloud[:, :, 1], cloud[:, :, 2:5], cloud[:, :, 6:9], cloud[:, :, 10:13]
1069
 
 
1083
  """
1084
  return 'UNLABELED' if int(c) == 0 else self.marker_names[int(c) - 1]
1085
 
1086
+ def export_train_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None) \
1087
+ -> Union[bytes, pd.DataFrame, np.array]:
1088
+ if output_file is None:
1089
+ df = pd.DataFrame(self.extract_training_translations(r))
 
1090
  return df.to_csv(index=False).encode('utf-8')
1091
 
1092
+ elif output_file.suffix == '.npy':
1093
+ array_4d = self.extract_training_translations(r)
1094
+ np.save(str(output_file), array_4d)
1095
+ self._print(f'Exported train data to {output_file}', 0)
1096
+ return array_4d
1097
+
1098
+ elif output_file.suffix == '.h5':
1099
+ array_4d = self.extract_training_translations(r)
1100
+ with h5py.File(output_file, 'w') as h5f:
1101
+ h5f.create_dataset('array_data', data=array_4d, compression='gzip', compression_opts=9)
1102
+ self._print(f'Exported train data to {output_file}', 0)
1103
+ return array_4d
1104
+
1105
+ else:
1106
+ raise ValueError('Invalid file extension. Must be .csv or .npy')
1107
+
1108
+ def export_test_data(self, output_file: Path, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
1109
+ merged: bool = True) -> Union[np.array, Tuple[np.array, np.array]]:
1110
+ # Retrieve the clean world transforms.
1111
+ # If merged is True, this will be one array of shape (n_frames, pc_size, 14).
1112
+ # If merged is False, this will be two arrays, one of shape (n_frames, 73 * n_actors, 14),
1113
+ # and one of shape (n_frames, n_unlabeled_markers, 14).
1114
+ array_4d = self.extract_inf_translations(r, merged=merged)
1115
 
1116
+ if output_file.suffix == '.h5':
1117
+ with h5py.File(output_file, 'w') as h5f:
1118
+ if merged:
1119
+ # If merged, this can be one dataset.
1120
+ h5f.create_dataset('merged_data', data=array_4d, compression='gzip', compression_opts=9)
1121
 
1122
+ else:
1123
+ # If not merged, we split it up because array_4d is a tuple of 2.
1124
+ h5f.create_dataset('labeled', data=array_4d[0], compression='gzip', compression_opts=9)
1125
+ h5f.create_dataset('unlabeled', data=array_4d[1], compression='gzip', compression_opts=9)
1126
+
1127
+ self._print(f'Exported test data to {output_file}', 0)
1128
+
1129
+ return array_4d
1130
 
1131
  def export_fbx(self, output_file: Path = None) -> bool:
1132
  """
 
1143
  # Initialize the exporter with the output file path
1144
  result = exporter.Initialize(str(output_file))
1145
  if not result:
1146
+ self._print(f"Failed to initialize the exporter for file '{output_file}'.", 0)
1147
  return False
1148
 
1149
  # Export the scene
1150
  result = exporter.Export(self.scene)
1151
  if not result:
1152
+ self._print(f"Failed to export the scene to file '{output_file}'.", 0)
1153
  return False
1154
 
1155
  # Clean up the manager and exporter
1156
  exporter.Destroy()
1157
 
1158
+ self._print('Export finished', 0)
1159
+
1160
  return True
1161
 
1162
  def remove_node(self, node: fbx.FbxNode, recursive: bool = False) -> bool:
 
1290
  for marker_class, (marker_name, marker) in enumerate(self.markers[actor].items(), start=1):
1291
  marker_keys = actor_keys.get(marker_class)
1292
  if marker_keys:
1293
+ self._print(f'Replacing keys for {marker_name}', 1)
1294
  self.replace_keyframes_per_marker(marker, marker_keys)
1295
 
1296
  def replace_keyframes_for_all_actors(self, key_dict: dict) -> None:
 
1299
  :param key_dict: `dict` with all actor keyframes.
1300
  """
1301
  for actor_idx in range(self.actor_count):
1302
+ actor_dict = key_dict.get(actor_idx + 1)
1303
  if actor_dict:
1304
+ self._print(f'Replacing keys for actor {actor_idx}', 1)
1305
  self.replace_keyframes_per_actor(actor_idx, actor_dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
labeler/data_setup.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Tuple
3
+
4
+ import numpy as np
5
+ import torch
6
+ from torch.utils import data
7
+ import math
8
+
9
+
10
+ def apply_random_y_rotation(point_cloud_data: torch.Tensor) -> torch.Tensor:
11
+ # Convert the random angle from degrees to radians
12
+ angle = (torch.rand(1).item() * 2 - 1) * 180 * torch.tensor(math.pi / 180, device='cuda')
13
+
14
+ # Create the rotation matrix for the y-axis
15
+ rotation_matrix = torch.tensor([[torch.cos(angle), 0, torch.sin(angle)],
16
+ [0, 1, 0],
17
+ [-torch.sin(angle), 0, torch.cos(angle)]], device='cuda')
18
+
19
+ # Apply the rotation to the point cloud data
20
+ return torch.matmul(point_cloud_data, rotation_matrix.T)
21
+
22
+
23
+ class PointCloudDataset(data.Dataset):
24
+ def __init__(self, file: Path,
25
+ n_samples=100,
26
+ max_actors: int = 8,
27
+ translation_factor=0.1,
28
+ max_overlap: Tuple[float] = (0.2, 0.2, 0.2)):
29
+ point_clouds_np = torch.tensor(np.load(str(file)), dtype=torch.float32, device='cuda')
30
+ self.sparse_point_clouds = point_clouds_np
31
+ self.n_samples = n_samples
32
+ self.max_actors = max_actors
33
+ self.translation_factor = translation_factor
34
+ self.max_overlap = max_overlap
35
+
36
+ # Generate a random permutation of indices.
37
+ self.indices = torch.randperm(len(self.sparse_point_clouds))
38
+
39
+ dataset = []
40
+ for _ in range(n_samples):
41
+ accumulated_cloud = []
42
+ # TODO: Get a random number up to the max of actors.
43
+ # TODO: Transform one row of the available rows, and check if it doesn't overlap.
44
+ # TODO: Accumulate all actors into one point cloud and append that to dataset.
45
+ # TODO: __getitem__() needs to get one of these point cloud rows.
46
+ for i in range(max_actors):
47
+
48
+ # Get a point cloud from the tensor using the shuffled index, shape (1, 1024).
49
+ point_cloud = self.sparse_point_clouds[self.indices[index]]
50
+
51
+ point_cloud_data = point_cloud[:, 2:5] # returns shape: (1024, 3)
52
+
53
+ valid_transform = False
54
+ while not valid_transform:
55
+
56
+ point_cloud = point_cloud_data.clone()
57
+ # Randomly translate the point cloud along the x and z axes
58
+
59
+ self.apply_random_translation(point_cloud)
60
+ # Apply random rotation around the y-axis
61
+ rotated_point_cloud_data = apply_random_y_rotation(point_cloud)
62
+
63
+ if not does_overlap(accumulated_cloud, point_cloud, self.max_overlap):
64
+ accumulated_cloud.append(point_cloud)
65
+ valid_transform = True
66
+
67
+ def apply_random_translation(self, point_cloud: torch.Tensor) -> None:
68
+ x_translation = (torch.rand(1).item() * 2 - 1) * self.translation_factor
69
+ z_translation = (torch.rand(1).item() * 2 - 1) * self.translation_factor
70
+ point_cloud[:, [0, 2]] += torch.tensor([x_translation, z_translation], device='cuda')
71
+
72
+ def fill_point_cloud(self, point_cloud):
73
+ target_num_points = 73 * self.max_actors
74
+ current_num_points = point_cloud.shape[1]
75
+
76
+ if current_num_points < target_num_points:
77
+ num_points_to_add = target_num_points - current_num_points
78
+ random_indices = torch.randint(0, current_num_points, (num_points_to_add,))
79
+ additional_points = point_cloud[:, random_indices, :]
80
+
81
+ filled_point_cloud = torch.cat((point_cloud, additional_points), dim=1)
82
+ else:
83
+ filled_point_cloud = point_cloud
84
+
85
+ return filled_point_cloud
86
+
87
+ def __getitem__(self, index):
88
+
89
+ point_cloud = np.vstack(accumulated_cloud)
90
+ # Separate the labels from the point cloud data
91
+ actor_labels = point_cloud[:, :, 0] # shape: (1024,)
92
+ marker_labels = point_cloud[:, :, 1] # shape: (1024,)
93
+
94
+ return actor_labels, marker_labels, rotated_point_cloud_data
95
+
96
+ def __len__(self):
97
+ return len(self.sparse_point_clouds)
98
+
99
+
100
+ def does_overlap(accumulated_point_cloud, new_point_cloud, overlap_thresholds=(0.2, 0.2, 0.2)):
101
+ def project_to_axis(point_cloud, axis):
102
+ projected_points = point_cloud.clone()
103
+ projected_points[:, axis] = 0
104
+ return projected_points
105
+
106
+ def get_bounding_box_2d(points):
107
+ min_values, _ = torch.min(points, dim=0)
108
+ max_values, _ = torch.max(points, dim=0)
109
+ return min_values, max_values
110
+
111
+ def check_surface_area_overlap(bb1_min, bb1_max, bb2_min, bb2_max, axis, overlap_threshold):
112
+ bb1_area = (bb1_max[axis] - bb1_min[axis]) * (bb1_max[1] - bb1_min[1])
113
+ bb2_area = (bb2_max[axis] - bb2_min[axis]) * (bb2_max[1] - bb2_min[1])
114
+
115
+ overlap_min = torch.max(bb1_min, bb2_min)
116
+ overlap_max = torch.min(bb1_max, bb2_max)
117
+
118
+ overlap_area = (overlap_max[axis] - overlap_min[axis]) * (overlap_max[1] - overlap_min[1])
119
+ overlap_area = torch.max(torch.tensor(0.0, device='cuda'), overlap_area) # Clamp to 0 if negative
120
+
121
+ overlap_percentage = overlap_area / torch.min(bb1_area, bb2_area)
122
+
123
+ return overlap_percentage >= overlap_threshold
124
+
125
+ new_point_cloud_xz = project_to_axis(new_point_cloud, 1) # Project to xz-plane (remove y-axis values)
126
+ new_point_cloud_min, new_point_cloud_max = get_bounding_box_2d(new_point_cloud_xz)
127
+
128
+ overlaps = []
129
+
130
+ for pc in accumulated_point_cloud:
131
+ for axis in range(len(overlap_thresholds)):
132
+ pc_xz = project_to_axis(pc, axis) # Project to xz-plane (remove y-axis values)
133
+ pc_min, pc_max = get_bounding_box_2d(pc_xz)
134
+
135
+ if all(
136
+ check_surface_area_overlap(
137
+ new_point_cloud_min,
138
+ new_point_cloud_max,
139
+ pc_min,
140
+ pc_max,
141
+ axis,
142
+ overlap_thresholds[axis],
143
+ )
144
+ for axis in range(len(overlap_thresholds))
145
+ ):
146
+ return True
147
+
148
+ return False
149
+
150
+
151
+ class NoOverlapDataLoader(data.DataLoader):
152
+ def __init__(self, dataset: data.Dataset, max_overlap: Tuple[float] = (0.2, 0.2, 0.2), *args, **kwargs):
153
+ super().__init__(dataset, *args, **kwargs)
154
+ self.max_overlap = max_overlap
155
+
156
+ def __iter__(self):
157
+ accumulated_point_clouds = []
158
+ for actor_labels, marker_labels, point_cloud_data in super().__iter__():
159
+ if not does_overlap(accumulated_point_clouds, point_cloud_data, self.max_overlap):
160
+ accumulated_point_clouds.append(point_cloud_data)
161
+ yield actor_labels, marker_labels, point_cloud_data
preprocess_files.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import shutil
3
+
4
+ # Import custom libs.
5
+ import fbx_handler
6
+ import utils
7
+
8
+
9
+ def process_fbx_files(source_folder: Path, train_folder: Path, test_folder: Path, v: int = 1):
10
+ # Delete the existing folders and make them again, because the array4d_to_h5 function will append
11
+ # new data to any existing files.
12
+ shutil.rmtree(train_folder)
13
+ shutil.rmtree(test_folder)
14
+ train_folder.mkdir(parents=True, exist_ok=True)
15
+ test_folder.mkdir(parents=True, exist_ok=True)
16
+
17
+ files = list(source_folder.glob('*.fbx'))
18
+ # files = [Path('G:/Firestorm/mocap-ai/data/fbx/mes-1/HangoutSpot_1_003.fbx')]
19
+
20
+ # Create Paths to new files that will contain all data.
21
+ train_all = train_folder / 'ALL.h5'
22
+ test_all = test_folder / 'ALL.h5'
23
+
24
+ with utils.Timer('Extracting took'):
25
+ # Iterate through all .fbx files in the source folder
26
+ for idx, fbx_file in enumerate(files):
27
+ print(f'{idx + 1}/{len(files)}: {fbx_file}')
28
+ # Create a new class object with the file path.
29
+ my_obj = fbx_handler.FBXContainer(fbx_file, max_actors=4, pc_size=296, debug=0, save_init=True)
30
+ # Init world transforms for labeled and unlabeled data. This will store all relevant transform info.
31
+ with utils.Timer('Getting world transforms took'):
32
+ my_obj.init_world_transforms()
33
+ # Define the export file path with the same file name but in the export folder
34
+ export_train_path = train_folder / fbx_file.with_suffix('.h5').name
35
+ export_test_path = test_folder / fbx_file.with_suffix('.h5').name
36
+
37
+ # Get the train data as an array of shape (n_valid_frames, 73, 14).
38
+ # This will also export it to a h5 file just in case.
39
+ train_data = my_obj.export_train_data(export_train_path)
40
+ print(f'Train shape: {train_data.shape}')
41
+ # Append the array to the existing ALL file.
42
+ utils.array4d_to_h5(array_4ds=(train_data,),
43
+ output_file=train_all,
44
+ datasets=(fbx_file.stem,))
45
+
46
+ # Do the same thing for the test data.
47
+ test_data = my_obj.export_test_data(export_test_path, merged=False)
48
+ print(f'Test labeled shape: {test_data[0].shape}')
49
+ print(f'Test unlabeled shape: {test_data[1].shape}')
50
+ print(f'Minimum cloud size: {test_data[0].shape[1] + test_data[1].shape[1]}')
51
+ utils.array4d_to_h5(array_4ds=test_data,
52
+ output_file=test_all,
53
+ group=fbx_file.stem,
54
+ datasets=('labeled', 'unlabeled'))
55
+
56
+ print('--- FINAL ---')
57
+ # Just to be sure, print the shapes of the final results.
58
+ with utils.Timer('Loading training data took'):
59
+ print(f"Final train shape: {utils.h5_to_array4d(train_all, mode='train').shape}")
60
+
61
+ with utils.Timer('Loading testing data took'):
62
+ print(f"Final test shape: {utils.h5_to_array4d(test_all, mode='test').shape}")
63
+
64
+
65
+ if __name__ == '__main__':
66
+ source = Path('G:/Firestorm/mocap-ai/data/fbx/mes-1/')
67
+ train = Path('G:/Firestorm/mocap-ai/data/h5/mes-1/train')
68
+ test = Path('G:/Firestorm/mocap-ai/data/h5/mes-1/test')
69
+
70
+ with utils.Timer('Full execution took'):
71
+ process_fbx_files(source, train, test)
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
  streamlit~=1.21.0
2
  pandas~=1.3.5
3
- numpy~=1.21.5
 
 
 
1
  streamlit~=1.21.0
2
  pandas~=1.3.5
3
+ numpy~=1.21.5
4
+ torch~=1.13.1
5
+ h5py
utils.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cProfile
2
+ import pstats
3
+ import time
4
+ from pathlib import Path
5
+ from typing import List, Tuple
6
+
7
+ import h5py
8
+ import numpy as np
9
+
10
+
11
+ def append_suffix_to_file(file_path: Path, suffix: str = '_INF', ext: str = None):
12
+ """
13
+ Adds a suffix to the given file path.
14
+ :param file_path: `Path` object to the original file.
15
+ :param suffix: `str` suffix to add to the end of the original file name.
16
+ :param ext: `str` potential new file extension.
17
+ :return: Updated `Path`.
18
+ """
19
+ if ext:
20
+ file_path = file_path.with_suffix(ext)
21
+ new_file_name = file_path.stem + suffix + file_path.suffix
22
+ return file_path.with_name(new_file_name)
23
+
24
+
25
+ def is_int_in_list(n: int, l: List[int]) -> int:
26
+ if l[0] > n:
27
+ return 0
28
+
29
+ for e in l:
30
+ if e == n:
31
+ return 1
32
+ elif e > n:
33
+ return 0
34
+
35
+ return 0
36
+
37
+
38
+ def array4d_to_h5(array_4ds: Tuple, output_file: Path, group: str = None, datasets: Tuple = 'array_data'):
39
+ if len(array_4ds) != len(datasets):
40
+ raise ValueError(f'Amount of arrays {len(array_4ds)} must match amount of dataset names {len(datasets)}.')
41
+ with h5py.File(output_file, 'a') as h5f:
42
+ if group is not None:
43
+ grp = h5f.create_group(group)
44
+ for i in range(len(array_4ds)):
45
+ grp.create_dataset(name=datasets[i], data=array_4ds[i], compression='gzip', compression_opts=9)
46
+ else:
47
+ for i in range(len(array_4ds)):
48
+ h5f.create_dataset(name=datasets[i], data=array_4ds[i], compression='gzip', compression_opts=9)
49
+
50
+
51
+ def h5_to_array4d(input_file: Path, mode: str = 'train', pc_size: int = 1024) -> np.array:
52
+ with h5py.File(input_file, 'r') as h5f:
53
+ if mode == 'train':
54
+ return np.vstack([np.array(h5f[key]) for key in h5f.keys()])
55
+
56
+ data = []
57
+ for grp_name in h5f.keys():
58
+ grp = h5f[grp_name]
59
+ labeled = np.array(grp['labeled'])
60
+ unlabeled = np.array(grp['unlabeled'])
61
+ data.append(merge_labeled_and_unlabeled_data(labeled, unlabeled, pc_size=pc_size))
62
+
63
+ return np.vstack(data)
64
+
65
+
66
+ def merge_labeled_and_unlabeled_data(labeled: np.array, unlabeled: np.array, pc_size: int) -> np.array:
67
+ missing = pc_size - (labeled.shape[1] + unlabeled.shape[1])
68
+ if missing <= 0:
69
+ # Returns shape (n_frames, self.pc_size, 14).
70
+ return np.concatenate((unlabeled, labeled), axis=1)[:, -pc_size:]
71
+
72
+ missing_markers = np.random.rand(labeled.shape[0], missing, labeled.shape[-1])
73
+ missing_markers[:, :, 0] = 0.
74
+ missing_markers[:, :, 1] = 0.
75
+
76
+ # Returns shape (n_frames, self.pc_size, 14).
77
+ return np.concatenate((missing_markers,
78
+ unlabeled,
79
+ labeled), axis=1)
80
+
81
+
82
+ class Timer:
83
+ def __init__(self, txt: str = 'Execution time: ', profiler: bool = False):
84
+ self.txt = txt
85
+ self.profiler = profiler
86
+
87
+ def __enter__(self):
88
+ self.start_time = time.time()
89
+ if self.profiler:
90
+ self.p = cProfile.Profile()
91
+ self.p.enable()
92
+ return self
93
+
94
+ def __exit__(self, exc_type, exc_val, exc_tb):
95
+ self.end_time = time.time()
96
+ dif = self.end_time - self.start_time
97
+ print(f"{self.txt}: {dif:.4f} seconds")
98
+
99
+ if self.profiler:
100
+ self.p.disable()
101
+ stats = pstats.Stats(self.p).sort_stats('time')
102
+ stats.print_stats()
103
+
104
+