Spaces:
Sleeping
Sleeping
Upload 35 files
Browse files- PoseClassification/__pycache__/bootstrap.cpython-312.pyc +0 -0
- PoseClassification/__pycache__/pose_classifier.cpython-312.pyc +0 -0
- PoseClassification/__pycache__/pose_embedding.cpython-312.pyc +0 -0
- PoseClassification/__pycache__/utils.cpython-312.pyc +0 -0
- PoseClassification/__pycache__/visualize.cpython-312.pyc +0 -0
- PoseClassification/bootstrap.py +242 -0
- PoseClassification/pose_classifier.py +208 -0
- PoseClassification/pose_embedding.py +237 -0
- PoseClassification/pose_embedding_2.py +246 -0
- PoseClassification/utils.py +129 -0
- PoseClassification/visualize.py +139 -0
- README.md +191 -14
- README.md.old +146 -0
- app.py +95 -0
- classify_video.py +231 -0
- hello.py +6 -0
- interface_pages/__init__.py +0 -0
- interface_pages/__pycache__/__init__.cpython-312.pyc +0 -0
- interface_pages/__pycache__/about_page.cpython-312.pyc +0 -0
- interface_pages/__pycache__/home_page.cpython-312.pyc +0 -0
- interface_pages/__pycache__/yoga_position_from_stream.cpython-312.pyc +0 -0
- interface_pages/__pycache__/yoga_position_from_video.cpython-312.pyc +0 -0
- interface_pages/about_page.py +11 -0
- interface_pages/home_page.py +11 -0
- interface_pages/yoga_position_from_stream.py +31 -0
- interface_pages/yoga_position_from_video.py +17 -0
- pushups_counter.py +162 -0
- pyproject.toml +24 -0
- requirements.txt +16 -0
- src/image.png +0 -0
- src/logo_impredalam.jpg +0 -0
- static/styles.css +24 -0
- uv.lock +0 -0
- yoga_position.py +515 -0
- yoga_position_gradio.py +248 -0
PoseClassification/__pycache__/bootstrap.cpython-312.pyc
ADDED
Binary file (12.7 kB). View file
|
|
PoseClassification/__pycache__/pose_classifier.cpython-312.pyc
ADDED
Binary file (8.15 kB). View file
|
|
PoseClassification/__pycache__/pose_embedding.cpython-312.pyc
ADDED
Binary file (9.31 kB). View file
|
|
PoseClassification/__pycache__/utils.cpython-312.pyc
ADDED
Binary file (4.93 kB). View file
|
|
PoseClassification/__pycache__/visualize.cpython-312.pyc
ADDED
Binary file (6.14 kB). View file
|
|
PoseClassification/bootstrap.py
ADDED
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
from matplotlib import pyplot as plt
|
3 |
+
import numpy as np
|
4 |
+
import os, csv
|
5 |
+
from PIL import Image, ImageDraw
|
6 |
+
import sys
|
7 |
+
import tqdm
|
8 |
+
|
9 |
+
from mediapipe.python.solutions import drawing_utils as mp_drawing
|
10 |
+
from mediapipe.python.solutions import pose as mp_pose
|
11 |
+
|
12 |
+
from PoseClassification.utils import show_image
|
13 |
+
|
14 |
+
class BootstrapHelper(object):
|
15 |
+
"""Helps to bootstrap images and filter pose samples for classification."""
|
16 |
+
|
17 |
+
def __init__(self, images_in_folder, images_out_folder, csvs_out_folder):
|
18 |
+
self._images_in_folder = images_in_folder
|
19 |
+
self._images_out_folder = images_out_folder
|
20 |
+
self._csvs_out_folder = csvs_out_folder
|
21 |
+
|
22 |
+
# Get list of pose classes and print image statistics.
|
23 |
+
self._pose_class_names = sorted(
|
24 |
+
[n for n in os.listdir(self._images_in_folder) if not n.startswith(".")]
|
25 |
+
)
|
26 |
+
|
27 |
+
def bootstrap(self, per_pose_class_limit=None):
|
28 |
+
"""Bootstraps images in a given folder.
|
29 |
+
|
30 |
+
Required image in folder (same use for image out folder):
|
31 |
+
pushups_up/
|
32 |
+
image_001.jpg
|
33 |
+
image_002.jpg
|
34 |
+
...
|
35 |
+
pushups_down/
|
36 |
+
image_001.jpg
|
37 |
+
image_002.jpg
|
38 |
+
...
|
39 |
+
...
|
40 |
+
|
41 |
+
Produced CSVs out folder:
|
42 |
+
pushups_up.csv
|
43 |
+
pushups_down.csv
|
44 |
+
|
45 |
+
Produced CSV structure with pose 3D landmarks:
|
46 |
+
sample_00001,x1,y1,z1,x2,y2,z2,....
|
47 |
+
sample_00002,x1,y1,z1,x2,y2,z2,....
|
48 |
+
"""
|
49 |
+
# Create output folder for CVSs.
|
50 |
+
if not os.path.exists(self._csvs_out_folder):
|
51 |
+
os.makedirs(self._csvs_out_folder)
|
52 |
+
|
53 |
+
for pose_class_name in self._pose_class_names:
|
54 |
+
print("Bootstrapping ", pose_class_name, file=sys.stderr)
|
55 |
+
|
56 |
+
# Paths for the pose class.
|
57 |
+
images_in_folder = os.path.join(self._images_in_folder, pose_class_name)
|
58 |
+
images_out_folder = os.path.join(self._images_out_folder, pose_class_name)
|
59 |
+
csv_out_path = os.path.join(self._csvs_out_folder, pose_class_name + ".csv")
|
60 |
+
if not os.path.exists(images_out_folder):
|
61 |
+
os.makedirs(images_out_folder)
|
62 |
+
|
63 |
+
with open(csv_out_path, "w") as csv_out_file:
|
64 |
+
csv_out_writer = csv.writer(
|
65 |
+
csv_out_file, delimiter=",", quoting=csv.QUOTE_MINIMAL
|
66 |
+
)
|
67 |
+
# Get list of images.
|
68 |
+
image_names = sorted(
|
69 |
+
[n for n in os.listdir(images_in_folder) if not n.startswith(".")]
|
70 |
+
)
|
71 |
+
if per_pose_class_limit is not None:
|
72 |
+
image_names = image_names[:per_pose_class_limit]
|
73 |
+
|
74 |
+
# Bootstrap every image.
|
75 |
+
for image_name in tqdm.tqdm(image_names):
|
76 |
+
# Load image.
|
77 |
+
input_frame = cv2.imread(os.path.join(images_in_folder, image_name))
|
78 |
+
input_frame = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
79 |
+
|
80 |
+
# Initialize fresh pose tracker and run it.
|
81 |
+
# with mp_pose.Pose(upper_body_only=False) as pose_tracker:
|
82 |
+
with mp_pose.Pose() as pose_tracker:
|
83 |
+
result = pose_tracker.process(image=input_frame)
|
84 |
+
pose_landmarks = result.pose_landmarks
|
85 |
+
|
86 |
+
# Save image with pose prediction (if pose was detected).
|
87 |
+
output_frame = input_frame.copy()
|
88 |
+
if pose_landmarks is not None:
|
89 |
+
mp_drawing.draw_landmarks(
|
90 |
+
image=output_frame,
|
91 |
+
landmark_list=pose_landmarks,
|
92 |
+
connections=mp_pose.POSE_CONNECTIONS,
|
93 |
+
)
|
94 |
+
output_frame = cv2.cvtColor(output_frame, cv2.COLOR_RGB2BGR)
|
95 |
+
cv2.imwrite(
|
96 |
+
os.path.join(images_out_folder, image_name), output_frame
|
97 |
+
)
|
98 |
+
|
99 |
+
# Save landmarks if pose was detected.
|
100 |
+
if pose_landmarks is not None:
|
101 |
+
# Get landmarks.
|
102 |
+
frame_height, frame_width = (
|
103 |
+
output_frame.shape[0],
|
104 |
+
output_frame.shape[1],
|
105 |
+
)
|
106 |
+
pose_landmarks = np.array(
|
107 |
+
[
|
108 |
+
[
|
109 |
+
lmk.x * frame_width,
|
110 |
+
lmk.y * frame_height,
|
111 |
+
lmk.z * frame_width,
|
112 |
+
]
|
113 |
+
for lmk in pose_landmarks.landmark
|
114 |
+
],
|
115 |
+
dtype=np.float32,
|
116 |
+
)
|
117 |
+
assert pose_landmarks.shape == (
|
118 |
+
33,
|
119 |
+
3,
|
120 |
+
), "Unexpected landmarks shape: {}".format(pose_landmarks.shape)
|
121 |
+
csv_out_writer.writerow(
|
122 |
+
[image_name] + pose_landmarks.flatten().astype(str).tolist()
|
123 |
+
)
|
124 |
+
|
125 |
+
# Draw XZ projection and concatenate with the image.
|
126 |
+
projection_xz = self._draw_xz_projection(
|
127 |
+
output_frame=output_frame, pose_landmarks=pose_landmarks
|
128 |
+
)
|
129 |
+
output_frame = np.concatenate((output_frame, projection_xz), axis=1)
|
130 |
+
|
131 |
+
def _draw_xz_projection(self, output_frame, pose_landmarks, r=0.5, color="red"):
|
132 |
+
frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
133 |
+
img = Image.new("RGB", (frame_width, frame_height), color="white")
|
134 |
+
|
135 |
+
if pose_landmarks is None:
|
136 |
+
return np.asarray(img)
|
137 |
+
|
138 |
+
# Scale radius according to the image width.
|
139 |
+
r *= frame_width * 0.01
|
140 |
+
|
141 |
+
draw = ImageDraw.Draw(img)
|
142 |
+
for idx_1, idx_2 in mp_pose.POSE_CONNECTIONS:
|
143 |
+
# Flip Z and move hips center to the center of the image.
|
144 |
+
x1, y1, z1 = pose_landmarks[idx_1] * [1, 1, -1] + [0, 0, frame_height * 0.5]
|
145 |
+
x2, y2, z2 = pose_landmarks[idx_2] * [1, 1, -1] + [0, 0, frame_height * 0.5]
|
146 |
+
|
147 |
+
draw.ellipse([x1 - r, z1 - r, x1 + r, z1 + r], fill=color)
|
148 |
+
draw.ellipse([x2 - r, z2 - r, x2 + r, z2 + r], fill=color)
|
149 |
+
draw.line([x1, z1, x2, z2], width=int(r), fill=color)
|
150 |
+
|
151 |
+
return np.asarray(img)
|
152 |
+
|
153 |
+
def align_images_and_csvs(self, print_removed_items=False):
|
154 |
+
"""Makes sure that image folders and CSVs have the same sample.
|
155 |
+
|
156 |
+
Leaves only intersetion of samples in both image folders and CSVs.
|
157 |
+
"""
|
158 |
+
for pose_class_name in self._pose_class_names:
|
159 |
+
# Paths for the pose class.
|
160 |
+
images_out_folder = os.path.join(self._images_out_folder, pose_class_name)
|
161 |
+
csv_out_path = os.path.join(self._csvs_out_folder, pose_class_name + ".csv")
|
162 |
+
|
163 |
+
# Read CSV into memory.
|
164 |
+
rows = []
|
165 |
+
with open(csv_out_path) as csv_out_file:
|
166 |
+
csv_out_reader = csv.reader(csv_out_file, delimiter=",")
|
167 |
+
for row in csv_out_reader:
|
168 |
+
rows.append(row)
|
169 |
+
|
170 |
+
# Image names left in CSV.
|
171 |
+
image_names_in_csv = []
|
172 |
+
|
173 |
+
# Re-write the CSV removing lines without corresponding images.
|
174 |
+
with open(csv_out_path, "w") as csv_out_file:
|
175 |
+
csv_out_writer = csv.writer(
|
176 |
+
csv_out_file, delimiter=",", quoting=csv.QUOTE_MINIMAL
|
177 |
+
)
|
178 |
+
for row in rows:
|
179 |
+
image_name = row[0]
|
180 |
+
image_path = os.path.join(images_out_folder, image_name)
|
181 |
+
if os.path.exists(image_path):
|
182 |
+
image_names_in_csv.append(image_name)
|
183 |
+
csv_out_writer.writerow(row)
|
184 |
+
elif print_removed_items:
|
185 |
+
print("Removed image from CSV: ", image_path)
|
186 |
+
|
187 |
+
# Remove images without corresponding line in CSV.
|
188 |
+
for image_name in os.listdir(images_out_folder):
|
189 |
+
if image_name not in image_names_in_csv:
|
190 |
+
image_path = os.path.join(images_out_folder, image_name)
|
191 |
+
os.remove(image_path)
|
192 |
+
if print_removed_items:
|
193 |
+
print("Removed image from folder: ", image_path)
|
194 |
+
|
195 |
+
def analyze_outliers(self, outliers):
|
196 |
+
"""Classifies each sample against all other to find outliers.
|
197 |
+
|
198 |
+
If sample is classified differrently than the original class - it should
|
199 |
+
either be deleted or more similar samples should be added.
|
200 |
+
"""
|
201 |
+
for outlier in outliers:
|
202 |
+
image_path = os.path.join(
|
203 |
+
self._images_out_folder, outlier.sample.class_name, outlier.sample.name
|
204 |
+
)
|
205 |
+
|
206 |
+
print("Outlier")
|
207 |
+
print(" sample path = ", image_path)
|
208 |
+
print(" sample class = ", outlier.sample.class_name)
|
209 |
+
print(" detected class = ", outlier.detected_class)
|
210 |
+
print(" all classes = ", outlier.all_classes)
|
211 |
+
|
212 |
+
img = cv2.imread(image_path)
|
213 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
214 |
+
show_image(img, figsize=(20, 20))
|
215 |
+
|
216 |
+
def remove_outliers(self, outliers):
|
217 |
+
"""Removes outliers from the image folders."""
|
218 |
+
for outlier in outliers:
|
219 |
+
image_path = os.path.join(
|
220 |
+
self._images_out_folder, outlier.sample.class_name, outlier.sample.name
|
221 |
+
)
|
222 |
+
os.remove(image_path)
|
223 |
+
|
224 |
+
def print_images_in_statistics(self):
|
225 |
+
"""Prints statistics from the input image folder."""
|
226 |
+
self._print_images_statistics(self._images_in_folder, self._pose_class_names)
|
227 |
+
|
228 |
+
def print_images_out_statistics(self):
|
229 |
+
"""Prints statistics from the output image folder."""
|
230 |
+
self._print_images_statistics(self._images_out_folder, self._pose_class_names)
|
231 |
+
|
232 |
+
def _print_images_statistics(self, images_folder, pose_class_names):
|
233 |
+
print("Number of images per pose class:")
|
234 |
+
for pose_class_name in pose_class_names:
|
235 |
+
n_images = len(
|
236 |
+
[
|
237 |
+
n
|
238 |
+
for n in os.listdir(os.path.join(images_folder, pose_class_name))
|
239 |
+
if not n.startswith(".")
|
240 |
+
]
|
241 |
+
)
|
242 |
+
print(" {}: {}".format(pose_class_name, n_images))
|
PoseClassification/pose_classifier.py
ADDED
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import os, csv
|
3 |
+
|
4 |
+
|
5 |
+
class PoseSample(object):
|
6 |
+
def __init__(self, name, landmarks, class_name, embedding):
|
7 |
+
self.name = name
|
8 |
+
self.landmarks = landmarks
|
9 |
+
self.class_name = class_name
|
10 |
+
self.embedding = embedding
|
11 |
+
|
12 |
+
|
13 |
+
class PoseSampleOutlier(object):
|
14 |
+
def __init__(self, sample, detected_class, all_classes):
|
15 |
+
self.sample = sample
|
16 |
+
self.detected_class = detected_class
|
17 |
+
self.all_classes = all_classes
|
18 |
+
|
19 |
+
|
20 |
+
class PoseClassifier(object):
|
21 |
+
"""Classifies pose landmarks."""
|
22 |
+
|
23 |
+
def __init__(
|
24 |
+
self,
|
25 |
+
pose_samples_folder,
|
26 |
+
pose_embedder,
|
27 |
+
file_extension="csv",
|
28 |
+
file_separator=",",
|
29 |
+
n_landmarks=33,
|
30 |
+
n_dimensions=3,
|
31 |
+
top_n_by_max_distance=30,
|
32 |
+
top_n_by_mean_distance=10,
|
33 |
+
axes_weights=(1.0, 1.0, 0.2),
|
34 |
+
):
|
35 |
+
self._pose_embedder = pose_embedder
|
36 |
+
self._n_landmarks = n_landmarks
|
37 |
+
self._n_dimensions = n_dimensions
|
38 |
+
self._top_n_by_max_distance = top_n_by_max_distance
|
39 |
+
self._top_n_by_mean_distance = top_n_by_mean_distance
|
40 |
+
self._axes_weights = axes_weights
|
41 |
+
|
42 |
+
self._pose_samples = self._load_pose_samples(
|
43 |
+
pose_samples_folder,
|
44 |
+
file_extension,
|
45 |
+
file_separator,
|
46 |
+
n_landmarks,
|
47 |
+
n_dimensions,
|
48 |
+
pose_embedder,
|
49 |
+
)
|
50 |
+
|
51 |
+
def _load_pose_samples(
|
52 |
+
self,
|
53 |
+
pose_samples_folder,
|
54 |
+
file_extension,
|
55 |
+
file_separator,
|
56 |
+
n_landmarks,
|
57 |
+
n_dimensions,
|
58 |
+
pose_embedder,
|
59 |
+
):
|
60 |
+
"""Loads pose samples from a given folder.
|
61 |
+
|
62 |
+
Required folder structure:
|
63 |
+
neutral_standing.csv
|
64 |
+
pushups_down.csv
|
65 |
+
pushups_up.csv
|
66 |
+
squats_down.csv
|
67 |
+
...
|
68 |
+
|
69 |
+
Required CSV structure:
|
70 |
+
sample_00001,x1,y1,z1,x2,y2,z2,....
|
71 |
+
sample_00002,x1,y1,z1,x2,y2,z2,....
|
72 |
+
...
|
73 |
+
"""
|
74 |
+
# Each file in the folder represents one pose class.
|
75 |
+
file_names = [
|
76 |
+
name
|
77 |
+
for name in os.listdir(pose_samples_folder)
|
78 |
+
if name.endswith(file_extension)
|
79 |
+
]
|
80 |
+
|
81 |
+
pose_samples = []
|
82 |
+
for file_name in file_names:
|
83 |
+
# Use file name as pose class name.
|
84 |
+
class_name = file_name[: -(len(file_extension) + 1)]
|
85 |
+
|
86 |
+
# Parse CSV.
|
87 |
+
with open(os.path.join(pose_samples_folder, file_name)) as csv_file:
|
88 |
+
csv_reader = csv.reader(csv_file, delimiter=file_separator)
|
89 |
+
for row in csv_reader:
|
90 |
+
assert (
|
91 |
+
len(row) == n_landmarks * n_dimensions + 1
|
92 |
+
), "Wrong number of values: {}".format(len(row))
|
93 |
+
landmarks = np.array(row[1:], np.float32).reshape(
|
94 |
+
[n_landmarks, n_dimensions]
|
95 |
+
)
|
96 |
+
pose_samples.append(
|
97 |
+
PoseSample(
|
98 |
+
name=row[0],
|
99 |
+
landmarks=landmarks,
|
100 |
+
class_name=class_name,
|
101 |
+
embedding=pose_embedder(landmarks),
|
102 |
+
)
|
103 |
+
)
|
104 |
+
|
105 |
+
return pose_samples
|
106 |
+
|
107 |
+
def find_pose_sample_outliers(self):
|
108 |
+
"""Classifies each sample against the entire database."""
|
109 |
+
# Find outliers in target poses
|
110 |
+
outliers = []
|
111 |
+
for sample in self._pose_samples:
|
112 |
+
# Find nearest poses for the target one.
|
113 |
+
pose_landmarks = sample.landmarks.copy()
|
114 |
+
pose_classification = self.__call__(pose_landmarks)
|
115 |
+
class_names = [
|
116 |
+
class_name
|
117 |
+
for class_name, count in pose_classification.items()
|
118 |
+
if count == max(pose_classification.values())
|
119 |
+
]
|
120 |
+
|
121 |
+
# Sample is an outlier if nearest poses have different class or more than
|
122 |
+
# one pose class is detected as nearest.
|
123 |
+
if sample.class_name not in class_names or len(class_names) != 1:
|
124 |
+
outliers.append(
|
125 |
+
PoseSampleOutlier(sample, class_names, pose_classification)
|
126 |
+
)
|
127 |
+
|
128 |
+
return outliers
|
129 |
+
|
130 |
+
def __call__(self, pose_landmarks):
|
131 |
+
"""Classifies given pose.
|
132 |
+
|
133 |
+
Classification is done in two stages:
|
134 |
+
* First we pick top-N samples by MAX distance. It allows to remove samples
|
135 |
+
that are almost the same as given pose, but has few joints bent in the
|
136 |
+
other direction.
|
137 |
+
* Then we pick top-N samples by MEAN distance. After outliers are removed
|
138 |
+
on a previous step, we can pick samples that are closes on average.
|
139 |
+
|
140 |
+
Args:
|
141 |
+
pose_landmarks: NumPy array with 3D landmarks of shape (N, 3).
|
142 |
+
|
143 |
+
Returns:
|
144 |
+
Dictionary with count of nearest pose samples from the database. Sample:
|
145 |
+
{
|
146 |
+
'pushups_down': 8,
|
147 |
+
'pushups_up': 2,
|
148 |
+
}
|
149 |
+
"""
|
150 |
+
# Check that provided and target poses have the same shape.
|
151 |
+
assert pose_landmarks.shape == (
|
152 |
+
self._n_landmarks,
|
153 |
+
self._n_dimensions,
|
154 |
+
), "Unexpected shape: {}".format(pose_landmarks.shape)
|
155 |
+
|
156 |
+
# Get given pose embedding.
|
157 |
+
pose_embedding = self._pose_embedder(pose_landmarks)
|
158 |
+
flipped_pose_embedding = self._pose_embedder(
|
159 |
+
pose_landmarks * np.array([-1, 1, 1])
|
160 |
+
)
|
161 |
+
|
162 |
+
# Filter by max distance.
|
163 |
+
#
|
164 |
+
# That helps to remove outliers - poses that are almost the same as the
|
165 |
+
# given one, but has one joint bent into another direction and actually
|
166 |
+
# represnt a different pose class.
|
167 |
+
max_dist_heap = []
|
168 |
+
for sample_idx, sample in enumerate(self._pose_samples):
|
169 |
+
max_dist = min(
|
170 |
+
np.max(np.abs(sample.embedding - pose_embedding) * self._axes_weights),
|
171 |
+
np.max(
|
172 |
+
np.abs(sample.embedding - flipped_pose_embedding)
|
173 |
+
* self._axes_weights
|
174 |
+
),
|
175 |
+
)
|
176 |
+
max_dist_heap.append([max_dist, sample_idx])
|
177 |
+
|
178 |
+
max_dist_heap = sorted(max_dist_heap, key=lambda x: x[0])
|
179 |
+
max_dist_heap = max_dist_heap[: self._top_n_by_max_distance]
|
180 |
+
|
181 |
+
# Filter by mean distance.
|
182 |
+
#
|
183 |
+
# After removing outliers we can find the nearest pose by mean distance.
|
184 |
+
mean_dist_heap = []
|
185 |
+
for _, sample_idx in max_dist_heap:
|
186 |
+
sample = self._pose_samples[sample_idx]
|
187 |
+
mean_dist = min(
|
188 |
+
np.mean(np.abs(sample.embedding - pose_embedding) * self._axes_weights),
|
189 |
+
np.mean(
|
190 |
+
np.abs(sample.embedding - flipped_pose_embedding)
|
191 |
+
* self._axes_weights
|
192 |
+
),
|
193 |
+
)
|
194 |
+
mean_dist_heap.append([mean_dist, sample_idx])
|
195 |
+
|
196 |
+
mean_dist_heap = sorted(mean_dist_heap, key=lambda x: x[0])
|
197 |
+
mean_dist_heap = mean_dist_heap[: self._top_n_by_mean_distance]
|
198 |
+
|
199 |
+
# Collect results into map: (class_name -> n_samples)
|
200 |
+
class_names = [
|
201 |
+
self._pose_samples[sample_idx].class_name
|
202 |
+
for _, sample_idx in mean_dist_heap
|
203 |
+
]
|
204 |
+
result = {
|
205 |
+
class_name: class_names.count(class_name) for class_name in set(class_names)
|
206 |
+
}
|
207 |
+
|
208 |
+
return result
|
PoseClassification/pose_embedding.py
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import math
|
3 |
+
|
4 |
+
class FullBodyPoseEmbedding(object):
|
5 |
+
"""Converts 3D pose landmarks into 3D embedding."""
|
6 |
+
|
7 |
+
def __init__(self, torso_size_multiplier=2.5):
|
8 |
+
# Multiplier to apply to the torso to get minimal body size.
|
9 |
+
self._torso_size_multiplier = torso_size_multiplier
|
10 |
+
|
11 |
+
# Names of the landmarks as they appear in the prediction.
|
12 |
+
self._landmark_names = [
|
13 |
+
"nose",
|
14 |
+
"left_eye_inner",
|
15 |
+
"left_eye",
|
16 |
+
"left_eye_outer",
|
17 |
+
"right_eye_inner",
|
18 |
+
"right_eye",
|
19 |
+
"right_eye_outer",
|
20 |
+
"left_ear",
|
21 |
+
"right_ear",
|
22 |
+
"mouth_left",
|
23 |
+
"mouth_right",
|
24 |
+
"left_shoulder",
|
25 |
+
"right_shoulder",
|
26 |
+
"left_elbow",
|
27 |
+
"right_elbow",
|
28 |
+
"left_wrist",
|
29 |
+
"right_wrist",
|
30 |
+
"left_pinky_1",
|
31 |
+
"right_pinky_1",
|
32 |
+
"left_index_1",
|
33 |
+
"right_index_1",
|
34 |
+
"left_thumb_2",
|
35 |
+
"right_thumb_2",
|
36 |
+
"left_hip",
|
37 |
+
"right_hip",
|
38 |
+
"left_knee",
|
39 |
+
"right_knee",
|
40 |
+
"left_ankle",
|
41 |
+
"right_ankle",
|
42 |
+
"left_heel",
|
43 |
+
"right_heel",
|
44 |
+
"left_foot_index",
|
45 |
+
"right_foot_index",
|
46 |
+
]
|
47 |
+
|
48 |
+
def __call__(self, landmarks):
|
49 |
+
"""Normalizes pose landmarks and converts to embedding
|
50 |
+
|
51 |
+
Args:
|
52 |
+
landmarks - NumPy array with 3D landmarks of shape (N, 3).
|
53 |
+
|
54 |
+
Result:
|
55 |
+
Numpy array with pose embedding of shape (M, 3) where `M` is the number of
|
56 |
+
pairwise distances defined in `_get_pose_distance_embedding`.
|
57 |
+
"""
|
58 |
+
assert landmarks.shape[0] == len(
|
59 |
+
self._landmark_names
|
60 |
+
), "Unexpected number of landmarks: {}".format(landmarks.shape[0])
|
61 |
+
|
62 |
+
# Get pose landmarks.
|
63 |
+
landmarks = np.copy(landmarks)
|
64 |
+
|
65 |
+
# Normalize landmarks.
|
66 |
+
landmarks = self._normalize_pose_landmarks(landmarks)
|
67 |
+
|
68 |
+
# Get embedding.
|
69 |
+
embedding = self._get_pose_distance_embedding(landmarks)
|
70 |
+
|
71 |
+
return embedding
|
72 |
+
|
73 |
+
def _normalize_pose_landmarks(self, landmarks):
|
74 |
+
"""Normalizes landmarks translation and scale."""
|
75 |
+
landmarks = np.copy(landmarks)
|
76 |
+
|
77 |
+
# Normalize translation.
|
78 |
+
pose_center = self._get_pose_center(landmarks)
|
79 |
+
landmarks -= pose_center
|
80 |
+
|
81 |
+
# Normalize scale.
|
82 |
+
pose_size = self._get_pose_size(landmarks, self._torso_size_multiplier)
|
83 |
+
landmarks /= pose_size
|
84 |
+
# Multiplication by 100 is not required, but makes it eaasier to debug.
|
85 |
+
landmarks *= 100
|
86 |
+
|
87 |
+
return landmarks
|
88 |
+
|
89 |
+
def _get_pose_center(self, landmarks):
|
90 |
+
"""Calculates pose center as point between hips."""
|
91 |
+
left_hip = landmarks[self._landmark_names.index("left_hip")]
|
92 |
+
right_hip = landmarks[self._landmark_names.index("right_hip")]
|
93 |
+
center = (left_hip + right_hip) * 0.5
|
94 |
+
return center
|
95 |
+
|
96 |
+
def _get_pose_size(self, landmarks, torso_size_multiplier):
|
97 |
+
"""Calculates pose size.
|
98 |
+
|
99 |
+
It is the maximum of two values:
|
100 |
+
* Torso size multiplied by `torso_size_multiplier`
|
101 |
+
* Maximum distance from pose center to any pose landmark
|
102 |
+
"""
|
103 |
+
# This approach uses only 2D landmarks to compute pose size.
|
104 |
+
landmarks = landmarks[:, :2]
|
105 |
+
|
106 |
+
# Hips center.
|
107 |
+
left_hip = landmarks[self._landmark_names.index("left_hip")]
|
108 |
+
right_hip = landmarks[self._landmark_names.index("right_hip")]
|
109 |
+
hips = (left_hip + right_hip) * 0.5
|
110 |
+
|
111 |
+
# Shoulders center.
|
112 |
+
left_shoulder = landmarks[self._landmark_names.index("left_shoulder")]
|
113 |
+
right_shoulder = landmarks[self._landmark_names.index("right_shoulder")]
|
114 |
+
shoulders = (left_shoulder + right_shoulder) * 0.5
|
115 |
+
|
116 |
+
# Torso size as the minimum body size.
|
117 |
+
torso_size = np.linalg.norm(shoulders - hips)
|
118 |
+
|
119 |
+
# Max dist to pose center.
|
120 |
+
pose_center = self._get_pose_center(landmarks)
|
121 |
+
max_dist = np.max(np.linalg.norm(landmarks - pose_center, axis=1))
|
122 |
+
|
123 |
+
return max(torso_size * torso_size_multiplier, max_dist)
|
124 |
+
|
125 |
+
def _get_pose_distance_embedding(self, landmarks):
|
126 |
+
"""Converts pose landmarks into 3D embedding.
|
127 |
+
|
128 |
+
We use several pairwise 3D distances to form pose embedding. All distances
|
129 |
+
include X and Y components with sign. We differnt types of pairs to cover
|
130 |
+
different pose classes. Feel free to remove some or add new.
|
131 |
+
|
132 |
+
Args:
|
133 |
+
landmarks - NumPy array with 3D landmarks of shape (N, 3).
|
134 |
+
|
135 |
+
Result:
|
136 |
+
Numpy array with pose embedding of shape (M, 3) where `M` is the number of
|
137 |
+
pairwise distances.
|
138 |
+
"""
|
139 |
+
embedding = np.array(
|
140 |
+
[
|
141 |
+
# One joint.
|
142 |
+
self._get_distance(
|
143 |
+
self._get_average_by_names(landmarks, "left_hip", "right_hip"),
|
144 |
+
self._get_average_by_names(
|
145 |
+
landmarks, "left_shoulder", "right_shoulder"
|
146 |
+
),
|
147 |
+
),
|
148 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_elbow"),
|
149 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_elbow"),
|
150 |
+
self._get_distance_by_names(landmarks, "left_elbow", "left_wrist"),
|
151 |
+
self._get_distance_by_names(landmarks, "right_elbow", "right_wrist"),
|
152 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_knee"),
|
153 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_knee"),
|
154 |
+
self._get_distance_by_names(landmarks, "left_knee", "left_ankle"),
|
155 |
+
self._get_distance_by_names(landmarks, "right_knee", "right_ankle"),
|
156 |
+
# Two joints.
|
157 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_wrist"),
|
158 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_wrist"),
|
159 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_ankle"),
|
160 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_ankle"),
|
161 |
+
# Four joints.
|
162 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_wrist"),
|
163 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_wrist"),
|
164 |
+
# Five joints.
|
165 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_ankle"),
|
166 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_ankle"),
|
167 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_wrist"),
|
168 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_wrist"),
|
169 |
+
# Cross body.
|
170 |
+
self._get_distance_by_names(landmarks, "left_elbow", "right_elbow"),
|
171 |
+
self._get_distance_by_names(landmarks, "left_knee", "right_knee"),
|
172 |
+
self._get_distance_by_names(landmarks, "left_wrist", "right_wrist"),
|
173 |
+
self._get_distance_by_names(landmarks, "left_ankle", "right_ankle"),
|
174 |
+
# Body bent direction.
|
175 |
+
self._get_distance(
|
176 |
+
self._get_average_by_names(landmarks, 'left_wrist', 'left_ankle'),
|
177 |
+
landmarks[self._landmark_names.index('left_hip')]),
|
178 |
+
self._get_distance(
|
179 |
+
self._get_average_by_names(landmarks, 'right_wrist', 'right_ankle'),
|
180 |
+
landmarks[self._landmark_names.index('right_hip')]),
|
181 |
+
# Angle between landmarks - cf https://www.kaggle.com/code/venkatkumar001/yoga-pose-recognition-mediapipe
|
182 |
+
# self._calculateAngle(landmarks, "left_hip", "left_knee", "left_ankle"),
|
183 |
+
# self._calculateAngle(landmarks, "right_hip", "right_knee", "right_ankle"),
|
184 |
+
# self._calculateAngle(landmarks, "left_shoulder", "left_elbow", "left_wrist"),
|
185 |
+
# self._calculateAngle(landmarks, "right_shoulder", "right_elbow", "right_wrist")
|
186 |
+
|
187 |
+
]
|
188 |
+
)
|
189 |
+
# print(embedding)
|
190 |
+
# print(embbeding.shape)
|
191 |
+
# print(type(embedding))
|
192 |
+
# print(type(landmarks[self._landmark_names.index('right_hip')]))
|
193 |
+
# print(landmarks[self._landmark_names.index('right_hip')])
|
194 |
+
return embedding
|
195 |
+
|
196 |
+
def _get_average_by_names(self, landmarks, name_from, name_to):
|
197 |
+
lmk_from = landmarks[self._landmark_names.index(name_from)]
|
198 |
+
lmk_to = landmarks[self._landmark_names.index(name_to)]
|
199 |
+
return (lmk_from + lmk_to) * 0.5
|
200 |
+
|
201 |
+
def _get_distance_by_names(self, landmarks, name_from, name_to):
|
202 |
+
lmk_from = landmarks[self._landmark_names.index(name_from)]
|
203 |
+
lmk_to = landmarks[self._landmark_names.index(name_to)]
|
204 |
+
return self._get_distance(lmk_from, lmk_to)
|
205 |
+
|
206 |
+
def _get_distance(self, lmk_from, lmk_to):
|
207 |
+
return lmk_to - lmk_from
|
208 |
+
|
209 |
+
def _calculateAngle(self, landmarks, name1, name2, name3):
|
210 |
+
'''
|
211 |
+
This function calculates angle between three different landmarks.
|
212 |
+
Args:
|
213 |
+
landmark1: The first landmark containing the x,y and z coordinates.
|
214 |
+
landmark2: The second landmark containing the x,y and z coordinates.
|
215 |
+
landmark3: The third landmark containing the x,y and z coordinates.
|
216 |
+
Returns:
|
217 |
+
angle: The calculated angle between the three landmarks.
|
218 |
+
|
219 |
+
cf https://www.kaggle.com/code/venkatkumar001/yoga-pose-recognition-mediapipe
|
220 |
+
'''
|
221 |
+
|
222 |
+
# Get the required landmarks coordinates.
|
223 |
+
x1, y1, _ = landmarks[self._landmark_names.index(name1)]
|
224 |
+
x2, y2, _ = landmarks[self._landmark_names.index(name2)]
|
225 |
+
x3, y3, _ = landmarks[self._landmark_names.index(name3)]
|
226 |
+
|
227 |
+
# Calculate the angle between the three points
|
228 |
+
angle = math.degrees(math.atan2(y3 - y2, x3 - x2) - math.atan2(y1 - y2, x1 - x2))
|
229 |
+
|
230 |
+
# Check if the angle is less than zero.
|
231 |
+
if angle < 0:
|
232 |
+
|
233 |
+
# Add 360 to the found angle.
|
234 |
+
angle += 360
|
235 |
+
|
236 |
+
# Return the calculated angle.
|
237 |
+
return angle
|
PoseClassification/pose_embedding_2.py
ADDED
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import math
|
3 |
+
|
4 |
+
class FullBodyPoseEmbedding(object):
|
5 |
+
"""Converts 3D pose landmarks into 3D embedding."""
|
6 |
+
|
7 |
+
def __init__(self, torso_size_multiplier=2.5):
|
8 |
+
# Multiplier to apply to the torso to get minimal body size.
|
9 |
+
self._torso_size_multiplier = torso_size_multiplier
|
10 |
+
|
11 |
+
# Names of the landmarks as they appear in the prediction.
|
12 |
+
self._landmark_names = [
|
13 |
+
"nose",
|
14 |
+
"left_eye_inner",
|
15 |
+
"left_eye",
|
16 |
+
"left_eye_outer",
|
17 |
+
"right_eye_inner",
|
18 |
+
"right_eye",
|
19 |
+
"right_eye_outer",
|
20 |
+
"left_ear",
|
21 |
+
"right_ear",
|
22 |
+
"mouth_left",
|
23 |
+
"mouth_right",
|
24 |
+
"left_shoulder",
|
25 |
+
"right_shoulder",
|
26 |
+
"left_elbow",
|
27 |
+
"right_elbow",
|
28 |
+
"left_wrist",
|
29 |
+
"right_wrist",
|
30 |
+
"left_pinky_1",
|
31 |
+
"right_pinky_1",
|
32 |
+
"left_index_1",
|
33 |
+
"right_index_1",
|
34 |
+
"left_thumb_2",
|
35 |
+
"right_thumb_2",
|
36 |
+
"left_hip",
|
37 |
+
"right_hip",
|
38 |
+
"left_knee",
|
39 |
+
"right_knee",
|
40 |
+
"left_ankle",
|
41 |
+
"right_ankle",
|
42 |
+
"left_heel",
|
43 |
+
"right_heel",
|
44 |
+
"left_foot_index",
|
45 |
+
"right_foot_index",
|
46 |
+
]
|
47 |
+
|
48 |
+
def __call__(self, landmarks):
|
49 |
+
"""Normalizes pose landmarks and converts to embedding
|
50 |
+
|
51 |
+
Args:
|
52 |
+
landmarks - NumPy array with 3D landmarks of shape (N, 3).
|
53 |
+
|
54 |
+
Result:
|
55 |
+
Numpy array with pose embedding of shape (M, 3) where `M` is the number of
|
56 |
+
pairwise distances defined in `_get_pose_distance_embedding`.
|
57 |
+
"""
|
58 |
+
assert landmarks.shape[0] == len(
|
59 |
+
self._landmark_names
|
60 |
+
), "Unexpected number of landmarks: {}".format(landmarks.shape[0])
|
61 |
+
|
62 |
+
# Get pose landmarks.
|
63 |
+
landmarks = np.copy(landmarks)
|
64 |
+
|
65 |
+
# Normalize landmarks.
|
66 |
+
landmarks = self._normalize_pose_landmarks(landmarks)
|
67 |
+
|
68 |
+
# Get embedding.
|
69 |
+
embedding = self._get_pose_distance_embedding(landmarks)
|
70 |
+
|
71 |
+
# Add angle embedding
|
72 |
+
embedding_angle = self._get_pose_angle_embedding(landmarks)
|
73 |
+
|
74 |
+
assert embedding.shape == embedding_angle.shape, f"Error in embeddings shape : distance embed {embedding.shape} and angle {embedding_angle.shape}"
|
75 |
+
|
76 |
+
return embedding
|
77 |
+
|
78 |
+
def _normalize_pose_landmarks(self, landmarks):
|
79 |
+
"""Normalizes landmarks translation and scale."""
|
80 |
+
landmarks = np.copy(landmarks)
|
81 |
+
|
82 |
+
# Normalize translation.
|
83 |
+
pose_center = self._get_pose_center(landmarks)
|
84 |
+
landmarks -= pose_center
|
85 |
+
|
86 |
+
# Normalize scale.
|
87 |
+
pose_size = self._get_pose_size(landmarks, self._torso_size_multiplier)
|
88 |
+
landmarks /= pose_size
|
89 |
+
# Multiplication by 100 is not required, but makes it eaasier to debug.
|
90 |
+
landmarks *= 100
|
91 |
+
|
92 |
+
return landmarks
|
93 |
+
|
94 |
+
def _get_pose_center(self, landmarks):
|
95 |
+
"""Calculates pose center as point between hips."""
|
96 |
+
left_hip = landmarks[self._landmark_names.index("left_hip")]
|
97 |
+
right_hip = landmarks[self._landmark_names.index("right_hip")]
|
98 |
+
center = (left_hip + right_hip) * 0.5
|
99 |
+
return center
|
100 |
+
|
101 |
+
def _get_pose_size(self, landmarks, torso_size_multiplier):
|
102 |
+
"""Calculates pose size.
|
103 |
+
|
104 |
+
It is the maximum of two values:
|
105 |
+
* Torso size multiplied by `torso_size_multiplier`
|
106 |
+
* Maximum distance from pose center to any pose landmark
|
107 |
+
"""
|
108 |
+
# This approach uses only 2D landmarks to compute pose size.
|
109 |
+
landmarks = landmarks[:, :2]
|
110 |
+
|
111 |
+
# Hips center.
|
112 |
+
left_hip = landmarks[self._landmark_names.index("left_hip")]
|
113 |
+
right_hip = landmarks[self._landmark_names.index("right_hip")]
|
114 |
+
hips = (left_hip + right_hip) * 0.5
|
115 |
+
|
116 |
+
# Shoulders center.
|
117 |
+
left_shoulder = landmarks[self._landmark_names.index("left_shoulder")]
|
118 |
+
right_shoulder = landmarks[self._landmark_names.index("right_shoulder")]
|
119 |
+
shoulders = (left_shoulder + right_shoulder) * 0.5
|
120 |
+
|
121 |
+
# Torso size as the minimum body size.
|
122 |
+
torso_size = np.linalg.norm(shoulders - hips)
|
123 |
+
|
124 |
+
# Max dist to pose center.
|
125 |
+
pose_center = self._get_pose_center(landmarks)
|
126 |
+
max_dist = np.max(np.linalg.norm(landmarks - pose_center, axis=1))
|
127 |
+
|
128 |
+
return max(torso_size * torso_size_multiplier, max_dist)
|
129 |
+
|
130 |
+
def _get_pose_distance_embedding(self, landmarks):
|
131 |
+
"""Converts pose landmarks into 3D embedding.
|
132 |
+
|
133 |
+
We use several pairwise 3D distances to form pose embedding. All distances
|
134 |
+
include X and Y components with sign. We differnt types of pairs to cover
|
135 |
+
different pose classes. Feel free to remove some or add new.
|
136 |
+
|
137 |
+
Args:
|
138 |
+
landmarks - NumPy array with 3D landmarks of shape (N, 3).
|
139 |
+
|
140 |
+
Result:
|
141 |
+
Numpy array with pose embedding of shape (M, 3) where `M` is the number of
|
142 |
+
pairwise distances.
|
143 |
+
"""
|
144 |
+
embedding = np.array(
|
145 |
+
[
|
146 |
+
# One joint.
|
147 |
+
self._get_distance(
|
148 |
+
self._get_average_by_names(landmarks, "left_hip", "right_hip"),
|
149 |
+
self._get_average_by_names(
|
150 |
+
landmarks, "left_shoulder", "right_shoulder"
|
151 |
+
),
|
152 |
+
),
|
153 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_elbow"),
|
154 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_elbow"),
|
155 |
+
self._get_distance_by_names(landmarks, "left_elbow", "left_wrist"),
|
156 |
+
self._get_distance_by_names(landmarks, "right_elbow", "right_wrist"),
|
157 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_knee"),
|
158 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_knee"),
|
159 |
+
self._get_distance_by_names(landmarks, "left_knee", "left_ankle"),
|
160 |
+
self._get_distance_by_names(landmarks, "right_knee", "right_ankle"),
|
161 |
+
# Two joints.
|
162 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_wrist"),
|
163 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_wrist"),
|
164 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_ankle"),
|
165 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_ankle"),
|
166 |
+
# Four joints.
|
167 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_wrist"),
|
168 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_wrist"),
|
169 |
+
# Five joints.
|
170 |
+
self._get_distance_by_names(landmarks, "left_shoulder", "left_ankle"),
|
171 |
+
self._get_distance_by_names(landmarks, "right_shoulder", "right_ankle"),
|
172 |
+
self._get_distance_by_names(landmarks, "left_hip", "left_wrist"),
|
173 |
+
self._get_distance_by_names(landmarks, "right_hip", "right_wrist"),
|
174 |
+
# Cross body.
|
175 |
+
self._get_distance_by_names(landmarks, "left_elbow", "right_elbow"),
|
176 |
+
self._get_distance_by_names(landmarks, "left_knee", "right_knee"),
|
177 |
+
self._get_distance_by_names(landmarks, "left_wrist", "right_wrist"),
|
178 |
+
self._get_distance_by_names(landmarks, "left_ankle", "right_ankle"),
|
179 |
+
# Body bent direction.
|
180 |
+
self._get_distance(
|
181 |
+
self._get_average_by_names(landmarks, 'left_wrist', 'left_ankle'),
|
182 |
+
landmarks[self._landmark_names.index('left_hip')]),
|
183 |
+
self._get_distance(
|
184 |
+
self._get_average_by_names(landmarks, 'right_wrist', 'right_ankle'),
|
185 |
+
landmarks[self._landmark_names.index('right_hip')])
|
186 |
+
|
187 |
+
]
|
188 |
+
)
|
189 |
+
# print(embedding)
|
190 |
+
# print(embbeding.shape)
|
191 |
+
# print(type(embedding))
|
192 |
+
# print(type(landmarks[self._landmark_names.index('right_hip')]))
|
193 |
+
# print(landmarks[self._landmark_names.index('right_hip')])
|
194 |
+
return embedding
|
195 |
+
|
196 |
+
def _get_average_by_names(self, landmarks, name_from, name_to):
|
197 |
+
lmk_from = landmarks[self._landmark_names.index(name_from)]
|
198 |
+
lmk_to = landmarks[self._landmark_names.index(name_to)]
|
199 |
+
return (lmk_from + lmk_to) * 0.5
|
200 |
+
|
201 |
+
def _get_distance_by_names(self, landmarks, name_from, name_to):
|
202 |
+
lmk_from = landmarks[self._landmark_names.index(name_from)]
|
203 |
+
lmk_to = landmarks[self._landmark_names.index(name_to)]
|
204 |
+
return self._get_distance(lmk_from, lmk_to)
|
205 |
+
|
206 |
+
def _get_distance(self, lmk_from, lmk_to):
|
207 |
+
return lmk_to - lmk_from
|
208 |
+
|
209 |
+
def _get_pose_angle_embedding(self, landmarks):
|
210 |
+
embedding = [
|
211 |
+
# Angle between landmarks - cf https://www.kaggle.com/code/venkatkumar001/yoga-pose-recognition-mediapipe
|
212 |
+
self._calculateAngle(landmarks, "left_hip", "left_knee", "left_ankle"),
|
213 |
+
self._calculateAngle(landmarks, "right_hip", "right_knee", "right_ankle"),
|
214 |
+
self._calculateAngle(landmarks, "left_shoulder", "left_elbow", "left_wrist"),
|
215 |
+
self._calculateAngle(landmarks, "right_shoulder", "right_elbow", "right_wrist")
|
216 |
+
]
|
217 |
+
return embedding
|
218 |
+
|
219 |
+
def _calculateAngle(self, landmarks, name1, name2, name3):
|
220 |
+
'''
|
221 |
+
This function calculates angle between three different landmarks.
|
222 |
+
Args:
|
223 |
+
landmark1: The first landmark containing the x,y and z coordinates.
|
224 |
+
landmark2: The second landmark containing the x,y and z coordinates.
|
225 |
+
landmark3: The third landmark containing the x,y and z coordinates.
|
226 |
+
Returns:
|
227 |
+
angle: The calculated angle between the three landmarks.
|
228 |
+
|
229 |
+
cf https://www.kaggle.com/code/venkatkumar001/yoga-pose-recognition-mediapipe
|
230 |
+
'''
|
231 |
+
# Get the required landmarks coordinates.
|
232 |
+
x1, y1, _ = landmarks[self._landmark_names.index(name1)]
|
233 |
+
x2, y2, _ = landmarks[self._landmark_names.index(name2)]
|
234 |
+
x3, y3, _ = landmarks[self._landmark_names.index(name3)]
|
235 |
+
|
236 |
+
# Calculate the angle between the three points
|
237 |
+
angle = math.degrees(math.atan2(y3 - y2, x3 - x2) - math.atan2(y1 - y2, x1 - x2))
|
238 |
+
|
239 |
+
# Check if the angle is less than zero.
|
240 |
+
if angle < 0:
|
241 |
+
|
242 |
+
# Add 360 to the found angle.
|
243 |
+
angle += 360
|
244 |
+
|
245 |
+
# Return the calculated angle.
|
246 |
+
return angle
|
PoseClassification/utils.py
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from matplotlib import pyplot as plt
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
|
5 |
+
def show_image(img, figsize=(10, 10)):
|
6 |
+
"""Shows output PIL image."""
|
7 |
+
plt.figure(figsize=figsize)
|
8 |
+
plt.imshow(img)
|
9 |
+
plt.show()
|
10 |
+
|
11 |
+
|
12 |
+
class EMADictSmoothing(object):
|
13 |
+
"""Smoothes pose classification. Exponential moving average (EMA)."""
|
14 |
+
|
15 |
+
def __init__(self, window_size=10, alpha=0.2):
|
16 |
+
self._window_size = window_size
|
17 |
+
self._alpha = alpha
|
18 |
+
|
19 |
+
self._data_in_window = []
|
20 |
+
|
21 |
+
def __call__(self, data):
|
22 |
+
"""Smoothes given pose classification.
|
23 |
+
|
24 |
+
Smoothing is done by computing Exponential Moving Average for every pose
|
25 |
+
class observed in the given time window. Missed pose classes arre replaced
|
26 |
+
with 0.
|
27 |
+
|
28 |
+
Args:
|
29 |
+
data: Dictionary with pose classification. Sample:
|
30 |
+
{
|
31 |
+
'pushups_down': 8,
|
32 |
+
'pushups_up': 2,
|
33 |
+
}
|
34 |
+
|
35 |
+
Result:
|
36 |
+
Dictionary in the same format but with smoothed and float instead of
|
37 |
+
integer values. Sample:
|
38 |
+
{
|
39 |
+
'pushups_down': 8.3,
|
40 |
+
'pushups_up': 1.7,
|
41 |
+
}
|
42 |
+
"""
|
43 |
+
# Add new data to the beginning of the window for simpler code.
|
44 |
+
self._data_in_window.insert(0, data)
|
45 |
+
self._data_in_window = self._data_in_window[: self._window_size]
|
46 |
+
|
47 |
+
# Get all keys.
|
48 |
+
keys = set([key for data in self._data_in_window for key, _ in data.items()])
|
49 |
+
|
50 |
+
# Get smoothed values.
|
51 |
+
smoothed_data = dict()
|
52 |
+
for key in keys:
|
53 |
+
factor = 1.0
|
54 |
+
top_sum = 0.0
|
55 |
+
bottom_sum = 0.0
|
56 |
+
for data in self._data_in_window:
|
57 |
+
value = data[key] if key in data else 0.0
|
58 |
+
|
59 |
+
top_sum += factor * value
|
60 |
+
bottom_sum += factor
|
61 |
+
|
62 |
+
# Update factor.
|
63 |
+
factor *= 1.0 - self._alpha
|
64 |
+
|
65 |
+
smoothed_data[key] = top_sum / bottom_sum
|
66 |
+
|
67 |
+
return smoothed_data
|
68 |
+
|
69 |
+
|
70 |
+
class RepetitionCounter(object):
|
71 |
+
"""Counts number of repetitions of given target pose class."""
|
72 |
+
|
73 |
+
def __init__(self, class_name, enter_threshold=6, exit_threshold=4):
|
74 |
+
self._class_name = class_name
|
75 |
+
|
76 |
+
# If pose counter passes given threshold, then we enter the pose.
|
77 |
+
self._enter_threshold = enter_threshold
|
78 |
+
self._exit_threshold = exit_threshold
|
79 |
+
|
80 |
+
# Either we are in given pose or not.
|
81 |
+
self._pose_entered = False
|
82 |
+
|
83 |
+
# Number of times we exited the pose.
|
84 |
+
self._n_repeats = 0
|
85 |
+
|
86 |
+
@property
|
87 |
+
def n_repeats(self):
|
88 |
+
return self._n_repeats
|
89 |
+
|
90 |
+
def reset(self):
|
91 |
+
self._n_repeats = 0
|
92 |
+
|
93 |
+
def __call__(self, pose_classification):
|
94 |
+
"""Counts number of repetitions happend until given frame.
|
95 |
+
|
96 |
+
We use two thresholds. First you need to go above the higher one to enter
|
97 |
+
the pose, and then you need to go below the lower one to exit it. Difference
|
98 |
+
between the thresholds makes it stable to prediction jittering (which will
|
99 |
+
cause wrong counts in case of having only one threshold).
|
100 |
+
|
101 |
+
Args:
|
102 |
+
pose_classification: Pose classification dictionary on current frame.
|
103 |
+
Sample:
|
104 |
+
{
|
105 |
+
'pushups_down': 8.3,
|
106 |
+
'pushups_up': 1.7,
|
107 |
+
}
|
108 |
+
|
109 |
+
Returns:
|
110 |
+
Integer counter of repetitions.
|
111 |
+
"""
|
112 |
+
# Get pose confidence.
|
113 |
+
pose_confidence = 0.0
|
114 |
+
if self._class_name in pose_classification:
|
115 |
+
pose_confidence = pose_classification[self._class_name]
|
116 |
+
|
117 |
+
# On the very first frame or if we were out of the pose, just check if we
|
118 |
+
# entered it on this frame and update the state.
|
119 |
+
if not self._pose_entered:
|
120 |
+
self._pose_entered = pose_confidence > self._enter_threshold
|
121 |
+
return self._n_repeats
|
122 |
+
|
123 |
+
# If we were in the pose and are exiting it, then increase the counter and
|
124 |
+
# update the state.
|
125 |
+
if pose_confidence < self._exit_threshold:
|
126 |
+
self._n_repeats += 1
|
127 |
+
self._pose_entered = False
|
128 |
+
|
129 |
+
return self._n_repeats
|
PoseClassification/visualize.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import io
|
2 |
+
from PIL import Image, ImageFont, ImageDraw
|
3 |
+
import requests
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
|
6 |
+
|
7 |
+
class PoseClassificationVisualizer(object):
|
8 |
+
"""Keeps track of classifcations for every frame and renders them."""
|
9 |
+
|
10 |
+
def __init__(
|
11 |
+
self,
|
12 |
+
class_name,
|
13 |
+
plot_location_x=0.05,
|
14 |
+
plot_location_y=0.05,
|
15 |
+
plot_max_width=0.4,
|
16 |
+
plot_max_height=0.4,
|
17 |
+
plot_figsize=(9, 4),
|
18 |
+
plot_x_max=None,
|
19 |
+
plot_y_max=None,
|
20 |
+
counter_location_x=0.85,
|
21 |
+
counter_location_y=0.05,
|
22 |
+
counter_font_path="https://github.com/googlefonts/roboto/blob/main/src/hinted/Roboto-Regular.ttf?raw=true",
|
23 |
+
counter_font_color="red",
|
24 |
+
counter_font_size=0.15,
|
25 |
+
):
|
26 |
+
self._class_name = class_name
|
27 |
+
self._plot_location_x = plot_location_x
|
28 |
+
self._plot_location_y = plot_location_y
|
29 |
+
self._plot_max_width = plot_max_width
|
30 |
+
self._plot_max_height = plot_max_height
|
31 |
+
self._plot_figsize = plot_figsize
|
32 |
+
self._plot_x_max = plot_x_max
|
33 |
+
self._plot_y_max = plot_y_max
|
34 |
+
self._counter_location_x = counter_location_x
|
35 |
+
self._counter_location_y = counter_location_y
|
36 |
+
self._counter_font_path = counter_font_path
|
37 |
+
self._counter_font_color = counter_font_color
|
38 |
+
self._counter_font_size = counter_font_size
|
39 |
+
|
40 |
+
self._counter_font = None
|
41 |
+
|
42 |
+
self._pose_classification_history = []
|
43 |
+
self._pose_classification_filtered_history = []
|
44 |
+
|
45 |
+
def __call__(
|
46 |
+
self,
|
47 |
+
frame,
|
48 |
+
pose_classification,
|
49 |
+
pose_classification_filtered,
|
50 |
+
repetitions_count,
|
51 |
+
):
|
52 |
+
"""Renders pose classifcation and counter until given frame."""
|
53 |
+
# Extend classification history.
|
54 |
+
self._pose_classification_history.append(pose_classification)
|
55 |
+
self._pose_classification_filtered_history.append(pose_classification_filtered)
|
56 |
+
|
57 |
+
# Output frame with classification plot and counter.
|
58 |
+
output_img = Image.fromarray(frame)
|
59 |
+
|
60 |
+
output_width = output_img.size[0]
|
61 |
+
output_height = output_img.size[1]
|
62 |
+
|
63 |
+
# Draw the plot.
|
64 |
+
img = self._plot_classification_history(output_width, output_height)
|
65 |
+
img.thumbnail(
|
66 |
+
(
|
67 |
+
int(output_width * self._plot_max_width),
|
68 |
+
int(output_height * self._plot_max_height),
|
69 |
+
),
|
70 |
+
Image.LANCZOS,
|
71 |
+
)
|
72 |
+
output_img.paste(
|
73 |
+
img,
|
74 |
+
(
|
75 |
+
int(output_width * self._plot_location_x),
|
76 |
+
int(output_height * self._plot_location_y),
|
77 |
+
),
|
78 |
+
)
|
79 |
+
|
80 |
+
# Draw the count.
|
81 |
+
output_img_draw = ImageDraw.Draw(output_img)
|
82 |
+
if self._counter_font is None:
|
83 |
+
font_size = int(output_height * self._counter_font_size)
|
84 |
+
font_request = requests.get(self._counter_font_path, allow_redirects=True)
|
85 |
+
self._counter_font = ImageFont.truetype(
|
86 |
+
io.BytesIO(font_request.content), size=font_size
|
87 |
+
)
|
88 |
+
output_img_draw.text(
|
89 |
+
(
|
90 |
+
output_width * self._counter_location_x,
|
91 |
+
output_height * self._counter_location_y,
|
92 |
+
),
|
93 |
+
str(repetitions_count),
|
94 |
+
font=self._counter_font,
|
95 |
+
fill=self._counter_font_color,
|
96 |
+
)
|
97 |
+
|
98 |
+
return output_img
|
99 |
+
|
100 |
+
def _plot_classification_history(self, output_width, output_height):
|
101 |
+
fig = plt.figure(figsize=self._plot_figsize)
|
102 |
+
|
103 |
+
for classification_history in [
|
104 |
+
self._pose_classification_history,
|
105 |
+
self._pose_classification_filtered_history,
|
106 |
+
]:
|
107 |
+
y = []
|
108 |
+
for classification in classification_history:
|
109 |
+
if classification is None:
|
110 |
+
y.append(None)
|
111 |
+
elif self._class_name in classification:
|
112 |
+
y.append(classification[self._class_name])
|
113 |
+
else:
|
114 |
+
y.append(0)
|
115 |
+
plt.plot(y, linewidth=7)
|
116 |
+
|
117 |
+
plt.grid(axis="y", alpha=0.75)
|
118 |
+
plt.xlabel("Frame")
|
119 |
+
plt.ylabel("Confidence")
|
120 |
+
plt.title("Classification history for `{}`".format(self._class_name))
|
121 |
+
plt.legend(loc="upper right")
|
122 |
+
|
123 |
+
if self._plot_y_max is not None:
|
124 |
+
plt.ylim(top=self._plot_y_max)
|
125 |
+
if self._plot_x_max is not None:
|
126 |
+
plt.xlim(right=self._plot_x_max)
|
127 |
+
|
128 |
+
# Convert plot to image.
|
129 |
+
buf = io.BytesIO()
|
130 |
+
dpi = min(
|
131 |
+
output_width * self._plot_max_width / float(self._plot_figsize[0]),
|
132 |
+
output_height * self._plot_max_height / float(self._plot_figsize[1]),
|
133 |
+
)
|
134 |
+
fig.savefig(buf, dpi=dpi)
|
135 |
+
buf.seek(0)
|
136 |
+
img = Image.open(buf)
|
137 |
+
plt.close()
|
138 |
+
|
139 |
+
return img
|
README.md
CHANGED
@@ -1,14 +1,191 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Projet ACV-2
|
2 |
+
|
3 |
+
## Exécution rapide
|
4 |
+
|
5 |
+
**Installation uv**
|
6 |
+
|
7 |
+
> curl -LsSf https://astral.sh/uv/install.sh | sh
|
8 |
+
|
9 |
+
> uv self update
|
10 |
+
|
11 |
+
**Installation et execution du projet**
|
12 |
+
|
13 |
+
> git clone git@github.com:LexouLam/projet-acv-2.git
|
14 |
+
|
15 |
+
> cd projet-acv-2
|
16 |
+
|
17 |
+
> uv venv --python 3.12
|
18 |
+
|
19 |
+
> uv sync
|
20 |
+
|
21 |
+
> source .venv/bin/activate
|
22 |
+
|
23 |
+
> uv run classify\_video.py live --display
|
24 |
+
|
25 |
+
OU
|
26 |
+
|
27 |
+
> uv run classify\_video.py data/videos/tree_vid_1.mp4 --display
|
28 |
+
|
29 |
+
## Documentation
|
30 |
+
|
31 |
+
Team : Impredalam
|
32 |
+
|
33 |
+
INFO GLOBALE:
|
34 |
+
|
35 |
+
Gardez en tête que la branche “main” du projet ne doit jamais être bugée, le code qu’elle contient doit
|
36 |
+
toujours pouvoir s’exécuter (sauf bug non anticipé qui nécessitera un “hot-fix”).
|
37 |
+
|
38 |
+
======
|
39 |
+
JOUR 1
|
40 |
+
======
|
41 |
+
|
42 |
+
Le thème abordé est la détection et la classification de pose humaine dans le cadre d’une application de
|
43 |
+
sport à domicile.
|
44 |
+
|
45 |
+
------------------------------------------------------------------------------
|
46 |
+
TODO JOUR 1:
|
47 |
+
|
48 |
+
---DONE--- 1. Trouver un nom pour votre groupe et un nom pour le projet.
|
49 |
+
---DONE--- 2. Mettre en place un dépôt git
|
50 |
+
---DONE--- 3. Explorer la base de code déjà existante
|
51 |
+
- algorithme de détection/classification de poses,
|
52 |
+
- algorithme de comptage sur un flux vidéo.
|
53 |
+
---DONE--- 4. Constituer une base de données annotées pour « entraîner » l’algorithme avec quelques images de vous faisant des pompes.
|
54 |
+
---DONE--- 5. Préparer une vidéo démontrant la faisabilité d’un tel projet.
|
55 |
+
---DONE--- 6. Optionnel J1 : une démo live + un repo git structuré sans notebook.
|
56 |
+
|
57 |
+
------------------------------------------------------------------------------
|
58 |
+
1. stand-up ---DONE---
|
59 |
+
|
60 |
+
2. Prise en compte des exigences client suite à
|
61 |
+
la dernière livraison, ---DONE---
|
62 |
+
|
63 |
+
3. tération de code ---DONE---
|
64 |
+
|
65 |
+
4. 16h : livraison au client ---DONE---
|
66 |
+
|
67 |
+
5. 17h : concours de pompe ---POSTPONED---
|
68 |
+
|
69 |
+
------------------------------------------------------------------------------
|
70 |
+
RESULTAT:
|
71 |
+
|
72 |
+
Programme founctionnelle, qui détecte les pompes et les compte, formé avec les images d'Internet, nos propres photos, inversées horizontalement pour rendre l'ensemble de données plus grand et plus riche.
|
73 |
+
|
74 |
+
Acev un logo de notre équipe
|
75 |
+
------------------------------------------------------------------------------
|
76 |
+
|
77 |
+
======
|
78 |
+
JOUR 2
|
79 |
+
======
|
80 |
+
|
81 |
+
Développer le vrai projet qui pourra être utilisé par la société.
|
82 |
+
Sujet: cours de yoga : classification des positions classiques
|
83 |
+
|
84 |
+
Le client veut un programme python exécutable en ligne de commande avec une interface simple.
|
85 |
+
|
86 |
+
------------------------------------------------------------------------------
|
87 |
+
TODO JOUR 2:
|
88 |
+
|
89 |
+
---DONE--- 1. Choix d’un sujet parmi les quatres proposés.
|
90 |
+
2. Planification et répartition des tâches, structuration du projet git.
|
91 |
+
---DONE--- 3. Constitution d’une base de données adaptée au sujet choisi (réalisée vous-même, ou pas ?).
|
92 |
+
4. Sortir du notebook, script avec arguments.
|
93 |
+
https://docs.python.org/3/library/argparse.html
|
94 |
+
5. Implémentation des options (prioritairement, la possibilité d’afficher des informations pour débugger le programme facilement).
|
95 |
+
6. Documentation minimale pour lancer le programme.
|
96 |
+
7. Optionnel J2 : packagisation poetry ou équivalent + (très optionnel) tests fonctionnels/unitaires.
|
97 |
+
https://github.com/features/actions
|
98 |
+
|
99 |
+
------------------------------------------------------------------------------
|
100 |
+
1. 9h : prise en main du sujet et gestion de projet (création/répartition des tâches)
|
101 |
+
|
102 |
+
2. 9h45 : début du sprint de la journée
|
103 |
+
|
104 |
+
3. 16h : livraison au client
|
105 |
+
|
106 |
+
------------------------------------------------------------------------------
|
107 |
+
RESULTAT:
|
108 |
+
|
109 |
+
test
|
110 |
+
------------------------------------------------------------------------------
|
111 |
+
|
112 |
+
======
|
113 |
+
JOUR 3
|
114 |
+
======
|
115 |
+
|
116 |
+
1. stand-up,
|
117 |
+
|
118 |
+
2. prise en compte des exigences client suite à
|
119 |
+
la dernière livraison,
|
120 |
+
|
121 |
+
3. tération de code,
|
122 |
+
|
123 |
+
4. livraison au client
|
124 |
+
|
125 |
+
|
126 |
+
|
127 |
+
## Set-up environnement
|
128 |
+
|
129 |
+
**installation uv**
|
130 |
+
|
131 |
+
> curl -LsSf https://astral.sh/uv/install.sh | sh
|
132 |
+
> uv self update
|
133 |
+
|
134 |
+
**création environnement**
|
135 |
+
|
136 |
+
> mkdir projet_acv_2
|
137 |
+
> cd projet_acv_2/
|
138 |
+
> uv init
|
139 |
+
> uv venv --python 3.12
|
140 |
+
> uv add numpy matplotlib plotly jupyter opencv-python mediapipe
|
141 |
+
> uv add tqdm requests pillow scikit-learn
|
142 |
+
|
143 |
+
**création repo git si non créé**
|
144 |
+
|
145 |
+
> touch .gitignore
|
146 |
+
> git init
|
147 |
+
> git add .
|
148 |
+
> git commit -m "start repo"
|
149 |
+
> git remote add origin git@github.com:LexouLam/projet-acv-2.git
|
150 |
+
> git push --set-upstream origin master
|
151 |
+
> git push
|
152 |
+
|
153 |
+
**clone repo git et initialisation environnement**
|
154 |
+
|
155 |
+
> git clone git@github.com:LexouLam/projet-acv-2.git
|
156 |
+
> cd projet-acv-2
|
157 |
+
> uv sync
|
158 |
+
|
159 |
+
## Arguments script "classify_video.py"
|
160 |
+
|
161 |
+
> classify_video.py arg1
|
162 |
+
|
163 |
+
Inputs
|
164 |
+
|
165 |
+
**arg1** : "path/to/video.mp4" ou "live"
|
166 |
+
|
167 |
+
Outputs
|
168 |
+
|
169 |
+
Aucun pour l'instant...
|
170 |
+
|
171 |
+
|
172 |
+
|
173 |
+
|
174 |
+
## Modèle informations
|
175 |
+
|
176 |
+
**Pose Landmark Model (BlazePose GHUM 3D)**
|
177 |
+
https://camo.githubusercontent.com/d3afebfc801ee1a094c28604c7a0eb25f8b9c9925f75b0fff4c8c8b4871c0d28/68747470733a2f2f6d65646961706970652e6465762f696d616765732f6d6f62696c652f706f73655f747261636b696e675f66756c6c5f626f64795f6c616e646d61726b732e706e67
|
178 |
+
|
179 |
+
GUIDE: https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/pose.md
|
180 |
+
|
181 |
+

