| | 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}") |
| |
|