Commit ·
bc45d7d
0
Parent(s):
Initial commit
Browse files- .gitignore +20 -0
- LICENSE +21 -0
- README.md +53 -0
- augment.py +227 -0
- augs_transformer.log +67 -0
- cnn_runner.py +222 -0
- commands.txt +52 -0
- configs.py +63 -0
- dataset.py +171 -0
- evaluate.py +169 -0
- generate_keypoints.py +693 -0
- models/__init__.py +4 -0
- models/cnn.py +11 -0
- models/lstm.py +23 -0
- models/transformer.py +61 -0
- models/xgboost.py +27 -0
- requirements.txt +7 -0
- runner.py +88 -0
- train_nn.py +300 -0
- train_xgb.py +170 -0
- 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
|