Source code for iris.nodes.normalization.nonlinear_normalization

from enum import Enum
from typing import Collection

import numpy as np
from pydantic import PositiveInt

from iris.io.class_configs import Algorithm
from iris.io.dataclasses import EyeOrientation, GeometryPolygons, IRImage, NoiseMask, NormalizedIris
from iris.io.errors import NormalizationError
from iris.nodes.normalization.utils import correct_orientation, generate_iris_mask, normalize_all, to_uint8


[docs]class NonlinearType(str, Enum): """Makes wrapper for params.""" default = "default" wyatt = "wyatt"
[docs]class NonlinearNormalization(Algorithm): """Implementation of a normalization algorithm which uses nonlinear squared transformation to map image pixels. Algorithm steps: 1) Create nonlinear grids of sampling radii based on parameters: res_in_r, intermediate_radiuses. 2) Compute the mapping between the normalized image pixel location and the original image location. 3) Obtain pixel values of normalized image using bilinear intepolation. References: [1] H J Wyatt, A 'minimum-wear-and-tear' meshwork for the iris, https://core.ac.uk/download/pdf/82071136.pdf [2] W-S Chen, J-C Li, Fast Non-linear Normalization Algorithm for Iris Recognition, https://www.scitepress.org/papers/2010/28409/28409.pdf """
[docs] class Parameters(Algorithm.Parameters): """Parameters class for NonlinearNormalization.""" res_in_r: PositiveInt intermediate_radiuses: Collection[float] oversat_threshold: PositiveInt
__parameters_type__ = Parameters def __init__( self, res_in_r: PositiveInt = 128, oversat_threshold: PositiveInt = 254, method: NonlinearType = NonlinearType.default, ) -> None: """Assign parameters. Args: res_in_r (PositiveInt): Normalized image r resolution. Defaults to 128. oversat_threshold (PositiveInt, optional): threshold for masking over-satuated pixels. Defaults to 254. method (NonlinearType, optional): method for generating nonlinear grids. Defaults to NonlinearType.default. """ super().__init__( res_in_r=res_in_r, intermediate_radiuses=np.array( [self._getgrids(max(0, res_in_r), p2i_ratio, method) for p2i_ratio in np.arange(0, 101)] ), oversat_threshold=oversat_threshold, )
[docs] def run( self, image: IRImage, noise_mask: NoiseMask, extrapolated_contours: GeometryPolygons, eye_orientation: EyeOrientation, ) -> NormalizedIris: """Normalize iris using nonlinear transformation when sampling points from cartisian to polar coordinates. Args: image (IRImage): Input image to normalize. noise_mask (NoiseMask): Noise mask. extrapolated_contours (GeometryPolygons): Extrapolated contours. eye_orientation (EyeOrientation): Eye orientation angle. Returns: NormalizedIris: NormalizedIris object containing normalized image and iris mask. """ if len(extrapolated_contours.pupil_array) != len(extrapolated_contours.iris_array): raise NormalizationError("The number of extrapolated iris and pupil points must be the same.") pupil_points, iris_points = correct_orientation( extrapolated_contours.pupil_array, extrapolated_contours.iris_array, eye_orientation.angle, ) p2i_ratio = extrapolated_contours.pupil_diameter / extrapolated_contours.iris_diameter if p2i_ratio <= 0 or p2i_ratio >= 1: raise NormalizationError(f"Invalid pupil to iris ratio, not in the range (0,1): {p2i_ratio}.") iris_mask = generate_iris_mask(extrapolated_contours, noise_mask.mask) iris_mask[image.img_data >= self.params.oversat_threshold] = False src_points = self._generate_correspondences(pupil_points, iris_points, p2i_ratio) normalized_image, normalized_mask = normalize_all( image=image.img_data, iris_mask=iris_mask, src_points=src_points ) normalized_iris = NormalizedIris( normalized_image=to_uint8(normalized_image), normalized_mask=normalized_mask, ) return normalized_iris
def _getgrids(self, res_in_r: PositiveInt, P_r100: PositiveInt, method: NonlinearType) -> np.ndarray: """Generate radius grids for nonlinear normalization based on p2i_ratio (pupil_to_iris ratio). Args: res_in_r (PositiveInt): Normalized image r resolution. P_r100 (PositiveInt): pupil_to_iris ratio in percentage, range in [0,100] method (NonlinearType): method for generating nonlinear grids. Returns: np.ndarray: nonlinear sampling grids for normalization """ if method == NonlinearType.default: p = np.arange(28, max(74 - P_r100, P_r100 - 14)) ** 2 q = (p - p[0]) / (p[-1] - p[0]) # Normalize q lin_space = np.linspace(0, 1.0, res_in_r + 1) grids = np.interp(lin_space, np.linspace(0, 1.0, len(q)), q) return grids[:-1] + np.diff(grids) / 2 # Return midpoint values if method == NonlinearType.wyatt: P_ref = 0.9 c = 1.08 P_r = P_r100 / 100 n = res_in_r + 1 i_values = np.arange(1, n) cos_theta_r = (i_values**c * (2 * (n - 1) ** c * P_ref + i_values**c * (1 - P_ref))) / ( (n - 1) ** c * (1 + P_ref) * ((n - 1) ** c * P_ref + i_values**c * (1 - P_ref)) ) coefficients = np.vstack( [ np.ones_like(i_values) * (1 - P_r), 2 * P_r - (1 - P_r**2) * cos_theta_r, -(1 + P_r) * P_r * cos_theta_r, ] ).T roots = np.array([np.roots(coeff) for coeff in coefficients]) delta = np.array([r[r > 0][0] if np.any(r > 0) else 0 for r in roots]) # Take only the positive root return delta def _generate_correspondences( self, pupil_points: np.ndarray, iris_points: np.ndarray, p2i_ratio: float ) -> np.ndarray: """Generate corresponding positions in original image. Args: pupil_points (np.ndarray): Pupil bounding points. NumPy array of shape (num_points x 2). iris_points (np.ndarray): Iris bounding points. NumPy array of shape (num_points x 2). p2i_ratio (float): Pupil to iris ratio. Returns: np.ndarray: generated corresponding points. """ src_points = np.array( [ pupil_points + x * (iris_points - pupil_points) for x in self.params.intermediate_radiuses[round(100 * p2i_ratio)] ] ) return np.round(src_points).astype(int)