IntegrationTest / models /gaze_eye_fusion.py
Abdelrahman Almatrooshi
Integrate L2CS-Net gaze estimation
2eba0cc
# Fuses calibrated gaze position with eye openness (EAR) for focus detection.
# Takes L2CS gaze angles + MediaPipe landmarks, outputs screen coords + focus decision.
import math
import numpy as np
from .gaze_calibration import GazeCalibration
from .eye_scorer import compute_avg_ear
_EAR_BLINK = 0.18
_ON_SCREEN_MARGIN = 0.08
class GazeEyeFusion:
def __init__(self, calibration, ear_weight=0.3, gaze_weight=0.7, focus_threshold=0.52):
if not calibration.is_fitted:
raise ValueError("Calibration must be fitted first")
self._cal = calibration
self._ear_w = ear_weight
self._gaze_w = gaze_weight
self._threshold = focus_threshold
self._smooth_x = 0.5
self._smooth_y = 0.5
self._alpha = 0.5
def update(self, yaw_rad, pitch_rad, landmarks):
gx, gy = self._cal.predict(yaw_rad, pitch_rad)
# EMA smooth the gaze position
self._smooth_x += self._alpha * (gx - self._smooth_x)
self._smooth_y += self._alpha * (gy - self._smooth_y)
gx, gy = self._smooth_x, self._smooth_y
on_screen = (
-_ON_SCREEN_MARGIN <= gx <= 1.0 + _ON_SCREEN_MARGIN and
-_ON_SCREEN_MARGIN <= gy <= 1.0 + _ON_SCREEN_MARGIN
)
ear = None
ear_score = 1.0
if landmarks is not None:
ear = compute_avg_ear(landmarks)
ear_score = 0.0 if ear < _EAR_BLINK else min(ear / 0.30, 1.0)
# penalise gaze near screen edges
gaze_score = 1.0 if on_screen else 0.0
if on_screen:
dx = max(0.0, abs(gx - 0.5) - 0.3)
dy = max(0.0, abs(gy - 0.5) - 0.3)
gaze_score = max(0.0, 1.0 - math.sqrt(dx**2 + dy**2) * 5.0)
score = float(np.clip(self._gaze_w * gaze_score + self._ear_w * ear_score, 0, 1))
return {
"gaze_x": round(float(gx), 4),
"gaze_y": round(float(gy), 4),
"on_screen": on_screen,
"ear": round(ear, 4) if ear is not None else None,
"focus_score": round(score, 4),
"focused": score >= self._threshold,
}
def reset(self):
self._smooth_x = 0.5
self._smooth_y = 0.5