|
import cv2 |
|
import mediapipe as mp |
|
import numpy as np |
|
import pandas as pd |
|
import pickle |
|
|
|
from .utils import ( |
|
calculate_distance, |
|
extract_important_keypoints, |
|
get_static_file_url, |
|
get_drawing_color, |
|
) |
|
|
|
mp_pose = mp.solutions.pose |
|
mp_drawing = mp.solutions.drawing_utils |
|
|
|
|
|
def analyze_foot_knee_placement( |
|
results, |
|
stage: str, |
|
foot_shoulder_ratio_thresholds: list, |
|
knee_foot_ratio_thresholds: dict, |
|
visibility_threshold: int, |
|
) -> dict: |
|
""" |
|
Calculate the ratio between the foot and shoulder for FOOT PLACEMENT analysis |
|
|
|
Calculate the ratio between the knee and foot for KNEE PLACEMENT analysis |
|
|
|
Return result explanation: |
|
-1: Unknown result due to poor visibility |
|
0: Correct knee placement |
|
1: Placement too tight |
|
2: Placement too wide |
|
""" |
|
analyzed_results = { |
|
"foot_placement": -1, |
|
"knee_placement": -1, |
|
} |
|
|
|
landmarks = results.pose_landmarks.landmark |
|
|
|
|
|
left_foot_index_vis = landmarks[ |
|
mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value |
|
].visibility |
|
right_foot_index_vis = landmarks[ |
|
mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value |
|
].visibility |
|
|
|
left_knee_vis = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].visibility |
|
right_knee_vis = landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].visibility |
|
|
|
|
|
if ( |
|
left_foot_index_vis < visibility_threshold |
|
or right_foot_index_vis < visibility_threshold |
|
or left_knee_vis < visibility_threshold |
|
or right_knee_vis < visibility_threshold |
|
): |
|
return analyzed_results |
|
|
|
|
|
left_shoulder = [ |
|
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x, |
|
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y, |
|
] |
|
right_shoulder = [ |
|
landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x, |
|
landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y, |
|
] |
|
shoulder_width = calculate_distance(left_shoulder, right_shoulder) |
|
|
|
|
|
left_foot_index = [ |
|
landmarks[mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value].x, |
|
landmarks[mp_pose.PoseLandmark.LEFT_FOOT_INDEX.value].y, |
|
] |
|
right_foot_index = [ |
|
landmarks[mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value].x, |
|
landmarks[mp_pose.PoseLandmark.RIGHT_FOOT_INDEX.value].y, |
|
] |
|
foot_width = calculate_distance(left_foot_index, right_foot_index) |
|
|
|
|
|
foot_shoulder_ratio = round(foot_width / shoulder_width, 1) |
|
|
|
|
|
min_ratio_foot_shoulder, max_ratio_foot_shoulder = foot_shoulder_ratio_thresholds |
|
if min_ratio_foot_shoulder <= foot_shoulder_ratio <= max_ratio_foot_shoulder: |
|
analyzed_results["foot_placement"] = 0 |
|
elif foot_shoulder_ratio < min_ratio_foot_shoulder: |
|
analyzed_results["foot_placement"] = 1 |
|
elif foot_shoulder_ratio > max_ratio_foot_shoulder: |
|
analyzed_results["foot_placement"] = 2 |
|
|
|
|
|
left_knee_vis = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].visibility |
|
right_knee_vis = landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].visibility |
|
|
|
|
|
if left_knee_vis < visibility_threshold or right_knee_vis < visibility_threshold: |
|
print("Cannot see foot") |
|
return analyzed_results |
|
|
|
|
|
left_knee = [ |
|
landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x, |
|
landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y, |
|
] |
|
right_knee = [ |
|
landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].x, |
|
landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].y, |
|
] |
|
knee_width = calculate_distance(left_knee, right_knee) |
|
|
|
|
|
knee_foot_ratio = round(knee_width / foot_width, 1) |
|
|
|
|
|
up_min_ratio_knee_foot, up_max_ratio_knee_foot = knee_foot_ratio_thresholds.get( |
|
"up" |
|
) |
|
( |
|
middle_min_ratio_knee_foot, |
|
middle_max_ratio_knee_foot, |
|
) = knee_foot_ratio_thresholds.get("middle") |
|
down_min_ratio_knee_foot, down_max_ratio_knee_foot = knee_foot_ratio_thresholds.get( |
|
"down" |
|
) |
|
|
|
if stage == "up": |
|
if up_min_ratio_knee_foot <= knee_foot_ratio <= up_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 0 |
|
elif knee_foot_ratio < up_min_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 1 |
|
elif knee_foot_ratio > up_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 2 |
|
elif stage == "middle": |
|
if middle_min_ratio_knee_foot <= knee_foot_ratio <= middle_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 0 |
|
elif knee_foot_ratio < middle_min_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 1 |
|
elif knee_foot_ratio > middle_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 2 |
|
elif stage == "down": |
|
if down_min_ratio_knee_foot <= knee_foot_ratio <= down_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 0 |
|
elif knee_foot_ratio < down_min_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 1 |
|
elif knee_foot_ratio > down_max_ratio_knee_foot: |
|
analyzed_results["knee_placement"] = 2 |
|
|
|
return analyzed_results |
|
|
|
|
|
class SquatDetection: |
|
ML_MODEL_PATH = get_static_file_url("model/squat_model.pkl") |
|
|
|
PREDICTION_PROB_THRESHOLD = 0.7 |
|
VISIBILITY_THRESHOLD = 0.6 |
|
FOOT_SHOULDER_RATIO_THRESHOLDS = [1.2, 2.8] |
|
KNEE_FOOT_RATIO_THRESHOLDS = { |
|
"up": [0.5, 1.0], |
|
"middle": [0.7, 1.0], |
|
"down": [0.7, 1.1], |
|
} |
|
|
|
def __init__(self) -> None: |
|
self.init_important_landmarks() |
|
self.load_machine_learning_model() |
|
|
|
self.current_stage = "" |
|
self.previous_stage = { |
|
"feet": "", |
|
"knee": "", |
|
} |
|
self.counter = 0 |
|
self.results = [] |
|
self.has_error = False |
|
|
|
def init_important_landmarks(self) -> None: |
|
""" |
|
Determine Important landmarks for squat detection |
|
""" |
|
|
|
self.important_landmarks = [ |
|
"NOSE", |
|
"LEFT_SHOULDER", |
|
"RIGHT_SHOULDER", |
|
"LEFT_HIP", |
|
"RIGHT_HIP", |
|
"LEFT_KNEE", |
|
"RIGHT_KNEE", |
|
"LEFT_ANKLE", |
|
"RIGHT_ANKLE", |
|
] |
|
|
|
|
|
self.headers = ["label"] |
|
|
|
for lm in self.important_landmarks: |
|
self.headers += [ |
|
f"{lm.lower()}_x", |
|
f"{lm.lower()}_y", |
|
f"{lm.lower()}_z", |
|
f"{lm.lower()}_v", |
|
] |
|
|
|
def load_machine_learning_model(self) -> None: |
|
""" |
|
Load machine learning model |
|
""" |
|
if not self.ML_MODEL_PATH: |
|
raise Exception("Cannot found squat model") |
|
|
|
try: |
|
with open(self.ML_MODEL_PATH, "rb") as f: |
|
self.model = pickle.load(f) |
|
except Exception as e: |
|
raise Exception(f"Error loading model, {e}") |
|
|
|
def handle_detected_results(self, video_name: str) -> tuple: |
|
""" |
|
Save error frame as evidence |
|
""" |
|
file_name, _ = video_name.split(".") |
|
save_folder = get_static_file_url("images") |
|
for index, error in enumerate(self.results): |
|
try: |
|
image_name = f"{file_name}_{index}.jpg" |
|
cv2.imwrite(f"{save_folder}/{file_name}_{index}.jpg", error["frame"]) |
|
self.results[index]["frame"] = image_name |
|
except Exception as e: |
|
print("ERROR cannot save frame: " + str(e)) |
|
self.results[index]["frame"] = None |
|
|
|
return self.results, self.counter |
|
|
|
def clear_results(self) -> None: |
|
self.current_stage = "" |
|
self.previous_stage = { |
|
"feet": "", |
|
"knee": "", |
|
} |
|
self.counter = 0 |
|
self.results = [] |
|
self.has_error = False |
|
|
|
def detect(self, mp_results, image, timestamp) -> None: |
|
""" |
|
Make Squat Errors detection |
|
""" |
|
try: |
|
|
|
|
|
row = extract_important_keypoints(mp_results, self.important_landmarks) |
|
X = pd.DataFrame([row], columns=self.headers[1:]) |
|
|
|
|
|
predicted_class = self.model.predict(X)[0] |
|
prediction_probabilities = self.model.predict_proba(X)[0] |
|
prediction_probability = round( |
|
prediction_probabilities[prediction_probabilities.argmax()], 2 |
|
) |
|
|
|
|
|
if ( |
|
predicted_class == "down" |
|
and prediction_probability >= self.PREDICTION_PROB_THRESHOLD |
|
): |
|
self.current_stage = "down" |
|
elif ( |
|
self.current_stage == "down" |
|
and predicted_class == "up" |
|
and prediction_probability >= self.PREDICTION_PROB_THRESHOLD |
|
): |
|
self.current_stage = "up" |
|
self.counter += 1 |
|
|
|
|
|
analyzed_results = analyze_foot_knee_placement( |
|
results=mp_results, |
|
stage=self.current_stage, |
|
foot_shoulder_ratio_thresholds=self.FOOT_SHOULDER_RATIO_THRESHOLDS, |
|
knee_foot_ratio_thresholds=self.KNEE_FOOT_RATIO_THRESHOLDS, |
|
visibility_threshold=self.VISIBILITY_THRESHOLD, |
|
) |
|
|
|
foot_placement_evaluation = analyzed_results["foot_placement"] |
|
knee_placement_evaluation = analyzed_results["knee_placement"] |
|
|
|
|
|
if foot_placement_evaluation == -1: |
|
feet_placement = "unknown" |
|
elif foot_placement_evaluation == 0: |
|
feet_placement = "correct" |
|
elif foot_placement_evaluation == 1: |
|
feet_placement = "too tight" |
|
elif foot_placement_evaluation == 2: |
|
feet_placement = "too wide" |
|
|
|
|
|
if feet_placement == "correct": |
|
if knee_placement_evaluation == -1: |
|
knee_placement = "unknown" |
|
elif knee_placement_evaluation == 0: |
|
knee_placement = "correct" |
|
elif knee_placement_evaluation == 1: |
|
knee_placement = "too tight" |
|
elif knee_placement_evaluation == 2: |
|
knee_placement = "too wide" |
|
else: |
|
knee_placement = "unknown" |
|
|
|
|
|
|
|
if feet_placement in ["too tight", "too wide"]: |
|
|
|
if self.previous_stage["feet"] == feet_placement: |
|
pass |
|
|
|
elif self.previous_stage["feet"] != feet_placement: |
|
self.results.append( |
|
{ |
|
"stage": f"feet {feet_placement}", |
|
"frame": image, |
|
"timestamp": timestamp, |
|
} |
|
) |
|
|
|
self.previous_stage["feet"] = feet_placement |
|
|
|
|
|
if knee_placement in ["too tight", "too wide"]: |
|
|
|
if self.previous_stage["knee"] == knee_placement: |
|
pass |
|
|
|
elif self.previous_stage["knee"] != knee_placement: |
|
self.results.append( |
|
{ |
|
"stage": f"knee {knee_placement}", |
|
"frame": image, |
|
"timestamp": timestamp, |
|
} |
|
) |
|
|
|
self.previous_stage["knee"] = knee_placement |
|
|
|
if feet_placement in ["too tight", "too wide"] or knee_placement in [ |
|
"too tight", |
|
"too wide", |
|
]: |
|
self.has_error = True |
|
else: |
|
self.has_error = False |
|
|
|
|
|
|
|
landmark_color, connection_color = get_drawing_color(self.has_error) |
|
mp_drawing.draw_landmarks( |
|
image, |
|
mp_results.pose_landmarks, |
|
mp_pose.POSE_CONNECTIONS, |
|
mp_drawing.DrawingSpec( |
|
color=landmark_color, thickness=2, circle_radius=2 |
|
), |
|
mp_drawing.DrawingSpec( |
|
color=connection_color, thickness=2, circle_radius=1 |
|
), |
|
) |
|
|
|
|
|
cv2.rectangle(image, (0, 0), (300, 40), (245, 117, 16), -1) |
|
|
|
|
|
cv2.putText( |
|
image, |
|
"COUNT", |
|
(10, 12), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.3, |
|
(0, 0, 0), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
cv2.putText( |
|
image, |
|
f'{str(self.counter)}, {predicted_class.split(" ")[0]}, {str(prediction_probability)}', |
|
(5, 25), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.5, |
|
(255, 255, 255), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
|
|
|
|
cv2.putText( |
|
image, |
|
"FEET", |
|
(130, 12), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.3, |
|
(0, 0, 0), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
cv2.putText( |
|
image, |
|
feet_placement, |
|
(125, 25), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.5, |
|
(255, 255, 255), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
|
|
|
|
cv2.putText( |
|
image, |
|
"KNEE", |
|
(225, 12), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.3, |
|
(0, 0, 0), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
cv2.putText( |
|
image, |
|
knee_placement, |
|
(220, 25), |
|
cv2.FONT_HERSHEY_COMPLEX, |
|
0.5, |
|
(255, 255, 255), |
|
1, |
|
cv2.LINE_AA, |
|
) |
|
|
|
except Exception as e: |
|
print(f"Error while detecting squat errors: {e}") |
|
|