amitesh11's picture
Upload 3019 files
369fac9 verified
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
# * Visibility check of important landmarks for foot placement analysis
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 visibility of any keypoints is low cancel the analysis
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
# * Calculate shoulder width
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)
# * Calculate 2-foot width
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)
# * Calculate foot and shoulder ratio
foot_shoulder_ratio = round(foot_width / shoulder_width, 1)
# * Analyze FOOT PLACEMENT
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
# * Visibility check of important landmarks for knee placement analysis
left_knee_vis = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].visibility
right_knee_vis = landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].visibility
# If visibility of any keypoints is low cancel the analysis
if left_knee_vis < visibility_threshold or right_knee_vis < visibility_threshold:
print("Cannot see foot")
return analyzed_results
# * Calculate 2 knee width
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)
# * Calculate foot and shoulder ratio
knee_foot_ratio = round(knee_width / foot_width, 1)
# * Analyze KNEE placement
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",
]
# Generate all columns of the data frame
self.headers = ["label"] # Label column
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:
# * Model prediction for SQUAT counter
# Extract keypoints from frame for the input
row = extract_important_keypoints(mp_results, self.important_landmarks)
X = pd.DataFrame([row], columns=self.headers[1:])
# Make prediction and its probability
predicted_class = self.model.predict(X)[0]
prediction_probabilities = self.model.predict_proba(X)[0]
prediction_probability = round(
prediction_probabilities[prediction_probabilities.argmax()], 2
)
# Evaluate model prediction
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
# Analyze squat pose
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"]
# * Evaluate FEET PLACEMENT error
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"
# * Evaluate KNEE PLACEMENT error
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"
# Stage management for saving results
# * Feet placement
if feet_placement in ["too tight", "too wide"]:
# Stage not change
if self.previous_stage["feet"] == feet_placement:
pass
# Stage from correct to error
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
# * Knee placement
if knee_placement in ["too tight", "too wide"]:
# Stage not change
if self.previous_stage["knee"] == knee_placement:
pass
# Stage from correct to error
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
# Visualization
# Draw landmarks and connections
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
),
)
# Status box
cv2.rectangle(image, (0, 0), (300, 40), (245, 117, 16), -1)
# Display class
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,
)
# Display Feet and Shoulder width ratio
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,
)
# Display knee and Shoulder width ratio
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}")