Fixed the math to align the poses, checked it in Houdini.
Browse filesAdded a method to remove clipping poses during train data extraction.
- fbx_handler.py +31 -14
- preprocess_files.py +65 -49
- 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.,
|
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 |
-
|
|
|
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
|
914 |
# Then divide by volume dimensions, to normalize to the total area of the capture volume.
|
915 |
-
w[
|
916 |
-
|
917 |
-
w[
|
918 |
-
|
919 |
-
|
920 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
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
|
|
|
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
|
52 |
with h5py.File(input_file, 'r') as h5f:
|
53 |
-
|
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'])
|