Eyemovement / src /eye_eyebrow_detector.py
shayan5422's picture
Upload 10 files
496c8b9 verified
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.")