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, Sharpness
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.Pupil2IrisValidatorErrorConstriction: Raised if pupil is constricted. E.Pupil2IrisValidatorErrorDilation: Raised if pupil is dilated. E.Pupil2IrisValidatorErrorOffcenter: Raised if pupil and iris are offcenter. """ if val_arguments.pupil_to_iris_diameter_ratio < self.params.min_allowed_diameter_ratio: raise E.Pupil2IrisValidatorErrorConstriction( f"p2i_property={val_arguments.pupil_to_iris_diameter_ratio} is below min threshold {self.params.min_allowed_diameter_ratio}. Pupil is too constricted." ) if val_arguments.pupil_to_iris_diameter_ratio > self.params.max_allowed_diameter_ratio: raise E.Pupil2IrisValidatorErrorDilation( f"p2i_property={val_arguments.pupil_to_iris_diameter_ratio} is above max threshold {self.params.max_allowed_diameter_ratio}. Pupil is too dilated." ) if val_arguments.pupil_to_iris_center_dist_ratio > self.params.max_allowed_center_dist_ratio: raise E.Pupil2IrisValidatorErrorOffcenter( f"p2i_property={val_arguments.pupil_to_iris_center_dist_ratio} exceeds {self.params.max_allowed_center_dist_ratio}. Pupil and iris are off-center." )
[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 SharpnessValidator(Callback, Algorithm): """Validate that the normalized image is not too blurry. Raises: E.SharpnessEstimationError: If the sharpness score is below threshold. """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for SharpnessValidator objects.""" min_sharpness: float = Field(..., ge=0.0)
__parameters_type__ = Parameters def __init__(self, min_sharpness: float = 0.0) -> None: """Assign parameters. Args: min_sharpness (float): Minimum sharpness score. Sharpness computation min threshold that allows further sample processing. Defaults to 0.0 (by default every check will result in success). """ super().__init__(min_sharpness=min_sharpness)
[docs] def run(self, val_arguments: Sharpness) -> None: """Validate of sharpness estimation algorithm. Args: val_arguments (Sharpness): Computed result. Raises: E.SharpnessEstimationError: Raised if the sharpness score is below the desired threshold. """ if val_arguments.score < self.params.min_sharpness: raise E.SharpnessEstimationError( f"sharpness={val_arguments.score} < min_sharpness={self.params.min_sharpness}" )
[docs] def on_execute_end(self, result: Sharpness) -> None: """Wrap for validate method so that validator can be used as a Callback. Args: result (Sharpness): Sharpness resulted from computations. """ self.run(result)
[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.MaskTooSmallError: 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.MaskTooSmallError: 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.MaskTooSmallError( 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)