From efeb06cab8de1ee8517f06d2ba2e15b4a9031610 Mon Sep 17 00:00:00 2001 From: ck-zhang Date: Sun, 27 Apr 2025 00:24:03 +0800 Subject: [PATCH] Adaptive blink detection --- src/eyetrax/gaze.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/eyetrax/gaze.py b/src/eyetrax/gaze.py index cd6bb3d..fbba03e 100644 --- a/src/eyetrax/gaze.py +++ b/src/eyetrax/gaze.py @@ -5,12 +5,18 @@ import mediapipe as mp import numpy as np import pickle from pathlib import Path +from collections import deque from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler class GazeEstimator: - def __init__(self): + def __init__( + self, + ear_history_len: int = 50, + blink_threshold_ratio: float = 0.8, + min_history: int = 15, + ): self.face_mesh = mp.solutions.face_mesh.FaceMesh( static_image_mode=False, max_num_faces=1, @@ -21,6 +27,10 @@ class GazeEstimator: self.scaler = StandardScaler() self.variable_scaling: np.ndarray | None = None + self._ear_history = deque(maxlen=ear_history_len) + self._blink_ratio = blink_threshold_ratio + self._min_history = min_history + def extract_features(self, image): """ Takes in image and returns landmarks around the eye region @@ -30,7 +40,7 @@ class GazeEstimator: results = self.face_mesh.process(image_rgb) if not results.multi_face_landmarks: - return None, None + return None, False face_landmarks = results.multi_face_landmarks[0] landmarks = face_landmarks.landmark @@ -134,7 +144,13 @@ class GazeEstimator: right_EAR = right_eye_height / (right_eye_width + 1e-9) EAR = (left_EAR + right_EAR) / 2 - blink_detected = EAR < 0.2 + + self._ear_history.append(EAR) + if len(self._ear_history) >= self._min_history: + thr = float(np.mean(self._ear_history)) * self._blink_ratio + else: + thr = 0.2 + blink_detected = EAR < thr return features, blink_detected