Source code for iris.nodes.validators.object_validators

from typing import Tuple

import numpy as np
from pydantic import Field

import iris.io.errors as E
from iris.callbacks.callback_interface import Callback
from iris.io.class_configs import Algorithm
from iris.io.dataclasses import EyeOcclusion, GeometryPolygons, IrisTemplate, Offgaze, PupilToIrisProperty
from iris.utils.math import polygon_length


[docs]class Pupil2IrisPropertyValidator(Callback, Algorithm): """Validate that the pupil-to-iris ratio is within thresholds. Raises: E.PupilIrisPropertyEstimationError: If the pupil-to-iris ratio isn't within boundaries. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for Pupil2IrisPropertyValidator objects.""" min_allowed_diameter_ratio: float = Field(..., gt=0.0, lt=1.0) max_allowed_diameter_ratio: float = Field(..., gt=0.0, lt=1.0) max_allowed_center_dist_ratio: float = Field(..., ge=0.0, lt=1.0)
__parameters_type__ = Parameters def __init__( self, min_allowed_diameter_ratio: float = 0.0001, max_allowed_diameter_ratio: float = 0.9999, max_allowed_center_dist_ratio: float = 0.9999, ) -> None: """Assign parameters. Args: min_allowed_diameter_ratio (float): Minimum allowed pupil2iris diameter ratio. Defaults to 0.0001 (by default every check will result in success). max_allowed_diameter_ratio (float): Maximum allowed pupil2iris diameter ratio. Defaults to 0.9999 (by default every check will result in success). max_allowed_center_dist_ratio (float): Maximum allowed pupil2iris center distance ratio. Defaults to 0.9999 (by default every check will result in success). """ super().__init__( min_allowed_diameter_ratio=min_allowed_diameter_ratio, max_allowed_diameter_ratio=max_allowed_diameter_ratio, max_allowed_center_dist_ratio=max_allowed_center_dist_ratio, )
[docs] def run(self, val_arguments: PupilToIrisProperty) -> None: """Validate of pupil to iris calculation. Args: p2i_property (PupilToIrisProperty): Computation result. Raises: E.PupilIrisPropertyEstimationError: Raised if result isn't without previously specified boundaries. """ if not ( self.params.min_allowed_diameter_ratio <= val_arguments.pupil_to_iris_diameter_ratio <= self.params.max_allowed_diameter_ratio ): raise E.PupilIrisPropertyEstimationError( f"p2i_property={val_arguments.pupil_to_iris_diameter_ratio} is not within [{self.params.min_allowed_diameter_ratio}, {self.params.max_allowed_diameter_ratio}]." ) if val_arguments.pupil_to_iris_center_dist_ratio > self.params.max_allowed_center_dist_ratio: raise E.PupilIrisPropertyEstimationError( f"p2i_property={val_arguments.pupil_to_iris_center_dist_ratio} exceeds {self.params.max_allowed_center_dist_ratio}." )
[docs] def on_execute_end(self, result: PupilToIrisProperty) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: result (PupilToIrisProperty): Pupil2Iris property resulted from computations. """ self.run(result)
[docs]class OffgazeValidator(Callback, Algorithm): """Validate that the offgaze score is below threshold. Raises: E.OffgazeEstimationError: If the offgaze score is above threshold. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for OffgazeValidator objects.""" max_allowed_offgaze: float = Field(..., ge=0.0, le=1.0)
__parameters_type__ = Parameters def __init__(self, max_allowed_offgaze: float = 1.0) -> None: """Assign parameters. Args: max_allowed_offgaze (float): Offgaze computation result max threshold that allows further sample processing. Defaults to 1.0 (by default every check will result in success). """ super().__init__(max_allowed_offgaze=max_allowed_offgaze)
[docs] def run(self, val_arguments: Offgaze) -> None: """Validate of offgaze estimation algorithm. Args: val_arguments (Offgaze): Computed result. Raises: E.OffgazeEstimationError: Raised if result isn't greater then specified threshold. """ if not (val_arguments.score <= self.params.max_allowed_offgaze): raise E.OffgazeEstimationError( f"offgaze={val_arguments.score} > max_allowed_offgaze={self.params.max_allowed_offgaze}" )
[docs] def on_execute_end(self, result: Offgaze) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: result (Offgaze): Offgaze resulted from computations. """ self.run(result)
[docs]class OcclusionValidator(Callback, Algorithm): """Validate that the occlusion fration is above threshold. Raises: E.OcclusionError: If the occlusion fraction is below threshold. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for OcclusionValidator objects.""" min_allowed_occlusion: float = Field(..., ge=0.0, le=1.0)
__parameters_type__ = Parameters def __init__(self, min_allowed_occlusion: float = 0.0) -> None: """Assign parameters. Args: min_allowed_occlusion (float): Occlusion computation result min threshold that allows further sample processing. Defaults to 0.0 (by default every check will result in success). """ super().__init__(min_allowed_occlusion=min_allowed_occlusion)
[docs] def run(self, val_arguments: EyeOcclusion) -> None: """Validate of occlusion estimation algorithm. Args: val_arguments (EyeOcclusion): Computed result. Raises: E.OcclusionError: Raised if result isn't greater then specified threshold. """ if not (val_arguments.visible_fraction >= self.params.min_allowed_occlusion): raise E.OcclusionError( f"visible_fraction={val_arguments.visible_fraction} < min_allowed_occlusion={self.params.min_allowed_occlusion}." )
[docs] def on_execute_end(self, result: EyeOcclusion) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: result (EyeOcclusion): EyeOcclusion resulted from computations. """ self.run(result)
[docs]class IsPupilInsideIrisValidator(Callback, Algorithm): """Validate that the pupil is fully contained within the iris. Raises: E.IsPupilInsideIrisValidatorError: If the pupil polygon is not fully contained within the iris polygon. """
[docs] def run(self, val_arguments: GeometryPolygons) -> None: """Validate if extrapolated pupil polygons are withing extrapolated iris boundaries. Args: val_arguments (GeometryPolygons): Computed result. Raises: E.IsPupilInsideIrisValidatorError: Raised if the pupil polygon is not fully contained within the iris polygon. """ for point in val_arguments.pupil_array: if not self._check_pupil_point_is_inside_iris(point, val_arguments.iris_array): raise E.IsPupilInsideIrisValidatorError( "Entire extrapolated pupil polygon isn't included in an extrapolated iris polygon." )
[docs] def on_execute_end(self, result: GeometryPolygons) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: result (GeometryPolygons): GeometryPolygons resulted from computations. """ self.run(result)
def _check_pupil_point_is_inside_iris(self, point: np.ndarray, polygon_pts: np.ndarray) -> bool: """Check if pupil point is inside iris polygon. Reference: [1] https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/ Args: point (np.ndarray): Point x, y. polygon_sides (np.ndarray): Polygon points. Returns: bool: Check result. """ num_iris_points = len(polygon_pts) polygon_sides = [ (polygon_pts[i % num_iris_points], polygon_pts[(i + 1) % num_iris_points]) for i in range(num_iris_points) ] x, y = point to_right_ray = (point, np.array([float("inf"), y])) to_left_ray = (np.array([-float("inf"), y]), point) right_ray_intersections, left_ray_intersections = 0, 0 for poly_side in polygon_sides: if self._is_ray_intersecting_with_side(to_right_ray, poly_side, is_ray_pointing_to_left=False): right_ray_intersections += 1 if self._is_ray_intersecting_with_side(to_left_ray, poly_side, is_ray_pointing_to_left=True): left_ray_intersections += 1 return right_ray_intersections % 2 != 0 or left_ray_intersections % 2 != 0 def _is_ray_intersecting_with_side( self, ray_line: Tuple[np.ndarray, np.ndarray], side_line: Tuple[np.ndarray, np.ndarray], is_ray_pointing_to_left: bool, ) -> bool: """Check if ray is intersecting with a polygon side. Args: ray_line (Tuple[np.ndarray, np.ndarray]): Ray line two points. side_line (Tuple[np.ndarray, np.ndarray]): Side line two points. is_ray_pointing_to_left (bool): Is ray pointing to the left flag. Returns: bool: Check result. """ (ray_start_x, ray_start_y), (ray_end_x, ray_end_y) = ray_line (side_start_x, side_start_y), (side_end_x, side_end_y) = side_line if side_start_y == side_end_y: return side_start_y == ray_start_y # fmt: off intersection_x = (ray_start_y - side_start_y) * (side_start_x - side_end_x) / (side_start_y - side_end_y) + side_start_x # fmt: on is_along_side = side_start_x <= intersection_x < side_end_x or side_start_x >= intersection_x > side_end_x is_along_ray = intersection_x <= ray_end_x if is_ray_pointing_to_left else intersection_x >= ray_start_x return is_along_side and is_along_ray
[docs]class PolygonsLengthValidator(Callback, Algorithm): """Validate that the pupil and iris polygons have a sufficient length. Raises: E.GeometryEstimationError: If the total iris or pupil polygon length is below the desired threshold. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for PolygonsLengthValidator objects.""" min_iris_length: int = Field(..., ge=0) min_pupil_length: int = Field(..., ge=0)
__parameters_type__ = Parameters def __init__(self, min_iris_length: int = 150, min_pupil_length: int = 75) -> None: """Assign parameters. Args: min_iris_length (int): Minimum cumulated length of the iris polygon. If too small, the extrapolation algorithm won't work properly. Defaults to 150. min_pupil_length (int): Minimum cumulated length of the pupil polygon. If too small, the extrapolation algorithm won't work properly. Defaults to 75. """ super().__init__(min_iris_length=min_iris_length, min_pupil_length=min_pupil_length)
[docs] def run(self, val_arguments: GeometryPolygons) -> None: """Validate that the total iris and pupil polygon length is above the desired threshold. Args: val_arguments (GeometryPolygons): GeometryPolygons to be validated. Raises: E.GeometryEstimationError: Raised if the total iris or pupil polygon length is below the desired threshold. """ pupil_length = polygon_length(val_arguments.pupil_array) iris_length = polygon_length(val_arguments.iris_array) if pupil_length < self.params.min_pupil_length: raise E.GeometryEstimationError( f"Valid pupil polygon is too small: Got {pupil_length} px, min {self.params.min_pupil_length} px." ) if iris_length < self.params.min_iris_length: raise E.GeometryEstimationError( f"Valid iris polygon is too small: Got {iris_length} px, min {self.params.min_iris_length} px." )
[docs] def on_execute_start(self, input_polygons: GeometryPolygons, *args, **kwargs) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: input_polygons (GeometryPolygons): input GeometryPolygons to be validated. """ self.run(input_polygons)
[docs]class IsMaskTooSmallValidator(Callback, Algorithm): """Validate that the masked part of the IrisTemplate is small enough. The larger the mask, the less reliable information is available to create a robust identity. Raises: E.EncoderError: If the total number of non-masked bits is below threshold. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for IsMaskTooSmallValidator objects.""" min_maskcodes_size: int = Field(..., ge=0)
__parameters_type__ = Parameters def __init__(self, min_maskcodes_size: int = 0) -> None: """Assign parameters. Args: min_maskcodes_size (int): Minimum size of mask codes. If too small, valid iris texture is too small, should be rejected. """ super().__init__(min_maskcodes_size=min_maskcodes_size)
[docs] def run(self, val_arguments: IrisTemplate) -> None: """Validate that the total mask codes size is above the desired threshold. Args: val_arguments (IrisTemplate): IrisTemplate to be validated. Raises: E.EncoderError: Raised if the total mask codes size is below the desired threshold. """ maskcodes_size = np.sum(val_arguments.mask_codes) if maskcodes_size < self.params.min_maskcodes_size: raise E.EncoderError( f"Valid mask codes size is too small: Got {maskcodes_size} px, min {self.params.min_maskcodes_size} px." )
[docs] def on_execute_end(self, input_template: IrisTemplate, *args, **kwargs) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: input_template (IrisTemplate): input IrisTemplate to be validated. """ self.run(input_template)