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

Fixed the math to align the poses, checked it in Houdini.

Browse files

Added a method to remove clipping poses during train data extraction.

Files changed (3) hide show
  1. fbx_handler.py +31 -14
  2. preprocess_files.py +65 -49
  3. utils.py +6 -4
fbx_handler.py CHANGED
@@ -27,7 +27,7 @@ def center_axis(a: Union[List[float], np.array]) -> np.array:
27
  # Find the centroid by subtracting the lowest value from the highest value.
28
  _min = np.min(a)
29
  _max = np.max(a)
30
- _c = _max - _min
31
  # Center the array by subtracting the centroid.
32
  a -= _c
33
  return a
@@ -342,7 +342,7 @@ def get_world_transforms(actor_idx: int, marker_idx: int, m: fbx.FbxNode, r: Lis
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,
@@ -385,6 +385,9 @@ class FBXContainer:
385
  self.vol_x = volume_dims[0]
386
  self.vol_y = volume_dims[1]
387
  self.vol_z = volume_dims[2]
 
 
 
388
 
389
  self.scale = scale
390
 
@@ -853,6 +856,16 @@ class FBXContainer:
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)
@@ -864,19 +877,21 @@ class FBXContainer:
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,
@@ -903,21 +918,23 @@ class FBXContainer:
903
  :param w: `np.array` that can either be a timeline dense cloud or translation vectors.
904
  :return: Modified `np.array`.
