Source code for iris.nodes.matcher.utils

from typing import List, Literal, Optional, Tuple

import numpy as np

from iris.io.dataclasses import IrisTemplate
from iris.io.errors import MatcherError


[docs]def simple_hamming_distance( template_probe: IrisTemplate, template_gallery: IrisTemplate, rotation_shift: int = 15, normalise: bool = False, norm_mean: float = 0.45, norm_nb_bits: float = 12288, ) -> Tuple[float, int]: """Compute Hamming distance, without bells and whistles. Args: template_probe (IrisTemplate): Iris template from probe. template_gallery (IrisTemplate): Iris template from gallery. rotation_shift (int): Rotations allowed in matching, in columns. Defaults to 15. normalise (bool): Flag to normalize HD. Defaults to False. norm_mean (float): Peak of the non-match distribution. Defaults to 0.45. norm_nb_bits (float): Average number of bits visible in 2 randomly sampled iris codes. Defaults to 12288 (3/4 * total_bits_number for the iris code format v0.1). Returns: Tuple[float, int]: miminum Hamming distance and corresonding rotation shift. """ for probe_code, gallery_code in zip(template_probe.iris_codes, template_gallery.iris_codes): if probe_code.shape != gallery_code.shape: raise MatcherError("prove and gallery iriscode are of different sizes") best_dist = 1 rot_shift = 0 for current_shift in range(-rotation_shift, rotation_shift + 1): irisbits = [ np.roll(probe_code, current_shift, axis=1) != gallery_code for probe_code, gallery_code in zip(template_probe.iris_codes, template_gallery.iris_codes) ] maskbits = [ np.roll(probe_code, current_shift, axis=1) & gallery_code for probe_code, gallery_code in zip(template_probe.mask_codes, template_gallery.mask_codes) ] irisbitcount = sum([np.sum(x & y) for x, y in zip(irisbits, maskbits)]) maskbitcount = sum([maskbit.sum() for maskbit in maskbits]) if maskbitcount == 0: continue current_dist = irisbitcount / maskbitcount if normalise: current_dist = max(0, norm_mean - (norm_mean - current_dist) * np.sqrt(maskbitcount / norm_nb_bits)) if (current_dist < best_dist) or (current_dist == best_dist and current_shift == 0): best_dist = current_dist rot_shift = current_shift return best_dist, rot_shift
[docs]def normalized_HD(irisbitcount: int, maskbitcount: int, sqrt_totalbitcount: float, nm_dist: float) -> float: """Perform normalized HD calculation. Args: irisbitcount (int): nonmatched iriscode bit count. maskbitcount (int): common maskcode bit count. sqrt_totalbitcount (float): square root of bit counts. nm_dist (float): nonmatch distance used for normalized HD. Returns: float: normalized Hamming distance. """ norm_HD = max(0, nm_dist - (nm_dist - irisbitcount / maskbitcount) * np.sqrt(maskbitcount) / sqrt_totalbitcount) return norm_HD
[docs]def count_sqrt_totalbits( toal_codesize: int, half_width: List[int], weights: Optional[List[np.ndarray]] = None, ) -> Tuple[float, float, float]: """Count total amount of sqrt bits. Args: toal_codesizes (int): total size of iriscodes. half_width (List[int]): half width of iriscodes. weights (Optional[List[np.ndarray]] = None): list of weights table. Optional paremeter for weighted HD. Defaults to None. Returns: Tuple[float, float, float]: square root of bit counts from whole iris, top iris and bottom iris. """ sqrt_totalbitcount = np.sqrt(np.sum([np.sum(w) for w in weights])) if weights else np.sqrt(toal_codesize * 3 / 4) sqrt_totalbitcount_bot = ( np.sqrt(np.sum([np.sum(w[:, :hw, ...]) for w, hw in zip(weights, half_width)])) if weights else sqrt_totalbitcount / np.sqrt(2) ) sqrt_totalbitcount_top = ( np.sqrt(np.sum([np.sum(w[:, hw:, ...]) for w, hw in zip(weights, half_width)])) if weights else sqrt_totalbitcount / np.sqrt(2) ) return sqrt_totalbitcount, sqrt_totalbitcount_top, sqrt_totalbitcount_bot
[docs]def count_nonmatchbits( irisbits: np.ndarray, maskbits: np.ndarray, half_width: List[int], weights: Optional[List[np.ndarray]] = None, ) -> Tuple[int, int, int, int]: """Count nonmatch bits for Hammming distance. Args: irisbits (np.ndarray): nonmatch irisbits. maskbits (np.ndarray): common maskbits. half_width (List[int]): list of half of code width. weights (Optional[np.ndarray] = None): list of weights table. Optional paremeter for weighted HD. Defaults to None. Returns: Tuple[int, int, int, int]: nonmatch iriscode bit count and common maskcode bit count from top iris and bottom iris. """ if weights: irisbitcount_top = np.sum( [ np.sum(np.multiply(x[:, hw:, ...] & y[:, hw:, ...], z[:, hw:, ...])) for x, y, hw, z in zip(irisbits, maskbits, half_width, weights) ] ) maskbitcount_top = np.sum( [np.sum(np.multiply(x[:, hw:, ...], z[:, hw:, ...])) for x, hw, z in zip(maskbits, half_width, weights)] ) irisbitcount_bot = np.sum( [ np.sum(np.multiply(x[:, :hw, ...] & y[:, :hw, ...], z[:, :hw, ...])) for x, y, hw, z in zip(irisbits, maskbits, half_width, weights) ] ) maskbitcount_bot = np.sum( [np.sum(np.multiply(x[:, :hw, ...], z[:, :hw, ...])) for x, hw, z in zip(maskbits, half_width, weights)] ) else: irisbitcount_top = np.sum( [np.sum(x[:, hw:, ...] & y[:, hw:, ...]) for x, y, hw in zip(irisbits, maskbits, half_width)] ) maskbitcount_top = np.sum([np.sum(x[:, hw:, ...]) for x, hw in zip(maskbits, half_width)]) irisbitcount_bot = np.sum( [np.sum(x[:, :hw, ...] & y[:, :hw, ...]) for x, y, hw in zip(irisbits, maskbits, half_width)] ) maskbitcount_bot = np.sum([np.sum(x[:, :hw, ...]) for x, hw in zip(maskbits, half_width)]) return irisbitcount_top, maskbitcount_top, irisbitcount_bot, maskbitcount_bot
[docs]def hamming_distance( template_probe: IrisTemplate, template_gallery: IrisTemplate, rotation_shift: int, normalise: bool = False, nm_dist: float = 0.45, nm_type: Literal["linear", "sqrt"] = "sqrt", weights: Optional[List[np.ndarray]] = None, ) -> Tuple[float, int]: """Compute Hamming distance. Args: template_probe (IrisTemplate): Iris template from probe. template_gallery (IrisTemplate): Iris template from gallery. rotation_shift (int): rotation allowed in matching, converted to columns. normalise (bool): Flag to normalize HD. Defaults to False. nm_dist (float): nonmatch mean distance for normalized HD. Defaults to 0.45. nm_type (Literal["linear", "sqrt"]): type of normalized HD. Defaults to "sqrt". weights (Optional[List[np.ndarray]]= None): list of weights table. Optional paremeter for weighted HD. Defaults to None. Returns: Tuple[float, int]: miminum Hamming distance and corresonding rotation shift. """ half_codewidth = [] for probe_code, gallery_code in zip(template_probe.iris_codes, template_gallery.iris_codes): if probe_code.shape != gallery_code.shape: raise MatcherError("probe and gallery iris codes are of different sizes") if (probe_code.shape[1] % 2) != 0: raise MatcherError("number of columns of iris codes need to be even") half_codewidth.append(int(probe_code.shape[1] / 2)) if weights: for probe_code, w in zip(template_probe.iris_codes, weights): if probe_code.shape != w.shape: raise MatcherError("weights table and iris codes are of different sizes") if normalise: if weights: sqrt_totalbitcount, sqrt_totalbitcount_top, sqrt_totalbitcount_bot = count_sqrt_totalbits( np.sum([np.size(a) for a in template_probe.iris_codes]), half_codewidth, weights ) else: sqrt_totalbitcount, sqrt_totalbitcount_top, sqrt_totalbitcount_bot = count_sqrt_totalbits( np.sum([np.size(a) for a in template_probe.iris_codes]), half_codewidth ) # Calculate the Hamming distance between probe and gallery template. match_dist = 1 match_rot = 0 for shiftby in range(-rotation_shift, rotation_shift + 1): irisbits = [ np.roll(probe_code, shiftby, axis=1) != gallery_code for probe_code, gallery_code in zip(template_probe.iris_codes, template_gallery.iris_codes) ] maskbits = [ np.roll(probe_code, shiftby, axis=1) & gallery_code for probe_code, gallery_code in zip(template_probe.mask_codes, template_gallery.mask_codes) ] if weights: irisbitcount_top, maskbitcount_top, irisbitcount_bot, maskbitcount_bot = count_nonmatchbits( irisbits, maskbits, half_codewidth, weights ) else: irisbitcount_top, maskbitcount_top, irisbitcount_bot, maskbitcount_bot = count_nonmatchbits( irisbits, maskbits, half_codewidth ) maskbitcount = maskbitcount_top + maskbitcount_bot if maskbitcount == 0: continue if normalise: normdist_top = ( normalized_HD(irisbitcount_top, maskbitcount_top, sqrt_totalbitcount_top, nm_dist) if maskbitcount_top > 0 else 1 ) normdist_bot = ( normalized_HD(irisbitcount_bot, maskbitcount_bot, sqrt_totalbitcount_bot, nm_dist) if maskbitcount_bot > 0 else 1 ) if nm_type == "linear": Hdist = ( normalized_HD((irisbitcount_top + irisbitcount_bot), maskbitcount, sqrt_totalbitcount, nm_dist) / 2 + (normdist_top * maskbitcount_top + normdist_bot * maskbitcount_bot) / maskbitcount / 2 ) elif nm_type == "sqrt": w_top = np.sqrt(maskbitcount_top) w_bot = np.sqrt(maskbitcount_bot) Hdist = ( normalized_HD((irisbitcount_top + irisbitcount_bot), maskbitcount, sqrt_totalbitcount, nm_dist) / 2 + (normdist_top * w_top + normdist_bot * w_bot) / (w_top + w_bot) / 2 ) else: raise NotImplementedError( "Given `nm_type` not supported. Expected: Literal[\"linear\", \"sqrt\"]. Received {nm_type}." ) else: Hdist = (irisbitcount_top + irisbitcount_bot) / maskbitcount if (Hdist < match_dist) or (Hdist == match_dist and shiftby == 0): match_dist = Hdist match_rot = shiftby return match_dist, match_rot