|
182 |
+
|
183 |
+
Left shoulder (landmark 11)
|
184 |
+
Right shoulder (landmark 12)
|
185 |
+
Left elbow (landmark 13)
|
186 |
+
Right elbow (landmark 14)
|
187 |
+
Left wrist (landmark 15)
|
188 |
+
Right wrist (landmark 16)
|
189 |
+
Hips (landmarks 23 and 24)
|
190 |
+
|
191 |
+
|
README.md.old
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Projet ACV-2
|
2 |
+
|
3 |
+
Team : Impredalam
|
4 |
+
|
5 |
+
INFO GLOBALE:
|
6 |
+
|
7 |
+
Gardez en tête que la branche “main” du projet ne doit jamais être bugée, le code qu’elle contient doit
|
8 |
+
toujours pouvoir s’exécuter (sauf bug non anticipé qui nécessitera un “hot-fix”).
|
9 |
+
|
10 |
+
======
|
11 |
+
JOUR 1
|
12 |
+
======
|
13 |
+
|
14 |
+
Le thème abordé est la détection et la classification de pose humaine dans le cadre d’une application de
|
15 |
+
sport à domicile.
|
16 |
+
|
17 |
+
------------------------------------------------------------------------------
|
18 |
+
TODO JOUR 1:
|
19 |
+
|
20 |
+
---DONE--- 1. Trouver un nom pour votre groupe et un nom pour le projet.
|
21 |
+
---DONE--- 2. Mettre en place un dépôt git
|
22 |
+
---DONE--- 3. Explorer la base de code déjà existante
|
23 |
+
- algorithme de détection/classification de poses,
|
24 |
+
- algorithme de comptage sur un flux vidéo.
|
25 |
+
---DONE--- 4. Constituer une base de données annotées pour « entraîner » l’algorithme avec quelques images de vous faisant des pompes.
|
26 |
+
---DONE--- 5. Préparer une vidéo démontrant la faisabilité d’un tel projet.
|
27 |
+
---DONE--- 6. Optionnel J1 : une démo live + un repo git structuré sans notebook.
|
28 |
+
|
29 |
+
------------------------------------------------------------------------------
|
30 |
+
1. stand-up ---DONE---
|
31 |
+
|
32 |
+
2. Prise en compte des exigences client suite à
|
33 |
+
la dernière livraison, ---DONE---
|
34 |
+
|
35 |
+
3. tération de code ---DONE---
|
36 |
+
|
37 |
+
4. 16h : livraison au client ---DONE---
|
38 |
+
|
39 |
+
5. 17h : concours de pompe ---POSTPONED---
|
40 |
+
|
41 |
+
------------------------------------------------------------------------------
|
42 |
+
RESULTAT:
|
43 |
+
|
44 |
+
Programme founctionnelle, qui détecte les pompes et les compte, formé avec les images d'Internet, nos propres photos, inversées horizontalement pour rendre l'ensemble de données plus grand et plus riche.
|
45 |
+
|
46 |
+
Acev un logo de notre équipe
|
47 |
+
------------------------------------------------------------------------------
|
48 |
+
|
49 |
+
======
|
50 |
+
JOUR 2
|
51 |
+
======
|
52 |
+
|
53 |
+
Développer le vrai projet qui pourra être utilisé par la société.
|
54 |
+
Sujet: cours de yoga : classification des positions classiques
|
55 |
+
|
56 |
+
Le client veut un programme python exécutable en ligne de commande avec une interface simple.
|
57 |
+
|
58 |
+
------------------------------------------------------------------------------
|
59 |
+
TODO JOUR 2:
|
60 |
+
|
61 |
+
---DONE--- 1. Choix d’un sujet parmi les quatres proposés.
|
62 |
+
2. Planification et répartition des tâches, structuration du projet git.
|
63 |
+
---DONE--- 3. Constitution d’une base de données adaptée au sujet choisi (réalisée vous-même, ou pas ?).
|
64 |
+
4. Sortir du notebook, script avec arguments.
|
65 |
+
https://docs.python.org/3/library/argparse.html
|
66 |
+
5. Implémentation des options (prioritairement, la possibilité d’afficher des informations pour débugger le programme facilement).
|
67 |
+
6. Documentation minimale pour lancer le programme.
|
68 |
+
7. Optionnel J2 : packagisation poetry ou équivalent + (très optionnel) tests fonctionnels/unitaires.
|
69 |
+
https://github.com/features/actions
|
70 |
+
|
71 |
+
------------------------------------------------------------------------------
|
72 |
+
1. 9h : prise en main du sujet et gestion de projet (création/répartition des tâches)
|
73 |
+
|
74 |
+
2. 9h45 : début du sprint de la journée
|
75 |
+
|
76 |
+
3. 16h : livraison au client
|
77 |
+
|
78 |
+
------------------------------------------------------------------------------
|
79 |
+
RESULTAT:
|
80 |
+
|
81 |
+
test
|
82 |
+
------------------------------------------------------------------------------
|
83 |
+
|
84 |
+
======
|
85 |
+
JOUR 3
|
86 |
+
======
|
87 |
+
|
88 |
+
1. stand-up,
|
89 |
+
|
90 |
+
2. prise en compte des exigences client suite à
|
91 |
+
la dernière livraison,
|
92 |
+
|
93 |
+
3. tération de code,
|
94 |
+
|
95 |
+
4. livraison au client
|
96 |
+
|
97 |
+
|
98 |
+
|
99 |
+
## Set-up environnement
|
100 |
+
|
101 |
+
**installation uv**
|
102 |
+
|
103 |
+
> curl -LsSf https://astral.sh/uv/install.sh | sh
|
104 |
+
> uv self update
|
105 |
+
|
106 |
+
**création environnement**
|
107 |
+
|
108 |
+
> mkdir projet_acv_2
|
109 |
+
> cd projet_acv_2/
|
110 |
+
> uv init
|
111 |
+
> uv venv --python 3.12
|
112 |
+
> uv add numpy matplotlib plotly jupyter opencv-python mediapipe
|
113 |
+
> uv add tqdm requests pillow scikit-learn
|
114 |
+
|
115 |
+
**création repo git si non créé**
|
116 |
+
|
117 |
+
> touch .gitignore
|
118 |
+
> git init
|
119 |
+
> git add .
|
120 |
+
> git commit -m "start repo"
|
121 |
+
> git remote add origin git@github.com:LexouLam/projet-acv-2.git
|
122 |
+
> git push --set-upstream origin master
|
123 |
+
> git push
|
124 |
+
|
125 |
+
**clone repo git et initialisation environnement**
|
126 |
+
|
127 |
+
> git clone git@github.com:LexouLam/projet-acv-2.git
|
128 |
+
> cd projet-acv-2
|
129 |
+
> uv sync
|
130 |
+
|
131 |
+
|
132 |
+
|
133 |
+
**Pose Landmark Model (BlazePose GHUM 3D)**
|
134 |
+
https://camo.githubusercontent.com/d3afebfc801ee1a094c28604c7a0eb25f8b9c9925f75b0fff4c8c8b4871c0d28/68747470733a2f2f6d65646961706970652e6465762f696d616765732f6d6f62696c652f706f73655f747261636b696e675f66756c6c5f626f64795f6c616e646d61726b732e706e67
|
135 |
+
|
136 |
+
GUIDE: https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/pose.md
|
137 |
+
|
138 |
+

