import cv2 import dlib import numpy as np from scipy.spatial import distance as dist # For EAR calculation # Constants for detection # These values might need fine-tuning EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR = 0.15 # Factor for eyebrow-to-eye distance increase for "No" CALIBRATION_FRAMES = 60 # Number of frames for initial calibration EAR_THRESHOLD = 0.20 # Eye Aspect Ratio threshold for eye closure (Yes) # Path to the dlib shape predictor model file DLIB_SHAPE_PREDICTOR_PATH = "shape_predictor_68_face_landmarks.dat" # Display states STATE_YES = "Yes" STATE_NO = "No" STATE_NORMAL = "Normal" STATE_CALIBRATING = "Calibrating..." # Initialize dlib's face detector and facial landmark predictor try: detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(DLIB_SHAPE_PREDICTOR_PATH) except RuntimeError as e: print(f"[ERROR] Failed to load dlib model: {e}") print(f"Please download '{DLIB_SHAPE_PREDICTOR_PATH}' and place it near the script.") exit() # Landmark indices for eyes (used for EAR) # Dlib's left_eye (user's left) are points 42-47 (0-indexed in predictor) # Dlib's right_eye (user's right) are points 36-41 (user_L_eye_indices_start, user_L_eye_indices_end) = (42, 48) (user_R_eye_indices_start, user_R_eye_indices_end) = (36, 42) # Landmark indices for top of eyes (for No detection) # User's Left eye top: dlib points 43, 44 # User's Right eye top: dlib points 37, 38 user_L_eye_top_indices = [43, 44] user_R_eye_top_indices = [37, 38] # Landmark indices for eyebrows (for No detection) # User's Left eyebrow: dlib points 22-26. We'll average points 23, 24, 25. user_L_eyebrow_y_calc_indices = range(23, 26) # User's Right eyebrow: dlib points 17-21. We'll average points 18, 19, 20. user_R_eyebrow_y_calc_indices = range(18, 21) cap = cv2.VideoCapture(0) if not cap.isOpened(): print("[ERROR] Cannot open webcam.") exit() # Calibration variables calibration_counter = 0 # For eyebrows normal_user_L_eyebrow_y_avg = 0 normal_user_R_eyebrow_y_avg = 0 calibration_data_user_L_eyebrow_y = [] calibration_data_user_R_eyebrow_y = [] # For top of eyes normal_user_L_eye_top_y_avg = 0 normal_user_R_eye_top_y_avg = 0 calibration_data_user_L_eye_top_y = [] calibration_data_user_R_eye_top_y = [] # For calibrated distances normal_dist_L_eyebrow_to_eye = 0 normal_dist_R_eyebrow_to_eye = 0 def get_landmark_point(landmarks, index): return (landmarks.part(index).x, landmarks.part(index).y) def eye_aspect_ratio(eye_pts): A = dist.euclidean(eye_pts[1], eye_pts[5]) B = dist.euclidean(eye_pts[2], eye_pts[4]) C = dist.euclidean(eye_pts[0], eye_pts[3]) ear_val = (A + B) / (2.0 * C) return ear_val print("[INFO] Calibration started. Please look at the camera with a normal expression...") current_state = STATE_CALIBRATING while True: ret, frame = cap.read() if not ret: print("[ERROR] Failed to grab frame from webcam.") break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray) if calibration_counter >= CALIBRATION_FRAMES: current_state = STATE_NORMAL for face in faces: landmarks = predictor(gray, face) # --- Calculate current eyebrow Y positions --- user_L_eyebrow_current_y_pts = [landmarks.part(i).y for i in user_L_eyebrow_y_calc_indices] current_user_L_eyebrow_y_avg = np.mean(user_L_eyebrow_current_y_pts) if user_L_eyebrow_current_y_pts else 0 user_R_eyebrow_current_y_pts = [landmarks.part(i).y for i in user_R_eyebrow_y_calc_indices] current_user_R_eyebrow_y_avg = np.mean(user_R_eyebrow_current_y_pts) if user_R_eyebrow_current_y_pts else 0 # --- Calculate current top of eye Y positions --- user_L_eye_top_current_y_pts = [landmarks.part(i).y for i in user_L_eye_top_indices] current_user_L_eye_top_y_avg = np.mean(user_L_eye_top_current_y_pts) if user_L_eye_top_current_y_pts else 0 user_R_eye_top_current_y_pts = [landmarks.part(i).y for i in user_R_eye_top_indices] current_user_R_eye_top_y_avg = np.mean(user_R_eye_top_current_y_pts) if user_R_eye_top_current_y_pts else 0 # --- Eye Aspect Ratio for "Yes" detection --- user_L_eye_all_pts = np.array([get_landmark_point(landmarks, i) for i in range(user_L_eye_indices_start, user_L_eye_indices_end)], dtype="int") user_R_eye_all_pts = np.array([get_landmark_point(landmarks, i) for i in range(user_R_eye_indices_start, user_R_eye_indices_end)], dtype="int") left_ear = eye_aspect_ratio(user_L_eye_all_pts) right_ear = eye_aspect_ratio(user_R_eye_all_pts) avg_ear = (left_ear + right_ear) / 2.0 # --- Calibration Phase --- if calibration_counter < CALIBRATION_FRAMES: current_state = STATE_CALIBRATING calibration_data_user_L_eyebrow_y.append(current_user_L_eyebrow_y_avg) calibration_data_user_R_eyebrow_y.append(current_user_R_eyebrow_y_avg) calibration_data_user_L_eye_top_y.append(current_user_L_eye_top_y_avg) calibration_data_user_R_eye_top_y.append(current_user_R_eye_top_y_avg) calibration_counter += 1 if calibration_counter == CALIBRATION_FRAMES: normal_user_L_eyebrow_y_avg = np.mean(calibration_data_user_L_eyebrow_y) normal_user_R_eyebrow_y_avg = np.mean(calibration_data_user_R_eyebrow_y) normal_user_L_eye_top_y_avg = np.mean(calibration_data_user_L_eye_top_y) normal_user_R_eye_top_y_avg = np.mean(calibration_data_user_R_eye_top_y) # Distance = Y_eye_top - Y_eyebrow (larger means eyebrow is higher or eye is lower) normal_dist_L_eyebrow_to_eye = normal_user_L_eye_top_y_avg - normal_user_L_eyebrow_y_avg normal_dist_R_eyebrow_to_eye = normal_user_R_eye_top_y_avg - normal_user_R_eyebrow_y_avg print("[INFO] Calibration finished.") print(f"[INFO] Calibrated Avg L Eyebrow Y: {normal_user_L_eyebrow_y_avg:.2f}, Avg R Eyebrow Y: {normal_user_R_eyebrow_y_avg:.2f}") print(f"[INFO] Calibrated Avg L Eye Top Y: {normal_user_L_eye_top_y_avg:.2f}, Avg R Eye Top Y: {normal_user_R_eye_top_y_avg:.2f}") print(f"[INFO] Calibrated Dist L Eyebrow-Eye: {normal_dist_L_eyebrow_to_eye:.2f}, R Eyebrow-Eye: {normal_dist_R_eyebrow_to_eye:.2f}") print(f"[INFO] 'No' detection if current distance > normal_dist * (1 + {EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR})") print(f"[INFO] 'Yes' detection threshold (EAR must be <): {EAR_THRESHOLD:.2f}") # --- Detection Phase --- else: if normal_dist_L_eyebrow_to_eye != 0 and normal_dist_R_eyebrow_to_eye != 0: # Ensure calibration is done # Detect "Yes" (eyes closed) if avg_ear < EAR_THRESHOLD: current_state = STATE_YES # Detect "No" (eyebrows raised significantly relative to eyes) else: # Check for NO only if not YES current_dist_L = current_user_L_eye_top_y_avg - current_user_L_eyebrow_y_avg current_dist_R = current_user_R_eye_top_y_avg - current_user_R_eyebrow_y_avg threshold_dist_L = normal_dist_L_eyebrow_to_eye * (1 + EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR) threshold_dist_R = normal_dist_R_eyebrow_to_eye * (1 + EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR) # Handle cases where normal distance might be zero or negative (eyebrow very low) # If normal distance is small or negative, a small absolute increase might be enough. # This part might need more sophisticated handling if eyebrows are naturally very low or touching eyelids. # For now, simple multiplication factor. if normal_dist_L_eyebrow_to_eye <= 0: threshold_dist_L = normal_dist_L_eyebrow_to_eye + abs(normal_dist_L_eyebrow_to_eye * EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR) + 5 # Add a small fixed increase if normal_dist_R_eyebrow_to_eye <= 0: threshold_dist_R = normal_dist_R_eyebrow_to_eye + abs(normal_dist_R_eyebrow_to_eye * EYEBROW_TO_EYE_VERTICAL_DISTANCE_INCREASE_FACTOR) + 5 if current_dist_L > threshold_dist_L and current_dist_R > threshold_dist_R: current_state = STATE_NO # Optional: Draw landmarks for debugging # Eyebrows # for i in list(user_L_eyebrow_y_calc_indices) + list(user_R_eyebrow_y_calc_indices): # p = get_landmark_point(landmarks, i) # cv2.circle(frame, p, 2, (0, 255, 0), -1) # # Top of eyes # for i in user_L_eye_top_indices + user_R_eye_top_indices: # p = get_landmark_point(landmarks, i) # cv2.circle(frame, p, 2, (255, 0, 0), -1) # Display the detected state display_text = current_state color = (255, 255, 0) # Default for Normal/Calibrating if current_state == STATE_YES: color = (0, 255, 0) # Green for Yes elif current_state == STATE_NO: color = (0, 0, 255) # Red for No cv2.putText(frame, display_text, (50, 80), cv2.FONT_HERSHEY_SIMPLEX, 1.5, color, 3) cv2.imshow("Gesture Detection (Press 'q' to quit)", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() print("[INFO] Application closed.")