import math import numpy as np import cv2 from .pupil import Pupil class Eye(object): """ This class creates a new frame to isolate the eye and initiates the pupil detection. """ LEFT_EYE_POINTS = [36, 37, 38, 39, 40, 41] RIGHT_EYE_POINTS = [42, 43, 44, 45, 46, 47] def __init__(self, original_frame, landmarks, side, calibration): self.frame = None self.origin = None self.center = None self.pupil = None self.landmark_points = None self._analyze(original_frame, landmarks, side, calibration) @staticmethod def _middle_point(p1, p2): """Returns the middle point (x,y) between two points Arguments: p1 (dlib.point): First point p2 (dlib.point): Second point """ x = int((p1.x + p2.x) / 2) y = int((p1.y + p2.y) / 2) return (x, y) def _isolate(self, frame, landmarks, points): """Isolate an eye, to have a frame without other part of the face. Arguments: frame (numpy.ndarray): Frame containing the face landmarks (dlib.full_object_detection): Facial landmarks for the face region points (list): Points of an eye (from the 68 Multi-PIE landmarks) """ region = np.array([(landmarks.part(point).x, landmarks.part(point).y) for point in points]) region = region.astype(np.int32) self.landmark_points = region # Applying a mask to get only the eye height, width = frame.shape[:2] black_frame = np.zeros((height, width), np.uint8) mask = np.full((height, width), 255, np.uint8) cv2.fillPoly(mask, [region], (0, 0, 0)) eye = cv2.bitwise_not(black_frame, frame.copy(), mask=mask) # Cropping on the eye margin = 5 min_x = np.min(region[:, 0]) - margin max_x = np.max(region[:, 0]) + margin min_y = np.min(region[:, 1]) - margin max_y = np.max(region[:, 1]) + margin self.frame = eye[min_y:max_y, min_x:max_x] self.origin = (min_x, min_y) height, width = self.frame.shape[:2] self.center = (width / 2, height / 2) def _blinking_ratio(self, landmarks, points): """Calculates a ratio that can indicate whether an eye is closed or not. It's the division of the width of the eye, by its height. Arguments: landmarks (dlib.full_object_detection): Facial landmarks for the face region points (list): Points of an eye (from the 68 Multi-PIE landmarks) Returns: The computed ratio """ left = (landmarks.part(points[0]).x, landmarks.part(points[0]).y) right = (landmarks.part(points[3]).x, landmarks.part(points[3]).y) top = self._middle_point(landmarks.part(points[1]), landmarks.part(points[2])) bottom = self._middle_point(landmarks.part(points[5]), landmarks.part(points[4])) eye_width = math.hypot((left[0] - right[0]), (left[1] - right[1])) eye_height = math.hypot((top[0] - bottom[0]), (top[1] - bottom[1])) try: ratio = eye_width / eye_height except ZeroDivisionError: ratio = None return ratio def _analyze(self, original_frame, landmarks, side, calibration): """Detects and isolates the eye in a new frame, sends data to the calibration and initializes Pupil object. Arguments: original_frame (numpy.ndarray): Frame passed by the user landmarks (dlib.full_object_detection): Facial landmarks for the face region side: Indicates whether it's the left eye (0) or the right eye (1) calibration (calibration.Calibration): Manages the binarization threshold value """ if side == 0: points = self.LEFT_EYE_POINTS elif side == 1: points = self.RIGHT_EYE_POINTS else: return self.blinking = self._blinking_ratio(landmarks, points) self._isolate(original_frame, landmarks, points) if not calibration.is_complete(): calibration.evaluate(self.frame, side) threshold = calibration.threshold(side) self.pupil = Pupil(self.frame, threshold)