|
139 |
+
|
140 |
+
Left shoulder (landmark 11)
|
141 |
+
Right shoulder (landmark 12)
|
142 |
+
Left elbow (landmark 13)
|
143 |
+
Right elbow (landmark 14)
|
144 |
+
Left wrist (landmark 15)
|
145 |
+
Right wrist (landmark 16)
|
146 |
+
Hips (landmarks 23 and 24)
|
app.py
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from interface_pages.home_page import home_page
|
3 |
+
from interface_pages.about_page import about_page
|
4 |
+
from interface_pages.yoga_position_from_stream import yoga_position_from_stream
|
5 |
+
from interface_pages.yoga_position_from_video import yoga_position_from_video
|
6 |
+
|
7 |
+
def main(page):
|
8 |
+
if page == "Home":
|
9 |
+
return home_page()
|
10 |
+
elif page == "About us":
|
11 |
+
return about_page()
|
12 |
+
elif page == "Yoga from stream":
|
13 |
+
return yoga_position_from_stream()
|
14 |
+
elif page == "Yoga from video":
|
15 |
+
return yoga_position_from_video()
|
16 |
+
|
17 |
+
def interface():
|
18 |
+
with gr.Blocks(css="static/styles.css") as demo:
|
19 |
+
|
20 |
+
# Layout with a Row to hold buttons and content
|
21 |
+
with gr.Row():
|
22 |
+
with gr.Column(scale=1, elem_classes=["menu-column"]):
|
23 |
+
# Vertical Navigation Buttons
|
24 |
+
home_button = gr.Button("Home", elem_classes=["menu-button"])
|
25 |
+
about_button = gr.Button("About us", elem_classes=["menu-button"])
|
26 |
+
yoga_stream_button = gr.Button("Yoga from stream", elem_classes=["menu-button"])
|
27 |
+
yoga_video_button = gr.Button("Yoga from video", elem_classes=["menu-button"])
|
28 |
+
|
29 |
+
# Create page contents
|
30 |
+
with gr.Column(elem_id="page-content") as page_content:
|
31 |
+
home_page_content = home_page()
|
32 |
+
about_page_content = about_page()
|
33 |
+
yoga_stream_content = yoga_position_from_stream()
|
34 |
+
yoga_video_content = yoga_position_from_video()
|
35 |
+
|
36 |
+
# Set initial visibility
|
37 |
+
home_page_content.visible = True
|
38 |
+
about_page_content.visible = False
|
39 |
+
yoga_stream_content.visible = False
|
40 |
+
yoga_video_content.visible = False
|
41 |
+
|
42 |
+
# Button click handlers
|
43 |
+
def show_page(page):
|
44 |
+
return [
|
45 |
+
gr.update(visible=(content == page))
|
46 |
+
for content in [
|
47 |
+
home_page_content,
|
48 |
+
about_page_content,
|
49 |
+
yoga_stream_content,
|
50 |
+
yoga_video_content,
|
51 |
+
]
|
52 |
+
]
|
53 |
+
|
54 |
+
home_button.click(
|
55 |
+
lambda: show_page(home_page_content),
|
56 |
+
outputs=[
|
57 |
+
home_page_content,
|
58 |
+
about_page_content,
|
59 |
+
yoga_stream_content,
|
60 |
+
yoga_video_content,
|
61 |
+
],
|
62 |
+
)
|
63 |
+
about_button.click(
|
64 |
+
lambda: show_page(about_page_content),
|
65 |
+
outputs=[
|
66 |
+
home_page_content,
|
67 |
+
about_page_content,
|
68 |
+
yoga_stream_content,
|
69 |
+
yoga_video_content,
|
70 |
+
],
|
71 |
+
)
|
72 |
+
yoga_stream_button.click(
|
73 |
+
lambda: show_page(yoga_stream_content),
|
74 |
+
outputs=[
|
75 |
+
home_page_content,
|
76 |
+
about_page_content,
|
77 |
+
yoga_stream_content,
|
78 |
+
yoga_video_content,
|
79 |
+
],
|
80 |
+
)
|
81 |
+
yoga_video_button.click(
|
82 |
+
lambda: show_page(yoga_video_content),
|
83 |
+
outputs=[
|
84 |
+
home_page_content,
|
85 |
+
about_page_content,
|
86 |
+
yoga_stream_content,
|
87 |
+
yoga_video_content,
|
88 |
+
],
|
89 |
+
)
|
90 |
+
|
91 |
+
return demo
|
92 |
+
|
93 |
+
|
94 |
+
if __name__ == "__main__":
|
95 |
+
interface().launch(share=True)
|
classify_video.py
ADDED
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import sys
|
3 |
+
import cv2
|
4 |
+
import numpy as np
|
5 |
+
from rich.console import Console
|
6 |
+
from rich.panel import Panel
|
7 |
+
from rich.align import Align
|
8 |
+
from rich.layout import Layout
|
9 |
+
from pyfiglet import Figlet
|
10 |
+
import mediapipe as mp
|
11 |
+
from PoseClassification.pose_embedding import FullBodyPoseEmbedding
|
12 |
+
from PoseClassification.pose_classifier import PoseClassifier
|
13 |
+
from PoseClassification.utils import EMADictSmoothing
|
14 |
+
from PoseClassification.visualize import PoseClassificationVisualizer
|
15 |
+
|
16 |
+
# For cross-platform compatibility
|
17 |
+
try:
|
18 |
+
import msvcrt # Windows
|
19 |
+
except ImportError:
|
20 |
+
import termios # Unix-like
|
21 |
+
import tty
|
22 |
+
|
23 |
+
|
24 |
+
def getch():
|
25 |
+
if sys.platform == "win32":
|
26 |
+
return msvcrt.getch().decode("utf-8")
|
27 |
+
else:
|
28 |
+
fd = sys.stdin.fileno()
|
29 |
+
old_settings = termios.tcgetattr(fd)
|
30 |
+
try:
|
31 |
+
tty.setraw(sys.stdin.fileno())
|
32 |
+
ch = sys.stdin.read(1)
|
33 |
+
finally:
|
34 |
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
35 |
+
return ch
|
36 |
+
|
37 |
+
|
38 |
+
def create_ascii_title(text):
|
39 |
+
f = Figlet(font="isometric2")
|
40 |
+
return f.renderText(text)
|
41 |
+
|
42 |
+
|
43 |
+
def main(input_source, display=False, output_file=None):
|
44 |
+
console = Console()
|
45 |
+
layout = Layout()
|
46 |
+
|
47 |
+
# Create ASCII title
|
48 |
+
ascii_title = create_ascii_title("YOGAI")
|
49 |
+
|
50 |
+
# Create the layout
|
51 |
+
layout.split(
|
52 |
+
Layout(Panel(Align.center(ascii_title), border_style="bold blue"), size=15),
|
53 |
+
Layout(name="main"),
|
54 |
+
)
|
55 |
+
|
56 |
+
is_live = input_source == "live"
|
57 |
+
if is_live:
|
58 |
+
layout["main"].update(
|
59 |
+
Panel(
|
60 |
+
"Processing live video from camera",
|
61 |
+
title="Video Classification",
|
62 |
+
border_style="bold blue",
|
63 |
+
)
|
64 |
+
)
|
65 |
+
else:
|
66 |
+
layout["main"].update(
|
67 |
+
Panel(
|
68 |
+
f"Processing video: {input_source}",
|
69 |
+
title="Video Classification",
|
70 |
+
border_style="bold blue",
|
71 |
+
)
|
72 |
+
)
|
73 |
+
|
74 |
+
console.print(layout)
|
75 |
+
|
76 |
+
# Initialize pose tracker, embedder, and classifier
|
77 |
+
mp_pose = mp.solutions.pose
|
78 |
+
pose_tracker = mp_pose.Pose()
|
79 |
+
pose_embedder = FullBodyPoseEmbedding()
|
80 |
+
pose_classifier = PoseClassifier(
|
81 |
+
pose_samples_folder="data/yoga_poses_csvs_out",
|
82 |
+
pose_embedder=pose_embedder,
|
83 |
+
top_n_by_max_distance=30,
|
84 |
+
top_n_by_mean_distance=10,
|
85 |
+
)
|
86 |
+
pose_classification_filter = EMADictSmoothing(window_size=10, alpha=0.2)
|
87 |
+
|
88 |
+
# Open the video source
|
89 |
+
if is_live:
|
90 |
+
video = cv2.VideoCapture(0)
|
91 |
+
fps = 30 # Assume 30 fps for live video
|
92 |
+
total_frames = float("inf") # Infinite frames for live video
|
93 |
+
else:
|
94 |
+
video = cv2.VideoCapture(input_source)
|
95 |
+
fps = video.get(cv2.CAP_PROP_FPS)
|
96 |
+
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
97 |
+
|
98 |
+
# Initialize pose timings (use lowercase for keys)
|
99 |
+
pose_timings = {
|
100 |
+
"chair": 0,
|
101 |
+
"cobra": 0,
|
102 |
+
"dog": 0,
|
103 |
+
"plank": 0,
|
104 |
+
"goddess": 0,
|
105 |
+
"tree": 0,
|
106 |
+
"warrior": 0,
|
107 |
+
"no pose detected": 0,
|
108 |
+
"fallen": 0,
|
109 |
+
}
|
110 |
+
|
111 |
+
frame_count = 0
|
112 |
+
while True:
|
113 |
+
ret, frame = video.read()
|
114 |
+
if not ret:
|
115 |
+
if is_live:
|
116 |
+
console.print(
|
117 |
+
"[bold red]Error reading from camera. Exiting...[/bold red]"
|
118 |
+
)
|
119 |
+
break
|
120 |
+
|
121 |
+
# Process the frame
|
122 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
123 |
+
result = pose_tracker.process(image=frame_rgb)
|
124 |
+
|
125 |
+
if result.pose_landmarks is not None:
|
126 |
+
# Draw landmarks on the frame
|
127 |
+
mp.solutions.drawing_utils.draw_landmarks(
|
128 |
+
frame, result.pose_landmarks, mp_pose.POSE_CONNECTIONS
|
129 |
+
)
|
130 |
+
|
131 |
+
frame_height, frame_width = frame.shape[0], frame.shape[1]
|
132 |
+
pose_landmarks = np.array(
|
133 |
+
[
|
134 |
+
[lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
135 |
+
for lmk in result.pose_landmarks.landmark
|
136 |
+
],
|
137 |
+
dtype=np.float32,
|
138 |
+
)
|
139 |
+
|
140 |
+
# Classify the pose
|
141 |
+
pose_classification = pose_classifier(pose_landmarks)
|
142 |
+
pose_classification_filtered = pose_classification_filter(
|
143 |
+
pose_classification
|
144 |
+
)
|
145 |
+
|
146 |
+
# Update pose timings (only for the pose with highest confidence)
|
147 |
+
max_pose = max(
|
148 |
+
pose_classification_filtered, key=pose_classification_filtered.get
|
149 |
+
).lower()
|
150 |
+
pose_timings[max_pose] += 1 / fps
|
151 |
+
else:
|
152 |
+
pose_timings["no pose detected"] += 1 / fps
|
153 |
+
|
154 |
+
frame_count += 1
|
155 |
+
if frame_count % 30 == 0: # Update every 30 frames
|
156 |
+
panel_content = (
|
157 |
+
f"[bold]Chair:[/bold] {pose_timings['chair']:.2f}s\n"
|
158 |
+
f"[bold]Cobra:[/bold] {pose_timings['cobra']:.2f}s\n"
|
159 |
+
f"[bold]Dog:[/bold] {pose_timings['dog']:.2f}s\n"
|
160 |
+
f"[bold]Plank:[/bold] {pose_timings['plank']:.2f}s\n"
|
161 |
+
f"[bold]Goddess:[/bold] {pose_timings['goddess']:.2f}s\n"
|
162 |
+
f"[bold]Tree:[/bold] {pose_timings['tree']:.2f}s\n"
|
163 |
+
f"[bold]Warrior:[/bold] {pose_timings['warrior']:.2f}s\n"
|
164 |
+
f"---\n"
|
165 |
+
f"[bold]No pose detected:[/bold] {pose_timings['no pose detected']:.2f}s\n"
|
166 |
+
f"[bold]Fallen:[/bold] {pose_timings['fallen']:.2f}s"
|
167 |
+
)
|
168 |
+
if not is_live:
|
169 |
+
panel_content += f"\n\nProcessed {frame_count}/{total_frames} frames"
|
170 |
+
|
171 |
+
layout["main"].update(
|
172 |
+
Panel(
|
173 |
+
panel_content,
|
174 |
+
title="Classification Results",
|
175 |
+
border_style="bold green",
|
176 |
+
)
|
177 |
+
)
|
178 |
+
console.print(layout)
|
179 |
+
|
180 |
+
if display:
|
181 |
+
cv2.imshow("Video", frame)
|
182 |
+
if cv2.waitKey(1) & 0xFF == ord("q"):
|
183 |
+
break
|
184 |
+
|
185 |
+
video.release()
|
186 |
+
if display:
|
187 |
+
cv2.destroyAllWindows()
|
188 |
+
|
189 |
+
# Final results
|
190 |
+
final_panel_content = (
|
191 |
+
f"[bold]Chair:[/bold] {pose_timings['chair']:.2f}s\n"
|
192 |
+
f"[bold]Cobra:[/bold] {pose_timings['cobra']:.2f}s\n"
|
193 |
+
f"[bold]Dog:[/bold] {pose_timings['dog']:.2f}s\n"
|
194 |
+
f"[bold]Plank:[/bold] {pose_timings['plank']:.2f}s\n"
|
195 |
+
f"[bold]Goddess:[/bold] {pose_timings['goddess']:.2f}s\n"
|
196 |
+
f"[bold]Tree:[/bold] {pose_timings['tree']:.2f}s\n"
|
197 |
+
f"[bold]Warrior:[/bold] {pose_timings['warrior']:.2f}s\n"
|
198 |
+
f"---\n"
|
199 |
+
f"[bold]No pose detected:[/bold] {pose_timings['no pose detected']:.2f}s\n"
|
200 |
+
f"[bold]Fallen:[/bold] {pose_timings['fallen']:.2f}s"
|
201 |
+
)
|
202 |
+
layout["main"].update(
|
203 |
+
Panel(
|
204 |
+
final_panel_content,
|
205 |
+
title="Final Classification Results",
|
206 |
+
border_style="bold green",
|
207 |
+
)
|
208 |
+
)
|
209 |
+
console.print(layout)
|
210 |
+
|
211 |
+
if output_file:
|
212 |
+
console.print(f"[green]Output saved to: {output_file}[/green]")
|
213 |
+
|
214 |
+
|
215 |
+
if __name__ == "__main__":
|
216 |
+
parser = argparse.ArgumentParser(
|
217 |
+
description="Classify poses in a video file or from live camera."
|
218 |
+
)
|
219 |
+
parser.add_argument("input", help="Input video file or 'live' for camera feed")
|
220 |
+
parser.add_argument(
|
221 |
+
"--display", action="store_true", help="Display the video with detected poses"
|
222 |
+
)
|
223 |
+
parser.add_argument("--output", help="Output video file")
|
224 |
+
|
225 |
+
if len(sys.argv) == 1:
|
226 |
+
parser.print_help(sys.stderr)
|
227 |
+
sys.exit(1)
|
228 |
+
|
229 |
+
args = parser.parse_args()
|
230 |
+
|
231 |
+
main(args.input, args.display, args.output)
|
hello.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def main():
|
2 |
+
print("Hello from projet-acv-2!")
|
3 |
+
|
4 |
+
|
5 |
+
if __name__ == "__main__":
|
6 |
+
main()
|
interface_pages/__init__.py
ADDED
File without changes
|
interface_pages/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (184 Bytes). View file
|
|
interface_pages/__pycache__/about_page.cpython-312.pyc
ADDED
Binary file (438 Bytes). View file
|
|
interface_pages/__pycache__/home_page.cpython-312.pyc
ADDED
Binary file (500 Bytes). View file
|
|
interface_pages/__pycache__/yoga_position_from_stream.cpython-312.pyc
ADDED
Binary file (1.61 kB). View file
|
|
interface_pages/__pycache__/yoga_position_from_video.cpython-312.pyc
ADDED
Binary file (613 Bytes). View file
|
|
interface_pages/about_page.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
|
3 |
+
|
4 |
+
def about_page():
|
5 |
+
return gr.Markdown(
|
6 |
+
"""
|
7 |
+
# About Us
|
8 |
+
|
9 |
+
WYOGAI — the BEST.
|
10 |
+
"""
|
11 |
+
)
|
interface_pages/home_page.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
|
3 |
+
|
4 |
+
def home_page():
|
5 |
+
return gr.Markdown(
|
6 |
+
"""
|
7 |
+
# Welcome to YOGAI App!
|
8 |
+
|
9 |
+
This is your home page where you can explore different yoga practices.
|
10 |
+
"""
|
11 |
+
)
|
interface_pages/yoga_position_from_stream.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
|
3 |
+
|
4 |
+
def yoga_position_from_stream():
|
5 |
+
def download_video(video_path):
|
6 |
+
if video_path:
|
7 |
+
return video_path
|
8 |
+
return None
|
9 |
+
|
10 |
+
with gr.Column() as yoga_stream:
|
11 |
+
gr.Markdown("# Yoga from Stream")
|
12 |
+
gr.Markdown(
|
13 |
+
"Stream live yoga sessions and practice along with our expert instructors."
|
14 |
+
)
|
15 |
+
video_feed = gr.Video(source="webcam", streaming=True, interactive=True)
|
16 |
+
download_button = gr.Button("Download Recorded Video")
|
17 |
+
video_output = gr.Video()
|
18 |
+
|
19 |
+
download_button.click(
|
20 |
+
download_video,
|
21 |
+
inputs=[video_feed], # Changed from video_output to video_feed
|
22 |
+
outputs=[gr.File()],
|
23 |
+
)
|
24 |
+
|
25 |
+
return yoga_stream
|
26 |
+
|
27 |
+
|
28 |
+
if __name__ == "__main__":
|
29 |
+
with gr.Blocks() as demo:
|
30 |
+
yoga_position_from_stream()
|
31 |
+
demo.launch()
|
interface_pages/yoga_position_from_video.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
|
3 |
+
|
4 |
+
def yoga_position_from_video():
|
5 |
+
return gr.Markdown(
|
6 |
+
"""
|
7 |
+
# Yoga from Video
|
8 |
+
|
9 |
+
Watch pre-recorded yoga sessions and practice at your convenience.
|
10 |
+
|
11 |
+
Select a video below:
|
12 |
+
|
13 |
+
- Beginner Yoga
|
14 |
+
- Advanced Techniques
|
15 |
+
- Restorative Yoga
|
16 |
+
"""
|
17 |
+
)
|
pushups_counter.py
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import tqdm
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
from mediapipe.python.solutions import drawing_utils as mp_drawing
|
5 |
+
import mediapipe as mp
|
6 |
+
from PoseClassification.pose_embedding import FullBodyPoseEmbedding
|
7 |
+
from PoseClassification.pose_classifier import PoseClassifier
|
8 |
+
from PoseClassification.utils import EMADictSmoothing
|
9 |
+
from PoseClassification.utils import RepetitionCounter
|
10 |
+
from PoseClassification.visualize import PoseClassificationVisualizer
|
11 |
+
|
12 |
+
mp_pose = mp.solutions.pose
|
13 |
+
pose_tracker = mp_pose.Pose()
|
14 |
+
|
15 |
+
pose_samples_folder = "data/fitness_poses_csvs_out"
|
16 |
+
class_name = "pushups_down"
|
17 |
+
|
18 |
+
pose_embedder = FullBodyPoseEmbedding()
|
19 |
+
|
20 |
+
pose_classifier = PoseClassifier(
|
21 |
+
pose_samples_folder=pose_samples_folder,
|
22 |
+
pose_embedder=pose_embedder,
|
23 |
+
top_n_by_max_distance=30,
|
24 |
+
top_n_by_mean_distance=10,
|
25 |
+
)
|
26 |
+
|
27 |
+
pose_classification_filter = EMADictSmoothing(window_size=10, alpha=0.2)
|
28 |
+
|
29 |
+
repetition_counter = RepetitionCounter(
|
30 |
+
class_name=class_name, enter_threshold=6, exit_threshold=4
|
31 |
+
)
|
32 |
+
|
33 |
+
pose_classification_visualizer = PoseClassificationVisualizer(
|
34 |
+
class_name=class_name, plot_x_max=1000, plot_y_max=10
|
35 |
+
)
|
36 |
+
|
37 |
+
video_cap = cv2.VideoCapture(0)
|
38 |
+
video_fps = 30
|
39 |
+
video_width = 1280
|
40 |
+
video_height = 720
|
41 |
+
video_cap.set(cv2.CAP_PROP_FRAME_WIDTH, video_width)
|
42 |
+
video_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, video_height)
|
43 |
+
|
44 |
+
frame_idx = 0
|
45 |
+
output_frame = None
|
46 |
+
|
47 |
+
try:
|
48 |
+
with tqdm.tqdm(position=0, leave=True) as pbar:
|
49 |
+
while True:
|
50 |
+
success, input_frame = video_cap.read()
|
51 |
+
if not success:
|
52 |
+
print("Unable to read input video frame, breaking!")
|
53 |
+
break
|
54 |
+
|
55 |
+
# Run pose tracker
|
56 |
+
input_frame_rgb = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
57 |
+
result = pose_tracker.process(image=input_frame_rgb)
|
58 |
+
pose_landmarks = result.pose_landmarks
|
59 |
+
|
60 |
+
# Prepare the output frame
|
61 |
+
output_frame = input_frame.copy()
|
62 |
+
|
63 |
+
# Add a white banner on top
|
64 |
+
banner_height = 180
|
65 |
+
output_frame[0:banner_height, :] = (255, 255, 255) # White color
|
66 |
+
|
67 |
+
# Load the logo image
|
68 |
+
logo = cv2.imread("src/logo_impredalam.jpg")
|
69 |
+
logo_height, logo_width = logo.shape[:2]
|
70 |
+
logo = cv2.resize(
|
71 |
+
logo, (logo_width // 3, logo_height // 3)
|
72 |
+
) # Resize to 1/3 scale
|
73 |
+
|
74 |
+
# Overlay the logo on the upper right corner
|
75 |
+
output_frame[0 : logo.shape[0], output_frame.shape[1] - logo.shape[1] :] = (
|
76 |
+
logo
|
77 |
+
)
|
78 |
+
if pose_landmarks is not None:
|
79 |
+
mp_drawing.draw_landmarks(
|
80 |
+
image=output_frame,
|
81 |
+
landmark_list=pose_landmarks,
|
82 |
+
connections=mp_pose.POSE_CONNECTIONS,
|
83 |
+
)
|
84 |
+
|
85 |
+
# Get landmarks
|
86 |
+
frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
87 |
+
pose_landmarks = np.array(
|
88 |
+
[
|
89 |
+
[lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
90 |
+
for lmk in pose_landmarks.landmark
|
91 |
+
],
|
92 |
+
dtype=np.float32,
|
93 |
+
)
|
94 |
+
assert pose_landmarks.shape == (
|
95 |
+
33,
|
96 |
+
3,
|
97 |
+
), "Unexpected landmarks shape: {}".format(pose_landmarks.shape)
|
98 |
+
|
99 |
+
# Classify the pose on the current frame
|
100 |
+
pose_classification = pose_classifier(pose_landmarks)
|
101 |
+
|
102 |
+
# Smooth classification using EMA
|
103 |
+
pose_classification_filtered = pose_classification_filter(
|
104 |
+
pose_classification
|
105 |
+
)
|
106 |
+
|
107 |
+
# Count repetitions
|
108 |
+
repetitions_count = repetition_counter(pose_classification_filtered)
|
109 |
+
|
110 |
+
# Display repetitions count on the frame
|
111 |
+
cv2.putText(
|
112 |
+
output_frame,
|
113 |
+
f"Push-Ups: {repetitions_count}",
|
114 |
+
(10, 30),
|
115 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
116 |
+
1,
|
117 |
+
(0, 0, 0),
|
118 |
+
2,
|
119 |
+
cv2.LINE_AA,
|
120 |
+
)
|
121 |
+
# Display classified pose on the frame
|
122 |
+
cv2.putText(
|
123 |
+
output_frame,
|
124 |
+
f"Pose: {pose_classification}",
|
125 |
+
(10, 70),
|
126 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
127 |
+
1.2, # Smaller font size
|
128 |
+
(0, 0, 0),
|
129 |
+
1, # Thinner line
|
130 |
+
cv2.LINE_AA,
|
131 |
+
)
|
132 |
+
else:
|
133 |
+
# If no landmarks are detected, still display the last count
|
134 |
+
repetitions_count = repetition_counter.n_repeats
|
135 |
+
cv2.putText(
|
136 |
+
output_frame,
|
137 |
+
f"Push-Ups: {repetitions_count}",
|
138 |
+
(10, 30),
|
139 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
140 |
+
1,
|
141 |
+
(0, 255, 0),
|
142 |
+
2,
|
143 |
+
cv2.LINE_AA,
|
144 |
+
)
|
145 |
+
|
146 |
+
cv2.imshow("Push-Up Counter", output_frame)
|
147 |
+
|
148 |
+
key = cv2.waitKey(1) & 0xFF
|
149 |
+
if key == ord("q"):
|
150 |
+
break
|
151 |
+
elif key == ord("r"):
|
152 |
+
repetition_counter.reset()
|
153 |
+
print("Counter reset!")
|
154 |
+
|
155 |
+
frame_idx += 1
|
156 |
+
pbar.update()
|
157 |
+
|
158 |
+
finally:
|
159 |
+
|
160 |
+
pose_tracker.close()
|
161 |
+
video_cap.release()
|
162 |
+
cv2.destroyAllWindows()
|
pyproject.toml
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "projet-acv-2"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"argparse>=1.4.0",
|
9 |
+
"ffmpeg>=1.4",
|
10 |
+
"gradio>=3.36.1",
|
11 |
+
"jupyter>=1.1.1",
|
12 |
+
"matplotlib>=3.9.2",
|
13 |
+
"mediapipe>=0.10.15",
|
14 |
+
"numpy>=1.26.4",
|
15 |
+
"opencv-python>=4.10.0.84",
|
16 |
+
"pillow>=11.0.0",
|
17 |
+
"plotly>=5.24.1",
|
18 |
+
"pyfiglet>=1.0.2",
|
19 |
+
"requests>=2.32.3",
|
20 |
+
"rich>=13.9.2",
|
21 |
+
"scikit-learn>=1.5.2",
|
22 |
+
"streamlit>=1.9.0",
|
23 |
+
"tqdm>=4.66.5",
|
24 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
argparse>=1.4.0
|
2 |
+
ffmpeg>=1.4
|
3 |
+
gradio>=3.36.1
|
4 |
+
jupyter>=1.1.1
|
5 |
+
matplotlib>=3.9.2
|
6 |
+
mediapipe>=0.10.15
|
7 |
+
numpy>=1.26.4
|
8 |
+
opencv-python>=4.10.0.84
|
9 |
+
pillow>=11.0.0
|
10 |
+
plotly>=5.24.1
|
11 |
+
pyfiglet>=1.0.2
|
12 |
+
requests>=2.32.3
|
13 |
+
rich>=13.9.2
|
14 |
+
scikit-learn>=1.5.2
|
15 |
+
streamlit>=1.9.0
|
16 |
+
tqdm>=4.66.5
|
src/image.png
ADDED
![]() |
src/logo_impredalam.jpg
ADDED
![]() |
static/styles.css
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.menu-column {
|
2 |
+
background-color: #4CAF50; /* Background color of the menu */
|
3 |
+
padding: 20px; /* Padding around the menu */
|
4 |
+
height: 100vh; /* Full height for the menu */
|
5 |
+
}
|
6 |
+
|
7 |
+
.menu-button {
|
8 |
+
color: white; /* Text color for the buttons */
|
9 |
+
background-color: transparent; /* Transparent background */
|
10 |
+
border: none; /* No border */
|
11 |
+
padding: 10px 15px; /* Padding for the buttons */
|
12 |
+
width: 100%; /* Full width for buttons */
|
13 |
+
text-align: left; /* Align text to the left */
|
14 |
+
cursor: pointer; /* Pointer cursor on hover */
|
15 |
+
transition: background-color 0.3s; /* Smooth transition */
|
16 |
+
}
|
17 |
+
|
18 |
+
.menu-button:hover {
|
19 |
+
background-color: rgba(255, 255, 255, 0.2); /* Light hover effect */
|
20 |
+
}
|
21 |
+
|
22 |
+
.gradio-container {
|
23 |
+
margin-top: 0; /* Remove top margin to allow for full height */
|
24 |
+
}
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
yoga_position.py
ADDED
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import tqdm
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
import re
|
5 |
+
import os
|
6 |
+
from mediapipe.python.solutions import drawing_utils as mp_drawing
|
7 |
+
import mediapipe as mp
|
8 |
+
from PoseClassification.pose_embedding import FullBodyPoseEmbedding
|
9 |
+
from PoseClassification.pose_classifier import PoseClassifier
|
10 |
+
from PoseClassification.utils import EMADictSmoothing
|
11 |
+
# from PoseClassification.utils import RepetitionCounter
|
12 |
+
from PoseClassification.visualize import PoseClassificationVisualizer
|
13 |
+
import argparse
|
14 |
+
from PoseClassification.utils import show_image
|
15 |
+
|
16 |
+
def main():
|
17 |
+
#Load arguments
|
18 |
+
parser = argparse.ArgumentParser()
|
19 |
+
parser.add_argument("video_path", help="string video path in")
|
20 |
+
args = parser.parse_args()
|
21 |
+
|
22 |
+
video_path_in = args.video_path
|
23 |
+
direct_video=False
|
24 |
+
if video_path_in=="live":
|
25 |
+
video_path_in='data/live.mp4'
|
26 |
+
direct_video=True
|
27 |
+
|
28 |
+
video_path_out = re.sub(r'.mp4', r'_classified_video.mp4', video_path_in)
|
29 |
+
results_classification_path_out = re.sub(r'.mp4', r'_classified_results.csv', video_path_in)
|
30 |
+
|
31 |
+
|
32 |
+
# Instruction if direct flux video : not for now
|
33 |
+
if direct_video :
|
34 |
+
video_cap = cv2.VideoCapture(0)
|
35 |
+
video_fps = 30
|
36 |
+
video_width = 1280
|
37 |
+
video_height = 720
|
38 |
+
|
39 |
+
class_name='tree'
|
40 |
+
|
41 |
+
# Initialize tracker, classifier and current position.
|
42 |
+
# Initialize tracker.
|
43 |
+
mp_pose = mp.solutions.pose
|
44 |
+
pose_tracker = mp_pose.Pose()
|
45 |
+
# Folder with pose class CSVs. That should be the same folder you used while
|
46 |
+
# building classifier to output CSVs.
|
47 |
+
pose_samples_folder = 'data/yoga_poses_csvs_out'
|
48 |
+
# Initialize embedder.
|
49 |
+
pose_embedder = FullBodyPoseEmbedding()
|
50 |
+
# Initialize classifier.
|
51 |
+
# Check that you are using the same parameters as during bootstrapping.
|
52 |
+
pose_classifier = PoseClassifier(
|
53 |
+
pose_samples_folder=pose_samples_folder,
|
54 |
+
pose_embedder=pose_embedder,
|
55 |
+
top_n_by_max_distance=30,
|
56 |
+
top_n_by_mean_distance=10)
|
57 |
+
|
58 |
+
# Initialize list of results
|
59 |
+
position_list=[]
|
60 |
+
frame_list=[]
|
61 |
+
|
62 |
+
# Initialize EMA smoothing.
|
63 |
+
pose_classification_filter = EMADictSmoothing(
|
64 |
+
window_size=10,
|
65 |
+
alpha=0.2)
|
66 |
+
|
67 |
+
# Initialize renderer.
|
68 |
+
pose_classification_visualizer = PoseClassificationVisualizer(
|
69 |
+
class_name=class_name,
|
70 |
+
plot_x_max=1000,
|
71 |
+
# Graphic looks nicer if it's the same as `top_n_by_mean_distance`.
|
72 |
+
plot_y_max=10)
|
73 |
+
|
74 |
+
# Open output video.
|
75 |
+
out_video = cv2.VideoWriter(video_path_out, cv2.VideoWriter_fourcc(*'mp4v'), video_fps, (video_width, video_height))
|
76 |
+
|
77 |
+
# Initialize list of results
|
78 |
+
frame_idx = 0
|
79 |
+
current_position = {"none":10.0}
|
80 |
+
|
81 |
+
output_frame = None
|
82 |
+
try:
|
83 |
+
with tqdm.tqdm(position=0, leave=True) as pbar:
|
84 |
+
while True:
|
85 |
+
#on rajoute à chaque itération la valeur de current_position et de frame_idx
|
86 |
+
position_list.append(current_position)
|
87 |
+
frame_list.append(frame_idx)
|
88 |
+
|
89 |
+
#on renvoie les deux valeurs au fur et à mesure
|
90 |
+
with open(results_classification_path_out, 'a') as f:
|
91 |
+
f.write(f'{frame_idx};{current_position}\n')
|
92 |
+
|
93 |
+
success, input_frame = video_cap.read()
|
94 |
+
if not success:
|
95 |
+
print("Unable to read input video frame, breaking!")
|
96 |
+
break
|
97 |
+
|
98 |
+
# Run pose tracker
|
99 |
+
input_frame_rgb = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
100 |
+
result = pose_tracker.process(image=input_frame_rgb)
|
101 |
+
pose_landmarks = result.pose_landmarks
|
102 |
+
|
103 |
+
# Prepare the output frame
|
104 |
+
output_frame = input_frame.copy()
|
105 |
+
|
106 |
+
# Add a white banner on top
|
107 |
+
banner_height = 180
|
108 |
+
output_frame[0:banner_height, :] = (255, 255, 255) # White color
|
109 |
+
|
110 |
+
# Load the logo image
|
111 |
+
logo = cv2.imread("src/logo_impredalam.jpg")
|
112 |
+
logo_height, logo_width = logo.shape[:2]
|
113 |
+
logo = cv2.resize(
|
114 |
+
logo, (logo_width // 3, logo_height // 3)
|
115 |
+
) # Resize to 1/3 scale
|
116 |
+
|
117 |
+
# Overlay the logo on the upper right corner
|
118 |
+
output_frame[0 : logo.shape[0], output_frame.shape[1] - logo.shape[1] :] = (
|
119 |
+
logo
|
120 |
+
)
|
121 |
+
if pose_landmarks is not None:
|
122 |
+
mp_drawing.draw_landmarks(
|
123 |
+
image=output_frame,
|
124 |
+
landmark_list=pose_landmarks,
|
125 |
+
connections=mp_pose.POSE_CONNECTIONS,
|
126 |
+
)
|
127 |
+
|
128 |
+
# Get landmarks
|
129 |
+
frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
130 |
+
pose_landmarks = np.array(
|
131 |
+
[
|
132 |
+
[lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
133 |
+
for lmk in pose_landmarks.landmark
|
134 |
+
],
|
135 |
+
dtype=np.float32,
|
136 |
+
)
|
137 |
+
assert pose_landmarks.shape == (
|
138 |
+
33,
|
139 |
+
3,
|
140 |
+
), "Unexpected landmarks shape: {}".format(pose_landmarks.shape)
|
141 |
+
|
142 |
+
# Classify the pose on the current frame
|
143 |
+
pose_classification = pose_classifier(pose_landmarks)
|
144 |
+
|
145 |
+
# Smooth classification using EMA
|
146 |
+
pose_classification_filtered = pose_classification_filter(pose_classification)
|
147 |
+
current_position=pose_classification_filtered
|
148 |
+
|
149 |
+
# Count repetitions
|
150 |
+
# repetitions_count = repetition_counter(pose_classification_filtered)
|
151 |
+
|
152 |
+
# Display repetitions count on the frame
|
153 |
+
# cv2.putText(
|
154 |
+
# output_frame,
|
155 |
+
# f"Push-Ups: {repetitions_count}",
|
156 |
+
# (10, 30),
|
157 |
+
# cv2.FONT_HERSHEY_SIMPLEX,
|
158 |
+
# 1,
|
159 |
+
# (0, 0, 0),
|
160 |
+
# 2,
|
161 |
+
# cv2.LINE_AA,
|
162 |
+
# )
|
163 |
+
# Display classified pose on the frame
|
164 |
+
cv2.putText(
|
165 |
+
output_frame,
|
166 |
+
f"Pose: {current_position}",
|
167 |
+
(10, 70),
|
168 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
169 |
+
1.2, # Smaller font size
|
170 |
+
(0, 0, 0),
|
171 |
+
1, # Thinner line
|
172 |
+
cv2.LINE_AA,
|
173 |
+
)
|
174 |
+
else:
|
175 |
+
# If no landmarks are detected, still display the last count
|
176 |
+
# repetitions_count = repetition_counter.n_repeats
|
177 |
+
# cv2.putText(
|
178 |
+
# output_frame,
|
179 |
+
# f"Push-Ups: {repetitions_count}",
|
180 |
+
# (10, 30),
|
181 |
+
# cv2.FONT_HERSHEY_SIMPLEX,
|
182 |
+
# 1,
|
183 |
+
# (0, 255, 0),
|
184 |
+
# 2,
|
185 |
+
# cv2.LINE_AA,
|
186 |
+
# )
|
187 |
+
current_position={'None':10.0}
|
188 |
+
cv2.putText(
|
189 |
+
output_frame,
|
190 |
+
f"Pose: {current_position}",
|
191 |
+
(10, 70),
|
192 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
193 |
+
1.2, # Smaller font size
|
194 |
+
(0, 0, 0),
|
195 |
+
1, # Thinner line
|
196 |
+
cv2.LINE_AA,
|
197 |
+
)
|
198 |
+
|
199 |
+
cv2.imshow("Yoga position classification", output_frame)
|
200 |
+
|
201 |
+
key = cv2.waitKey(1) & 0xFF
|
202 |
+
if key == ord("q"):
|
203 |
+
break
|
204 |
+
elif key == ord("r"):
|
205 |
+
# repetition_counter.reset()
|
206 |
+
print("Counter reset!")
|
207 |
+
|
208 |
+
frame_idx += 1
|
209 |
+
pbar.update()
|
210 |
+
|
211 |
+
finally:
|
212 |
+
|
213 |
+
pose_tracker.close()
|
214 |
+
video_cap.release()
|
215 |
+
cv2.destroyAllWindows()
|
216 |
+
|
217 |
+
# Instruction if recorded video with video_path_in
|
218 |
+
else:
|
219 |
+
assert type(video_path_in)==str, "Error in video path format, not a string. Abort."
|
220 |
+
# Open video and get video parameters and check if video is OK
|
221 |
+
video_cap = cv2.VideoCapture(video_path_in)
|
222 |
+
video_n_frames = video_cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
223 |
+
video_fps = video_cap.get(cv2.CAP_PROP_FPS)
|
224 |
+
video_width = int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
225 |
+
video_height = int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
226 |
+
assert type(video_n_frames)==float, 'Error in input video frames type. Abort.'
|
227 |
+
assert video_n_frames>0.0, 'Error in input video frames number : no frame. Abort.'
|
228 |
+
|
229 |
+
class_name='tree'
|
230 |
+
|
231 |
+
# Initialize tracker, classifier and current position.
|
232 |
+
# Initialize tracker.
|
233 |
+
mp_pose = mp.solutions.pose
|
234 |
+
pose_tracker = mp_pose.Pose()
|
235 |
+
# Folder with pose class CSVs. That should be the same folder you used while
|
236 |
+
# building classifier to output CSVs.
|
237 |
+
pose_samples_folder = 'data/yoga_poses_csvs_out'
|
238 |
+
# Initialize embedder.
|
239 |
+
pose_embedder = FullBodyPoseEmbedding()
|
240 |
+
# Initialize classifier.
|
241 |
+
# Check that you are using the same parameters as during bootstrapping.
|
242 |
+
pose_classifier = PoseClassifier(
|
243 |
+
pose_samples_folder=pose_samples_folder,
|
244 |
+
pose_embedder=pose_embedder,
|
245 |
+
top_n_by_max_distance=30,
|
246 |
+
top_n_by_mean_distance=10)
|
247 |
+
|
248 |
+
# Initialize list of results
|
249 |
+
position_list=[]
|
250 |
+
frame_list=[]
|
251 |
+
|
252 |
+
# Initialize EMA smoothing.
|
253 |
+
pose_classification_filter = EMADictSmoothing(
|
254 |
+
window_size=10,
|
255 |
+
alpha=0.2)
|
256 |
+
|
257 |
+
# Initialize renderer.
|
258 |
+
pose_classification_visualizer = PoseClassificationVisualizer(
|
259 |
+
class_name=class_name,
|
260 |
+
plot_x_max=video_n_frames,
|
261 |
+
# Graphic looks nicer if it's the same as `top_n_by_mean_distance`.
|
262 |
+
plot_y_max=10)
|
263 |
+
|
264 |
+
# Open output video.
|
265 |
+
out_video = cv2.VideoWriter(video_path_out, cv2.VideoWriter_fourcc(*'mp4v'), video_fps, (video_width, video_height))
|
266 |
+
|
267 |
+
# Initialize list of results
|
268 |
+
frame_idx = 0
|
269 |
+
current_position = {"none":10.0}
|
270 |
+
|
271 |
+
output_frame = None
|
272 |
+
with tqdm.tqdm(total=video_n_frames, position=0, leave=True) as pbar:
|
273 |
+
while True:
|
274 |
+
#on rajoute à chaque itération la valeur de current_position et de frame_idx
|
275 |
+
position_list.append(current_position)
|
276 |
+
frame_list.append(frame_idx)
|
277 |
+
|
278 |
+
#on renvoie les deux valeurs au fur et à mesure
|
279 |
+
with open(results_classification_path_out, 'a') as f:
|
280 |
+
f.write(f'{frame_idx};{current_position}\n')
|
281 |
+
|
282 |
+
# Get next frame of the video.
|
283 |
+
success, input_frame = video_cap.read()
|
284 |
+
if not success:
|
285 |
+
print("unable to read input video frame, breaking!")
|
286 |
+
break
|
287 |
+
|
288 |
+
# Run pose tracker.
|
289 |
+
input_frame = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
290 |
+
result = pose_tracker.process(image=input_frame)
|
291 |
+
pose_landmarks = result.pose_landmarks
|
292 |
+
|
293 |
+
# Draw pose prediction.
|
294 |
+
output_frame = input_frame.copy()
|
295 |
+
if pose_landmarks is not None:
|
296 |
+
mp_drawing.draw_landmarks(
|
297 |
+
image=output_frame,
|
298 |
+
landmark_list=pose_landmarks,
|
299 |
+
connections=mp_pose.POSE_CONNECTIONS)
|
300 |
+
|
301 |
+
if pose_landmarks is not None:
|
302 |
+
# Get landmarks.
|
303 |
+
frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
304 |
+
pose_landmarks = np.array([[lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
305 |
+
for lmk in pose_landmarks.landmark], dtype=np.float32)
|
306 |
+
assert pose_landmarks.shape == (33, 3), 'Unexpected landmarks shape: {}'.format(pose_landmarks.shape)
|
307 |
+
|
308 |
+
# Classify the pose on the current frame.
|
309 |
+
pose_classification = pose_classifier(pose_landmarks)
|
310 |
+
|
311 |
+
# Smooth classification using EMA.
|
312 |
+
pose_classification_filtered = pose_classification_filter(pose_classification)
|
313 |
+
|
314 |
+
current_position=pose_classification_filtered
|
315 |
+
# Count repetitions.
|
316 |
+
# repetitions_count = repetition_counter(pose_classification_filtered)
|
317 |
+
else:
|
318 |
+
# No pose => no classification on current frame.
|
319 |
+
pose_classification = None
|
320 |
+
|
321 |
+
# Still add empty classification to the filter to maintaing correct
|
322 |
+
# smoothing for future frames.
|
323 |
+
pose_classification_filtered = pose_classification_filter(dict())
|
324 |
+
pose_classification_filtered = None
|
325 |
+
|
326 |
+
current_position='None'
|
327 |
+
# Don't update the counter presuming that person is 'frozen'. Just
|
328 |
+
# take the latest repetitions count.
|
329 |
+
# repetitions_count = repetition_counter.n_repeats
|
330 |
+
|
331 |
+
# Draw classification plot and repetition counter.
|
332 |
+
output_frame = pose_classification_visualizer(
|
333 |
+
frame=output_frame,
|
334 |
+
pose_classification=pose_classification,
|
335 |
+
pose_classification_filtered=pose_classification_filtered,
|
336 |
+
repetitions_count='0'
|
337 |
+
)
|
338 |
+
|
339 |
+
# Save the output frame.
|
340 |
+
out_video.write(cv2.cvtColor(np.array(output_frame), cv2.COLOR_RGB2BGR))
|
341 |
+
|
342 |
+
# Show intermediate frames of the video to track progress.
|
343 |
+
if frame_idx % 50 == 0:
|
344 |
+
show_image(output_frame)
|
345 |
+
|
346 |
+
frame_idx += 1
|
347 |
+
pbar.update()
|
348 |
+
|
349 |
+
# Close output video.
|
350 |
+
out_video.release()
|
351 |
+
|
352 |
+
# Release MediaPipe resources.
|
353 |
+
pose_tracker.close()
|
354 |
+
|
355 |
+
# Show the last frame of the video.
|
356 |
+
if output_frame is not None:
|
357 |
+
show_image(output_frame)
|
358 |
+
|
359 |
+
video_cap.release()
|
360 |
+
|
361 |
+
|
362 |
+
|
363 |
+
|
364 |
+
return current_position #string between ['Chair', 'Cobra', 'Dog', 'Goddess', 'Plank', 'Tree', 'Warrior', 'None' = nonfallen, 'Fall']
|
365 |
+
|
366 |
+
# mp_pose = mp.solutions.pose
|
367 |
+
# pose_tracker = mp_pose.Pose()
|
368 |
+
|
369 |
+
# pose_samples_folder = "data/yoga_poses_csvs_out"
|
370 |
+
# class_name = "tree"
|
371 |
+
|
372 |
+
# pose_embedder = FullBodyPoseEmbedding()
|
373 |
+
|
374 |
+
# pose_classifier = PoseClassifier(
|
375 |
+
# pose_samples_folder=pose_samples_folder,
|
376 |
+
# pose_embedder=pose_embedder,
|
377 |
+
# top_n_by_max_distance=30,
|
378 |
+
# top_n_by_mean_distance=10,
|
379 |
+
# )
|
380 |
+
|
381 |
+
# pose_classification_filter = EMADictSmoothing(window_size=10, alpha=0.2)
|
382 |
+
|
383 |
+
# repetition_counter = RepetitionCounter(
|
384 |
+
# class_name=class_name, enter_threshold=6, exit_threshold=4
|
385 |
+
# )
|
386 |
+
|
387 |
+
# pose_classification_visualizer = PoseClassificationVisualizer(
|
388 |
+
# class_name=class_name, plot_x_max=1000, plot_y_max=10
|
389 |
+
# )
|
390 |
+
|
391 |
+
# video_cap = cv2.VideoCapture(0)
|
392 |
+
# video_fps = 30
|
393 |
+
# video_width = int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
394 |
+
# video_height = int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
395 |
+
|
396 |
+
# frame_idx = 0
|
397 |
+
# output_frame = None
|
398 |
+
|
399 |
+
# try:
|
400 |
+
# with tqdm.tqdm(position=0, leave=True) as pbar:
|
401 |
+
# while True:
|
402 |
+
# success, input_frame = video_cap.read()
|
403 |
+
# if not success:
|
404 |
+
# print("Unable to read input video frame, breaking!")
|
405 |
+
# break
|
406 |
+
|
407 |
+
# # Run pose tracker
|
408 |
+
# input_frame_rgb = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
409 |
+
# result = pose_tracker.process(image=input_frame_rgb)
|
410 |
+
# pose_landmarks = result.pose_landmarks
|
411 |
+
|
412 |
+
# # Prepare the output frame
|
413 |
+
# output_frame = input_frame.copy()
|
414 |
+
# if pose_landmarks is not None:
|
415 |
+
# mp_drawing.draw_landmarks(
|
416 |
+
# image=output_frame,
|
417 |
+
# landmark_list=pose_landmarks,
|
418 |
+
# connections=mp_pose.POSE_CONNECTIONS,
|
419 |
+
# )
|
420 |
+
|
421 |
+
# # Get landmarks
|
422 |
+
# frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
423 |
+
# pose_landmarks = np.array(
|
424 |
+
# [
|
425 |
+
# [lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
426 |
+
# for lmk in pose_landmarks.landmark
|
427 |
+
# ],
|
428 |
+
# dtype=np.float32,
|
429 |
+
# )
|
430 |
+
# assert pose_landmarks.shape == (
|
431 |
+
# 33,
|
432 |
+
# 3,
|
433 |
+
# ), "Unexpected landmarks shape: {}".format(pose_landmarks.shape)
|
434 |
+
|
435 |
+
# # Classify the pose on the current frame
|
436 |
+
# pose_classification = pose_classifier(pose_landmarks)
|
437 |
+
|
438 |
+
# # Smooth classification using EMA
|
439 |
+
# pose_classification_filtered = pose_classification_filter(
|
440 |
+
# pose_classification
|
441 |
+
# )
|
442 |
+
|
443 |
+
# # Count repetitions
|
444 |
+
# # repetitions_count = repetition_counter(pose_classification_filtered)
|
445 |
+
|
446 |
+
# # Display repetitions count on the frame
|
447 |
+
# # cv2.putText(
|
448 |
+
# # output_frame,
|
449 |
+
# # f"Push-Ups: {repetitions_count}",
|
450 |
+
# # (10, 30),
|
451 |
+
# # cv2.FONT_HERSHEY_SIMPLEX,
|
452 |
+
# # 1,
|
453 |
+
# # (0, 255, 0),
|
454 |
+
# # 2,
|
455 |
+
# # cv2.LINE_AA,
|
456 |
+
# # )
|
457 |
+
|
458 |
+
# # Display classified pose on the frame
|
459 |
+
# cv2.putText(
|
460 |
+
# output_frame,
|
461 |
+
# f"Pose: {pose_classification}",
|
462 |
+
# (10, 70),
|
463 |
+
# cv2.FONT_HERSHEY_SIMPLEX,
|
464 |
+
# 1,
|
465 |
+
# (255, 0, 0),
|
466 |
+
# 2,
|
467 |
+
# cv2.LINE_AA,
|
468 |
+
# )
|
469 |
+
# else:
|
470 |
+
# # If no landmarks are detected, still display the last count
|
471 |
+
# # repetitions_count = repetition_counter.n_repeats
|
472 |
+
# # cv2.putText(
|
473 |
+
# # output_frame,
|
474 |
+
# # f"Push-Ups: {repetitions_count}",
|
475 |
+
# # (10, 30),
|
476 |
+
# # cv2.FONT_HERSHEY_SIMPLEX,
|
477 |
+
# # 1,
|
478 |
+
# # (0, 255, 0),
|
479 |
+
# # 2,
|
480 |
+
# # cv2.LINE_AA,
|
481 |
+
# # )
|
482 |
+
# # If no landmarks are detected, still display the last classified pose
|
483 |
+
# # Display classified pose on the frame
|
484 |
+
# cv2.putText(
|
485 |
+
# output_frame,
|
486 |
+
# f"Pose: {pose_classification}",
|
487 |
+
# (10, 70),
|
488 |
+
# cv2.FONT_HERSHEY_SIMPLEX,
|
489 |
+
# 1,
|
490 |
+
# (255, 0, 0),
|
491 |
+
# 2,
|
492 |
+
# cv2.LINE_AA,
|
493 |
+
# )
|
494 |
+
|
495 |
+
# cv2.imshow("Yoga pose classification", output_frame)
|
496 |
+
|
497 |
+
# key = cv2.waitKey(1) & 0xFF
|
498 |
+
# if key == ord("q"):
|
499 |
+
# break
|
500 |
+
# elif key == ord("r"):
|
501 |
+
# # repetition_counter.reset()
|
502 |
+
# print("Counter reset!")
|
503 |
+
|
504 |
+
# frame_idx += 1
|
505 |
+
# pbar.update()
|
506 |
+
|
507 |
+
# finally:
|
508 |
+
|
509 |
+
# pose_tracker.close()
|
510 |
+
# video_cap.release()
|
511 |
+
# cv2.destroyAllWindows()
|
512 |
+
|
513 |
+
|
514 |
+
if __name__ == "__main__":
|
515 |
+
main()
|
yoga_position_gradio.py
ADDED
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import tqdm
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
import re
|
5 |
+
import os
|
6 |
+
from mediapipe.python.solutions import drawing_utils as mp_drawing
|
7 |
+
import mediapipe as mp
|
8 |
+
from PoseClassification.pose_embedding import FullBodyPoseEmbedding
|
9 |
+
from PoseClassification.pose_classifier import PoseClassifier
|
10 |
+
from PoseClassification.utils import EMADictSmoothing
|
11 |
+
# from PoseClassification.utils import RepetitionCounter
|
12 |
+
from PoseClassification.visualize import PoseClassificationVisualizer
|
13 |
+
import argparse
|
14 |
+
from PoseClassification.utils import show_image
|
15 |
+
|
16 |
+
|
17 |
+
def check_major_current_position(positions_detected:dict, threshold_position) -> str:
|
18 |
+
'''
|
19 |
+
return the major position between those detected in frame, or return none
|
20 |
+
|
21 |
+
INPUTS
|
22 |
+
positions_detected :
|
23 |
+
dict of positions given by position classifier and pose_classification_filtered
|
24 |
+
{'pose1':8.0, 'pose2':2.0}
|
25 |
+
threshold_position :
|
26 |
+
values strictly below are considered "none" position
|
27 |
+
|
28 |
+
OUTPUT
|
29 |
+
major_position :
|
30 |
+
string with position (classes from classifier and "none")
|
31 |
+
|
32 |
+
'''
|
33 |
+
if max(positions_detected.values())<float(threshold_position):
|
34 |
+
major_position='none'
|
35 |
+
else:
|
36 |
+
major_position=max(positions_detected, key=positions_detected.get)
|
37 |
+
return major_position
|
38 |
+
|
39 |
+
|
40 |
+
def yoga_position_classifier():
|
41 |
+
#Load arguments
|
42 |
+
parser = argparse.ArgumentParser()
|
43 |
+
|
44 |
+
parser.add_argument("video_path", help="string video path in")
|
45 |
+
args = parser.parse_args()
|
46 |
+
|
47 |
+
video_path_in = args.video_path
|
48 |
+
direct_video=False
|
49 |
+
|
50 |
+
if video_path_in=="live":
|
51 |
+
video_path_in='data/live.mp4'
|
52 |
+
direct_video=True
|
53 |
+
|
54 |
+
video_path_out = re.sub(r'.mp4', r'_classified_video.mp4', video_path_in)
|
55 |
+
results_classification_path_out = re.sub(r'.mp4', r'_classified_results.csv', video_path_in)
|
56 |
+
|
57 |
+
|
58 |
+
# Initialize tracker, classifier and current position.
|
59 |
+
# Initialize tracker.
|
60 |
+
mp_pose = mp.solutions.pose
|
61 |
+
pose_tracker = mp_pose.Pose()
|
62 |
+
# Folder with pose class CSVs. That should be the same folder you used while
|
63 |
+
# building classifier to output CSVs.
|
64 |
+
pose_samples_folder = 'data/yoga_poses_csvs_out'
|
65 |
+
# Initialize embedder.
|
66 |
+
pose_embedder = FullBodyPoseEmbedding()
|
67 |
+
# Initialize classifier.
|
68 |
+
# Check that you are using the same parameters as during bootstrapping.
|
69 |
+
pose_classifier = PoseClassifier(
|
70 |
+
pose_samples_folder=pose_samples_folder,
|
71 |
+
pose_embedder=pose_embedder,
|
72 |
+
top_n_by_max_distance=30,
|
73 |
+
top_n_by_mean_distance=10)
|
74 |
+
|
75 |
+
|
76 |
+
# Initialize EMA smoothing.
|
77 |
+
pose_classification_filter = EMADictSmoothing(
|
78 |
+
window_size=10,
|
79 |
+
alpha=0.2)
|
80 |
+
|
81 |
+
|
82 |
+
# Initialize list of results
|
83 |
+
position_list=[]
|
84 |
+
frame_list=[]
|
85 |
+
|
86 |
+
# Instruction if direct flux video
|
87 |
+
if direct_video :
|
88 |
+
video_cap = cv2.VideoCapture(0)
|
89 |
+
# Instruction if path video
|
90 |
+
else :
|
91 |
+
assert type(video_path_in)==str, "Error in video path format, not a string. Abort."
|
92 |
+
# Open video and get video parameters and check if video is OK
|
93 |
+
video_cap = cv2.VideoCapture(video_path_in)
|
94 |
+
video_n_frames = video_cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
95 |
+
assert type(video_n_frames)==float, 'Error in input video frames type. Abort.'
|
96 |
+
assert video_n_frames>0.0, 'Error in input video frames number : no frame. Abort.'
|
97 |
+
|
98 |
+
video_fps = video_cap.get(cv2.CAP_PROP_FPS)
|
99 |
+
video_width = int(video_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
100 |
+
video_height = int(video_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
101 |
+
|
102 |
+
class_names=['chair', 'cobra', 'dog', 'goddess', 'plank', 'tree', 'warrior', 'none']
|
103 |
+
position_threshold = 8.0
|
104 |
+
|
105 |
+
# Open output video.
|
106 |
+
out_video = cv2.VideoWriter(video_path_out, cv2.VideoWriter_fourcc(*'mp4v'), video_fps, (video_width, video_height))
|
107 |
+
|
108 |
+
# Initialize results
|
109 |
+
frame_idx = 0
|
110 |
+
current_position = {"none":10.0}
|
111 |
+
output_frame = None
|
112 |
+
|
113 |
+
position_timer = 0
|
114 |
+
previous_position_major = 'none'
|
115 |
+
|
116 |
+
try:
|
117 |
+
with tqdm.tqdm(position=0, leave=True) as pbar:
|
118 |
+
while True:
|
119 |
+
# Get current time from beggining of video
|
120 |
+
time_sec = float(frame_idx*(1/video_fps))
|
121 |
+
|
122 |
+
# Get current major position (str)
|
123 |
+
current_position_major = check_major_current_position(current_position, position_threshold)
|
124 |
+
|
125 |
+
success, input_frame = video_cap.read()
|
126 |
+
if not success:
|
127 |
+
print("Unable to read input video frame, breaking!")
|
128 |
+
break
|
129 |
+
|
130 |
+
# Run pose tracker
|
131 |
+
input_frame_rgb = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
132 |
+
result = pose_tracker.process(image=input_frame_rgb)
|
133 |
+
pose_landmarks = result.pose_landmarks
|
134 |
+
|
135 |
+
# Prepare the output frame
|
136 |
+
output_frame = input_frame.copy()
|
137 |
+
|
138 |
+
# Add a white banner on top
|
139 |
+
banner_height = int(video_height//10)
|
140 |
+
output_frame[0:banner_height, :] = (255, 255, 255) # White color
|
141 |
+
|
142 |
+
# Load the logo image
|
143 |
+
logo = cv2.imread("src/logo_impredalam.jpg")
|
144 |
+
logo_height, logo_width = logo.shape[:2]
|
145 |
+
logo_height_rescaled = banner_height
|
146 |
+
logo_width_rescaled = int((logo_width*logo_height_rescaled)// logo_height )
|
147 |
+
logo = cv2.resize(logo, (logo_width_rescaled, logo_height_rescaled)) # Resize to banner scale
|
148 |
+
|
149 |
+
# Overlay the logo on the upper right corner
|
150 |
+
output_frame[0 : logo.shape[0], output_frame.shape[1] - logo.shape[1] :] = (logo)
|
151 |
+
|
152 |
+
# If landmarks are detected
|
153 |
+
if pose_landmarks is not None:
|
154 |
+
mp_drawing.draw_landmarks(
|
155 |
+
image=output_frame,
|
156 |
+
landmark_list=pose_landmarks,
|
157 |
+
connections=mp_pose.POSE_CONNECTIONS,)
|
158 |
+
|
159 |
+
# Get landmarks
|
160 |
+
frame_height, frame_width = output_frame.shape[0], output_frame.shape[1]
|
161 |
+
pose_landmarks = np.array(
|
162 |
+
[
|
163 |
+
[lmk.x * frame_width, lmk.y * frame_height, lmk.z * frame_width]
|
164 |
+
for lmk in pose_landmarks.landmark
|
165 |
+
],
|
166 |
+
dtype=np.float32,)
|
167 |
+
assert pose_landmarks.shape == (33,3,), "Unexpected landmarks shape: {}".format(pose_landmarks.shape)
|
168 |
+
|
169 |
+
# Classify the pose on the current frame
|
170 |
+
pose_classification = pose_classifier(pose_landmarks)
|
171 |
+
|
172 |
+
# Smooth classification using EMA
|
173 |
+
pose_classification_filtered = pose_classification_filter(pose_classification)
|
174 |
+
current_position=pose_classification_filtered
|
175 |
+
current_position_major=check_major_current_position(current_position, position_threshold)
|
176 |
+
|
177 |
+
# If no landmarks are detected
|
178 |
+
else:
|
179 |
+
|
180 |
+
current_position={'none':10.0}
|
181 |
+
current_position_major=check_major_current_position(current_position, position_threshold)
|
182 |
+
|
183 |
+
|
184 |
+
# If landmarks or no landmarks detected :
|
185 |
+
|
186 |
+
# Compute position timer according to current and previous position
|
187 |
+
if current_position_major==previous_position_major:
|
188 |
+
#increase position_timer
|
189 |
+
position_timer+=(1/video_fps)
|
190 |
+
else:
|
191 |
+
previous_position_major=current_position_major
|
192 |
+
position_timer=0
|
193 |
+
|
194 |
+
# Display current position on frame
|
195 |
+
cv2.putText(
|
196 |
+
output_frame,
|
197 |
+
f"Pose: {current_position_major}",
|
198 |
+
(int(0+(1//50*video_width)), int(0+banner_height//3)), #coord
|
199 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
200 |
+
float(0.9*(video_height/video_width)), # Font size
|
201 |
+
(0, 0, 0), #color
|
202 |
+
1, # Thinner line
|
203 |
+
cv2.LINE_AA,)
|
204 |
+
|
205 |
+
# Display current position timer on frame
|
206 |
+
cv2.putText(
|
207 |
+
output_frame,
|
208 |
+
f"Duration: {int(position_timer)} seconds",
|
209 |
+
(int(0+(1//50*video_width)), int(0+(2*banner_height)//3)), #coord
|
210 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
211 |
+
float(0.9*(video_height/video_width)), # Font size
|
212 |
+
(0, 0, 0), #color
|
213 |
+
1, # Thinner line
|
214 |
+
cv2.LINE_AA,)
|
215 |
+
|
216 |
+
# Show output frame
|
217 |
+
cv2.imshow("Yoga position", output_frame)
|
218 |
+
|
219 |
+
# Add current_position (dict) and frame index to list (output file for debug)
|
220 |
+
position_list.append(current_position)
|
221 |
+
frame_list.append(frame_idx)
|
222 |
+
# Output file for debug
|
223 |
+
with open(results_classification_path_out, 'a') as f:
|
224 |
+
f.write(f'{frame_idx},{current_position}\n')
|
225 |
+
|
226 |
+
key = cv2.waitKey(1) & 0xFF
|
227 |
+
if key == ord("q"):
|
228 |
+
break
|
229 |
+
elif key == ord("r"):
|
230 |
+
current_position = {'none':10.0}
|
231 |
+
print("Position reset !")
|
232 |
+
|
233 |
+
frame_idx += 1
|
234 |
+
pbar.update()
|
235 |
+
|
236 |
+
finally:
|
237 |
+
pose_tracker.close()
|
238 |
+
video_cap.release()
|
239 |
+
cv2.destroyAllWindows()
|
240 |
+
# Close output video.
|
241 |
+
out_video.release()
|
242 |
+
|
243 |
+
return frame_list, position_list
|
244 |
+
|
245 |
+
|
246 |
+
|
247 |
+
if __name__ == "__main__":
|
248 |
+
yoga_position_classifier()
|