905
  """
906
- if w.ndim != 3:
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.
914
  # Then divide by volume dimensions, to normalize to the total area of the capture volume.
915
- w[:, :, start + 0] = np.clip(w[:, :, start + 0], -(self.vol_x * 0.5),
916
- self.vol_x * 0.5) * self.scale / self.vol_x
917
- w[:, :, start + 1] = np.clip(w[:, :, start + 1], -(self.vol_y * 0.5),
918
- self.vol_y * 0.5) * self.scale / self.vol_y
919
- w[:, :, start + 2] = np.clip(w[:, :, start + 2], -(self.vol_z * 0.5),
920
- self.vol_z * 0.5) * self.scale / self.vol_z
 
 
 
 
921
 
922
  return w
923
 
 
27
  # Find the centroid by subtracting the lowest value from the highest value.
28
  _min = np.min(a)
29
  _max = np.max(a)
30
+ _c = _max - (_max - _min) * 0.5
31
  # Center the array by subtracting the centroid.
32
  a -= _c
33
  return a
 
342
 
343
  class FBXContainer:
344
  def __init__(self, fbx_file: Path,
345
+ volume_dims: Tuple[float] = (10., 10., 10.),
346
  max_actors: int = 8,
347
  pc_size: int = 1024,
348
  scale: float = 0.01,
 
385
  self.vol_x = volume_dims[0]
386
  self.vol_y = volume_dims[1]
387
  self.vol_z = volume_dims[2]
388
+ self.hvol_x = volume_dims[0] / 2
389
+ self.hvol_y = volume_dims[1] / 2
390
+ self.hvol_z = volume_dims[2] / 2
391
 
392
  self.scale = scale
393
 
 
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)
864
+ mask_z2 = (arr[:, :, 4] > -self.hvol_z / self.scale).all(axis=1)
865
+ mask = mask_x1 & mask_x2 & mask_z1 & mask_z2
866
+ # print(mask.shape, mask)
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)
 
877
  flattened = self.labeled_world_transforms.reshape(-1, l_shape[2], l_shape[3])
878
  # Isolates the poses with all keyframes present by checking the last elements.
879
  # Start with the mask.
880
+ # Returns shape of (n_frames * n_actors, 73, 15).
881
  mask = (flattened[..., -1] == 1)
882
  # We only need a filter for the first dimension, so use .all to check if all markers
883
  # have a keyframe. This results in shape (n_frames * n_actors,).
884
  mask = mask.all(axis=1)
885
  # Now isolate the right frames with the mask and remove the last element of the last dimension,
886
  # because it won't be useful anymore.
887
+ # Also remove any frames that cross the limits of the volume.
888
+ valid_poses = self.remove_clipping_poses(flattened[mask][..., :-1])
889
 
890
+ # Now we need to center the tx and tz axes of each individual pose.
891
  for valid_pose in valid_poses:
892
  for axis in [2, 4]:
893
  valid_pose[:, axis] = center_axis(valid_pose[:, axis])
894
+ # Finally, scale the data to the correct size by normalizing.
895
  return self.transform_translations(valid_poses)
896
 
897
  def extract_inf_translations(self, r: Union[int, Tuple[int, int], Tuple[int, int, int]] = None,
 
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
925
 
926
+ # First multiply by self.scale, which turns centimeters to meters.
927
  # Then divide by volume dimensions, to normalize to the total area of the capture volume.
928
+ w[..., start + 0] = w[..., start + 0] * self.scale / self.hvol_x
929
+ w[..., start + 1] = w[..., start + 1] * self.scale / self.hvol_y
930
+ w[..., start + 2] = w[..., start + 2] * self.scale / self.hvol_z
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
 
preprocess_files.py CHANGED
@@ -1,14 +1,62 @@
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)
@@ -17,55 +65,23 @@ def process_fbx_files(source_folder: Path, train_folder: Path, test_folder: Path
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)
 
1
  from pathlib import Path
2
  import shutil
3
+ import multiprocessing
4
 
5
  # Import custom libs.
6
  import fbx_handler
7
  import utils
8
 
9
 
10
+ source = Path('G:/Firestorm/mocap-ai/data/fbx/mes-2/')
11
+ train_folder = Path('G:/Firestorm/mocap-ai/data/h5/mes-2/train')
12
+ test_folder = Path('G:/Firestorm/mocap-ai/data/h5/mes-2/test')
13
+
14
+
15
+ def process_fbx_file(fbx_file: Path):
16
+ # Define the export file path with the same file name but in the export folder
17
+ export_train_path = train_folder / fbx_file.with_suffix('.h5').name
18
+ export_test_path = test_folder / fbx_file.with_suffix('.h5').name
19
+
20
+ # If both export files already exist, skip this file.
21
+ if export_train_path.exists() and export_test_path.exists():
22
+ print(f'{fbx_file} done already.')
23
+ return
24
+ else:
25
+ print(fbx_file)
26
+
27
+ # Create a new class object with the file path.
28
+ my_obj = fbx_handler.FBXContainer(fbx_file, max_actors=4, pc_size=296, debug=0, save_init=True)
29
+ # Init world transforms for labeled and unlabeled data. This will store all relevant transform info.
30
+ with utils.Timer('Getting world transforms took'):
31
+ try:
32
+ my_obj.init_world_transforms()
33
+ except BaseException as e:
34
+ print(e)
35
+ return
36
+
37
+ try:
38
+ # Get the train data as an array of shape (n_valid_frames, 73, 14).
39
+ # This will also export it to a h5 file just in case.
40
+ train_data = my_obj.export_train_data(export_train_path)
41
+ print(f'Train shape: {train_data.shape}')
42
+ except BaseException as e:
43
+ print(e)
44
+ return
45
+
46
+ try:
47
+ # Do the same thing for the test data.
48
+ test_data = my_obj.export_test_data(export_test_path, merged=False)
49
+ print(f'Test labeled shape: {test_data[0].shape}')
50
+ print(f'Test unlabeled shape: {test_data[1].shape}')
51
+ print(f'Minimum cloud size: {test_data[0].shape[1] + test_data[1].shape[1]}')
52
+ except BaseException as e:
53
+ print(e)
54
+ return
55
+
56
+
57
+ def process_fbx_files(source_folder: Path, v: int = 1):
58
  # Delete the existing folders and make them again, because the array4d_to_h5 function will append
59
+ # # new data to any existing files.
60
  shutil.rmtree(train_folder)
61
  shutil.rmtree(test_folder)
62
  train_folder.mkdir(parents=True, exist_ok=True)
 
65
  files = list(source_folder.glob('*.fbx'))
66
  # files = [Path('G:/Firestorm/mocap-ai/data/fbx/mes-1/HangoutSpot_1_003.fbx')]
67
 
68
+ # # Create Paths to new files that will contain all data.
69
+ # train_all = train_folder / 'ALL.h5'
70
+ # test_all = test_folder / 'ALL.h5'
71
+
72
+ with multiprocessing.Pool(4) as pool:
73
+ pool.map(process_fbx_file, files)
74
+
75
+ # print('--- FINAL ---')
76
+ # # Just to be sure, print the shapes of the final results.
77
+ # with utils.Timer('Loading training data took'):
78
+ # print(f"Final train shape: {utils.h5_to_array4d(train_all).shape}")
79
+ #
80
+ # with utils.Timer('Loading testing data took'):
81
+ # print(f"Final test shape: {utils.combined_test_h5_to_array4d(test_all).shape}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
 
84
  if __name__ == '__main__':
 
 
 
85
 
86
  with utils.Timer('Full execution took'):
87
+ process_fbx_files(source)
utils.py CHANGED
@@ -48,13 +48,15 @@ def array4d_to_h5(array_4ds: Tuple, output_file: Path, group: str = None, datase
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'])
 
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) -> np.array:
52
  with h5py.File(input_file, 'r') as h5f:
53
+ return np.vstack([np.array(h5f[key]) for key in h5f.keys()])
 
54
 
55
+
56
+ def combined_test_h5_to_array4d(input_file: Path, pc_size: int = 1024) -> np.array:
57
+ with h5py.File(input_file, 'r') as h5f:
58
  data = []
59
+ for grp_name in list(h5f.keys()):
60
  grp = h5f[grp_name]
61
  labeled = np.array(grp['labeled'])
62
  unlabeled = np.array(grp['unlabeled'])