Commit ·
4cdc77a
1
Parent(s): cfbaa51
CPR Partial Code Cleaning
Browse files- CPR_Module/Common/chest_initializer.py +42 -51
- CPR_Module/Common/keypoints.py +0 -1
- CPR_Module/Common/logging_config.py +0 -1
- CPR_Module/Common/posture_analyzer.py +4 -6
- CPR_Module/Common/role_classifier.py +18 -35
- CPR_Module/Common/shoulders_analyzer.py +1 -1
- CPR_Module/Common/threaded_camera.py +1 -1
- CPR_Module/Common/warnings_overlayer.py +0 -2
- CPR_Module/Common/wrists_midpoint_analyzer.py +1 -1
- CPR_Module/Educational_Mode/CPRAnalyzer.py +88 -146
- CPR_Module/Educational_Mode/graph_plotter.py +13 -11
- CPR_Module/Educational_Mode/metrics_calculator.py +5 -27
- CPR_Module/Educational_Mode/pose_estimation.py +0 -3
- CPR_Module/Emergency_Mode/graph_plotter.py +0 -2
- CPR_Module/Emergency_Mode/main.py +1 -50
- CPR_Module/Emergency_Mode/metrics_calculator.py +4 -26
- CPR_Module/Emergency_Mode/pose_estimation.py +0 -4
- README.md +0 -2
CPR_Module/Common/chest_initializer.py
CHANGED
|
@@ -5,7 +5,7 @@ from CPR_Module.Common.keypoints import CocoKeypoints
|
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
| 7 |
class ChestInitializer:
|
| 8 |
-
"""
|
| 9 |
|
| 10 |
def __init__(self):
|
| 11 |
self.chest_params = None
|
|
@@ -13,7 +13,7 @@ class ChestInitializer:
|
|
| 13 |
self.expected_chest_params = None
|
| 14 |
|
| 15 |
def estimate_chest_region(self, keypoints, bounding_box, frame_width, frame_height):
|
| 16 |
-
"""Estimate
|
| 17 |
try:
|
| 18 |
# Unpack bounding box and calculate shoulder dimensions
|
| 19 |
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = bounding_box
|
|
@@ -24,71 +24,62 @@ class ChestInitializer:
|
|
| 24 |
right_shoulder = keypoints[CocoKeypoints.RIGHT_SHOULDER.value]
|
| 25 |
|
| 26 |
# Midpoints calculation
|
| 27 |
-
shoulder_center = np.array([
|
| 28 |
-
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
#&
|
| 31 |
# If the x-coordinate shoulder center is closer to that of the Bottom-Right bbox corner (2)
|
| 32 |
# then the orientation is "right"
|
| 33 |
# If the x-coordinate shoulder center is closer to that of the Top-Left bbox corner (1)
|
| 34 |
# then the orientation is "left"
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
chest_center_from_shoulder = np.array([chest_center_from_shoulder_x, chest_center_from_shoulder_y])
|
| 44 |
-
|
| 45 |
-
# Chest dimensions (85% of shoulder width, 40% height)
|
| 46 |
chest_dx = bbox_delta_y * 0.8
|
| 47 |
chest_dy = bbox_delta_y * 1.75
|
| 48 |
|
| 49 |
# Calculate region coordinates
|
| 50 |
-
x1 =
|
| 51 |
-
y1 =
|
| 52 |
-
x2 =
|
| 53 |
-
y2 =
|
| 54 |
-
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if x2 <= x1 or y2 <= y1:
|
| 63 |
return None
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
return (cx, cy, cw, ch)
|
| 72 |
|
| 73 |
except (IndexError, TypeError, ValueError) as e:
|
| 74 |
cpr_logger.error(f"Chest estimation error: {e}")
|
| 75 |
return None
|
| 76 |
|
| 77 |
def estimate_chest_region_weighted_avg(self, frame_width, frame_height, window_size=60, min_samples=3):
|
| 78 |
-
"""
|
| 79 |
-
Calculate stabilized chest parameters using weighted averaging with boundary checks.
|
| 80 |
-
|
| 81 |
-
Args:
|
| 82 |
-
self.chest_params_history: List of recent chest parameters [(cx, cy, cw, ch), ...]
|
| 83 |
-
frame_width: Width of the video frame
|
| 84 |
-
frame_height: Height of the video frame
|
| 85 |
-
window_size: Number of recent frames to consider (default: 5)
|
| 86 |
-
min_samples: Minimum valid samples required (default: 3)
|
| 87 |
-
|
| 88 |
-
Returns:
|
| 89 |
-
Tuple of (cx, cy, cw, ch) as integers within frame boundaries,
|
| 90 |
-
or None if insufficient data or invalid rectangle
|
| 91 |
-
"""
|
| 92 |
if not self.chest_params_history:
|
| 93 |
return None
|
| 94 |
|
|
@@ -134,7 +125,7 @@ class ChestInitializer:
|
|
| 134 |
return None
|
| 135 |
|
| 136 |
def draw_expected_chest_region(self, frame):
|
| 137 |
-
"""
|
| 138 |
if self.expected_chest_params is None:
|
| 139 |
return frame
|
| 140 |
|
|
@@ -149,7 +140,7 @@ class ChestInitializer:
|
|
| 149 |
|
| 150 |
cv2.circle(frame, (int(cx), int(cy)), 8, (128, 128, 0), -1)
|
| 151 |
|
| 152 |
-
cv2.putText(frame, "
|
| 153 |
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (128, 128, 0), 2)
|
| 154 |
|
| 155 |
return frame
|
|
|
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
| 7 |
class ChestInitializer:
|
| 8 |
+
"""Estimates and stabilizes chest region parameters based on keypoints and bounding box."""
|
| 9 |
|
| 10 |
def __init__(self):
|
| 11 |
self.chest_params = None
|
|
|
|
| 13 |
self.expected_chest_params = None
|
| 14 |
|
| 15 |
def estimate_chest_region(self, keypoints, bounding_box, frame_width, frame_height):
|
| 16 |
+
"""Estimate chest region parameters based on keypoints and bounding box."""
|
| 17 |
try:
|
| 18 |
# Unpack bounding box and calculate shoulder dimensions
|
| 19 |
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = bounding_box
|
|
|
|
| 24 |
right_shoulder = keypoints[CocoKeypoints.RIGHT_SHOULDER.value]
|
| 25 |
|
| 26 |
# Midpoints calculation
|
| 27 |
+
shoulder_center = np.array([
|
| 28 |
+
(left_shoulder[0] + right_shoulder[0]) / 2,
|
| 29 |
+
(left_shoulder[1] + right_shoulder[1]) / 2
|
| 30 |
+
])
|
| 31 |
|
| 32 |
+
#& Handling different patient positions
|
| 33 |
# If the x-coordinate shoulder center is closer to that of the Bottom-Right bbox corner (2)
|
| 34 |
# then the orientation is "right"
|
| 35 |
# If the x-coordinate shoulder center is closer to that of the Top-Left bbox corner (1)
|
| 36 |
# then the orientation is "left"
|
| 37 |
+
if abs(shoulder_center[0] - bbox_x2) < abs(shoulder_center[0] - bbox_x1): # Right orientation
|
| 38 |
+
chest_center_x = shoulder_center[0] - 0.3 * bbox_delta_y
|
| 39 |
+
chest_center_y = shoulder_center[1] - 0.1 * bbox_delta_y
|
| 40 |
+
else: # Left orientation
|
| 41 |
+
chest_center_x = shoulder_center[0] + 1.0 * bbox_delta_y
|
| 42 |
+
chest_center_y = shoulder_center[1] - 0.1 * bbox_delta_y
|
| 43 |
+
|
| 44 |
+
# Chest dimensions
|
|
|
|
|
|
|
|
|
|
| 45 |
chest_dx = bbox_delta_y * 0.8
|
| 46 |
chest_dy = bbox_delta_y * 1.75
|
| 47 |
|
| 48 |
# Calculate region coordinates
|
| 49 |
+
x1 = chest_center_x - chest_dx / 2
|
| 50 |
+
y1 = chest_center_y - chest_dy / 2
|
| 51 |
+
x2 = chest_center_x + chest_dx / 2
|
| 52 |
+
y2 = chest_center_y + chest_dy / 2
|
| 53 |
+
|
| 54 |
+
# First clamp the bounding box to frame boundaries
|
| 55 |
+
bbox_x1 = max(0, min(bbox_x1, frame_width - 1))
|
| 56 |
+
bbox_y1 = max(0, min(bbox_y1, frame_height - 1))
|
| 57 |
+
bbox_x2 = max(0, min(bbox_x2, frame_width - 1))
|
| 58 |
+
bbox_y2 = max(0, min(bbox_y2, frame_height - 1))
|
| 59 |
+
|
| 60 |
+
# Clamp to bounding box (which is already clamped to frame)
|
| 61 |
+
x1 = max(bbox_x1, min(x1, bbox_x2))
|
| 62 |
+
y1 = max(bbox_y1, min(y1, bbox_y2))
|
| 63 |
+
x2 = max(bbox_x1, min(x2, bbox_x2))
|
| 64 |
+
y2 = max(bbox_y1, min(y2, bbox_y2))
|
| 65 |
+
|
| 66 |
+
# Final validity check
|
| 67 |
if x2 <= x1 or y2 <= y1:
|
| 68 |
return None
|
| 69 |
|
| 70 |
+
return (
|
| 71 |
+
(x1 + x2) / 2, # cx
|
| 72 |
+
(y1 + y2) / 2, # cy
|
| 73 |
+
x2 - x1, # cw
|
| 74 |
+
y2 - y1 # ch
|
| 75 |
+
)
|
|
|
|
| 76 |
|
| 77 |
except (IndexError, TypeError, ValueError) as e:
|
| 78 |
cpr_logger.error(f"Chest estimation error: {e}")
|
| 79 |
return None
|
| 80 |
|
| 81 |
def estimate_chest_region_weighted_avg(self, frame_width, frame_height, window_size=60, min_samples=3):
|
| 82 |
+
"""Estimate chest region using weighted average of recent parameters."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
if not self.chest_params_history:
|
| 84 |
return None
|
| 85 |
|
|
|
|
| 125 |
return None
|
| 126 |
|
| 127 |
def draw_expected_chest_region(self, frame):
|
| 128 |
+
"""Draw the expected chest region on the frame."""
|
| 129 |
if self.expected_chest_params is None:
|
| 130 |
return frame
|
| 131 |
|
|
|
|
| 140 |
|
| 141 |
cv2.circle(frame, (int(cx), int(cy)), 8, (128, 128, 0), -1)
|
| 142 |
|
| 143 |
+
cv2.putText(frame, "CHEST", (x1, max(10, y1 - 5)),
|
| 144 |
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (128, 128, 0), 2)
|
| 145 |
|
| 146 |
return frame
|
CPR_Module/Common/keypoints.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# keypoints.py
|
| 2 |
from enum import Enum
|
| 3 |
|
| 4 |
class CocoKeypoints(Enum):
|
|
|
|
|
|
|
| 1 |
from enum import Enum
|
| 2 |
|
| 3 |
class CocoKeypoints(Enum):
|
CPR_Module/Common/logging_config.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# logging_config.py
|
| 2 |
import logging
|
| 3 |
|
| 4 |
# 1. Set default log level here (change this value as needed)
|
|
|
|
|
|
|
| 1 |
import logging
|
| 2 |
|
| 3 |
# 1. Set default log level here (change this value as needed)
|
CPR_Module/Common/posture_analyzer.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
| 1 |
-
# posture_analyzer.py
|
| 2 |
import math
|
| 3 |
-
import cv2
|
| 4 |
import numpy as np
|
| 5 |
|
| 6 |
from CPR_Module.Common.keypoints import CocoKeypoints
|
| 7 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 8 |
|
| 9 |
class PostureAnalyzer:
|
| 10 |
-
"""
|
| 11 |
|
| 12 |
def __init__(self, right_arm_angle_threshold, left_arm_angle_threshold, wrist_distance_threshold, history_length_to_average):
|
| 13 |
self.history_length_to_average = history_length_to_average
|
|
@@ -79,7 +77,7 @@ class PostureAnalyzer:
|
|
| 79 |
return warnings
|
| 80 |
|
| 81 |
def _check_hands_on_chest(self, keypoints, chest_params):
|
| 82 |
-
"""Check
|
| 83 |
|
| 84 |
# Get the wrist keypoints
|
| 85 |
left_wrist = keypoints[CocoKeypoints.LEFT_WRIST.value]
|
|
@@ -87,8 +85,9 @@ class PostureAnalyzer:
|
|
| 87 |
|
| 88 |
warnings = []
|
| 89 |
try:
|
|
|
|
| 90 |
if chest_params is None:
|
| 91 |
-
return ["Both hands not on chest!"]
|
| 92 |
|
| 93 |
cx, cy, cw, ch = chest_params
|
| 94 |
left_in = right_in = False
|
|
@@ -117,7 +116,6 @@ class PostureAnalyzer:
|
|
| 117 |
|
| 118 |
return warnings
|
| 119 |
|
| 120 |
-
|
| 121 |
def validate_posture(self, keypoints, chest_params):
|
| 122 |
"""Run all posture validations (returns aggregated warnings)"""
|
| 123 |
warnings = []
|
|
|
|
|
|
|
| 1 |
import math
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
|
| 4 |
from CPR_Module.Common.keypoints import CocoKeypoints
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
| 7 |
class PostureAnalyzer:
|
| 8 |
+
"""Analyzes the Rescuer's posture during CPR based on the rescuer's keypoints and the patient's chest region."""
|
| 9 |
|
| 10 |
def __init__(self, right_arm_angle_threshold, left_arm_angle_threshold, wrist_distance_threshold, history_length_to_average):
|
| 11 |
self.history_length_to_average = history_length_to_average
|
|
|
|
| 77 |
return warnings
|
| 78 |
|
| 79 |
def _check_hands_on_chest(self, keypoints, chest_params):
|
| 80 |
+
"""Check if both hands are on the chest (returns warnings)"""
|
| 81 |
|
| 82 |
# Get the wrist keypoints
|
| 83 |
left_wrist = keypoints[CocoKeypoints.LEFT_WRIST.value]
|
|
|
|
| 85 |
|
| 86 |
warnings = []
|
| 87 |
try:
|
| 88 |
+
# Fallback
|
| 89 |
if chest_params is None:
|
| 90 |
+
return ["Both hands not on chest!"]
|
| 91 |
|
| 92 |
cx, cy, cw, ch = chest_params
|
| 93 |
left_in = right_in = False
|
|
|
|
| 116 |
|
| 117 |
return warnings
|
| 118 |
|
|
|
|
| 119 |
def validate_posture(self, keypoints, chest_params):
|
| 120 |
"""Run all posture validations (returns aggregated warnings)"""
|
| 121 |
warnings = []
|
CPR_Module/Common/role_classifier.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
# role_classifier.py
|
| 2 |
-
import cv2
|
| 3 |
-
import numpy as np
|
| 4 |
from ultralytics.utils.plotting import Annotator
|
| 5 |
|
| 6 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 7 |
|
| 8 |
class RoleClassifier:
|
| 9 |
-
"""
|
| 10 |
|
| 11 |
def __init__(self, proximity_thresh=0.3):
|
| 12 |
self.proximity_thresh = proximity_thresh
|
|
@@ -25,15 +22,14 @@ class RoleClassifier:
|
|
| 25 |
if width == 0 or height == 0:
|
| 26 |
return -1
|
| 27 |
|
| 28 |
-
return 1 if height > width else 0
|
| 29 |
|
| 30 |
except (TypeError, ValueError) as e:
|
| 31 |
cpr_logger.error(f"Verticality score calculation error: {e}")
|
| 32 |
return -1
|
| 33 |
|
| 34 |
def _calculate_bounding_box_center(self, bounding_box):
|
| 35 |
-
"""Calculate the center coordinates of a bounding box.
|
| 36 |
-
"""
|
| 37 |
x1, y1, x2, y2 = bounding_box
|
| 38 |
return (x1 + x2) / 2, (y1 + y2) / 2
|
| 39 |
|
|
@@ -42,29 +38,18 @@ class RoleClassifier:
|
|
| 42 |
return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2)**0.5
|
| 43 |
|
| 44 |
def _calculate_bbox_areas(self, rescuer_bbox, patient_bbox):
|
| 45 |
-
"""
|
| 46 |
-
Calculate bounding box areas for rescuer and patient.
|
| 47 |
-
|
| 48 |
-
Args:
|
| 49 |
-
rescuer_bbox: [x1, y1, x2, y2] coordinates of rescuer's bounding box
|
| 50 |
-
patient_bbox: [x1, y1, x2, y2] coordinates of patient's bounding box
|
| 51 |
-
|
| 52 |
-
Returns:
|
| 53 |
-
Tuple: (rescuer_area, patient_area) in pixels
|
| 54 |
-
"""
|
| 55 |
def compute_area(bbox):
|
| 56 |
if bbox is None:
|
| 57 |
return 0
|
| 58 |
-
width = bbox[2] - bbox[0]
|
| 59 |
height = bbox[3] - bbox[1] # y2 - y1
|
| 60 |
return abs(width * height) # Absolute value to handle negative coordinates
|
| 61 |
|
| 62 |
return compute_area(rescuer_bbox), compute_area(patient_bbox)
|
| 63 |
|
| 64 |
def classify_roles(self, results, prev_rescuer_processed_results=None, prev_patient_processed_results=None):
|
| 65 |
-
"""
|
| 66 |
-
Classify roles of rescuer and patient based on detected keypoints and bounding boxes.
|
| 67 |
-
"""
|
| 68 |
|
| 69 |
processed_results = []
|
| 70 |
|
|
@@ -74,12 +59,10 @@ class RoleClassifier:
|
|
| 74 |
prev_rescuer_bbox = prev_rescuer_processed_results["bounding_box"]
|
| 75 |
prev_patient_bbox = prev_patient_processed_results["bounding_box"]
|
| 76 |
|
| 77 |
-
rescuer_area = (prev_rescuer_bbox
|
| 78 |
-
patient_area = (prev_patient_bbox[2]-prev_patient_bbox[0])*(prev_patient_bbox[3]-prev_patient_bbox[1])
|
| 79 |
threshold = rescuer_area + patient_area
|
| 80 |
|
| 81 |
-
for i, (box, keypoints) in enumerate(zip(results.boxes.xywh.cpu().numpy(),
|
| 82 |
-
results.keypoints.xy.cpu().numpy())):
|
| 83 |
try:
|
| 84 |
# Convert box to [x1,y1,x2,y2] format
|
| 85 |
x_center, y_center, width, height = box
|
|
@@ -99,6 +82,7 @@ class RoleClassifier:
|
|
| 99 |
|
| 100 |
# Calculate features
|
| 101 |
verticality_score = self._calculate_verticality_score(bounding_box)
|
|
|
|
| 102 |
#!We already have the center coordinates from the bounding box, no need to recalculate it.
|
| 103 |
bounding_box_center = self._calculate_bounding_box_center(bounding_box)
|
| 104 |
|
|
@@ -115,24 +99,23 @@ class RoleClassifier:
|
|
| 115 |
cpr_logger.error(f"Error processing detection {i}: {e}")
|
| 116 |
continue
|
| 117 |
|
| 118 |
-
#
|
| 119 |
-
patient_candidates = [res for res in processed_results
|
| 120 |
-
if res['verticality_score'] == 0]
|
| 121 |
|
| 122 |
# If more than one horizontal person, select person with lowest center (likely lying down)
|
| 123 |
if len(patient_candidates) > 1:
|
| 124 |
-
patient_candidates = sorted(patient_candidates,
|
| 125 |
-
key=lambda x: x['bounding_box_center'][1])[:1] # Sort by y-coordinate
|
| 126 |
|
| 127 |
patient = patient_candidates[0] if patient_candidates else None
|
| 128 |
|
| 129 |
-
#
|
| 130 |
rescuer = None
|
| 131 |
if patient:
|
| 132 |
# Find vertical people who aren't the patient
|
| 133 |
potential_rescuers = [
|
| 134 |
res for res in processed_results
|
| 135 |
if res['verticality_score'] == 1
|
|
|
|
| 136 |
#! Useless condition because the patient was horizontal
|
| 137 |
and res['original_index'] != patient['original_index']
|
| 138 |
]
|
|
@@ -150,11 +133,11 @@ class RoleClassifier:
|
|
| 150 |
# Create annotator object
|
| 151 |
annotator = Annotator(frame)
|
| 152 |
|
| 153 |
-
# Draw rescuer
|
| 154 |
if self.rescuer_processed_results:
|
| 155 |
try:
|
| 156 |
x1, y1, x2, y2 = map(int, self.rescuer_processed_results["bounding_box"])
|
| 157 |
-
annotator.box_label((x1, y1, x2, y2), "Rescuer
|
| 158 |
|
| 159 |
if "keypoints" in self.rescuer_processed_results:
|
| 160 |
keypoints = self.rescuer_processed_results["keypoints"]
|
|
@@ -162,11 +145,11 @@ class RoleClassifier:
|
|
| 162 |
except Exception as e:
|
| 163 |
cpr_logger.error(f"Error drawing rescuer: {str(e)}")
|
| 164 |
|
| 165 |
-
# Draw patient
|
| 166 |
if self.patient_processed_results:
|
| 167 |
try:
|
| 168 |
x1, y1, x2, y2 = map(int, self.patient_processed_results["bounding_box"])
|
| 169 |
-
annotator.box_label((x1, y1, x2, y2), "Patient
|
| 170 |
|
| 171 |
if "keypoints" in self.patient_processed_results:
|
| 172 |
keypoints = self.patient_processed_results["keypoints"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from ultralytics.utils.plotting import Annotator
|
| 2 |
|
| 3 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 4 |
|
| 5 |
class RoleClassifier:
|
| 6 |
+
"""Classify roles of rescuer and patient based on detected keypoints and bounding boxes."""
|
| 7 |
|
| 8 |
def __init__(self, proximity_thresh=0.3):
|
| 9 |
self.proximity_thresh = proximity_thresh
|
|
|
|
| 22 |
if width == 0 or height == 0:
|
| 23 |
return -1
|
| 24 |
|
| 25 |
+
return 1 if height > width else 0
|
| 26 |
|
| 27 |
except (TypeError, ValueError) as e:
|
| 28 |
cpr_logger.error(f"Verticality score calculation error: {e}")
|
| 29 |
return -1
|
| 30 |
|
| 31 |
def _calculate_bounding_box_center(self, bounding_box):
|
| 32 |
+
"""Calculate the center coordinates of a bounding box."""
|
|
|
|
| 33 |
x1, y1, x2, y2 = bounding_box
|
| 34 |
return (x1 + x2) / 2, (y1 + y2) / 2
|
| 35 |
|
|
|
|
| 38 |
return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2)**0.5
|
| 39 |
|
| 40 |
def _calculate_bbox_areas(self, rescuer_bbox, patient_bbox):
|
| 41 |
+
""" Calculate areas of rescuer and patient bounding boxes."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def compute_area(bbox):
|
| 43 |
if bbox is None:
|
| 44 |
return 0
|
| 45 |
+
width = bbox[2] - bbox[0] # x2 - x1
|
| 46 |
height = bbox[3] - bbox[1] # y2 - y1
|
| 47 |
return abs(width * height) # Absolute value to handle negative coordinates
|
| 48 |
|
| 49 |
return compute_area(rescuer_bbox), compute_area(patient_bbox)
|
| 50 |
|
| 51 |
def classify_roles(self, results, prev_rescuer_processed_results=None, prev_patient_processed_results=None):
|
| 52 |
+
"""Classify rescuer and patient roles based on detection results."""
|
|
|
|
|
|
|
| 53 |
|
| 54 |
processed_results = []
|
| 55 |
|
|
|
|
| 59 |
prev_rescuer_bbox = prev_rescuer_processed_results["bounding_box"]
|
| 60 |
prev_patient_bbox = prev_patient_processed_results["bounding_box"]
|
| 61 |
|
| 62 |
+
rescuer_area, patient_area = self._calculate_bbox_areas(prev_rescuer_bbox, prev_patient_bbox)
|
|
|
|
| 63 |
threshold = rescuer_area + patient_area
|
| 64 |
|
| 65 |
+
for i, (box, keypoints) in enumerate(zip(results.boxes.xywh.cpu().numpy(), results.keypoints.xy.cpu().numpy())):
|
|
|
|
| 66 |
try:
|
| 67 |
# Convert box to [x1,y1,x2,y2] format
|
| 68 |
x_center, y_center, width, height = box
|
|
|
|
| 82 |
|
| 83 |
# Calculate features
|
| 84 |
verticality_score = self._calculate_verticality_score(bounding_box)
|
| 85 |
+
|
| 86 |
#!We already have the center coordinates from the bounding box, no need to recalculate it.
|
| 87 |
bounding_box_center = self._calculate_bounding_box_center(bounding_box)
|
| 88 |
|
|
|
|
| 99 |
cpr_logger.error(f"Error processing detection {i}: {e}")
|
| 100 |
continue
|
| 101 |
|
| 102 |
+
# Identify the patient (horizontal posture)
|
| 103 |
+
patient_candidates = [res for res in processed_results if res['verticality_score'] == 0]
|
|
|
|
| 104 |
|
| 105 |
# If more than one horizontal person, select person with lowest center (likely lying down)
|
| 106 |
if len(patient_candidates) > 1:
|
| 107 |
+
patient_candidates = sorted(patient_candidates, key=lambda x: x['bounding_box_center'][1])[:1] # Sort by y-coordinate
|
|
|
|
| 108 |
|
| 109 |
patient = patient_candidates[0] if patient_candidates else None
|
| 110 |
|
| 111 |
+
# Identify the rescuer
|
| 112 |
rescuer = None
|
| 113 |
if patient:
|
| 114 |
# Find vertical people who aren't the patient
|
| 115 |
potential_rescuers = [
|
| 116 |
res for res in processed_results
|
| 117 |
if res['verticality_score'] == 1
|
| 118 |
+
|
| 119 |
#! Useless condition because the patient was horizontal
|
| 120 |
and res['original_index'] != patient['original_index']
|
| 121 |
]
|
|
|
|
| 133 |
# Create annotator object
|
| 134 |
annotator = Annotator(frame)
|
| 135 |
|
| 136 |
+
# Draw rescuer
|
| 137 |
if self.rescuer_processed_results:
|
| 138 |
try:
|
| 139 |
x1, y1, x2, y2 = map(int, self.rescuer_processed_results["bounding_box"])
|
| 140 |
+
annotator.box_label((x1, y1, x2, y2), "Rescuer", color=(0, 255, 0))
|
| 141 |
|
| 142 |
if "keypoints" in self.rescuer_processed_results:
|
| 143 |
keypoints = self.rescuer_processed_results["keypoints"]
|
|
|
|
| 145 |
except Exception as e:
|
| 146 |
cpr_logger.error(f"Error drawing rescuer: {str(e)}")
|
| 147 |
|
| 148 |
+
# Draw patient
|
| 149 |
if self.patient_processed_results:
|
| 150 |
try:
|
| 151 |
x1, y1, x2, y2 = map(int, self.patient_processed_results["bounding_box"])
|
| 152 |
+
annotator.box_label((x1, y1, x2, y2), "Patient", color=(0, 0, 255))
|
| 153 |
|
| 154 |
if "keypoints" in self.patient_processed_results:
|
| 155 |
keypoints = self.patient_processed_results["keypoints"]
|
CPR_Module/Common/shoulders_analyzer.py
CHANGED
|
@@ -4,7 +4,7 @@ from CPR_Module.Common.keypoints import CocoKeypoints
|
|
| 4 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 5 |
|
| 6 |
class ShouldersAnalyzer:
|
| 7 |
-
"""
|
| 8 |
|
| 9 |
def __init__(self):
|
| 10 |
self.shoulder_distance = None
|
|
|
|
| 4 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 5 |
|
| 6 |
class ShouldersAnalyzer:
|
| 7 |
+
"""Calculate and store shoulder distance for CPR rescuer"""
|
| 8 |
|
| 9 |
def __init__(self):
|
| 10 |
self.shoulder_distance = None
|
CPR_Module/Common/threaded_camera.py
CHANGED
|
@@ -26,7 +26,7 @@ class ThreadedCamera:
|
|
| 26 |
|
| 27 |
cpr_logger.info(f"[VIDEO CAPTURE] Requested FPS: {requested_fps}, Set Success: {set_success}, Actual FPS: {actual_fps}")
|
| 28 |
|
| 29 |
-
# The buffer should be able to hold a lag of up to
|
| 30 |
number_of_seconds_to_buffer = 5
|
| 31 |
queue_size = int(actual_fps * number_of_seconds_to_buffer)
|
| 32 |
self.q = Queue(maxsize=queue_size)
|
|
|
|
| 26 |
|
| 27 |
cpr_logger.info(f"[VIDEO CAPTURE] Requested FPS: {requested_fps}, Set Success: {set_success}, Actual FPS: {actual_fps}")
|
| 28 |
|
| 29 |
+
# The buffer should be able to hold a lag of up to "number_of_seconds_to_buffer" seconds
|
| 30 |
number_of_seconds_to_buffer = 5
|
| 31 |
queue_size = int(actual_fps * number_of_seconds_to_buffer)
|
| 32 |
self.q = Queue(maxsize=queue_size)
|
CPR_Module/Common/warnings_overlayer.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
import os
|
| 4 |
-
import sys
|
| 5 |
|
| 6 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 7 |
|
|
@@ -108,7 +107,6 @@ class WarningsOverlayer:
|
|
| 108 |
writer.release()
|
| 109 |
cpr_logger.info(f"\n[POST-PROCESS] Final output saved to: {final_path}")
|
| 110 |
|
| 111 |
-
|
| 112 |
def _draw_warnings(self, frame, active_warnings):
|
| 113 |
"""Draw all warnings in a single vertical drawer"""
|
| 114 |
frame_height = frame.shape[0]
|
|
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
import os
|
|
|
|
| 4 |
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
|
|
|
| 107 |
writer.release()
|
| 108 |
cpr_logger.info(f"\n[POST-PROCESS] Final output saved to: {final_path}")
|
| 109 |
|
|
|
|
| 110 |
def _draw_warnings(self, frame, active_warnings):
|
| 111 |
"""Draw all warnings in a single vertical drawer"""
|
| 112 |
frame_height = frame.shape[0]
|
CPR_Module/Common/wrists_midpoint_analyzer.py
CHANGED
|
@@ -5,7 +5,7 @@ from CPR_Module.Common.keypoints import CocoKeypoints
|
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
| 7 |
class WristsMidpointAnalyzer:
|
| 8 |
-
"""
|
| 9 |
|
| 10 |
def __init__(self, allowed_distance_between_wrists=170):
|
| 11 |
self.allowed_distance_between_wrists = allowed_distance_between_wrists
|
|
|
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
| 7 |
class WristsMidpointAnalyzer:
|
| 8 |
+
"""Calculate and visualize midpoint between wrists for CPR rescuer"""
|
| 9 |
|
| 10 |
def __init__(self, allowed_distance_between_wrists=170):
|
| 11 |
self.allowed_distance_between_wrists = allowed_distance_between_wrists
|
CPR_Module/Educational_Mode/CPRAnalyzer.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
-
# main.py
|
| 2 |
import cv2
|
| 3 |
import time
|
| 4 |
import math
|
| 5 |
import numpy as np
|
| 6 |
import os
|
| 7 |
-
import sys
|
| 8 |
|
| 9 |
from CPR_Module.Educational_Mode.pose_estimation import PoseEstimator
|
| 10 |
from CPR_Module.Educational_Mode.metrics_calculator import MetricsCalculator
|
|
@@ -20,7 +18,7 @@ from CPR_Module.Common.warnings_overlayer import WarningsOverlayer
|
|
| 20 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 21 |
|
| 22 |
class CPRAnalyzer:
|
| 23 |
-
"""Main
|
| 24 |
|
| 25 |
def __init__(self, input_video, video_output_path, plot_output_path, requested_fps):
|
| 26 |
|
|
@@ -85,11 +83,11 @@ class CPRAnalyzer:
|
|
| 85 |
cpr_logger.info("[INIT] Previous results initialized")
|
| 86 |
|
| 87 |
#& Fundamental timing parameters (in seconds)
|
| 88 |
-
self.MIN_ERROR_DURATION = 0.5
|
| 89 |
-
self.REPORTING_INTERVAL = 5.0
|
| 90 |
-
self.SAMPLING_INTERVAL = 0.2
|
| 91 |
-
self.KEEP_RATE_AND_DEPTH_WARNINGS_INTERVAL = 3.0
|
| 92 |
-
self.MIN_CHUNK_LENGTH_TO_REPORT = 3.0
|
| 93 |
|
| 94 |
# Derived frame counts
|
| 95 |
self.sampling_interval_frames = int(round(self.fps * self.SAMPLING_INTERVAL))
|
|
@@ -126,7 +124,6 @@ class CPRAnalyzer:
|
|
| 126 |
self.consecutive_frames_with_posture_errors_counters = {warning: 0 for warning in self.possible_warnings}
|
| 127 |
|
| 128 |
#& Initialize variables for reporting warnings
|
| 129 |
-
|
| 130 |
self.rate_and_depth_warnings_from_the_last_report = []
|
| 131 |
cpr_logger.info("[INIT] Rate and depth warnings from the last report initialized")
|
| 132 |
|
|
@@ -151,75 +148,6 @@ class CPRAnalyzer:
|
|
| 151 |
self.return_rate_and_depth_warnings_interval_frames_counter = self.return_rate_and_depth_warnings_interval_frames
|
| 152 |
cpr_logger.info("[INIT] Formatted warnings initialized")
|
| 153 |
|
| 154 |
-
def _initialize_video_writer(self, frame):
|
| 155 |
-
"""Initialize writer with safe fallback options"""
|
| 156 |
-
height, width = frame.shape[:2]
|
| 157 |
-
effective_fps = self.fps / max(1, self.sampling_interval_frames)
|
| 158 |
-
|
| 159 |
-
# Try different codec/container combinations
|
| 160 |
-
for codec, ext, fmt in [('avc1', 'mp4', 'mp4v'), # H.264
|
| 161 |
-
('MJPG', 'avi', 'avi'),
|
| 162 |
-
('XVID', 'avi', 'avi')]:
|
| 163 |
-
fourcc = cv2.VideoWriter_fourcc(*codec)
|
| 164 |
-
writer = cv2.VideoWriter(self.video_output_path, fourcc, effective_fps, (width, height))
|
| 165 |
-
|
| 166 |
-
if writer.isOpened():
|
| 167 |
-
self.video_writer = writer
|
| 168 |
-
self._writer_initialized = True
|
| 169 |
-
cpr_logger.info(f"[VIDEO WRITER] Initialized with {codec} codec")
|
| 170 |
-
return
|
| 171 |
-
else:
|
| 172 |
-
writer.release()
|
| 173 |
-
|
| 174 |
-
cpr_logger.info("[ERROR] Failed to initialize any video writer!")
|
| 175 |
-
self._writer_initialized = False
|
| 176 |
-
|
| 177 |
-
def _handle_chunk_end(self):
|
| 178 |
-
"""Helper to handle chunk termination logic"""
|
| 179 |
-
self._calculate_rate_and_depth_for_chunk()
|
| 180 |
-
cpr_logger.info(f"[RUN ANALYSIS] Calculated rate and depth for the chunk")
|
| 181 |
-
|
| 182 |
-
rate_and_depth_warnings = self._get_rate_and_depth_warnings()
|
| 183 |
-
|
| 184 |
-
# If the chunk is too short, we don't want to report any warnings it might contain.
|
| 185 |
-
if (self.chunk_end_frame_index - self.chunk_start_frame_index) < self.min_chunk_length_to_report_frames:
|
| 186 |
-
rate_and_depth_warnings = []
|
| 187 |
-
|
| 188 |
-
self.cached_rate_and_depth_warnings = rate_and_depth_warnings
|
| 189 |
-
self.return_rate_and_depth_warnings_interval_frames_counter = self.return_rate_and_depth_warnings_interval_frames
|
| 190 |
-
cpr_logger.info(f"[RUN ANALYSIS] Retrieved rate and depth warnings for the chunk")
|
| 191 |
-
|
| 192 |
-
self.rate_and_depth_warnings.append({
|
| 193 |
-
'start_frame': self.chunk_start_frame_index,
|
| 194 |
-
'end_frame': self.chunk_end_frame_index,
|
| 195 |
-
'rate_and_depth_warnings': rate_and_depth_warnings,
|
| 196 |
-
})
|
| 197 |
-
cpr_logger.info(f"[RUN ANALYSIS] Assigned rate and depth warnings region data")
|
| 198 |
-
|
| 199 |
-
self.shoulders_analyzer.reset_shoulder_distances()
|
| 200 |
-
self.wrists_midpoint_analyzer.reset_midpoint_history()
|
| 201 |
-
cpr_logger.info(f"[RUN ANALYSIS] Reset shoulder distances and midpoint history for the chunk")
|
| 202 |
-
|
| 203 |
-
def _handle_posture_warnings_region_end(self):
|
| 204 |
-
"""Helper to handle posture warnings region termination"""
|
| 205 |
-
self.posture_warnings.append({
|
| 206 |
-
'start_frame': self.posture_warnings_region_start_frame_index,
|
| 207 |
-
'end_frame': self.posture_warnings_region_end_frame_index,
|
| 208 |
-
'posture_warnings': self.cached_posture_warnings.copy(),
|
| 209 |
-
})
|
| 210 |
-
cpr_logger.info(f"[RUN ANALYSIS] Assigned posture warnings region data")
|
| 211 |
-
|
| 212 |
-
def _start_new_chunk(self, chunk_type="chunk"):
|
| 213 |
-
"""Helper to initialize new chunk"""
|
| 214 |
-
self.chunk_start_frame_index = self.frame_counter
|
| 215 |
-
self.waiting_to_start_new_chunk = False
|
| 216 |
-
cpr_logger.info(f"[CHUNK] New {chunk_type} started at {self.frame_counter}")
|
| 217 |
-
|
| 218 |
-
def _start_new_posture_warnings_region(self):
|
| 219 |
-
"""Helper to initialize new posture warnings region"""
|
| 220 |
-
self.posture_warnings_region_start_frame_index = self.frame_counter
|
| 221 |
-
cpr_logger.info(f"[POSTURE WARNINGS] New region started at {self.frame_counter}")
|
| 222 |
-
|
| 223 |
def run_analysis_video(self):
|
| 224 |
try:
|
| 225 |
cpr_logger.info("[RUN ANALYSIS] Starting analysis")
|
|
@@ -437,36 +365,7 @@ class CPRAnalyzer:
|
|
| 437 |
cpr_logger.info(f"[TIMING] Report and plot elapsed time: {report_and_plot_elapsed_time:.2f}s")
|
| 438 |
return self.graph_plotter._chunks_json_data
|
| 439 |
|
| 440 |
-
|
| 441 |
-
"""Combine warnings into a simple structured response"""
|
| 442 |
-
|
| 443 |
-
if self.cached_posture_warnings:
|
| 444 |
-
return {
|
| 445 |
-
"status": "warning",
|
| 446 |
-
"posture_warnings": self.cached_posture_warnings,
|
| 447 |
-
"rate_and_depth_warnings": [],
|
| 448 |
-
}
|
| 449 |
-
|
| 450 |
-
if (self.cached_rate_and_depth_warnings) and (self.return_rate_and_depth_warnings_interval_frames_counter > 0):
|
| 451 |
-
self.return_rate_and_depth_warnings_interval_frames_counter -= 1
|
| 452 |
-
|
| 453 |
-
return {
|
| 454 |
-
"status": "warning",
|
| 455 |
-
"posture_warnings": [],
|
| 456 |
-
"rate_and_depth_warnings": self.cached_rate_and_depth_warnings,
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
return {
|
| 460 |
-
"status": "ok",
|
| 461 |
-
"posture_warnings": [],
|
| 462 |
-
"rate_and_depth_warnings": [],
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
def _handle_frame_rotation(self, frame):
|
| 466 |
-
if frame.shape[1] > frame.shape[0]: # Width > Height
|
| 467 |
-
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
| 468 |
-
return frame
|
| 469 |
-
|
| 470 |
def _process_frame(self, frame):
|
| 471 |
#* Warnings for real time feedback
|
| 472 |
warnings = []
|
|
@@ -594,7 +493,6 @@ class CPRAnalyzer:
|
|
| 594 |
return warnings, has_appended_midpoint
|
| 595 |
|
| 596 |
def _compose_frame(self, frame, is_part_of_a_posture_warnings_region):
|
| 597 |
-
# Chest Region
|
| 598 |
if frame is not None:
|
| 599 |
frame = self.chest_initializer.draw_expected_chest_region(frame)
|
| 600 |
cpr_logger.info(f"[VISUALIZATION] Drawn chest region")
|
|
@@ -606,6 +504,58 @@ class CPRAnalyzer:
|
|
| 606 |
|
| 607 |
return frame
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
def _calculate_rate_and_depth_for_chunk(self):
|
| 610 |
try:
|
| 611 |
result = self.metrics_calculator.handle_chunk(np.array(self.wrists_midpoint_analyzer.midpoint_history), self.chunk_start_frame_index, self.chunk_end_frame_index, self.fps, np.array(self.shoulders_analyzer.shoulder_distance_history), self.sampling_interval_frames)
|
|
@@ -645,41 +595,33 @@ class CPRAnalyzer:
|
|
| 645 |
|
| 646 |
return rate_and_depth_warnings
|
| 647 |
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
# Configuration
|
| 652 |
-
requested_fps = 30
|
| 653 |
-
input_video = r"D:\CPR_education\CPR\End to End\Code Refactor\video_2.mp4"
|
| 654 |
-
# Validate input file exists
|
| 655 |
-
if not os.path.exists(input_video):
|
| 656 |
-
cpr_logger.error(f"[ERROR] Input video not found at: {input_video}")
|
| 657 |
-
sys.exit(1)
|
| 658 |
-
|
| 659 |
-
output_dir = r"D:\CPR_education\CPR\End to End\Code Refactor\Output"
|
| 660 |
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
|
|
|
|
|
|
|
|
| 1 |
import cv2
|
| 2 |
import time
|
| 3 |
import math
|
| 4 |
import numpy as np
|
| 5 |
import os
|
|
|
|
| 6 |
|
| 7 |
from CPR_Module.Educational_Mode.pose_estimation import PoseEstimator
|
| 8 |
from CPR_Module.Educational_Mode.metrics_calculator import MetricsCalculator
|
|
|
|
| 18 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 19 |
|
| 20 |
class CPRAnalyzer:
|
| 21 |
+
"""Main class for analyzing CPR performance from video input."""
|
| 22 |
|
| 23 |
def __init__(self, input_video, video_output_path, plot_output_path, requested_fps):
|
| 24 |
|
|
|
|
| 83 |
cpr_logger.info("[INIT] Previous results initialized")
|
| 84 |
|
| 85 |
#& Fundamental timing parameters (in seconds)
|
| 86 |
+
self.MIN_ERROR_DURATION = 0.5 # Require sustained errors for X second
|
| 87 |
+
self.REPORTING_INTERVAL = 5.0 # Generate reports every Y seconds
|
| 88 |
+
self.SAMPLING_INTERVAL = 0.2 # Analyze every Z seconds
|
| 89 |
+
self.KEEP_RATE_AND_DEPTH_WARNINGS_INTERVAL = 3.0 # Keep rate and depth warnings for W seconds
|
| 90 |
+
self.MIN_CHUNK_LENGTH_TO_REPORT = 3.0 # Minimum chunk length to report warnings
|
| 91 |
|
| 92 |
# Derived frame counts
|
| 93 |
self.sampling_interval_frames = int(round(self.fps * self.SAMPLING_INTERVAL))
|
|
|
|
| 124 |
self.consecutive_frames_with_posture_errors_counters = {warning: 0 for warning in self.possible_warnings}
|
| 125 |
|
| 126 |
#& Initialize variables for reporting warnings
|
|
|
|
| 127 |
self.rate_and_depth_warnings_from_the_last_report = []
|
| 128 |
cpr_logger.info("[INIT] Rate and depth warnings from the last report initialized")
|
| 129 |
|
|
|
|
| 148 |
self.return_rate_and_depth_warnings_interval_frames_counter = self.return_rate_and_depth_warnings_interval_frames
|
| 149 |
cpr_logger.info("[INIT] Formatted warnings initialized")
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
def run_analysis_video(self):
|
| 152 |
try:
|
| 153 |
cpr_logger.info("[RUN ANALYSIS] Starting analysis")
|
|
|
|
| 365 |
cpr_logger.info(f"[TIMING] Report and plot elapsed time: {report_and_plot_elapsed_time:.2f}s")
|
| 366 |
return self.graph_plotter._chunks_json_data
|
| 367 |
|
| 368 |
+
#^############################## Frame Processing & Composition ##############################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
def _process_frame(self, frame):
|
| 370 |
#* Warnings for real time feedback
|
| 371 |
warnings = []
|
|
|
|
| 493 |
return warnings, has_appended_midpoint
|
| 494 |
|
| 495 |
def _compose_frame(self, frame, is_part_of_a_posture_warnings_region):
|
|
|
|
| 496 |
if frame is not None:
|
| 497 |
frame = self.chest_initializer.draw_expected_chest_region(frame)
|
| 498 |
cpr_logger.info(f"[VISUALIZATION] Drawn chest region")
|
|
|
|
| 504 |
|
| 505 |
return frame
|
| 506 |
|
| 507 |
+
#^############################## Start/End of Chunk/Posture Warnings Region Handling ##############################
|
| 508 |
+
def _handle_chunk_end(self):
|
| 509 |
+
"""Helper to handle chunk termination logic"""
|
| 510 |
+
|
| 511 |
+
self._calculate_rate_and_depth_for_chunk()
|
| 512 |
+
cpr_logger.info(f"[RUN ANALYSIS] Calculated rate and depth for the chunk")
|
| 513 |
+
|
| 514 |
+
rate_and_depth_warnings = self._get_rate_and_depth_warnings()
|
| 515 |
+
|
| 516 |
+
# If the chunk is too short, we don't want to report any warnings it might contain.
|
| 517 |
+
if (self.chunk_end_frame_index - self.chunk_start_frame_index) < self.min_chunk_length_to_report_frames:
|
| 518 |
+
rate_and_depth_warnings = []
|
| 519 |
+
|
| 520 |
+
self.cached_rate_and_depth_warnings = rate_and_depth_warnings
|
| 521 |
+
self.return_rate_and_depth_warnings_interval_frames_counter = self.return_rate_and_depth_warnings_interval_frames
|
| 522 |
+
cpr_logger.info(f"[RUN ANALYSIS] Retrieved rate and depth warnings for the chunk")
|
| 523 |
+
|
| 524 |
+
self.rate_and_depth_warnings.append({
|
| 525 |
+
'start_frame': self.chunk_start_frame_index,
|
| 526 |
+
'end_frame': self.chunk_end_frame_index,
|
| 527 |
+
'rate_and_depth_warnings': rate_and_depth_warnings,
|
| 528 |
+
})
|
| 529 |
+
cpr_logger.info(f"[RUN ANALYSIS] Assigned rate and depth warnings region data")
|
| 530 |
+
|
| 531 |
+
self.shoulders_analyzer.reset_shoulder_distances()
|
| 532 |
+
self.wrists_midpoint_analyzer.reset_midpoint_history()
|
| 533 |
+
cpr_logger.info(f"[RUN ANALYSIS] Reset shoulder distances and midpoint history for the chunk")
|
| 534 |
+
|
| 535 |
+
def _handle_posture_warnings_region_end(self):
|
| 536 |
+
"""Helper to handle posture warnings region termination"""
|
| 537 |
+
|
| 538 |
+
self.posture_warnings.append({
|
| 539 |
+
'start_frame': self.posture_warnings_region_start_frame_index,
|
| 540 |
+
'end_frame': self.posture_warnings_region_end_frame_index,
|
| 541 |
+
'posture_warnings': self.cached_posture_warnings.copy(),
|
| 542 |
+
})
|
| 543 |
+
cpr_logger.info(f"[RUN ANALYSIS] Assigned posture warnings region data")
|
| 544 |
+
|
| 545 |
+
def _start_new_chunk(self, chunk_type="chunk"):
|
| 546 |
+
"""Helper to initialize new chunk"""
|
| 547 |
+
|
| 548 |
+
self.chunk_start_frame_index = self.frame_counter
|
| 549 |
+
self.waiting_to_start_new_chunk = False
|
| 550 |
+
cpr_logger.info(f"[CHUNK] New {chunk_type} started at {self.frame_counter}")
|
| 551 |
+
|
| 552 |
+
def _start_new_posture_warnings_region(self):
|
| 553 |
+
"""Helper to initialize new posture warnings region"""
|
| 554 |
+
|
| 555 |
+
self.posture_warnings_region_start_frame_index = self.frame_counter
|
| 556 |
+
cpr_logger.info(f"[POSTURE WARNINGS] New region started at {self.frame_counter}")
|
| 557 |
+
|
| 558 |
+
#^############################## Rate & Depth Calculations ##############################
|
| 559 |
def _calculate_rate_and_depth_for_chunk(self):
|
| 560 |
try:
|
| 561 |
result = self.metrics_calculator.handle_chunk(np.array(self.wrists_midpoint_analyzer.midpoint_history), self.chunk_start_frame_index, self.chunk_end_frame_index, self.fps, np.array(self.shoulders_analyzer.shoulder_distance_history), self.sampling_interval_frames)
|
|
|
|
| 595 |
|
| 596 |
return rate_and_depth_warnings
|
| 597 |
|
| 598 |
+
#^############################## Video Writer ##############################
|
| 599 |
+
def _initialize_video_writer(self, frame):
|
| 600 |
+
"""Initialize writer with safe fallback options"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
|
| 602 |
+
height, width = frame.shape[:2]
|
| 603 |
+
effective_fps = self.fps / max(1, self.sampling_interval_frames)
|
| 604 |
+
|
| 605 |
+
# Try different codec/container combinations
|
| 606 |
+
for codec, ext, fmt in [('avc1', 'mp4', 'mp4v'), # H.264
|
| 607 |
+
('MJPG', 'avi', 'avi'),
|
| 608 |
+
('XVID', 'avi', 'avi')]:
|
| 609 |
+
fourcc = cv2.VideoWriter_fourcc(*codec)
|
| 610 |
+
writer = cv2.VideoWriter(self.video_output_path, fourcc, effective_fps, (width, height))
|
| 611 |
+
|
| 612 |
+
if writer.isOpened():
|
| 613 |
+
self.video_writer = writer
|
| 614 |
+
self._writer_initialized = True
|
| 615 |
+
cpr_logger.info(f"[VIDEO WRITER] Initialized with {codec} codec")
|
| 616 |
+
return
|
| 617 |
+
else:
|
| 618 |
+
writer.release()
|
| 619 |
+
|
| 620 |
+
cpr_logger.info("[ERROR] Failed to initialize any video writer!")
|
| 621 |
+
self._writer_initialized = False
|
| 622 |
+
|
| 623 |
+
#^############################## Frame Rotation ##############################
|
| 624 |
+
def _handle_frame_rotation(self, frame):
|
| 625 |
+
if frame.shape[1] > frame.shape[0]: # Width > Height
|
| 626 |
+
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
| 627 |
+
return frame
|
CPR_Module/Educational_Mode/graph_plotter.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
| 1 |
import numpy as np
|
| 2 |
import matplotlib.pyplot as plt
|
| 3 |
-
import
|
| 4 |
-
import cv2
|
| 5 |
-
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
import os
|
| 7 |
|
| 8 |
-
from
|
| 9 |
|
| 10 |
class GraphPlotter:
|
| 11 |
-
"""Class to
|
| 12 |
|
| 13 |
def __init__(self):
|
| 14 |
self.chunks_y_preprocessed = []
|
|
@@ -33,6 +31,7 @@ class GraphPlotter:
|
|
| 33 |
|
| 34 |
def _assign_graph_data(self, chunks_y_preprocessed, chunks_peaks, chunks_depth, chunks_rate, chunks_start_and_end_indices, posture_warnings_regions, sampling_interval_in_frames, fps):
|
| 35 |
"""Assign data members for the class"""
|
|
|
|
| 36 |
self.chunks_y_preprocessed = chunks_y_preprocessed
|
| 37 |
self.chunks_peaks = chunks_peaks
|
| 38 |
self.chunks_depth = chunks_depth
|
|
@@ -40,12 +39,14 @@ class GraphPlotter:
|
|
| 40 |
self.chunks_start_and_end_indices = chunks_start_and_end_indices
|
| 41 |
self.posture_warnings_regions = posture_warnings_regions
|
| 42 |
self.sampling_interval_in_frames = sampling_interval_in_frames
|
| 43 |
-
self.fps = fps
|
| 44 |
|
| 45 |
cpr_logger.info(f"[Graph Plotter] Data members assigned with {len(self.chunks_start_and_end_indices)} chunks and {len(self.posture_warnings_regions)} error regions for a sampling interval of {self.sampling_interval_in_frames} frames and FPS {self.fps}")
|
| 46 |
|
| 47 |
def _plot_single_chunk(self, ax, chunk, idx, prev_last_point, prev_chunk_end):
|
|
|
|
| 48 |
(start_frame, end_frame), depth, rate = chunk
|
|
|
|
| 49 |
# Convert frames to time
|
| 50 |
chunk_frames = np.arange(start_frame, end_frame + 1, self.sampling_interval_in_frames)
|
| 51 |
chunk_times = chunk_frames / self.fps # Convert to seconds
|
|
@@ -108,11 +109,12 @@ class GraphPlotter:
|
|
| 108 |
|
| 109 |
def _plot_error_regions(self, ax, computed_error_regions):
|
| 110 |
"""Visualize error regions with adaptive symbol sizing"""
|
|
|
|
| 111 |
cpr_logger.info("[Graph Plotter] Rendering error regions:")
|
| 112 |
|
| 113 |
# Size parameters
|
| 114 |
-
target_width_ratio = 0.7
|
| 115 |
-
legend_size = 80
|
| 116 |
|
| 117 |
legend_handles = []
|
| 118 |
y_mid = np.mean(ax.get_ylim())
|
|
@@ -268,6 +270,7 @@ class GraphPlotter:
|
|
| 268 |
|
| 269 |
def _print_analysis_details(self, sorted_chunks):
|
| 270 |
"""Combined helper for printing chunks and error regions in seconds"""
|
|
|
|
| 271 |
cpr_logger.info(f"\n\n=== CPR Chunk Analysis ===")
|
| 272 |
display_idx = 0 # Separate counter for displayed indices
|
| 273 |
|
|
@@ -288,14 +291,13 @@ class GraphPlotter:
|
|
| 288 |
f"Time {start_sec:.2f}s - {end_sec:.2f}s ({duration_sec:.2f}s), "
|
| 289 |
f"Depth: {depth:.1f}cm, Rate: {rate:.1f}cpm")
|
| 290 |
|
| 291 |
-
#
|
| 292 |
-
|
| 293 |
chunk_data = {
|
| 294 |
"start": round(start_sec, 2),
|
| 295 |
"end": round(end_sec, 2),
|
| 296 |
"depth": round(depth, 1),
|
| 297 |
"rate": round(rate, 1)
|
| 298 |
-
|
| 299 |
self._chunks_json_data.append(chunk_data)
|
| 300 |
|
| 301 |
display_idx += 1
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import matplotlib.pyplot as plt
|
| 3 |
+
from matplotlib.ticker import MultipleLocator
|
|
|
|
|
|
|
| 4 |
import os
|
| 5 |
|
| 6 |
+
from CPR_Module.Common.logging_config import cpr_logger
|
| 7 |
|
| 8 |
class GraphPlotter:
|
| 9 |
+
"""Class to handle plotting of CPR motion curves and error regions with detailed metrics."""
|
| 10 |
|
| 11 |
def __init__(self):
|
| 12 |
self.chunks_y_preprocessed = []
|
|
|
|
| 31 |
|
| 32 |
def _assign_graph_data(self, chunks_y_preprocessed, chunks_peaks, chunks_depth, chunks_rate, chunks_start_and_end_indices, posture_warnings_regions, sampling_interval_in_frames, fps):
|
| 33 |
"""Assign data members for the class"""
|
| 34 |
+
|
| 35 |
self.chunks_y_preprocessed = chunks_y_preprocessed
|
| 36 |
self.chunks_peaks = chunks_peaks
|
| 37 |
self.chunks_depth = chunks_depth
|
|
|
|
| 39 |
self.chunks_start_and_end_indices = chunks_start_and_end_indices
|
| 40 |
self.posture_warnings_regions = posture_warnings_regions
|
| 41 |
self.sampling_interval_in_frames = sampling_interval_in_frames
|
| 42 |
+
self.fps = fps
|
| 43 |
|
| 44 |
cpr_logger.info(f"[Graph Plotter] Data members assigned with {len(self.chunks_start_and_end_indices)} chunks and {len(self.posture_warnings_regions)} error regions for a sampling interval of {self.sampling_interval_in_frames} frames and FPS {self.fps}")
|
| 45 |
|
| 46 |
def _plot_single_chunk(self, ax, chunk, idx, prev_last_point, prev_chunk_end):
|
| 47 |
+
|
| 48 |
(start_frame, end_frame), depth, rate = chunk
|
| 49 |
+
|
| 50 |
# Convert frames to time
|
| 51 |
chunk_frames = np.arange(start_frame, end_frame + 1, self.sampling_interval_in_frames)
|
| 52 |
chunk_times = chunk_frames / self.fps # Convert to seconds
|
|
|
|
| 109 |
|
| 110 |
def _plot_error_regions(self, ax, computed_error_regions):
|
| 111 |
"""Visualize error regions with adaptive symbol sizing"""
|
| 112 |
+
|
| 113 |
cpr_logger.info("[Graph Plotter] Rendering error regions:")
|
| 114 |
|
| 115 |
# Size parameters
|
| 116 |
+
target_width_ratio = 0.7
|
| 117 |
+
legend_size = 80
|
| 118 |
|
| 119 |
legend_handles = []
|
| 120 |
y_mid = np.mean(ax.get_ylim())
|
|
|
|
| 270 |
|
| 271 |
def _print_analysis_details(self, sorted_chunks):
|
| 272 |
"""Combined helper for printing chunks and error regions in seconds"""
|
| 273 |
+
|
| 274 |
cpr_logger.info(f"\n\n=== CPR Chunk Analysis ===")
|
| 275 |
display_idx = 0 # Separate counter for displayed indices
|
| 276 |
|
|
|
|
| 291 |
f"Time {start_sec:.2f}s - {end_sec:.2f}s ({duration_sec:.2f}s), "
|
| 292 |
f"Depth: {depth:.1f}cm, Rate: {rate:.1f}cpm")
|
| 293 |
|
| 294 |
+
#& Formatted json to mobile
|
|
|
|
| 295 |
chunk_data = {
|
| 296 |
"start": round(start_sec, 2),
|
| 297 |
"end": round(end_sec, 2),
|
| 298 |
"depth": round(depth, 1),
|
| 299 |
"rate": round(rate, 1)
|
| 300 |
+
}
|
| 301 |
self._chunks_json_data.append(chunk_data)
|
| 302 |
|
| 303 |
display_idx += 1
|
CPR_Module/Educational_Mode/metrics_calculator.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
| 1 |
-
# metrics_calculator.py
|
| 2 |
import numpy as np
|
| 3 |
from scipy.signal import savgol_filter, find_peaks
|
| 4 |
-
import matplotlib.pyplot as plt
|
| 5 |
import sys
|
| 6 |
-
import cv2
|
| 7 |
-
import os
|
| 8 |
|
| 9 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 10 |
|
|
@@ -85,11 +81,13 @@ class MetricsCalculator:
|
|
| 85 |
cpr_logger.info(f"\nERROR: Mismatch in expected and actual samples")
|
| 86 |
cpr_logger.info(f"Expected: {expected_samples} samples (frames {start}-{end} @ every {interval} frames)")
|
| 87 |
cpr_logger.info(f"Actual: {actual_y_exact_length} midoints points recieived")
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
|
| 90 |
except Exception as e:
|
| 91 |
cpr_logger.error(f"\nCRITICAL VALIDATION ERROR: {str(e)}")
|
| 92 |
-
|
| 93 |
|
| 94 |
#^ ################# Preprocessing #######################
|
| 95 |
|
|
@@ -407,7 +405,7 @@ class MetricsCalculator:
|
|
| 407 |
"""
|
| 408 |
|
| 409 |
# The program is terminated if the validation fails
|
| 410 |
-
self.validate_midpoints_and_frames_count_in_chunk(midpoints, chunk_start_frame_index, chunk_end_frame_index, sampling_interval_in_frames)
|
| 411 |
|
| 412 |
preprocessing_reult = self.preprocess_midpoints(midpoints)
|
| 413 |
if not preprocessing_reult:
|
|
@@ -448,23 +446,3 @@ class MetricsCalculator:
|
|
| 448 |
self.assign_chunk_data(chunk_start_frame_index, chunk_end_frame_index)
|
| 449 |
cpr_logger.info(f"Chunk {chunk_start_frame_index}-{chunk_end_frame_index} processed successfully")
|
| 450 |
return True
|
| 451 |
-
|
| 452 |
-
#^ ################# Comments #######################
|
| 453 |
-
# Between every two consecutive mini chunks, there wil be "sampling interval" frames unaccounted for.
|
| 454 |
-
# This is because when we reach the "reporting interval" number of frames, we terminate the first mini chunk.
|
| 455 |
-
# But we only start the next mini chunk when we detect the next successfully processed frame.
|
| 456 |
-
# Which is "sampling interval" frames later at the earliest.
|
| 457 |
-
# We can't just initialize the next mini chunk at the "reporting interval" frame, because we need to wait for the next successful frame.
|
| 458 |
-
# Becuase maybe the next frame is a frame with posture errors.
|
| 459 |
-
# For better visualization, we connect between the last point of the previous chunk and the first point of the next chunk if they are "sampling interval" frames apart.
|
| 460 |
-
# But that is only for visualization, all calculations are done on the original frames.
|
| 461 |
-
|
| 462 |
-
# Chunks that are too short can fail any stage of the "handle chunk" process.
|
| 463 |
-
# If they do, we vizualize what we have and ignore the rest.
|
| 464 |
-
# For example, a chunk with < 2 peaks will not be able to calculate the rate.
|
| 465 |
-
# So we will set it to zero and display the midpoints and detected peaks.
|
| 466 |
-
# If there are no peaks, we will set the rate to zero and display the midpoints.
|
| 467 |
-
|
| 468 |
-
# Problems with chunks could be:
|
| 469 |
-
# - Less than 3 seconds.
|
| 470 |
-
# - Not enough peaks to calculate depth and rate
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from scipy.signal import savgol_filter, find_peaks
|
|
|
|
| 3 |
import sys
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
|
|
|
| 81 |
cpr_logger.info(f"\nERROR: Mismatch in expected and actual samples")
|
| 82 |
cpr_logger.info(f"Expected: {expected_samples} samples (frames {start}-{end} @ every {interval} frames)")
|
| 83 |
cpr_logger.info(f"Actual: {actual_y_exact_length} midoints points recieived")
|
| 84 |
+
return np.array([])
|
| 85 |
+
|
| 86 |
+
return y_exact
|
| 87 |
|
| 88 |
except Exception as e:
|
| 89 |
cpr_logger.error(f"\nCRITICAL VALIDATION ERROR: {str(e)}")
|
| 90 |
+
return np.array([])
|
| 91 |
|
| 92 |
#^ ################# Preprocessing #######################
|
| 93 |
|
|
|
|
| 405 |
"""
|
| 406 |
|
| 407 |
# The program is terminated if the validation fails
|
| 408 |
+
midpoints = self.validate_midpoints_and_frames_count_in_chunk(midpoints, chunk_start_frame_index, chunk_end_frame_index, sampling_interval_in_frames)
|
| 409 |
|
| 410 |
preprocessing_reult = self.preprocess_midpoints(midpoints)
|
| 411 |
if not preprocessing_reult:
|
|
|
|
| 446 |
self.assign_chunk_data(chunk_start_frame_index, chunk_end_frame_index)
|
| 447 |
cpr_logger.info(f"Chunk {chunk_start_frame_index}-{chunk_end_frame_index} processed successfully")
|
| 448 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CPR_Module/Educational_Mode/pose_estimation.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
-
# pose_estimation.py
|
| 2 |
-
import cv2
|
| 3 |
import numpy as np
|
| 4 |
from ultralytics import YOLO
|
| 5 |
|
| 6 |
-
from CPR_Module.Common.keypoints import CocoKeypoints
|
| 7 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 8 |
|
| 9 |
class PoseEstimator:
|
|
|
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from ultralytics import YOLO
|
| 3 |
|
|
|
|
| 4 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 5 |
|
| 6 |
class PoseEstimator:
|
CPR_Module/Emergency_Mode/graph_plotter.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
import numpy as np
|
| 2 |
import matplotlib.pyplot as plt
|
| 3 |
-
import sys
|
| 4 |
-
import cv2
|
| 5 |
from matplotlib.ticker import MultipleLocator
|
| 6 |
import os
|
| 7 |
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
| 3 |
from matplotlib.ticker import MultipleLocator
|
| 4 |
import os
|
| 5 |
|
CPR_Module/Emergency_Mode/main.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
-
# main.py
|
| 2 |
import cv2
|
| 3 |
import time
|
| 4 |
import math
|
| 5 |
import numpy as np
|
| 6 |
-
import os
|
| 7 |
import sys
|
| 8 |
|
| 9 |
from CPR_Module.Emergency_Mode.pose_estimation import PoseEstimator
|
|
@@ -698,51 +697,3 @@ class CPRAnalyzer:
|
|
| 698 |
cpr_logger.info(f"[VISUALIZATION] Rate and depth warnings data: {rate_and_depth_warnings}")
|
| 699 |
|
| 700 |
return rate_and_depth_warnings
|
| 701 |
-
|
| 702 |
-
if __name__ == "__main__":
|
| 703 |
-
cpr_logger.info(f"[MAIN] CPR Analysis Started")
|
| 704 |
-
|
| 705 |
-
# Configuration
|
| 706 |
-
requested_fps = 30
|
| 707 |
-
base_dir = r"C:\Users\Fatema Kotb\Documents\CUFE 25\Year 04\GP\Spring\El7a2ny-Graduation-Project"
|
| 708 |
-
|
| 709 |
-
# Define input path
|
| 710 |
-
input_video = os.path.join(base_dir, "CPR", "Dataset", "Batch 2", "14.mp4")
|
| 711 |
-
|
| 712 |
-
# Validate input file exists
|
| 713 |
-
if not os.path.exists(input_video):
|
| 714 |
-
cpr_logger.error(f"[ERROR] Input video not found at: {input_video}")
|
| 715 |
-
sys.exit(1)
|
| 716 |
-
|
| 717 |
-
# Extract original filename without extension
|
| 718 |
-
original_name = os.path.splitext(os.path.basename(input_video))[0]
|
| 719 |
-
cpr_logger.info(f"[CONFIG] Original video name: {original_name}")
|
| 720 |
-
|
| 721 |
-
# Create output directory if it doesn't exist
|
| 722 |
-
output_dir = os.path.join(base_dir, "CPR", "End to End", "Code Refactor", "Output")
|
| 723 |
-
os.makedirs(output_dir, exist_ok=True)
|
| 724 |
-
|
| 725 |
-
# Set output paths using original name
|
| 726 |
-
video_output_path = os.path.join(output_dir, f"{original_name}_output.mp4")
|
| 727 |
-
plot_output_path = os.path.join(output_dir, f"{original_name}_output.png")
|
| 728 |
-
|
| 729 |
-
# Log paths for verification
|
| 730 |
-
cpr_logger.info(f"[CONFIG] Input video: {input_video}")
|
| 731 |
-
cpr_logger.info(f"[CONFIG] Video output: {video_output_path}")
|
| 732 |
-
cpr_logger.info(f"[CONFIG] Plot output: {plot_output_path}")
|
| 733 |
-
|
| 734 |
-
# Initialize and run analyzer
|
| 735 |
-
initialization_start_time = time.time()
|
| 736 |
-
analyzer = CPRAnalyzer(input_video, video_output_path, plot_output_path, requested_fps)
|
| 737 |
-
|
| 738 |
-
# Set plot output path in the analyzer
|
| 739 |
-
analyzer.plot_output_path = plot_output_path
|
| 740 |
-
|
| 741 |
-
initialization_end_time = time.time()
|
| 742 |
-
initialization_elapsed_time = initialization_end_time - initialization_start_time
|
| 743 |
-
cpr_logger.info(f"[TIMING] Initialization time: {initialization_elapsed_time:.2f}s")
|
| 744 |
-
|
| 745 |
-
try:
|
| 746 |
-
analyzer.run_analysis()
|
| 747 |
-
finally:
|
| 748 |
-
analyzer.socket_server.stop_server()
|
|
|
|
|
|
|
| 1 |
import cv2
|
| 2 |
import time
|
| 3 |
import math
|
| 4 |
import numpy as np
|
| 5 |
+
import os
|
| 6 |
import sys
|
| 7 |
|
| 8 |
from CPR_Module.Emergency_Mode.pose_estimation import PoseEstimator
|
|
|
|
| 697 |
cpr_logger.info(f"[VISUALIZATION] Rate and depth warnings data: {rate_and_depth_warnings}")
|
| 698 |
|
| 699 |
return rate_and_depth_warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CPR_Module/Emergency_Mode/metrics_calculator.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
| 1 |
-
# metrics_calculator.py
|
| 2 |
import numpy as np
|
| 3 |
from scipy.signal import savgol_filter, find_peaks
|
| 4 |
-
import matplotlib.pyplot as plt
|
| 5 |
import sys
|
| 6 |
-
import cv2
|
| 7 |
-
import os
|
| 8 |
|
| 9 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 10 |
|
|
@@ -85,11 +81,13 @@ class MetricsCalculator:
|
|
| 85 |
cpr_logger.info(f"\nERROR: Mismatch in expected and actual samples")
|
| 86 |
cpr_logger.info(f"Expected: {expected_samples} samples (frames {start}-{end} @ every {interval} frames)")
|
| 87 |
cpr_logger.info(f"Actual: {actual_y_exact_length} midoints points recieived")
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
|
| 90 |
except Exception as e:
|
| 91 |
cpr_logger.error(f"\nCRITICAL VALIDATION ERROR: {str(e)}")
|
| 92 |
-
|
| 93 |
|
| 94 |
#^ ################# Preprocessing #######################
|
| 95 |
|
|
@@ -454,23 +452,3 @@ class MetricsCalculator:
|
|
| 454 |
self.assign_chunk_data(chunk_start_frame_index, chunk_end_frame_index)
|
| 455 |
cpr_logger.info(f"Chunk {chunk_start_frame_index}-{chunk_end_frame_index} processed successfully")
|
| 456 |
return True
|
| 457 |
-
|
| 458 |
-
#^ ################# Comments #######################
|
| 459 |
-
# Between every two consecutive mini chunks, there wil be "sampling interval" frames unaccounted for.
|
| 460 |
-
# This is because when we reach the "reporting interval" number of frames, we terminate the first mini chunk.
|
| 461 |
-
# But we only start the next mini chunk when we detect the next successfully processed frame.
|
| 462 |
-
# Which is "sampling interval" frames later at the earliest.
|
| 463 |
-
# We can't just initialize the next mini chunk at the "reporting interval" frame, because we need to wait for the next successful frame.
|
| 464 |
-
# Becuase maybe the next frame is a frame with posture errors.
|
| 465 |
-
# For better visualization, we connect between the last point of the previous chunk and the first point of the next chunk if they are "sampling interval" frames apart.
|
| 466 |
-
# But that is only for visualization, all calculations are done on the original frames.
|
| 467 |
-
|
| 468 |
-
# Chunks that are too short can fail any stage of the "handle chunk" process.
|
| 469 |
-
# If they do, we vizualize what we have and ignore the rest.
|
| 470 |
-
# For example, a chunk with < 2 peaks will not be able to calculate the rate.
|
| 471 |
-
# So we will set it to zero and display the midpoints and detected peaks.
|
| 472 |
-
# If there are no peaks, we will set the rate to zero and display the midpoints.
|
| 473 |
-
|
| 474 |
-
# Problems with chunks could be:
|
| 475 |
-
# - Less than 3 seconds.
|
| 476 |
-
# - Not enough peaks to calculate depth and rate
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from scipy.signal import savgol_filter, find_peaks
|
|
|
|
| 3 |
import sys
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 6 |
|
|
|
|
| 81 |
cpr_logger.info(f"\nERROR: Mismatch in expected and actual samples")
|
| 82 |
cpr_logger.info(f"Expected: {expected_samples} samples (frames {start}-{end} @ every {interval} frames)")
|
| 83 |
cpr_logger.info(f"Actual: {actual_y_exact_length} midoints points recieived")
|
| 84 |
+
return np.array([]) # Return empty array to indicate failure
|
| 85 |
+
|
| 86 |
+
return y_exact # Return the exact y-values if validation passes
|
| 87 |
|
| 88 |
except Exception as e:
|
| 89 |
cpr_logger.error(f"\nCRITICAL VALIDATION ERROR: {str(e)}")
|
| 90 |
+
return np.array([]) # Return empty array to indicate failure
|
| 91 |
|
| 92 |
#^ ################# Preprocessing #######################
|
| 93 |
|
|
|
|
| 452 |
self.assign_chunk_data(chunk_start_frame_index, chunk_end_frame_index)
|
| 453 |
cpr_logger.info(f"Chunk {chunk_start_frame_index}-{chunk_end_frame_index} processed successfully")
|
| 454 |
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CPR_Module/Emergency_Mode/pose_estimation.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
| 1 |
-
# pose_estimation.py
|
| 2 |
-
import cv2
|
| 3 |
-
import numpy as np
|
| 4 |
from ultralytics import YOLO
|
| 5 |
|
| 6 |
-
from CPR_Module.Common.keypoints import CocoKeypoints
|
| 7 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 8 |
|
| 9 |
class PoseEstimator:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from ultralytics import YOLO
|
| 2 |
|
|
|
|
| 3 |
from CPR_Module.Common.logging_config import cpr_logger
|
| 4 |
|
| 5 |
class PoseEstimator:
|
README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-

|
| 2 |
-
|
| 3 |
Ever wondered what you'd do if someone **collapsed** in front of you?<br>
|
| 4 |
El7a2ny is a mobile app that trains you to handle medical emergencies and guides you step-by-step as they happen.<br>
|
| 5 |
No medical degree needed — just follow the app, stay calm, and become the hero someone desperately needs.<br>
|
|
|
|
|
|
|
|
|
|
| 1 |
Ever wondered what you'd do if someone **collapsed** in front of you?<br>
|
| 2 |
El7a2ny is a mobile app that trains you to handle medical emergencies and guides you step-by-step as they happen.<br>
|
| 3 |
No medical degree needed — just follow the app, stay calm, and become the hero someone desperately needs.<br>
|