Source code for iris.nodes.eye_properties_estimation.bisectors_method

from typing import Tuple

import numpy as np
from pydantic import Field

from iris.io.class_configs import Algorithm
from iris.io.dataclasses import EyeCenters, GeometryPolygons
from iris.io.errors import EyeCentersEstimationError


[docs]class BisectorsMethod(Algorithm): """Implementation of eye's center estimation algorithm using bisectors method for finding a circle center. This algorithm samples a given number of bisectors from the pupil and iris polygons, and averages their intersection to produce the polygon center. This method is robust against noise in the polygons, making it a good choice for non-perfect shapes. It is also robust to polygons missing parts of the circle arc, making it a good choice for partially-occluded shapes. LIMITATIONS: The iris and pupil can be approximated to circles, when the user is properly gazing at the camera. This requires that the cases of off-gaze have already been filtered out. """
[docs] class Parameters(Algorithm.Parameters): """Default Parameters for BisectorsMethod algorithm.""" num_bisectors: int = Field(..., gt=0) min_distance_between_sector_points: float = Field(..., gt=0.0, lt=1.0) max_iterations: int = Field(..., gt=0)
__parameters_type__ = Parameters def __init__( self, num_bisectors: int = 100, min_distance_between_sector_points: float = 0.75, max_iterations: int = 50, ) -> None: """Assign parameters. Args: num_bisectors (int, optional): Number of bisectors.. Defaults to 100. min_distance_between_sector_points (float, optional): Minimum distance between sectors expressed as a fractional value of a circular shape diameter. Defaults to 0.75. max_iterations (int, optional): Max iterations for bisector search.. Defaults to 50. """ super().__init__( num_bisectors=num_bisectors, min_distance_between_sector_points=min_distance_between_sector_points, max_iterations=max_iterations, )
[docs] def run(self, geometries: GeometryPolygons) -> EyeCenters: """Estimate eye's iris and pupil centers. Args: geometries (GeometryPolygons): Geometry polygons. Returns: EyeCenters: Eye's centers object. """ pupil_center_x, pupil_center_y = self._find_center_coords(geometries.pupil_array, geometries.pupil_diameter) iris_center_x, iris_center_y = self._find_center_coords(geometries.iris_array, geometries.iris_diameter) return EyeCenters(pupil_x=pupil_center_x, pupil_y=pupil_center_y, iris_x=iris_center_x, iris_y=iris_center_y)
def _find_center_coords(self, polygon: np.ndarray, diameter: float) -> Tuple[float, float]: """Find center coordinates of a polygon. Args: polygon (np.ndarray): np.ndarray. diameter (float): diameter of the polygon. Returns: Tuple[float, float]: Tuple with the center location coordinates (x, y). """ min_distance_between_sector_points_in_px = self.params.min_distance_between_sector_points * diameter first_bisectors_point, second_bisectors_point = self._calculate_perpendicular_bisectors( polygon, min_distance_between_sector_points_in_px ) return self._find_best_intersection(first_bisectors_point, second_bisectors_point) def _calculate_perpendicular_bisectors( self, polygon: np.ndarray, min_distance_between_sector_points_in_px: float ) -> Tuple[np.ndarray, np.ndarray]: """Calculate the perpendicular bisector of self.params.num_bisectors randomly chosen points from a polygon's vertices. A pair of points is used if their distance is larger then min_distance_between_sector_points_in_px. Args: polygon (np.ndarray): np.ndarray based on which we are searching the center of a circular shape. min_distance_between_sector_points_in_px (float): Minimum distance between sector points. Raises: EyeCentersEstimationError: Raised if not able to find enough random pairs of points on the arc with a large enough distance! Returns: Tuple[np.ndarray, np.ndarray]: Calculated perpendicular bisectors. """ np.random.seed(142857) bisectors_first_points = np.empty([0, 2]) bisectors_second_points = np.empty([0, 2]) for _ in range(self.params.max_iterations): random_indices = np.random.choice(len(polygon), size=(self.params.num_bisectors, 2)) first_drawn_points = polygon[random_indices[:, 0]] second_drawn_points = polygon[random_indices[:, 1]] norms = np.linalg.norm(first_drawn_points - second_drawn_points, axis=1) mask = norms > min_distance_between_sector_points_in_px bisectors_first_points = np.vstack([bisectors_first_points, first_drawn_points[mask]]) bisectors_second_points = np.vstack([bisectors_second_points, second_drawn_points[mask]]) if len(bisectors_first_points) >= self.params.num_bisectors: break else: raise EyeCentersEstimationError( "Not able to find enough random pairs of points on the arc with a large enough distance!" ) bisectors_first_points = bisectors_first_points[: self.params.num_bisectors] bisectors_second_points = bisectors_second_points[: self.params.num_bisectors] bisectors_center = (bisectors_first_points + bisectors_second_points) / 2 # Flip xs with ys and flip sign of on of them to create a 90deg rotation inv_bisectors_center_slope = np.fliplr(bisectors_second_points - bisectors_first_points) inv_bisectors_center_slope[:, 1] = -inv_bisectors_center_slope[:, 1] # Add perpendicular vector to center and normalize norm = np.linalg.norm(inv_bisectors_center_slope, axis=1) inv_bisectors_center_slope[:, 0] /= norm inv_bisectors_center_slope[:, 1] /= norm first_bisectors_point = bisectors_center - inv_bisectors_center_slope second_bisectors_point = bisectors_center + inv_bisectors_center_slope return first_bisectors_point, second_bisectors_point def _find_best_intersection(self, fst_points: np.ndarray, sec_points: np.ndarray) -> Tuple[float, float]: """fst_points and sec_points are NxD arrays defining N lines. D is the dimension of the space. This function returns the least squares intersection of the N lines from the system given by eq. 13 in http://cal.cs.illinois.edu/~johannes/research/LS_line_intersecpdf. Args: fst_points (np.ndarray): First bisectors points. sec_points (np.ndarray): Second bisectors points. Returns: Tuple[float, float]: Best intersection point. Reference: [1] http://cal.cs.illinois.edu/~johannes/research/LS_line_intersecpdf """ norm_bisectors = (sec_points - fst_points) / np.linalg.norm(sec_points - fst_points, axis=1)[:, np.newaxis] # Generate the array of all projectors I - n*n.T projections = np.eye(norm_bisectors.shape[1]) - norm_bisectors[:, :, np.newaxis] * norm_bisectors[:, np.newaxis] # Generate R matrix and q vector R = projections.sum(axis=0) q = (projections @ fst_points[:, :, np.newaxis]).sum(axis=0) # Solve the least squares problem for the intersection point p: Rp = q p = np.linalg.lstsq(R, q, rcond=None)[0] intersection_x, intersection_y = p return intersection_x.item(), intersection_y.item()