anthony01 commited on
Commit
bc45d7d
·
0 Parent(s):

Initial commit

Browse files
Files changed (21) hide show
  1. .gitignore +20 -0
  2. LICENSE +21 -0
  3. README.md +53 -0
  4. augment.py +227 -0
  5. augs_transformer.log +67 -0
  6. cnn_runner.py +222 -0
  7. commands.txt +52 -0
  8. configs.py +63 -0
  9. dataset.py +171 -0
  10. evaluate.py +169 -0
  11. generate_keypoints.py +693 -0
  12. models/__init__.py +4 -0
  13. models/cnn.py +11 -0
  14. models/lstm.py +23 -0
  15. models/transformer.py +61 -0
  16. models/xgboost.py +27 -0
  17. requirements.txt +7 -0
  18. runner.py +88 -0
  19. train_nn.py +300 -0
  20. train_xgb.py +170 -0
  21. utils.py +103 -0
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pth
3
+ *.npy
4
+ *.json
5
+ *.mp4
6
+ *.mov
7
+ *.avi
8
+ *.mkv
9
+
10
+ .venv/
11
+ .ipynb_checkpoints/
12
+ .DS_Store
13
+
14
+ processed_data/
15
+ env/
16
+ venv/
17
+ keypoints/
18
+ outputs/
19
+ logs/
20
+ train_test_paths/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) [2021] [AI4Bharat]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # INLCUDE - Isolated Indian Sign Language Recognition
2
+
3
+ This repository contains code for training models on [INCLUDE](https://zenodo.org/record/4010759) dataset
4
+
5
+ # Dependencies
6
+
7
+ Install the dependencies through the following command
8
+
9
+ ```bash
10
+ >> pip install -r requirements.txt
11
+ ```
12
+
13
+
14
+
15
+ ## Steps
16
+ - Download the INCLUDE dataset
17
+ - Run `generate_keypoints.py` to save keypoints from Mediapipe Hands and Blazepose for train, validation and test videos.
18
+ ```bash
19
+ >> python generate_keypoints.py --include_dir <path to downloaded dataset> --save_dir <path to save dir> --dataset <include/include50>
20
+ ```
21
+ - Run `runner.py` to train a machine learning model on the dataset
22
+ ```bash
23
+ >> python runner.py --dataset <include/include50> --use_augs --model transformer --data_dir <location to saved keypoints>
24
+ ```
25
+ - Use the `--use_pretrained` flag to either perform only inference using pretrained model or resume training with the pretrained model.
26
+ ```bash
27
+ >> python runner.py --dataset <include/include50> --use_augs --model transformer --data_dir <location to saved keypoints> --use_pretrained <evaluate/resume_training>
28
+ ```
29
+ - To get predictions for videos from a pretrained model, run the following command.
30
+ ```bash
31
+ >> python evaluate.py --data_dir <dir with videos>
32
+ ```
33
+
34
+ ## Citation
35
+
36
+ ```
37
+ @inproceedings{10.1145/3394171.3413528,
38
+ author = {Sridhar, Advaith and Ganesan, Rohith Gandhi and Kumar, Pratyush and Khapra, Mitesh},
39
+ title = {INCLUDE: A Large Scale Dataset for Indian Sign Language Recognition},
40
+ year = {2020},
41
+ isbn = {9781450379885},
42
+ publisher = {Association for Computing Machinery},
43
+ doi = {10.1145/3394171.3413528},
44
+ numpages = {10},
45
+ series = {MM '20}
46
+ }
47
+ ```
48
+
49
+ # INCLUDE
50
+ # INCLUDE
51
+ # INCLUDE
52
+ # INCLUDE
53
+ # INCLUDE
augment.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+
5
+ class Augmentation:
6
+ def __init__(self, aug_func, p=1):
7
+ self.aug_func = aug_func
8
+ self.p = p
9
+
10
+ def __call__(self, df):
11
+ if np.random.rand() <= self.p:
12
+ return self.aug_func(df)
13
+ return df
14
+
15
+
16
+ def OneOf(aug_a, aug_b):
17
+ if np.random.rand() < 0.5:
18
+ return aug_a
19
+ return aug_b
20
+
21
+
22
+ def plus7rotation(df):
23
+ # +7 degree rotation
24
+ df_augmented = pd.DataFrame()
25
+ df_augmented["uid"] = df["uid"]
26
+ df_augmented["pose"] = ""
27
+ df_augmented["hand1"] = ""
28
+ df_augmented["hand2"] = ""
29
+ df_augmented["label"] = df["label"]
30
+
31
+ theta = 7 * (np.pi / 180)
32
+ c, s = np.cos(theta), np.sin(theta)
33
+ rotation_matrix = np.array([[c, -s], [s, c]])
34
+
35
+ for i in range(df.shape[0]):
36
+ for col in ["pose", "hand1", "hand2"]:
37
+ matrix = np.array(df.loc[i, col], dtype=np.float64)
38
+ matrix = np.matmul(matrix, rotation_matrix)
39
+ matrix = np.where(np.isnan(matrix), None, matrix).tolist()
40
+ df_augmented.at[i, col] = matrix
41
+
42
+ return df_augmented
43
+
44
+
45
+ def minus7rotation(df):
46
+ # -7 degree rotation
47
+ df_augmented = pd.DataFrame()
48
+ df_augmented["uid"] = df["uid"]
49
+ df_augmented["pose"] = ""
50
+ df_augmented["hand1"] = ""
51
+ df_augmented["hand2"] = ""
52
+ df_augmented["label"] = df["label"]
53
+
54
+ theta = -7 * (np.pi / 180)
55
+ c, s = np.cos(theta), np.sin(theta)
56
+ rotation_matrix = np.array([[c, -s], [s, c]])
57
+
58
+ for i in range(df.shape[0]):
59
+ for col in ["pose", "hand1", "hand2"]:
60
+ matrix = np.array(df.loc[i, col], dtype=np.float64)
61
+ matrix = np.matmul(matrix, rotation_matrix)
62
+ matrix = np.where(np.isnan(matrix), None, matrix).tolist()
63
+ df_augmented.at[i, col] = matrix
64
+
65
+ return df_augmented
66
+
67
+
68
+ def gaussSample(df):
69
+ # Random Gaussian sampling
70
+ df_augmented = df.copy()
71
+ dv = 0.05 * 10 ** -2
72
+ sv = 0.08 * 10 ** -2
73
+ lv = 0.08 * 10 ** -1
74
+ sigma = [
75
+ sv,
76
+ dv,
77
+ dv,
78
+ dv,
79
+ dv,
80
+ dv,
81
+ dv,
82
+ sv,
83
+ sv,
84
+ sv,
85
+ sv,
86
+ lv,
87
+ lv,
88
+ lv,
89
+ lv,
90
+ sv,
91
+ sv,
92
+ sv,
93
+ sv,
94
+ sv,
95
+ sv,
96
+ sv,
97
+ sv,
98
+ lv,
99
+ lv,
100
+ ]
101
+
102
+ ## Check if keypoints is range [0, 1]
103
+ x_width = 1920
104
+ y_height = 1080
105
+ for i in range(df.shape[0]):
106
+ if np.count_nonzero(df.loc[i, "pose"]) == 0:
107
+ break
108
+
109
+ pose = np.array(df.loc[i, "pose"], dtype=np.float64)
110
+ pose[:, 0] /= x_width
111
+ pose[:, 1] /= y_height
112
+ pose_variance = np.column_stack((sigma, sigma))
113
+ pose = np.random.normal(pose, pose_variance)
114
+ pose[:, 0] *= x_width
115
+ pose[:, 1] *= y_height
116
+ pose = np.where(np.isnan(pose), None, pose).tolist()
117
+
118
+ hand1 = np.array(df.loc[i, "hand1"], dtype=np.float64)
119
+ hand1[:, 0] /= x_width
120
+ hand1[:, 1] /= y_height
121
+ hand1 = np.random.normal(hand1, dv)
122
+ hand1[:, 0] *= x_width
123
+ hand1[:, 1] *= y_height
124
+ hand1 = np.where(np.isnan(hand1), None, hand1).tolist()
125
+
126
+ hand2 = np.array(df.loc[i, "hand2"], dtype=np.float64)
127
+ hand2[:, 0] /= x_width
128
+ hand2[:, 1] /= y_height
129
+ hand2 = np.random.normal(hand2, dv)
130
+ hand2[:, 0] *= x_width
131
+ hand2[:, 1] *= y_height
132
+ hand2 = np.where(np.isnan(hand2), None, hand2).tolist()
133
+
134
+ df_augmented.at[i, "pose"] = pose
135
+ df_augmented.at[i, "hand1"] = hand1
136
+ df_augmented.at[i, "hand2"] = hand2
137
+
138
+ return df_augmented
139
+
140
+
141
+ def cutout(df):
142
+ # cutout
143
+ df_augmented = df.copy()
144
+
145
+ pad_idx = 0
146
+ for i in range(df.shape[0]):
147
+ if np.count_nonzero(df.loc[i, "pose"]) == 0:
148
+ pad_idx = i
149
+ break
150
+
151
+ for i in range(df.shape[0]):
152
+ if np.count_nonzero(df.loc[i, "pose"]) == 0:
153
+ break
154
+
155
+ if i < pad_idx:
156
+ pose = np.array(df.loc[i, "pose"])
157
+ hand1 = np.array(df.loc[i, "hand1"])
158
+ hand2 = np.array(df.loc[i, "hand2"])
159
+ pose_zero_idx = np.random.choice(25, 3, replace=False)
160
+ hand1_zero_idx = np.random.choice(21, 3, replace=False)
161
+ hand2_zero_idx = np.random.choice(21, 3, replace=False)
162
+
163
+ for i in pose_zero_idx:
164
+ pose[i] = [0, 0]
165
+ for i in hand1_zero_idx:
166
+ hand1[i] = [0, 0]
167
+ for i in hand2_zero_idx:
168
+ hand2[i] = [0, 0]
169
+
170
+ pose = pose.tolist()
171
+ hand1 = hand1.tolist()
172
+ hand2 = hand2.tolist()
173
+
174
+ df_augmented.at[i, "pose"] = pose
175
+ df_augmented.at[i, "hand1"] = hand1
176
+ df_augmented.at[i, "hand2"] = hand2
177
+
178
+ return df_augmented
179
+
180
+
181
+ def downsample(df):
182
+ # downsample
183
+ frame_len = df.shape[0]
184
+ if frame_len < 15:
185
+ return df.copy()
186
+
187
+ df_augmented = df.copy()
188
+ drop_idx = np.random.choice(frame_len, 15) # 154 frames , 15 frames
189
+ df_augmented = df_augmented.drop(index=drop_idx)
190
+ return df_augmented
191
+
192
+
193
+ def upsample(df):
194
+ # upsample
195
+ def get_avg(df, idx, col):
196
+ aug_points = (
197
+ (
198
+ np.array(df.loc[idx - 1, col], dtype=np.float64)
199
+ + np.array(df.loc[idx, col], dtype=np.float64)
200
+ )
201
+ / 2
202
+ ).tolist()
203
+ return np.where(np.isnan(aug_points), None, aug_points).tolist()
204
+
205
+ frame_length = df.shape[0]
206
+ additional_frames = frame_length // 10
207
+ df_augmented = pd.DataFrame(
208
+ index=np.arange(frame_length + additional_frames),
209
+ columns=["uid", "pose", "hand1", "hand2", "label"],
210
+ )
211
+ df_augmented["uid"] = df.iloc[0].loc["uid"]
212
+
213
+ j = 0
214
+ for i in range(df_augmented.shape[0]):
215
+ if i % 10 != 0 or i == 0:
216
+ df_augmented.at[i, "pose"] = df.loc[j, "pose"]
217
+ df_augmented.at[i, "hand1"] = df.loc[j, "hand1"]
218
+ df_augmented.at[i, "hand2"] = df.loc[j, "hand2"]
219
+ j += 1
220
+ continue
221
+
222
+ df_augmented.at[i, "pose"] = get_avg(df, j, "pose")
223
+ df_augmented.at[i, "hand1"] = get_avg(df, j, "hand1")
224
+ df_augmented.at[i, "hand2"] = get_avg(df, j, "hand2")
225
+
226
+ df_augmented["label"] = df.iloc[0].loc["label"]
227
+ return df_augmented
augs_transformer.log ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Epoch: 1, train loss: 4.454522191776949, train acc: 0.011764705882352941, val loss: 4.167257332801819, val acc: 0.0325
2
+ Epoch: 2, train loss: 4.139250940435073, train acc: 0.022058823529411766, val loss: 4.0199155569076535, val acc: 0.0375
3
+ Epoch: 3, train loss: 4.015947541068582, train acc: 0.008823529411764706, val loss: 4.006150960922241, val acc: 0.025
4
+ Epoch: 4, train loss: 3.9926619613871854, train acc: 0.023529411764705882, val loss: 3.9874421834945677, val acc: 0.0375
5
+ Epoch: 5, train loss: 3.9719819349401138, train acc: 0.023529411764705882, val loss: 4.061394691467285, val acc: 0.0125
6
+ Epoch: 6, train loss: 3.946228302226347, train acc: 0.011764705882352941, val loss: 4.082728552818298, val acc: 0.025
7
+ Epoch: 7, train loss: 3.9648723742541145, train acc: 0.020588235294117647, val loss: 4.055822134017944, val acc: 0.025
8
+ Epoch: 8, train loss: 3.946899579553043, train acc: 0.016176470588235296, val loss: 4.08174843788147, val acc: 0.0125
9
+ Epoch: 9, train loss: 3.9401227221769446, train acc: 0.026470588235294117, val loss: 4.105770111083984, val acc: 0.0
10
+ Epoch: 10, train loss: 3.9309966536129224, train acc: 0.025, val loss: 4.0282625436782835, val acc: 0.0
11
+ Epoch: 11, train loss: 3.9365149301641127, train acc: 0.01911764705882353, val loss: 4.040050387382507, val acc: 0.025
12
+ Epoch: 12, train loss: 3.9410057825200697, train acc: 0.010294117647058823, val loss: 4.0812424421310425, val acc: 0.025
13
+ Epoch: 13, train loss: 3.926714330561021, train acc: 0.03235294117647059, val loss: 4.062881135940552, val acc: 0.0375
14
+ Epoch: 14, train loss: 3.909536064372343, train acc: 0.030882352941176472, val loss: 4.074292922019959, val acc: 0.025
15
+ Epoch: 15, train loss: 3.9082603931427, train acc: 0.016176470588235296, val loss: 4.133523559570312, val acc: 0.0125
16
+ Epoch: 16, train loss: 3.9084412041832417, train acc: 0.029411764705882353, val loss: 4.1158226251602175, val acc: 0.0125
17
+ Epoch: 17, train loss: 3.9092662194195915, train acc: 0.013235294117647059, val loss: 4.096873998641968, val acc: 0.0125
18
+ Epoch: 1, train loss: 3.7796568870544434, train acc: 0.05416666666666667, val loss: 3.3048714796702066, val acc: 0.041666666666666664
19
+ Epoch: 2, train loss: 2.96195813814799, train acc: 0.07083333333333333, val loss: 2.759005864461263, val acc: 0.08333333333333333
20
+ Epoch: 3, train loss: 2.9263431151707966, train acc: 0.05, val loss: 2.9804892539978027, val acc: 0.125
21
+ Epoch: 4, train loss: 2.9008073091506956, train acc: 0.09583333333333334, val loss: 2.922832171122233, val acc: 0.125
22
+ Epoch: 5, train loss: 2.833145562807719, train acc: 0.07916666666666666, val loss: 2.975987990697225, val acc: 0.125
23
+ Epoch: 6, train loss: 2.8469337860743207, train acc: 0.05, val loss: 2.910189708073934, val acc: 0.0
24
+ Epoch: 7, train loss: 2.795868976910909, train acc: 0.07083333333333333, val loss: 2.9394245942433677, val acc: 0.041666666666666664
25
+ Epoch: 8, train loss: 2.8105882883071898, train acc: 0.06666666666666667, val loss: 2.85969344774882, val acc: 0.041666666666666664
26
+ Epoch: 9, train loss: 2.773549795150757, train acc: 0.075, val loss: 2.7630320390065513, val acc: 0.08333333333333333
27
+ Epoch: 10, train loss: 2.673665165901184, train acc: 0.11666666666666667, val loss: 2.9171629746754966, val acc: 0.041666666666666664
28
+ Epoch: 11, train loss: 2.7379541556040445, train acc: 0.09166666666666666, val loss: 2.762899319330851, val acc: 0.125
29
+ Epoch: 12, train loss: 2.7289541959762573, train acc: 0.08333333333333333, val loss: 2.7536725203196206, val acc: 0.08333333333333333
30
+ Epoch: 13, train loss: 2.6597306728363037, train acc: 0.1, val loss: 2.668971618016561, val acc: 0.125
31
+ Epoch: 14, train loss: 2.6211634238560992, train acc: 0.13333333333333333, val loss: 2.6979883511861167, val acc: 0.16666666666666666
32
+ Epoch: 15, train loss: 2.519361448287964, train acc: 0.12083333333333333, val loss: 2.602913777033488, val acc: 0.08333333333333333
33
+ Epoch: 16, train loss: 2.257799812157949, train acc: 0.20416666666666666, val loss: 2.008861700693766, val acc: 0.375
34
+ Epoch: 17, train loss: 1.9873774607976278, train acc: 0.2875, val loss: 1.886928955713908, val acc: 0.4166666666666667
35
+ Epoch: 18, train loss: 1.8596484184265136, train acc: 0.32083333333333336, val loss: 1.5878275632858276, val acc: 0.4166666666666667
36
+ Epoch: 19, train loss: 1.7037100315093994, train acc: 0.37916666666666665, val loss: 1.804202914237976, val acc: 0.25
37
+ Epoch: 20, train loss: 1.6452774405479431, train acc: 0.4083333333333333, val loss: 1.3881817261377971, val acc: 0.375
38
+ Epoch: 21, train loss: 1.4983825981616974, train acc: 0.4708333333333333, val loss: 1.309925119082133, val acc: 0.5
39
+ Epoch: 22, train loss: 1.4139545102914175, train acc: 0.42083333333333334, val loss: 1.2405953407287598, val acc: 0.5
40
+ Epoch: 23, train loss: 1.3079413563013076, train acc: 0.5416666666666666, val loss: 1.0623323718706768, val acc: 0.7083333333333334
41
+ Epoch: 24, train loss: 1.0987703889608382, train acc: 0.6458333333333334, val loss: 0.971619725227356, val acc: 0.7083333333333334
42
+ Epoch: 25, train loss: 1.0566187125941118, train acc: 0.6375, val loss: 1.001390238602956, val acc: 0.6666666666666666
43
+ Epoch: 26, train loss: 0.9702375590801239, train acc: 0.6625, val loss: 1.0340282917022705, val acc: 0.6666666666666666
44
+ Epoch: 27, train loss: 0.8936778622368972, train acc: 0.6791666666666667, val loss: 0.8146964112917582, val acc: 0.7916666666666666
45
+ Epoch: 28, train loss: 0.7876667633652688, train acc: 0.7291666666666666, val loss: 0.805329442024231, val acc: 0.7083333333333334
46
+ Epoch: 29, train loss: 0.7577408134937287, train acc: 0.7458333333333333, val loss: 0.6200626691182455, val acc: 0.8333333333333334
47
+ Epoch: 30, train loss: 0.7406811500589053, train acc: 0.7625, val loss: 0.756562332312266, val acc: 0.7083333333333334
48
+ Epoch: 31, train loss: 0.6476593340436617, train acc: 0.8, val loss: 0.41417930523554486, val acc: 0.9166666666666666
49
+ Epoch: 32, train loss: 0.6368213320771853, train acc: 0.7833333333333333, val loss: 0.7533869743347168, val acc: 0.7083333333333334
50
+ Epoch: 33, train loss: 0.8467342297236125, train acc: 0.7, val loss: 0.46440934141476947, val acc: 0.875
51
+ Epoch: 34, train loss: 0.6197561246653398, train acc: 0.7666666666666667, val loss: 0.3979839086532593, val acc: 0.8333333333333334
52
+ Epoch: 35, train loss: 0.6288725058237712, train acc: 0.7625, val loss: 0.35973605265220004, val acc: 0.8333333333333334
53
+ Epoch: 36, train loss: 0.6005093584458033, train acc: 0.7958333333333333, val loss: 0.4219923714796702, val acc: 0.7916666666666666
54
+ Epoch: 37, train loss: 0.5010966459910074, train acc: 0.8375, val loss: 0.3479422728220622, val acc: 0.875
55
+ Epoch: 38, train loss: 0.5946198473374049, train acc: 0.7791666666666667, val loss: 0.3922339826822281, val acc: 0.875
56
+ Epoch: 39, train loss: 0.4414273346463839, train acc: 0.8666666666666667, val loss: 0.25101877748966217, val acc: 0.9583333333333334
57
+ Epoch: 40, train loss: 0.42124902034799255, train acc: 0.8541666666666666, val loss: 0.27305885155995685, val acc: 0.9166666666666666
58
+ Epoch: 41, train loss: 0.39148166303833326, train acc: 0.8666666666666667, val loss: 0.3132087141275406, val acc: 0.875
59
+ Epoch: 42, train loss: 0.44249625156323114, train acc: 0.8416666666666667, val loss: 0.3087519307931264, val acc: 0.875
60
+ Epoch: 43, train loss: 0.399545910085241, train acc: 0.8833333333333333, val loss: 0.28369801739851636, val acc: 0.9166666666666666
61
+ Epoch: 44, train loss: 0.40615768507122996, train acc: 0.8583333333333333, val loss: 0.3653286099433899, val acc: 0.8333333333333334
62
+ Epoch: 45, train loss: 0.45086419557531676, train acc: 0.825, val loss: 0.3478381074965, val acc: 0.9166666666666666
63
+ Epoch: 46, train loss: 0.36654019008080163, train acc: 0.8583333333333333, val loss: 0.30720309416453045, val acc: 0.9166666666666666
64
+ Epoch: 47, train loss: 0.32245828956365585, train acc: 0.9083333333333333, val loss: 0.17537023747960725, val acc: 0.875
65
+ Epoch: 48, train loss: 0.45214990166326363, train acc: 0.8166666666666667, val loss: 0.19804753363132477, val acc: 0.9583333333333334
66
+ Epoch: 49, train loss: 0.34067513768871627, train acc: 0.8666666666666667, val loss: 0.2660779853661855, val acc: 0.9166666666666666
67
+ Epoch: 50, train loss: 0.31909968542555966, train acc: 0.875, val loss: 0.2961270858844121, val acc: 0.875
cnn_runner.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+
4
+ import numpy as np
5
+ import torch
6
+ from tqdm.auto import tqdm
7
+
8
+ from models import CNN
9
+ from configs import CnnConfig
10
+ from utils import load_json
11
+ import cv2
12
+ import glob
13
+
14
+
15
+ def replace_nan(x, y):
16
+ x = 0.0 if np.isnan(x) else x
17
+ y = 0.0 if np.isnan(y) else y
18
+ return x, y
19
+
20
+
21
+ def draw_hands(
22
+ image,
23
+ hand_x,
24
+ hand_y,
25
+ connections,
26
+ connection_color,
27
+ thickness,
28
+ point_color,
29
+ frame_length,
30
+ frame_width,
31
+ ):
32
+ for connection in connections:
33
+ x0 = hand_x[connection[0]] * frame_width
34
+ y0 = hand_y[connection[0]] * frame_length
35
+ x1 = hand_x[connection[1]] * frame_width
36
+ y1 = hand_y[connection[1]] * frame_length
37
+
38
+ x0, y0 = replace_nan(x0, y0)
39
+ x1, y1 = replace_nan(x1, y1)
40
+
41
+ cv2.line(
42
+ image,
43
+ (int(x0), int(y0)),
44
+ (int(x1), int(y1)),
45
+ connection_color,
46
+ thickness,
47
+ )
48
+
49
+ for x, y in zip(hand_x, hand_y):
50
+ x, y = replace_nan(x, y)
51
+ cv2.circle(image, (int(x), int(y)), thickness, point_color, thickness)
52
+
53
+ return image
54
+
55
+
56
+ def draw_pose(
57
+ image,
58
+ pose_x,
59
+ pose_y,
60
+ links,
61
+ connection_color,
62
+ thickness,
63
+ point_color,
64
+ frame_length,
65
+ frame_width,
66
+ ):
67
+ for link in links:
68
+ x0 = pose_x[link[0]] * frame_width
69
+ y0 = pose_y[link[0]] * frame_length
70
+ x1 = pose_x[link[1]] * frame_width
71
+ y1 = pose_y[link[1]] * frame_length
72
+
73
+ x0, y0 = replace_nan(x0, y0)
74
+ x1, y1 = replace_nan(x1, y1)
75
+
76
+ cv2.line(
77
+ image,
78
+ (int(x0), int(y0)),
79
+ (int(x1), int(y1)),
80
+ connection_color,
81
+ thickness,
82
+ )
83
+ cv2.circle(image, (int(x0), int(y0)), thickness, point_color, thickness)
84
+ cv2.circle(image, (int(x1), int(y1)), thickness, point_color, thickness)
85
+
86
+ return image
87
+
88
+
89
+ def cnn_feat(file_path, save_dir):
90
+ video_record = load_json(file_path)
91
+
92
+ FRAME_LENGTH = 1080
93
+ FRAME_WIDTH = 1920
94
+ POINT_COLOR = (255, 0, 0)
95
+ CONNECTION_COLOR = (0, 255, 0)
96
+ THICKNESS = 2
97
+
98
+ connections = [
99
+ (0, 1),
100
+ (1, 2),
101
+ (2, 3),
102
+ (3, 4),
103
+ (5, 6),
104
+ (6, 7),
105
+ (7, 8),
106
+ (9, 10),
107
+ (10, 11),
108
+ (11, 12),
109
+ (13, 14),
110
+ (14, 15),
111
+ (15, 16),
112
+ (17, 18),
113
+ (18, 19),
114
+ (19, 20),
115
+ (0, 5),
116
+ (5, 9),
117
+ (9, 13),
118
+ (13, 17),
119
+ (0, 17),
120
+ ]
121
+
122
+ links = [
123
+ (11, 12),
124
+ (11, 23),
125
+ (12, 24),
126
+ (23, 24),
127
+ (11, 13),
128
+ (13, 15),
129
+ (12, 14),
130
+ (14, 16),
131
+ (15, 21),
132
+ (15, 17),
133
+ (17, 19),
134
+ (19, 15),
135
+ (22, 16),
136
+ (16, 18),
137
+ (18, 20),
138
+ (16, 20),
139
+ ]
140
+
141
+ model = CNN(CnnConfig)
142
+ features = np.empty((0, CnnConfig.output_dim))
143
+
144
+ assert video_record["n_frames"] > 0, "Number of frames should be greater than zero"
145
+ for i in range(video_record["n_frames"]):
146
+ image = np.zeros((FRAME_LENGTH, FRAME_WIDTH, 3), np.uint8)
147
+ pose_x = video_record["pose_x"][i]
148
+ pose_y = video_record["pose_y"][i]
149
+ hand1_x = video_record["hand1_x"][i]
150
+ hand1_y = video_record["hand1_y"][i]
151
+ hand2_x = video_record["hand2_x"][i]
152
+ hand2_y = video_record["hand2_y"][i]
153
+
154
+ if hand1_x[0] != 0:
155
+ image = draw_hands(
156
+ image,
157
+ hand1_x,
158
+ hand1_y,
159
+ connections,
160
+ CONNECTION_COLOR,
161
+ THICKNESS,
162
+ POINT_COLOR,
163
+ FRAME_LENGTH,
164
+ FRAME_WIDTH,
165
+ )
166
+
167
+ if hand2_x[0] != 0:
168
+ image = draw_hands(
169
+ image,
170
+ hand2_x,
171
+ hand2_y,
172
+ connections,
173
+ CONNECTION_COLOR,
174
+ THICKNESS,
175
+ POINT_COLOR,
176
+ FRAME_LENGTH,
177
+ FRAME_WIDTH,
178
+ )
179
+
180
+ image = draw_pose(
181
+ image,
182
+ pose_x,
183
+ pose_y,
184
+ links,
185
+ CONNECTION_COLOR,
186
+ THICKNESS,
187
+ POINT_COLOR,
188
+ FRAME_LENGTH,
189
+ FRAME_WIDTH,
190
+ )
191
+ image = image.astype(np.float32) / 255
192
+ image = cv2.resize(image, (224, 224))
193
+ feat = model(torch.FloatTensor(image).permute(2, 0, 1).unsqueeze(0))
194
+ features = np.vstack([features, feat.numpy()])
195
+
196
+ save_path = os.path.join(save_dir, video_record["uid"] + ".npy")
197
+ np.save(save_path, features)
198
+
199
+
200
+ def runner(args, mode):
201
+ json_files = sorted(
202
+ glob.glob(
203
+ os.path.join(args.data_dir, f"{args.dataset}_{mode}_keypoints", "*.json")
204
+ )
205
+ )
206
+
207
+ save_dir = os.path.join(args.data_dir, f"{args.dataset}_{mode}_features")
208
+ if not os.path.exists(save_dir):
209
+ os.mkdir(save_dir)
210
+
211
+ saved_files = glob.glob(os.path.join(save_dir, "*.npy"))
212
+ if len(saved_files) != len(json_files):
213
+ for file_path in tqdm(json_files, desc=f"Saving CNN features - {mode} files"):
214
+ cnn_feat(file_path, save_dir)
215
+ else:
216
+ print(mode, "CNN features already exist!")
217
+
218
+
219
+ def save_cnn_features(args):
220
+ runner(args, mode="train")
221
+ runner(args, mode="val")
222
+ runner(args, mode="test")
commands.txt ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Then recreate it on any machine with:
2
+ python -m venv venv
3
+ source venv/bin/activate
4
+ pip install -r requirements.txt
5
+
6
+ #What to run now (recommended path)
7
+
8
+ #1. Sanity-check that videos are readable (this should now show 10/10):
9
+ venv/bin/python generate_keypoints.py \
10
+ --include_dir /home/ravijaanthony/Documents/dev/IIT/FYP/Code/DATA \
11
+ --save_dir ./processed_data -\
12
+ -dataset include50 \
13
+ --scan \
14
+ --preflight \
15
+ --preflight_n 10
16
+
17
+ #2. Generate keypoints using Holistic (pose + hands + face) and save eyebrow landmarks:
18
+ rm -rf processed_data/include50_*_keypoints
19
+ venv/bin/python generate_keypoints.py \
20
+ --include_dir /home/ravijaanthony/Documents/dev/IIT/FYP/Code/DATA \
21
+ --save_dir ./processed_data \
22
+ --dataset include50 \
23
+ --scan \
24
+ --use_holistic \
25
+ --face_mode full \
26
+ --splits all \
27
+ --split_seed 0
28
+
29
+ #3. Train (same as before, but make sure you use the venv python):
30
+ venv/bin/python runner.py \
31
+ --dataset include50 \
32
+ --use_augs \
33
+ --model transformer \
34
+ --data_dir ./processed_data \
35
+ --batch_size 8
36
+
37
+
38
+ # For INCLUDE dataset (263 classes)
39
+ venv/bin/python runner.py \
40
+ --dataset include \
41
+ --model transformer \
42
+ --transformer_size large \
43
+ --data_dir ./processed_data \
44
+ --use_pretrained evaluate
45
+
46
+ # For INCLUDE50 dataset (50 classes)
47
+ venv/bin/python runner.py \
48
+ --dataset include50 \
49
+ --model transformer \
50
+ --transformer_size small \
51
+ --data_dir ./processed_data \
52
+ --use_pretrained evaluate
configs.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ import transformers
3
+ import timm
4
+
5
+
6
+ @dataclass
7
+ class LstmConfig:
8
+ input_size: int = 134
9
+ hidden_size: int = 256
10
+ num_layers: int = 5
11
+ batch_first: bool = True
12
+ bidirectional: bool = True
13
+ dropout: float = 0.2
14
+
15
+
16
+ @dataclass
17
+ class XgbConfig:
18
+ booster: str = "gbtree"
19
+ silent: int = 0
20
+ max_depth: int = 2
21
+ subsample: float = 0.9923301318585108
22
+ colsample_bytree: float = 0.7747027267489391
23
+ reg_lambda: int = 3
24
+ objective: str = "multi:softprob"
25
+ eval_metric: str = "mlogloss"
26
+ tree_method: str = "gpu_hist" ## change it to `hist` if gpu not available
27
+
28
+
29
+ @dataclass
30
+ class TransformerConfig:
31
+ size: str
32
+ input_size: int = 134
33
+ max_position_embeddings: int = field(default=256, repr=False)
34
+ layer_norm_eps: float = field(default=1e-12, repr=False)
35
+ hidden_dropout_prob: float = field(default=0.1, repr=False)
36
+ hidden_size: int = field(default=512, repr=False)
37
+ num_attention_heads: int = field(default=8, repr=False)
38
+ num_hidden_layers: int = field(default=4, repr=False)
39
+ model_config: transformers.BertConfig = field(init=False)
40
+
41
+ def __post_init__(self):
42
+ assert self.size in ["small", "large"]
43
+ if self.size == "small":
44
+ self.hidden_size = 256
45
+ self.num_attention_heads = 4
46
+ self.num_hidden_layers = 2
47
+
48
+ self.model_config = transformers.BertConfig(
49
+ hidden_size=self.hidden_size,
50
+ num_attention_heads=self.num_attention_heads,
51
+ num_hidden_layers=self.num_hidden_layers,
52
+ max_position_embeddings=self.max_position_embeddings,
53
+ )
54
+
55
+
56
+ @dataclass
57
+ class CnnConfig:
58
+ model: str = "mobilenetv2_100"
59
+ output_dim: int = 1280
60
+
61
+ def __post_init__(self):
62
+ available_models = timm.list_models(pretrained=True)
63
+ assert self.model in available_models
dataset.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import glob
3
+ import os
4
+
5
+ import torch
6
+ from torch.utils import data
7
+ import numpy as np
8
+ import pandas as pd
9
+ from augment import (
10
+ Augmentation,
11
+ OneOf,
12
+ plus7rotation,
13
+ minus7rotation,
14
+ gaussSample,
15
+ cutout,
16
+ upsample,
17
+ downsample,
18
+ )
19
+
20
+
21
+ class KeypointsDataset(data.Dataset):
22
+ def __init__(
23
+ self,
24
+ keypoints_dir,
25
+ use_augs,
26
+ label_map,
27
+ mode="train",
28
+ max_frame_len=200,
29
+ frame_length=1080,
30
+ frame_width=1920,
31
+ ):
32
+ self.files = sorted(glob.glob(os.path.join(keypoints_dir, "*.json")))
33
+ self.mode = mode
34
+ self.use_augs = use_augs
35
+ self.label_map = label_map
36
+ self.max_frame_len = max_frame_len
37
+ self.frame_length = frame_length
38
+ self.frame_width = frame_width
39
+
40
+ # Filter out files with labels not in label_map
41
+ valid_files = []
42
+ for file_path in self.files:
43
+ row = pd.read_json(file_path, typ="series")
44
+ label = "".join([i for i in row.label if i.isalpha()]).lower()
45
+ if label in self.label_map:
46
+ valid_files.append(file_path)
47
+ self.files = valid_files
48
+
49
+ self.augs = [
50
+ Augmentation(OneOf(plus7rotation, minus7rotation), p=0.4),
51
+ Augmentation(gaussSample, p=0.4),
52
+ Augmentation(cutout, p=0.4),
53
+ Augmentation(OneOf(upsample, downsample), p=0.4),
54
+ ]
55
+
56
+ def augment(self, df):
57
+ for aug in self.augs:
58
+ df = aug(df)
59
+ return df
60
+
61
+ def interpolate(self, arr):
62
+
63
+ arr_x = arr[:, :, 0]
64
+ arr_x = pd.DataFrame(arr_x)
65
+ arr_x = arr_x.interpolate(method="linear", limit_direction="both").to_numpy()
66
+
67
+ arr_y = arr[:, :, 1]
68
+ arr_y = pd.DataFrame(arr_y)
69
+ arr_y = arr_y.interpolate(method="linear", limit_direction="both").to_numpy()
70
+
71
+ if np.count_nonzero(~np.isnan(arr_x)) == 0:
72
+ arr_x = np.zeros(arr_x.shape)
73
+ if np.count_nonzero(~np.isnan(arr_y)) == 0:
74
+ arr_y = np.zeros(arr_y.shape)
75
+
76
+ arr_x = arr_x * self.frame_width
77
+ arr_y = arr_y * self.frame_length
78
+
79
+ return np.stack([arr_x, arr_y], axis=-1)
80
+
81
+ def combine_xy(self, x, y):
82
+ x, y = np.array(x), np.array(y)
83
+ _, length = x.shape
84
+ x = x.reshape((-1, length, 1))
85
+ y = y.reshape((-1, length, 1))
86
+ return np.concatenate((x, y), -1).astype(np.float32)
87
+
88
+ def __getitem__(self, idx):
89
+ file_path = self.files[idx]
90
+ row = pd.read_json(file_path, typ="series")
91
+ label = row.label
92
+ label = "".join([i for i in label if i.isalpha()]).lower()
93
+
94
+ pose = self.combine_xy(row.pose_x, row.pose_y)
95
+ h1 = self.combine_xy(row.hand1_x, row.hand1_y)
96
+ h2 = self.combine_xy(row.hand2_x, row.hand2_y)
97
+
98
+ pose = self.interpolate(pose)
99
+ h1 = self.interpolate(h1)
100
+ h2 = self.interpolate(h2)
101
+
102
+ df = pd.DataFrame.from_dict(
103
+ {
104
+ "uid": row.uid,
105
+ "pose": pose.tolist(),
106
+ "hand1": h1.tolist(),
107
+ "hand2": h2.tolist(),
108
+ "label": label,
109
+ }
110
+ )
111
+ if self.mode == "train" and self.use_augs:
112
+ df = self.augment(df)
113
+
114
+ pose = (
115
+ np.array(list(map(np.array, df.pose.values)))
116
+ .reshape(-1, 50)
117
+ .astype(np.float32)
118
+ )
119
+ h1 = (
120
+ np.array(list(map(np.array, df.hand1.values)))
121
+ .reshape(-1, 42)
122
+ .astype(np.float32)
123
+ )
124
+ h2 = (
125
+ np.array(list(map(np.array, df.hand2.values)))
126
+ .reshape(-1, 42)
127
+ .astype(np.float32)
128
+ )
129
+ final_data = np.concatenate((pose, h1, h2), -1)
130
+ final_data = np.pad(
131
+ final_data,
132
+ ((0, self.max_frame_len - final_data.shape[0]), (0, 0)),
133
+ "constant",
134
+ )
135
+ return {
136
+ "uid": row.uid,
137
+ "data": torch.FloatTensor(final_data),
138
+ "label": self.label_map[label],
139
+ "lablel_string": label,
140
+ }
141
+
142
+ def __len__(self):
143
+ return len(self.files)
144
+
145
+
146
+ class FeaturesDatset(data.Dataset):
147
+ def __init__(self, features_dir, label_map, mode="train", max_frame_len=200):
148
+ self.features_dir = features_dir
149
+ self.file_paths = sorted(glob.glob(os.path.join(features_dir, "*.npy")))
150
+ self.label_map = label_map
151
+ self.mode = mode
152
+ self.max_frame_len = max_frame_len
153
+
154
+ def __getitem__(self, i):
155
+ file_path = self.file_paths[i]
156
+ data = np.load(file_path)
157
+ data = np.pad(
158
+ data,
159
+ ((0, self.max_frame_len - data.shape[0]), (0, 0)),
160
+ "constant",
161
+ )
162
+ label = os.path.basename(file_path).split("_")[0]
163
+ return {
164
+ "uid": os.path.basename(file_path).split(".")[0],
165
+ "data": torch.FloatTensor(data),
166
+ "label": self.label_map[label],
167
+ "lablel_string": label,
168
+ }
169
+
170
+ def __len__(self):
171
+ return len(self.file_paths)
evaluate.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import argparse
4
+ import json
5
+ import numpy as np
6
+ import pandas as pd
7
+ from tqdm import tqdm
8
+ import torch
9
+ import torch.nn as nn
10
+ from torch.utils import data
11
+ from generate_keypoints import process_video
12
+ from models import Transformer
13
+ from configs import TransformerConfig
14
+ from utils import load_json, load_label_map
15
+ import shutil
16
+
17
+ parser = argparse.ArgumentParser(description="Evaluate function")
18
+ parser.add_argument("--data_dir", required=True, help="data directory")
19
+ args = parser.parse_args()
20
+
21
+
22
+ class KeypointsDataset(data.Dataset):
23
+ def __init__(
24
+ self,
25
+ keypoints_dir,
26
+ max_frame_len=200,
27
+ frame_length=1080,
28
+ frame_width=1920,
29
+ ):
30
+ self.files = sorted(glob.glob(os.path.join(keypoints_dir, "*.json")))
31
+ self.max_frame_len = max_frame_len
32
+ self.frame_length = frame_length
33
+ self.frame_width = frame_width
34
+
35
+ def interpolate(self, arr):
36
+
37
+ arr_x = arr[:, :, 0]
38
+ arr_x = pd.DataFrame(arr_x)
39
+ arr_x = arr_x.interpolate(method="linear", limit_direction="both").to_numpy()
40
+
41
+ arr_y = arr[:, :, 1]
42
+ arr_y = pd.DataFrame(arr_y)
43
+ arr_y = arr_y.interpolate(method="linear", limit_direction="both").to_numpy()
44
+
45
+ if np.count_nonzero(~np.isnan(arr_x)) == 0:
46
+ arr_x = np.zeros(arr_x.shape)
47
+ if np.count_nonzero(~np.isnan(arr_y)) == 0:
48
+ arr_y = np.zeros(arr_y.shape)
49
+
50
+ arr_x = arr_x * self.frame_width
51
+ arr_y = arr_y * self.frame_length
52
+
53
+ return np.stack([arr_x, arr_y], axis=-1)
54
+
55
+ def combine_xy(self, x, y):
56
+ x, y = np.array(x), np.array(y)
57
+ _, length = x.shape
58
+ x = x.reshape((-1, length, 1))
59
+ y = y.reshape((-1, length, 1))
60
+ return np.concatenate((x, y), -1).astype(np.float32)
61
+
62
+ def __getitem__(self, idx):
63
+ file_path = self.files[idx]
64
+ row = pd.read_json(file_path, typ="series")
65
+ label = row.label
66
+ label = "".join([i for i in label if i.isalpha()]).lower()
67
+
68
+ pose = self.combine_xy(row.pose_x, row.pose_y)
69
+ h1 = self.combine_xy(row.hand1_x, row.hand1_y)
70
+ h2 = self.combine_xy(row.hand2_x, row.hand2_y)
71
+
72
+ pose = self.interpolate(pose)
73
+ h1 = self.interpolate(h1)
74
+ h2 = self.interpolate(h2)
75
+
76
+ df = pd.DataFrame.from_dict(
77
+ {
78
+ "uid": row.uid,
79
+ "pose": pose.tolist(),
80
+ "hand1": h1.tolist(),
81
+ "hand2": h2.tolist(),
82
+ "label": label,
83
+ }
84
+ )
85
+
86
+ pose = (
87
+ np.array(list(map(np.array, df.pose.values)))
88
+ .reshape(-1, 50)
89
+ .astype(np.float32)
90
+ )
91
+ h1 = (
92
+ np.array(list(map(np.array, df.hand1.values)))
93
+ .reshape(-1, 42)
94
+ .astype(np.float32)
95
+ )
96
+ h2 = (
97
+ np.array(list(map(np.array, df.hand2.values)))
98
+ .reshape(-1, 42)
99
+ .astype(np.float32)
100
+ )
101
+ final_data = np.concatenate((pose, h1, h2), -1)
102
+ final_data = np.pad(
103
+ final_data,
104
+ ((0, self.max_frame_len - final_data.shape[0]), (0, 0)),
105
+ "constant",
106
+ )
107
+ return {
108
+ "uid": row.uid,
109
+ "data": torch.FloatTensor(final_data),
110
+ }
111
+
112
+ def __len__(self):
113
+ return len(self.files)
114
+
115
+
116
+ @torch.no_grad()
117
+ def inference(dataloader, model, device, label_map):
118
+ model.eval()
119
+ predictions = []
120
+
121
+ for batch in tqdm(dataloader, desc="Eval"):
122
+ input_data = batch["data"].to(device)
123
+ output = model(input_data).detach().cpu()
124
+ output = torch.argmax(torch.softmax(output, dim=-1), dim=-1).numpy()
125
+ predictions.append({"uid": batch["uid"][0], "predicted_label": label_map[output[0]]})
126
+
127
+ return predictions
128
+
129
+
130
+ video_paths = glob.glob(os.path.join(args.data_dir, "*"))
131
+ save_dir = "keypoints_dir"
132
+ if os.path.isdir(save_dir):
133
+ shutil.rmtree(save_dir)
134
+ os.mkdir(save_dir)
135
+ for path in tqdm(video_paths, desc="Processing Videos"):
136
+ process_video(path, save_dir)
137
+
138
+ label_map = load_label_map("include")
139
+ dataset = KeypointsDataset(
140
+ keypoints_dir=save_dir,
141
+ max_frame_len=169,
142
+ )
143
+
144
+ dataloader = data.DataLoader(
145
+ dataset,
146
+ batch_size=1,
147
+ shuffle=False,
148
+ num_workers=4,
149
+ pin_memory=True,
150
+ )
151
+ label_map = dict(zip(label_map.values(), label_map.keys()))
152
+
153
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
154
+ config = TransformerConfig(size="large", max_position_embeddings=256)
155
+ model = Transformer(config=config, n_classes=263)
156
+ model = model.to(device)
157
+
158
+ pretrained_model_name = "include_no_cnn_transformer_large.pth"
159
+ pretrained_model_links = load_json("pretrained_links.json")
160
+ if not os.path.isfile(pretrained_model_name):
161
+ link = pretrained_model_links[pretrained_model_name]
162
+ torch.hub.download_url_to_file(link, pretrained_model_name, progress=True)
163
+
164
+ ckpt = torch.load(pretrained_model_name)
165
+ model.load_state_dict(ckpt["model"])
166
+ print("### Model loaded ###")
167
+
168
+ preds = inference(dataloader, model, device, label_map)
169
+ print(json.dumps(preds, indent=2))
generate_keypoints.py ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import glob
4
+ import multiprocessing
5
+ import argparse
6
+ import os.path
7
+ import cv2
8
+ import mediapipe as mp
9
+ from tqdm.auto import tqdm
10
+ from joblib import Parallel, delayed
11
+ import numpy as np
12
+ import gc
13
+ import warnings
14
+
15
+
16
+ # MediaPipe FaceMesh landmark indices (subset) for eyebrows.
17
+ # These are commonly used indices for left/right eyebrows.
18
+ _EYEBROW_IDXS = [
19
+ # left eyebrow
20
+ 70,
21
+ 63,
22
+ 105,
23
+ 66,
24
+ 107,
25
+ 55,
26
+ 65,
27
+ 52,
28
+ 53,
29
+ 46,
30
+ # right eyebrow
31
+ 336,
32
+ 296,
33
+ 334,
34
+ 293,
35
+ 300,
36
+ 276,
37
+ 283,
38
+ 282,
39
+ 295,
40
+ 285,
41
+ ]
42
+
43
+
44
+ def _label_from_path(path: str) -> str:
45
+ # Assumes dataset layout: <include_dir>/<label>/<video_file>
46
+ label = path.split("/")[-2]
47
+ return "".join([i for i in label if i.isalpha()]).lower()
48
+
49
+
50
+ def _uid_from_path(path: str, label: str) -> str:
51
+ uid = os.path.splitext(os.path.basename(path))[0]
52
+ return "_".join([label, uid])
53
+
54
+
55
+ def _try_open_video(path: str) -> cv2.VideoCapture:
56
+ cap = cv2.VideoCapture(path)
57
+ if cap.isOpened():
58
+ return cap
59
+
60
+ # Try forcing FFmpeg backend if available.
61
+ if hasattr(cv2, "CAP_FFMPEG"):
62
+ cap2 = cv2.VideoCapture(path, cv2.CAP_FFMPEG)
63
+ if cap2.isOpened():
64
+ return cap2
65
+
66
+ return cap
67
+
68
+
69
+ def _pose25_from_mp_pose(pose_landmarks):
70
+ if pose_landmarks is None:
71
+ return [np.nan] * 25, [np.nan] * 25
72
+
73
+ lm = pose_landmarks.landmark
74
+
75
+ def get_xy(i: int):
76
+ return lm[i].x, lm[i].y
77
+
78
+ def avg_xy(a: int, b: int):
79
+ ax, ay = get_xy(a)
80
+ bx, by = get_xy(b)
81
+ return (ax + bx) / 2.0, (ay + by) / 2.0
82
+
83
+ # Map MediaPipe Pose (33) -> OpenPose BODY_25-like layout used by this repo (25).
84
+ # 0 Nose
85
+ nose = get_xy(0)
86
+ # 1 Neck (approx: midpoint of shoulders)
87
+ neck = avg_xy(11, 12)
88
+
89
+ rshoulder = get_xy(12)
90
+ relbow = get_xy(14)
91
+ rwrist = get_xy(16)
92
+
93
+ lshoulder = get_xy(11)
94
+ lelbow = get_xy(13)
95
+ lwrist = get_xy(15)
96
+
97
+ midhip = avg_xy(23, 24)
98
+
99
+ rhip = get_xy(24)
100
+ rknee = get_xy(26)
101
+ rankle = get_xy(28)
102
+
103
+ lhip = get_xy(23)
104
+ lknee = get_xy(25)
105
+ lankle = get_xy(27)
106
+
107
+ # Eyes / ears
108
+ reye = get_xy(5)
109
+ leye = get_xy(2)
110
+ rear = get_xy(8)
111
+ lear = get_xy(7)
112
+
113
+ # Feet (MediaPipe doesn't provide small-toe separately; we duplicate foot_index)
114
+ lbigtoe = get_xy(31)
115
+ lsmalltoe = get_xy(31)
116
+ lheel = get_xy(29)
117
+
118
+ rbigtoe = get_xy(32)
119
+ rsmalltoe = get_xy(32)
120
+ rheel = get_xy(30)
121
+
122
+ points = [
123
+ nose,
124
+ neck,
125
+ rshoulder,
126
+ relbow,
127
+ rwrist,
128
+ lshoulder,
129
+ lelbow,
130
+ lwrist,
131
+ midhip,
132
+ rhip,
133
+ rknee,
134
+ rankle,
135
+ lhip,
136
+ lknee,
137
+ lankle,
138
+ reye,
139
+ leye,
140
+ rear,
141
+ lear,
142
+ lbigtoe,
143
+ lsmalltoe,
144
+ lheel,
145
+ rbigtoe,
146
+ rsmalltoe,
147
+ rheel,
148
+ ]
149
+
150
+ xs = [p[0] for p in points]
151
+ ys = [p[1] for p in points]
152
+ return xs, ys
153
+
154
+
155
+ def _hand21_from_landmarks(hand_landmarks):
156
+ if hand_landmarks is None:
157
+ return [np.nan] * 21, [np.nan] * 21
158
+ xs, ys = [], []
159
+ for landmark in hand_landmarks.landmark:
160
+ xs.append(landmark.x)
161
+ ys.append(landmark.y)
162
+ return xs, ys
163
+
164
+
165
+ def _face_from_landmarks(face_landmarks, face_mode: str):
166
+ if face_mode == "none":
167
+ return None, None
168
+
169
+ if face_mode == "full":
170
+ idxs = None
171
+ n = 468
172
+ elif face_mode == "eyebrows":
173
+ idxs = _EYEBROW_IDXS
174
+ n = len(_EYEBROW_IDXS)
175
+ else:
176
+ raise ValueError(f"Unsupported face_mode: {face_mode}")
177
+
178
+ if face_landmarks is None:
179
+ return [np.nan] * n, [np.nan] * n
180
+
181
+ lm = face_landmarks.landmark
182
+ if idxs is None:
183
+ xs = [p.x for p in lm]
184
+ ys = [p.y for p in lm]
185
+ return xs, ys
186
+
187
+ xs, ys = [], []
188
+ for i in idxs:
189
+ xs.append(lm[i].x)
190
+ ys.append(lm[i].y)
191
+ return xs, ys
192
+
193
+
194
+ def process_video(
195
+ path: str,
196
+ save_dir: str,
197
+ *,
198
+ use_holistic: bool = False,
199
+ face_mode: str = "none",
200
+ write_placeholders: bool = False,
201
+ ):
202
+ if not os.path.isfile(path):
203
+ warnings.warn(path + " file not found")
204
+ return
205
+
206
+ label = _label_from_path(path)
207
+ uid = _uid_from_path(path, label)
208
+
209
+ cap = _try_open_video(path)
210
+ if not cap.isOpened():
211
+ warnings.warn(f"OpenCV could not open video: {path}")
212
+ return
213
+
214
+ pose_points_x, pose_points_y = [], []
215
+ hand1_points_x, hand1_points_y = [], []
216
+ hand2_points_x, hand2_points_y = [], []
217
+ face_points_x, face_points_y = [], []
218
+
219
+ n_frames = 0
220
+
221
+ if use_holistic:
222
+ holistic = mp.solutions.holistic.Holistic(
223
+ static_image_mode=False,
224
+ model_complexity=1,
225
+ smooth_landmarks=True,
226
+ enable_segmentation=False,
227
+ refine_face_landmarks=True,
228
+ min_detection_confidence=0.5,
229
+ min_tracking_confidence=0.5,
230
+ )
231
+ try:
232
+ while cap.isOpened():
233
+ ret, image = cap.read()
234
+ if not ret:
235
+ break
236
+
237
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
238
+ results = holistic.process(image)
239
+
240
+ pose_x, pose_y = _pose25_from_mp_pose(results.pose_landmarks)
241
+ # Use holistic's left/right hands (subject perspective)
242
+ h1x, h1y = _hand21_from_landmarks(results.left_hand_landmarks)
243
+ h2x, h2y = _hand21_from_landmarks(results.right_hand_landmarks)
244
+
245
+ fx, fy = _face_from_landmarks(results.face_landmarks, face_mode)
246
+
247
+ pose_points_x.append(pose_x)
248
+ pose_points_y.append(pose_y)
249
+ hand1_points_x.append(h1x)
250
+ hand1_points_y.append(h1y)
251
+ hand2_points_x.append(h2x)
252
+ hand2_points_y.append(h2y)
253
+
254
+ if face_mode != "none":
255
+ face_points_x.append(fx)
256
+ face_points_y.append(fy)
257
+
258
+ n_frames += 1
259
+ finally:
260
+ holistic.close()
261
+ else:
262
+ # Legacy path: Pose + Hands separately (kept for backward compatibility).
263
+ hands = mp.solutions.hands.Hands(
264
+ min_detection_confidence=0.5, min_tracking_confidence=0.5
265
+ )
266
+ pose = mp.solutions.pose.Pose(
267
+ static_image_mode=False,
268
+ model_complexity=1,
269
+ enable_segmentation=False,
270
+ min_detection_confidence=0.5,
271
+ min_tracking_confidence=0.5,
272
+ )
273
+ try:
274
+ while cap.isOpened():
275
+ ret, image = cap.read()
276
+ if not ret:
277
+ break
278
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
279
+
280
+ hand_results = hands.process(image)
281
+ pose_results = pose.process(image)
282
+
283
+ # Hands
284
+ hand1 = (
285
+ hand_results.multi_hand_landmarks[0]
286
+ if hand_results.multi_hand_landmarks
287
+ else None
288
+ )
289
+ hand2 = (
290
+ hand_results.multi_hand_landmarks[1]
291
+ if hand_results.multi_hand_landmarks
292
+ and len(hand_results.multi_hand_landmarks) > 1
293
+ else None
294
+ )
295
+ h1x, h1y = _hand21_from_landmarks(hand1)
296
+ h2x, h2y = _hand21_from_landmarks(hand2)
297
+
298
+ # Pose (mapped to 25)
299
+ pose_x, pose_y = _pose25_from_mp_pose(pose_results.pose_landmarks)
300
+
301
+ pose_points_x.append(pose_x)
302
+ pose_points_y.append(pose_y)
303
+ hand1_points_x.append(h1x)
304
+ hand1_points_y.append(h1y)
305
+ hand2_points_x.append(h2x)
306
+ hand2_points_y.append(h2y)
307
+
308
+ n_frames += 1
309
+ finally:
310
+ hands.close()
311
+ pose.close()
312
+
313
+ cap.release()
314
+
315
+ if n_frames == 0 and not write_placeholders:
316
+ warnings.warn(f"No frames decoded (n_frames=0). Skipping: {path}")
317
+ return
318
+
319
+ # If no frames decoded but placeholders requested, mimic previous behavior.
320
+ if n_frames == 0 and write_placeholders:
321
+ pose_points_x = [[np.nan] * 25]
322
+ pose_points_y = [[np.nan] * 25]
323
+ hand1_points_x = [[np.nan] * 21]
324
+ hand1_points_y = [[np.nan] * 21]
325
+ hand2_points_x = [[np.nan] * 21]
326
+ hand2_points_y = [[np.nan] * 21]
327
+ if face_mode != "none":
328
+ n_face = 468 if face_mode == "full" else len(_EYEBROW_IDXS)
329
+ face_points_x = [[np.nan] * n_face]
330
+ face_points_y = [[np.nan] * n_face]
331
+
332
+ save_data = {
333
+ "uid": uid,
334
+ "label": label,
335
+ "pose_x": pose_points_x,
336
+ "pose_y": pose_points_y,
337
+ "hand1_x": hand1_points_x,
338
+ "hand1_y": hand1_points_y,
339
+ "hand2_x": hand2_points_x,
340
+ "hand2_y": hand2_points_y,
341
+ "n_frames": n_frames,
342
+ }
343
+ if face_mode != "none":
344
+ save_data["face_x"] = face_points_x
345
+ save_data["face_y"] = face_points_y
346
+
347
+ with open(os.path.join(save_dir, f"{uid}.json"), "w") as f:
348
+ json.dump(save_data, f)
349
+
350
+ del save_data
351
+ gc.collect()
352
+
353
+
354
+ def _build_label_dir_map(include_dir: str) -> dict[str, str]:
355
+ """Maps lowercased directory name -> actual directory name."""
356
+ mapping: dict[str, str] = {}
357
+ try:
358
+ for name in os.listdir(include_dir):
359
+ full = os.path.join(include_dir, name)
360
+ if os.path.isdir(full):
361
+ mapping[name.lower()] = name
362
+ cleaned = ''.join([c for c in name if c.isalpha()]).lower()
363
+ if cleaned and cleaned not in mapping:
364
+ mapping[cleaned] = name
365
+ except FileNotFoundError:
366
+ return mapping
367
+ return mapping
368
+
369
+
370
+ def _resolve_video_path(include_dir: str, rel_path: str, label_dir_map: dict[str, str]) -> str:
371
+ """Resolves train_test_paths entries against different dataset folder layouts.
372
+
373
+ Supports:
374
+ 1) Official INCLUDE layout: <include_dir>/<Category>/<N. Label>/<file>
375
+ 2) Flat label layout: <include_dir>/<Label>/<file>
376
+ """
377
+ direct = os.path.join(include_dir, rel_path)
378
+ if os.path.isfile(direct):
379
+ return direct
380
+
381
+ # Try <include_dir>/<label>/<filename>
382
+ parts = [p for p in rel_path.split('/') if p]
383
+ filename = os.path.basename(rel_path)
384
+ label = None
385
+ if len(parts) >= 2:
386
+ label_part = parts[-2]
387
+ label = ''.join([c for c in label_part if c.isalpha()]).lower()
388
+
389
+ if label:
390
+ dir_name = label_dir_map.get(label)
391
+ if dir_name:
392
+ cand = os.path.join(include_dir, dir_name, filename)
393
+ if os.path.isfile(cand):
394
+ return cand
395
+
396
+ # Sometimes folders are title-cased (e.g., Court)
397
+ for variant in (label.capitalize(), label.title()):
398
+ dir_name = label_dir_map.get(variant.lower())
399
+ if dir_name:
400
+ cand = os.path.join(include_dir, dir_name, filename)
401
+ if os.path.isfile(cand):
402
+ return cand
403
+
404
+ return direct
405
+
406
+
407
+ def load_file(path, include_dir):
408
+ with open(path, "r") as fp:
409
+ rel_paths = [ln.strip() for ln in fp.read().splitlines() if ln.strip()]
410
+
411
+ label_dir_map = _build_label_dir_map(include_dir)
412
+ return [_resolve_video_path(include_dir, rel, label_dir_map) for rel in rel_paths]
413
+
414
+
415
+ def scan_include_dir_videos(include_dir: str) -> list[str]:
416
+ """Scans include_dir for videos in a flat label-folder layout.
417
+
418
+ Expected layout:
419
+ <include_dir>/<Label>/*.MOV (or mp4/avi/mkv)
420
+ """
421
+ exts = ("*.MOV", "*.mov", "*.MP4", "*.mp4", "*.AVI", "*.avi", "*.MKV", "*.mkv")
422
+ videos: list[str] = []
423
+ for ext in exts:
424
+ videos.extend(glob.glob(os.path.join(include_dir, "*", ext)))
425
+ # Deduplicate + stable order
426
+ return sorted(set(videos))
427
+
428
+
429
+ def split_paths(paths: list[str], seed: int = 0, train_ratio: float = 0.8, val_ratio: float = 0.1):
430
+ if not paths:
431
+ return [], [], []
432
+ rng = np.random.default_rng(seed)
433
+ idx = np.arange(len(paths))
434
+ rng.shuffle(idx)
435
+ paths = [paths[i] for i in idx]
436
+
437
+ n = len(paths)
438
+ n_train = int(train_ratio * n)
439
+ n_val = int(val_ratio * n)
440
+
441
+ train = paths[:n_train]
442
+ val = paths[n_train : n_train + n_val]
443
+ test = paths[n_train + n_val :]
444
+ return train, val, test
445
+
446
+
447
+ def load_train_test_val_paths(include_dir: str, dataset: str):
448
+ train_paths = load_file(f"train_test_paths/{dataset}_train.txt", include_dir)
449
+ val_paths = load_file(f"train_test_paths/{dataset}_val.txt", include_dir)
450
+ test_paths = load_file(f"train_test_paths/{dataset}_test.txt", include_dir)
451
+ return train_paths, val_paths, test_paths
452
+
453
+
454
+ def save_keypoints(
455
+ *,
456
+ dataset: str,
457
+ file_paths: list[str],
458
+ mode: str,
459
+ save_root: str,
460
+ n_jobs: int,
461
+ use_holistic: bool,
462
+ face_mode: str,
463
+ limit: int,
464
+ no_parallel: bool,
465
+ write_placeholders: bool,
466
+ ):
467
+ save_dir = os.path.join(save_root, f"{dataset}_{mode}_keypoints")
468
+ os.makedirs(save_dir, exist_ok=True)
469
+
470
+ if limit and limit > 0:
471
+ file_paths = file_paths[:limit]
472
+
473
+ existing = [p for p in file_paths if os.path.isfile(p)]
474
+ missing = len(file_paths) - len(existing)
475
+ if missing:
476
+ warnings.warn(f"{missing} {mode} videos missing on disk (skipping them)")
477
+
478
+ if no_parallel or n_jobs == 1:
479
+ for path in tqdm(existing, desc=f"processing {mode} videos"):
480
+ process_video(
481
+ path,
482
+ save_dir,
483
+ use_holistic=use_holistic,
484
+ face_mode=face_mode,
485
+ write_placeholders=write_placeholders,
486
+ )
487
+ return
488
+
489
+ Parallel(n_jobs=n_jobs, backend="multiprocessing")(
490
+ delayed(process_video)(
491
+ path,
492
+ save_dir,
493
+ use_holistic=use_holistic,
494
+ face_mode=face_mode,
495
+ write_placeholders=write_placeholders,
496
+ )
497
+ for path in tqdm(existing, desc=f"processing {mode} videos")
498
+ )
499
+
500
+
501
+ def preflight_videos(video_paths: list[str], n: int = 10):
502
+ """Quickly checks existence + OpenCV decode for a small sample."""
503
+ sample = video_paths[: max(0, n)]
504
+ exists = 0
505
+ opened = 0
506
+ first_read_ok = 0
507
+
508
+ for path in sample:
509
+ if not os.path.isfile(path):
510
+ continue
511
+ exists += 1
512
+ cap = _try_open_video(path)
513
+ if not cap.isOpened():
514
+ cap.release()
515
+ continue
516
+ opened += 1
517
+ ok, _frame = cap.read()
518
+ if ok:
519
+ first_read_ok += 1
520
+ cap.release()
521
+
522
+ print('Preflight results:')
523
+ print(f' sample size: {len(sample)}')
524
+ print(f' exists on disk: {exists}')
525
+ print(f' OpenCV opened: {opened}')
526
+ print(f' first frame decoded: {first_read_ok}')
527
+
528
+
529
+ if __name__ == "__main__":
530
+ parser = argparse.ArgumentParser(description="Generate keypoints from MediaPipe")
531
+ parser.add_argument(
532
+ "--include_dir",
533
+ default="",
534
+ type=str,
535
+ required=True,
536
+ help="path to the location of INCLUDE/INCLUDE50 videos",
537
+ )
538
+ parser.add_argument(
539
+ "--save_dir",
540
+ default="",
541
+ type=str,
542
+ required=True,
543
+ help="location to output json files",
544
+ )
545
+ parser.add_argument(
546
+ "--dataset", default="include", type=str, help="options: include or include50"
547
+ )
548
+ parser.add_argument(
549
+ "--video",
550
+ default=None,
551
+ type=str,
552
+ help="process a single video path (bypasses train_test_paths)",
553
+ )
554
+ parser.add_argument(
555
+ "--use_holistic",
556
+ action="store_true",
557
+ help="use MediaPipe Holistic (pose+hands+face) instead of separate pose/hands",
558
+ )
559
+ parser.add_argument(
560
+ "--face_mode",
561
+ default="none",
562
+ choices=["none", "eyebrows", "full"],
563
+ help="face landmarks to save when using holistic",
564
+ )
565
+ parser.add_argument(
566
+ "--splits",
567
+ default="all",
568
+ choices=["train", "val", "test", "all"],
569
+ help="which splits to process",
570
+ )
571
+ parser.add_argument(
572
+ "--limit",
573
+ default=0,
574
+ type=int,
575
+ help="process only the first N videos per split (useful for testing)",
576
+ )
577
+ parser.add_argument(
578
+ "--jobs",
579
+ default=multiprocessing.cpu_count(),
580
+ type=int,
581
+ help="number of parallel workers",
582
+ )
583
+ parser.add_argument(
584
+ "--no_parallel",
585
+ action="store_true",
586
+ help="disable multiprocessing (easier debugging)",
587
+ )
588
+ parser.add_argument(
589
+ "--write_placeholders",
590
+ action="store_true",
591
+ help="write placeholder NaN JSONs even when videos can't be decoded",
592
+ )
593
+
594
+ parser.add_argument(
595
+ "--preflight",
596
+ action="store_true",
597
+ help="only check that videos exist and can be decoded, then exit",
598
+ )
599
+ parser.add_argument(
600
+ "--preflight_n",
601
+ default=10,
602
+ type=int,
603
+ help="number of videos to check in preflight",
604
+ )
605
+
606
+ parser.add_argument(
607
+ "--scan",
608
+ action="store_true",
609
+ help="ignore train_test_paths and scan include_dir/<label>/* for videos, then make a random train/val/test split",
610
+ )
611
+ parser.add_argument(
612
+ "--split_seed",
613
+ default=0,
614
+ type=int,
615
+ help="seed used when --scan creates train/val/test splits",
616
+ )
617
+
618
+ args = parser.parse_args()
619
+
620
+ if args.video is not None:
621
+ single_dir = os.path.join(args.save_dir, f"{args.dataset}_single_keypoints")
622
+ os.makedirs(single_dir, exist_ok=True)
623
+ process_video(
624
+ args.video,
625
+ single_dir,
626
+ use_holistic=args.use_holistic,
627
+ face_mode=args.face_mode,
628
+ write_placeholders=args.write_placeholders,
629
+ )
630
+ print(f"Saved single-video keypoints to: {single_dir}")
631
+ raise SystemExit(0)
632
+
633
+ if args.scan:
634
+ all_paths = scan_include_dir_videos(args.include_dir)
635
+ train_paths, val_paths, test_paths = split_paths(all_paths, seed=args.split_seed)
636
+ else:
637
+ train_paths, val_paths, test_paths = load_train_test_val_paths(
638
+ args.include_dir, args.dataset
639
+ )
640
+
641
+ if args.preflight:
642
+ paths = []
643
+ if args.splits in ("train", "all"):
644
+ paths.extend(train_paths)
645
+ if args.splits in ("val", "all"):
646
+ paths.extend(val_paths)
647
+ if args.splits in ("test", "all"):
648
+ paths.extend(test_paths)
649
+ preflight_videos(paths, n=args.preflight_n)
650
+ raise SystemExit(0)
651
+
652
+
653
+ if args.splits in ("val", "all"):
654
+ save_keypoints(
655
+ dataset=args.dataset,
656
+ file_paths=val_paths,
657
+ mode="val",
658
+ save_root=args.save_dir,
659
+ n_jobs=args.jobs,
660
+ use_holistic=args.use_holistic,
661
+ face_mode=args.face_mode,
662
+ limit=args.limit,
663
+ no_parallel=args.no_parallel,
664
+ write_placeholders=args.write_placeholders,
665
+ )
666
+
667
+ if args.splits in ("test", "all"):
668
+ save_keypoints(
669
+ dataset=args.dataset,
670
+ file_paths=test_paths,
671
+ mode="test",
672
+ save_root=args.save_dir,
673
+ n_jobs=args.jobs,
674
+ use_holistic=args.use_holistic,
675
+ face_mode=args.face_mode,
676
+ limit=args.limit,
677
+ no_parallel=args.no_parallel,
678
+ write_placeholders=args.write_placeholders,
679
+ )
680
+
681
+ if args.splits in ("train", "all"):
682
+ save_keypoints(
683
+ dataset=args.dataset,
684
+ file_paths=train_paths,
685
+ mode="train",
686
+ save_root=args.save_dir,
687
+ n_jobs=args.jobs,
688
+ use_holistic=args.use_holistic,
689
+ face_mode=args.face_mode,
690
+ limit=args.limit,
691
+ no_parallel=args.no_parallel,
692
+ write_placeholders=args.write_placeholders,
693
+ )
models/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .lstm import LSTM
2
+ from .transformer import Transformer
3
+ from .xgboost import Xgboost
4
+ from .cnn import CNN
models/cnn.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+ import timm
3
+
4
+
5
+ class CNN(nn.Module):
6
+ def __init__(self, config):
7
+ super().__init__()
8
+ self.model = timm.create_model(config.model, pretrained=True, num_classes=0)
9
+
10
+ def forward(self, x):
11
+ return self.model(x).detach()
models/lstm.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import asdict
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+
8
+ class LSTM(nn.Module):
9
+ def __init__(self, config, n_classes=50):
10
+ super().__init__()
11
+ config_dict = asdict(config)
12
+ self.lstm = nn.LSTM(**config_dict)
13
+ in_features = (
14
+ config.hidden_size * 2 if config.bidirectional else config.hidden_size
15
+ )
16
+ self.l1 = nn.Linear(in_features=in_features, out_features=n_classes)
17
+
18
+ def forward(self, x):
19
+ x, (_, _) = self.lstm(x)
20
+ x = torch.max(x, dim=1).values
21
+ x = F.dropout(x, p=0.3)
22
+ x = self.l1(x)
23
+ return x
models/transformer.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import transformers
5
+
6
+
7
+ class PositionEmbedding(nn.Module):
8
+ def __init__(self, config):
9
+ super().__init__()
10
+ self.position_embeddings = nn.Embedding(
11
+ config.max_position_embeddings, config.hidden_size
12
+ )
13
+ self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
14
+ self.dropout = nn.Dropout(config.hidden_dropout_prob)
15
+ self.register_buffer(
16
+ "position_ids", torch.arange(config.max_position_embeddings).expand((1, -1))
17
+ )
18
+ self.position_embedding_type = getattr(
19
+ config, "position_embedding_type", "absolute"
20
+ )
21
+
22
+ def forward(self, x):
23
+ input_shape = x.size()
24
+ seq_length = input_shape[1]
25
+ position_ids = self.position_ids[:, :seq_length]
26
+
27
+ position_embeddings = self.position_embeddings(position_ids)
28
+ embeddings = x + position_embeddings
29
+ embeddings = self.LayerNorm(embeddings)
30
+ embeddings = self.dropout(embeddings)
31
+ return embeddings
32
+
33
+
34
+ class Transformer(nn.Module):
35
+ def __init__(self, config, n_classes=50):
36
+ super().__init__()
37
+ self.l1 = nn.Linear(
38
+ in_features=config.input_size, out_features=config.hidden_size
39
+ )
40
+ self.embedding = PositionEmbedding(config)
41
+
42
+ setattr(config.model_config, "_attn_implementation", "eager")
43
+
44
+ self.layers = nn.ModuleList(
45
+ [
46
+ transformers.BertLayer(config.model_config)
47
+ for _ in range(config.num_hidden_layers)
48
+ ]
49
+ )
50
+ self.l2 = nn.Linear(in_features=config.hidden_size, out_features=n_classes)
51
+
52
+ def forward(self, x):
53
+ x = self.l1(x)
54
+ x = self.embedding(x)
55
+ for layer in self.layers:
56
+ x = layer(x)[0]
57
+
58
+ x = torch.max(x, dim=1).values
59
+ x = F.dropout(x, p=0.2)
60
+ x = self.l2(x)
61
+ return x
models/xgboost.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import asdict
2
+ import pickle
3
+
4
+ import xgboost
5
+
6
+
7
+ class Xgboost:
8
+ def __init__(self, config):
9
+ config = asdict(config)
10
+ self.model = xgboost.XGBClassifier(use_label_encoder=False, **config)
11
+
12
+ def __call__(self, x):
13
+ return self.model.predict(x)
14
+
15
+ def fit(self, x_train, y_train, x_val, y_val):
16
+ self.model.fit(
17
+ x_train,
18
+ y_train,
19
+ eval_set=[(x_train, y_train), (x_val, y_val)],
20
+ verbose=True,
21
+ )
22
+
23
+ def save(self, save_path):
24
+ pickle.dump(self.model, open(save_path, "wb"))
25
+
26
+ def load(self, load_path):
27
+ self.model = pickle.load(open(load_path, "rb"))
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers
3
+ timm
4
+ mediapipe
5
+ tqdm
6
+ scikit-learn
7
+ xgboost ## installs CPU version, check docs for GPU compatible version
runner.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ import argparse
3
+
4
+ import train_nn
5
+ import train_xgb
6
+ from cnn_runner import save_cnn_features
7
+
8
+ parser = argparse.ArgumentParser(
9
+ description="INCLUDE trainer for xgboost, lstm and transformer"
10
+ )
11
+ parser.add_argument("--seed", default=0, type=int, help="seed value")
12
+ parser.add_argument(
13
+ "--dataset", default="include", type=str, help="options: include or include50"
14
+ )
15
+ parser.add_argument(
16
+ "--use_augs",
17
+ action="store_true",
18
+ help="use augmented data",
19
+ )
20
+ parser.add_argument(
21
+ "--use_cnn",
22
+ action="store_true",
23
+ help="use mobilenet to convert keypoints to videos and generate embeddings from CNN",
24
+ )
25
+ parser.add_argument(
26
+ "--model",
27
+ default="lstm",
28
+ type=str,
29
+ help="options: lstm, transformer, xgboost",
30
+ )
31
+ parser.add_argument(
32
+ "--data_dir",
33
+ default="",
34
+ type=str,
35
+ required=True,
36
+ help="location to train, val and test json files",
37
+ )
38
+ parser.add_argument(
39
+ "--save_path",
40
+ default="./",
41
+ type=str,
42
+ help="location to save trained model",
43
+ )
44
+ parser.add_argument(
45
+ "--epochs", default=50, type=int, help="number of epochs to train the model"
46
+ )
47
+ parser.add_argument("--batch_size", default=128, type=int, help="batch size of data")
48
+ parser.add_argument(
49
+ "--learning_rate",
50
+ default=1e-4,
51
+ type=float,
52
+ help="learning rate for training neural net",
53
+ )
54
+ parser.add_argument(
55
+ "--transformer_size", default="small", type=str, help="options: small, large"
56
+ )
57
+ parser.add_argument(
58
+ "--use_pretrained",
59
+ default=None,
60
+ help="use pretrained model. options: evaluate, resume_training",
61
+ )
62
+ args = parser.parse_args()
63
+
64
+
65
+ if __name__ == "__main__":
66
+
67
+ if args.model == "xgboost":
68
+ if args.use_pretrained:
69
+ raise Exception("Pre-trained models are not available for XGBoost")
70
+ if args.use_cnn:
71
+ warnings.warn(
72
+ "use_cnn flag set to true for xgboost model. xgboost will not use cnn features"
73
+ )
74
+ train_xgb.fit(args)
75
+ train_xgb.evaluate(args)
76
+
77
+ else:
78
+ if args.use_cnn:
79
+ save_cnn_features(args)
80
+ if args.use_augs:
81
+ warnings.warn("cannot perform augmentation on cnn features")
82
+ if args.use_pretrained == "evaluate":
83
+ train_nn.evaluate(args)
84
+ print("### Evaluated from pretrained model ###")
85
+ else:
86
+ print("### Starting to train. ###")
87
+ train_nn.fit(args)
88
+ train_nn.evaluate(args)
train_nn.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import json
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch.utils import data
8
+ from sklearn.metrics import accuracy_score
9
+ from tqdm import tqdm
10
+
11
+ from models import CNN, LSTM, Transformer
12
+ from configs import CnnConfig, LstmConfig, TransformerConfig
13
+ from utils import (
14
+ seed_everything,
15
+ AverageMeter,
16
+ EarlyStopping,
17
+ load_label_map,
18
+ get_experiment_name,
19
+ load_json,
20
+ )
21
+ from dataset import KeypointsDataset, FeaturesDatset
22
+
23
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
+
25
+
26
+ def train(dataloader, model, optimizer, device):
27
+ model.train()
28
+
29
+ losses = AverageMeter()
30
+ accuracy = AverageMeter()
31
+
32
+ pbar = tqdm(dataloader, desc="Training")
33
+ for batch in pbar:
34
+ input_data = batch["data"].to(device)
35
+ label = batch["label"].to(device)
36
+
37
+ optimizer.zero_grad()
38
+ preds = model(input_data)
39
+ loss = F.cross_entropy(preds, label)
40
+ loss.backward()
41
+ optimizer.step()
42
+
43
+ losses.update(loss.item())
44
+ preds = preds.detach().cpu()
45
+ accuracy.update(
46
+ accuracy_score(
47
+ label.cpu().numpy(),
48
+ torch.argmax(torch.softmax(preds, dim=-1), dim=-1).numpy(),
49
+ )
50
+ )
51
+ pbar.set_postfix(loss=losses.avg, accuracy=accuracy.avg)
52
+
53
+ torch.cuda.empty_cache()
54
+ loss_avg = losses.avg
55
+ accuracy_avg = accuracy.avg
56
+ return loss_avg, accuracy_avg
57
+
58
+
59
+ @torch.no_grad()
60
+ def validate(dataloader, model, device):
61
+ model.eval()
62
+
63
+ losses = AverageMeter()
64
+ accuracy = AverageMeter()
65
+
66
+ pbar = tqdm(dataloader, desc="Eval")
67
+ for batch in pbar:
68
+ input_data = batch["data"].to(device)
69
+ label = batch["label"].to(device)
70
+
71
+ preds = model(input_data)
72
+ loss = F.cross_entropy(preds, label)
73
+
74
+ losses.update(loss.item())
75
+ preds = preds.detach().cpu()
76
+ accuracy.update(
77
+ accuracy_score(
78
+ label.cpu().numpy(),
79
+ torch.argmax(torch.softmax(preds, dim=-1), dim=-1).numpy(),
80
+ )
81
+ )
82
+ pbar.set_postfix(loss=losses.avg, accuracy=accuracy.avg)
83
+
84
+ torch.cuda.empty_cache()
85
+ loss_avg = losses.avg
86
+ accuracy_avg = accuracy.avg
87
+ return loss_avg, accuracy_avg
88
+
89
+
90
+ def change_max_pos_embd(args, new_mpe_size, n_classes):
91
+ config = TransformerConfig(
92
+ size=args.transformer_size, max_position_embeddings=new_mpe_size
93
+ )
94
+ if args.use_cnn:
95
+ config.input_size = CnnConfig.output_dim
96
+ model = Transformer(config=config, n_classes=n_classes)
97
+ model = model.to(device)
98
+ return model
99
+
100
+
101
+ def pretrained_name(args):
102
+ load_modelName = args.dataset
103
+ if args.use_cnn:
104
+ load_modelName += "_use_cnn"
105
+ else:
106
+ load_modelName += "_no_cnn"
107
+ if args.model == "lstm":
108
+ load_modelName += "_lstm.pth"
109
+ elif args.model == "transformer":
110
+ load_modelName += "_transformer"
111
+ if args.transformer_size == "large":
112
+ load_modelName += "_large.pth"
113
+ elif args.transformer_size == "small":
114
+ load_modelName += "_small.pth"
115
+ return load_modelName
116
+
117
+
118
+ def load_pretrained(args, n_classes, model, optimizer=None, scheduler=None):
119
+ load_modelName = pretrained_name(args)
120
+ pretrained_model_links = load_json("pretrained_links.json")
121
+
122
+ if not os.path.isfile(load_modelName):
123
+ link = pretrained_model_links[load_modelName]
124
+ torch.hub.download_url_to_file(link, load_modelName, progress=True)
125
+
126
+ if args.model == "transformer":
127
+ model = change_max_pos_embd(args, new_mpe_size=256, n_classes=n_classes)
128
+
129
+ ckpt = torch.load(load_modelName)
130
+ model.load_state_dict(ckpt["model"])
131
+ if args.use_pretrained == "resume_training":
132
+ optimizer.load_state_dict(ckpt["optimizer"])
133
+ scheduler.load_state_dict(ckpt["scheduler"])
134
+
135
+ return model, optimizer, scheduler
136
+
137
+
138
+ def fit(args):
139
+ exp_name = get_experiment_name(args)
140
+ logging_path = os.path.join(args.save_path, exp_name) + ".log"
141
+ logging.basicConfig(filename=logging_path, level=logging.INFO, format="%(message)s")
142
+ seed_everything(args.seed)
143
+ label_map = load_label_map(args.dataset)
144
+
145
+ if args.use_cnn:
146
+ train_dataset = FeaturesDatset(
147
+ features_dir=os.path.join(args.data_dir, f"{args.dataset}_train_features"),
148
+ label_map=label_map,
149
+ mode="train",
150
+ )
151
+ val_dataset = FeaturesDatset(
152
+ features_dir=os.path.join(args.data_dir, f"{args.dataset}_val_features"),
153
+ label_map=label_map,
154
+ mode="val",
155
+ )
156
+
157
+ else:
158
+ train_dataset = KeypointsDataset(
159
+ keypoints_dir=os.path.join(
160
+ args.data_dir, f"{args.dataset}_train_keypoints"
161
+ ),
162
+ use_augs=args.use_augs,
163
+ label_map=label_map,
164
+ mode="train",
165
+ max_frame_len=169,
166
+ )
167
+ val_dataset = KeypointsDataset(
168
+ keypoints_dir=os.path.join(args.data_dir, f"{args.dataset}_val_keypoints"),
169
+ use_augs=False,
170
+ label_map=label_map,
171
+ mode="val",
172
+ max_frame_len=169,
173
+ )
174
+
175
+ train_dataloader = data.DataLoader(
176
+ train_dataset,
177
+ batch_size=args.batch_size,
178
+ shuffle=True,
179
+ num_workers=4,
180
+ pin_memory=True,
181
+ )
182
+ val_dataloader = data.DataLoader(
183
+ val_dataset,
184
+ batch_size=args.batch_size,
185
+ shuffle=False,
186
+ num_workers=4,
187
+ pin_memory=True,
188
+ )
189
+
190
+ n_classes = 50
191
+ if args.dataset == "include":
192
+ n_classes = 263
193
+
194
+ if args.model == "lstm":
195
+ config = LstmConfig()
196
+ if args.use_cnn:
197
+ config.input_size = CnnConfig.output_dim
198
+ model = LSTM(config=config, n_classes=n_classes)
199
+ else:
200
+ config = TransformerConfig(size=args.transformer_size)
201
+ if args.use_cnn:
202
+ config.input_size = CnnConfig.output_dim
203
+ model = Transformer(config=config, n_classes=n_classes)
204
+
205
+ model = model.to(device)
206
+ optimizer = torch.optim.AdamW(
207
+ model.parameters(), lr=args.learning_rate, weight_decay=0.01
208
+ )
209
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
210
+ optimizer, mode="max", factor=0.2
211
+ )
212
+
213
+ if args.use_pretrained == "resume_training":
214
+ model, optimizer, scheduler = load_pretrained(
215
+ args, n_classes, model, optimizer, scheduler
216
+ )
217
+
218
+ model_path = os.path.join(args.save_path, exp_name) + ".pth"
219
+ es = EarlyStopping(patience=15, mode="max")
220
+ for epoch in range(args.epochs):
221
+ print(f"Epoch: {epoch+1}/{args.epochs}")
222
+ train_loss, train_acc = train(train_dataloader, model, optimizer, device)
223
+ val_loss, val_acc = validate(val_dataloader, model, device)
224
+ logging.info(
225
+ "Epoch: {}, train loss: {}, train acc: {}, val loss: {}, val acc: {}".format(
226
+ epoch + 1, train_loss, train_acc, val_loss, val_acc
227
+ )
228
+ )
229
+ scheduler.step(val_acc)
230
+ es(
231
+ model_path=model_path,
232
+ epoch_score=val_acc,
233
+ model=model,
234
+ optimizer=optimizer,
235
+ scheduler=scheduler,
236
+ )
237
+ if es.early_stop:
238
+ print("Early stopping")
239
+ break
240
+
241
+ print("### Training Complete ###")
242
+
243
+
244
+ def evaluate(args):
245
+ label_map = load_label_map(args.dataset)
246
+ n_classes = 50
247
+ if args.dataset == "include":
248
+ n_classes = 263
249
+
250
+ if args.use_cnn:
251
+ dataset = FeaturesDatset(
252
+ features_dir=os.path.join(args.data_dir, f"{args.dataset}_test_features"),
253
+ label_map=label_map,
254
+ mode="test",
255
+ )
256
+
257
+ else:
258
+ dataset = KeypointsDataset(
259
+ keypoints_dir=os.path.join(args.data_dir, f"{args.dataset}_test_keypoints"),
260
+ use_augs=False,
261
+ label_map=label_map,
262
+ mode="test",
263
+ max_frame_len=169,
264
+ )
265
+
266
+ dataloader = data.DataLoader(
267
+ dataset,
268
+ batch_size=args.batch_size,
269
+ shuffle=False,
270
+ num_workers=4,
271
+ pin_memory=True,
272
+ )
273
+
274
+ if args.model == "lstm":
275
+ config = LstmConfig()
276
+ if args.use_cnn:
277
+ config.input_size = CnnConfig.output_dim
278
+ model = LSTM(config=config, n_classes=n_classes)
279
+ else:
280
+ config = TransformerConfig(size=args.transformer_size)
281
+ if args.use_cnn:
282
+ config.input_size = CnnConfig.output_dim
283
+ model = Transformer(config=config, n_classes=n_classes)
284
+
285
+ model = model.to(device)
286
+
287
+ if args.use_pretrained == "evaluate":
288
+ model, _, _ = load_pretrained(args, n_classes, model)
289
+ print("### Model loaded ###")
290
+
291
+ else:
292
+ exp_name = get_experiment_name(args)
293
+ model_path = os.path.join(args.save_path, exp_name) + ".pth"
294
+ ckpt = torch.load(model_path)
295
+ model.load_state_dict(ckpt["model"])
296
+ print("### Model loaded ###")
297
+
298
+ test_loss, test_acc = validate(dataloader, model, device)
299
+ print("Evaluation Results:")
300
+ print(f"Loss: {test_loss}, Accuracy: {test_acc}")
train_xgb.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from sklearn.metrics import accuracy_score
4
+ import pandas as pd
5
+ import numpy as np
6
+
7
+ from models import Xgboost
8
+ from configs import XgbConfig
9
+ from utils import get_experiment_name, load_label_map
10
+ from augment import (
11
+ plus7rotation,
12
+ minus7rotation,
13
+ gaussSample,
14
+ cutout,
15
+ upsample,
16
+ downsample,
17
+ )
18
+ from tqdm.auto import tqdm
19
+
20
+
21
+ def flatten(arr, max_seq_len=200):
22
+ arr = np.array(arr)
23
+ arr = np.pad(arr, ((0, max_seq_len - arr.shape[0]), (0, 0)), "constant")
24
+ arr = arr.flatten()
25
+ return arr
26
+
27
+
28
+ def combine_xy(x, y):
29
+ x, y = np.array(x), np.array(y)
30
+ _, length = x.shape
31
+ x = x.reshape((-1, length, 1))
32
+ y = y.reshape((-1, length, 1))
33
+ return np.concatenate((x, y), -1).astype(np.float32)
34
+
35
+
36
+ def split_xy(data):
37
+ value_x, value_y = [], []
38
+ for row in data:
39
+ row = np.asarray(row)
40
+ if row.shape == ():
41
+ continue
42
+ value_x.append(row[:, 0])
43
+ value_y.append(row[:, 1])
44
+ value_x, value_y = np.asarray(value_x), np.asarray(value_y)
45
+ return value_x, value_y
46
+
47
+
48
+ def augment_sample(df, augs):
49
+ df = df.copy()
50
+ pose = combine_xy(df.pose_x, df.pose_y)
51
+ h1 = combine_xy(df.hand1_x, df.hand1_y)
52
+ h2 = combine_xy(df.hand2_x, df.hand2_y)
53
+ input_df = pd.DataFrame.from_dict(
54
+ {
55
+ "uid": df.uid,
56
+ "pose": pose.tolist(),
57
+ "hand1": h1.tolist(),
58
+ "hand2": h2.tolist(),
59
+ "label": df.label,
60
+ }
61
+ )
62
+ augmented_samples = []
63
+ for augmentation in augs:
64
+ df_augmented = augmentation(input_df)
65
+ pose_x, pose_y = split_xy(df_augmented.pose)
66
+ hand1_x, hand1_y = split_xy(df_augmented.hand1)
67
+ hand2_x, hand2_y = split_xy(df_augmented.hand2)
68
+ save_df = pd.Series(
69
+ {
70
+ "uid": df.uid + "_" + augmentation.__name__,
71
+ "label": df.label,
72
+ "pose_x": pose_x.tolist(),
73
+ "pose_y": pose_y.tolist(),
74
+ "hand1_x": hand1_x.tolist(),
75
+ "hand1_y": hand1_y.tolist(),
76
+ "hand2_x": hand2_x.tolist(),
77
+ "hand2_y": hand2_y.tolist(),
78
+ "n_frames": df.n_frames,
79
+ }
80
+ )
81
+ augmented_samples.append(save_df)
82
+
83
+ return pd.concat(augmented_samples, axis=0)
84
+
85
+
86
+ def preprocess(df, use_augs, label_map, mode):
87
+ feature_cols = ["pose_x", "pose_y", "hand1_x", "hand1_y", "hand2_x", "hand2_y"]
88
+ x, y = [], []
89
+ i = 0
90
+ no_of_videos = df.shape[0]
91
+ pbar = tqdm(total=no_of_videos, desc=f"Processing {mode} file....")
92
+ while i < no_of_videos:
93
+ if use_augs and mode == "train":
94
+ augs = [
95
+ plus7rotation,
96
+ minus7rotation,
97
+ gaussSample,
98
+ cutout,
99
+ upsample,
100
+ downsample,
101
+ ]
102
+ augmented_rows = augment_sample(df.iloc[i], augs)
103
+ df = pd.concat([df, augmented_rows], axis=0)
104
+ row = df.loc[i, feature_cols]
105
+ flatten_features = np.hstack(list(map(flatten, row.values)))
106
+ x.append(flatten_features)
107
+ y.append(label_map[df.loc[i, "label"]])
108
+ i += 1
109
+ pbar.update(1)
110
+ x = np.stack(x)
111
+ y = np.array(y)
112
+ return x, y
113
+
114
+
115
+ def load_dataframe(files):
116
+ series = []
117
+ for file_path in files:
118
+ series.append(pd.read_json(file_path, typ="series"))
119
+ return pd.concat(series, axis=0)
120
+
121
+
122
+ def fit(args):
123
+ train_files = sorted(
124
+ glob.glob(
125
+ os.path.join(args.data_dir, f"{args.dataset}_train_keypoints", "*.json")
126
+ )
127
+ )
128
+ val_files = sorted(
129
+ glob.glob(
130
+ os.path.join(args.data_dir, f"{args.dataset}_val_keypoints", "*.json")
131
+ )
132
+ )
133
+
134
+ train_df = load_dataframe(train_files)
135
+ val_df = load_dataframe(val_files)
136
+
137
+ label_map = load_label_map(args.dataset)
138
+ x_train, y_train = preprocess(train_df, args.use_augs, label_map, "train")
139
+ x_val, y_val = preprocess(val_df, args.use_augs, label_map, "val")
140
+
141
+ config = XgbConfig()
142
+ model = Xgboost(config=config)
143
+ model.fit(x_train, y_train, x_val, y_val)
144
+
145
+ exp_name = get_experiment_name(args)
146
+ save_path = os.path.join(args.save_dir, exp_name, ".pickle.dat")
147
+ model.save(save_path)
148
+
149
+
150
+ def evaluate(args):
151
+ test_files = sorted(
152
+ glob.glob(
153
+ os.path.join(args.data_dir, f"{args.dataset}_test_keypoints", "*.json")
154
+ )
155
+ )
156
+
157
+ test_df = load_dataframe(test_files)
158
+
159
+ label_map = load_label_map(args.dataset)
160
+ x_test, y_test = preprocess(test_df, args.use_augs, label_map, "test")
161
+
162
+ exp_name = get_experiment_name(args)
163
+ config = XgbConfig()
164
+ model = Xgboost(config=config)
165
+ load_path = os.path.join(args.save_dir, exp_name, ".pickle.dat")
166
+ model.load(load_path)
167
+ print("### Model loaded ###")
168
+
169
+ test_preds = model(x_test)
170
+ print("Test accuracy:", accuracy_score(y_test, test_preds))
utils.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import random
3
+ import os
4
+ import numpy as np
5
+ import json
6
+
7
+
8
+ def seed_everything(seed):
9
+ random.seed(seed)
10
+ torch.manual_seed(seed)
11
+ torch.cuda.manual_seed_all(seed)
12
+ torch.backends.cudnn.deterministic = True
13
+ np.random.seed(seed)
14
+ os.environ["PYTHONHASHSEED"] = str(seed)
15
+
16
+
17
+ def load_json(path):
18
+ with open(path, "r") as f:
19
+ json_file = json.load(f)
20
+ return json_file
21
+
22
+
23
+ def load_label_map(dataset):
24
+ file_path = f"label_maps/label_map_{dataset}.json"
25
+ return load_json(file_path)
26
+
27
+
28
+ def get_experiment_name(args):
29
+ exp_name = ""
30
+ if args.use_cnn:
31
+ exp_name += "cnn_"
32
+ if args.use_augs:
33
+ exp_name += "augs_"
34
+ exp_name += args.model
35
+ return exp_name
36
+
37
+
38
+ class AverageMeter:
39
+ def __init__(self):
40
+ self.reset()
41
+
42
+ def reset(self):
43
+ self.val = 0
44
+ self.avg = 0
45
+ self.sum = 0
46
+ self.count = 0
47
+
48
+ def update(self, val, n=1):
49
+ self.val = val
50
+ self.sum += val * n
51
+ self.count += n
52
+ self.avg = self.sum / self.count
53
+
54
+
55
+ class EarlyStopping:
56
+ def __init__(self, patience=5, mode="min", delta=0.0):
57
+ self.patience = patience
58
+ self.counter = 0
59
+ self.mode = mode
60
+ self.best_score = None
61
+ self.early_stop = False
62
+ self.delta = delta
63
+ if self.mode == "min":
64
+ self.val_score = np.inf
65
+ else:
66
+ self.val_score = -np.inf
67
+
68
+ def __call__(self, model_path, epoch_score, model, optimizer, scheduler=None):
69
+
70
+ if self.mode == "min":
71
+ score = -1.0 * epoch_score
72
+ else:
73
+ score = np.copy(epoch_score)
74
+
75
+ if self.best_score is None:
76
+ self.best_score = score
77
+ self.save_checkpoint(epoch_score, model, optimizer, scheduler, model_path)
78
+ elif score <= self.best_score + self.delta:
79
+ self.counter += 1
80
+ if self.counter >= self.patience:
81
+ self.early_stop = True
82
+ else:
83
+ self.best_score = score
84
+ self.save_checkpoint(epoch_score, model, optimizer, scheduler, model_path)
85
+ self.counter = 0
86
+
87
+ def save_checkpoint(self, epoch_score, model, optimizer, scheduler, model_path):
88
+ if epoch_score not in [-np.inf, np.inf, -np.nan, np.nan]:
89
+ print(
90
+ "Validation score improved ({} --> {}). Saving model!".format(
91
+ self.val_score, epoch_score
92
+ )
93
+ )
94
+ torch.save(
95
+ {
96
+ "model": model.state_dict(),
97
+ "optimizer": optimizer.state_dict(),
98
+ "scheduler": scheduler.state_dict() if scheduler else scheduler,
99
+ "score": epoch_score,
100
+ },
101
+ model_path,
102
+ )
103
+ self.val_score = epoch_score