Skip to content

Commit

Permalink
Merge pull request #61 from worldcoin/yichen1-ai-5172-refactor-hd-mat…
Browse files Browse the repository at this point in the history
…ching

Yichen1 ai 5172 refactor hd matching
  • Loading branch information
ycbiometrics authored Jan 28, 2025
2 parents e0e4054 + 3149e91 commit b9a771b
Show file tree
Hide file tree
Showing 17 changed files with 270 additions and 388 deletions.
12 changes: 8 additions & 4 deletions docs/source/examples/matching_entities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ Create a ``HammingDistanceMatcher`` matcher object.
def __init__(
self,
rotation_shift: int = 15,
nm_dist: Optional[confloat(ge=0, le=1, strict=True)] = None,
normalise: bool = True,
nm_dist: Optional[confloat(ge=0, le=1, strict=True)] = 0.45,
separate_half_matching: bool = True,
weights: Optional[List[np.ndarray]] = None,
) -> None:
"""Assign parameters.
Args:
rotation_shift (int): rotations allowed in matching, converted to shifts in columns. Defaults to 15.
nm_dist (Optional[confloat(ge=0, le = 1, strict=True)]): nonmatch distance used for normalized HD. Optional paremeter for normalized HD. Defaults to None.
Args:
rotation_shift (int): rotations allowed in matching, experessed in iris code columns. Defaults to 15.
normalise (bool = False): Flag to normalize HD. Defaults to True.
nm_dist (Optional[confloat(ge=0, le = 1, strict=True)]): nonmatch distance used for normalized HD. Optional paremeter for normalized HD. Defaults to 0.45.
separate_half_matching (bool): separate the upper and lower halves for matching. Defaults to True.
weights (Optional[List[np.ndarray]]): list of weights table. Optional paremeter for weighted HD. Defaults to None.
"""
Expand Down
5 changes: 4 additions & 1 deletion src/iris/io/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,25 @@ class PupilIrisPropertyEstimationError(Exception):

pass


class Pupil2IrisValidatorErrorDilation(Exception):
"""Pupil2IrisValidatorErrorDilation error class."""

pass


class Pupil2IrisValidatorErrorConstriction(Exception):
"""Pupil2IrisValidatorErrorConstriction error class."""

pass


class Pupil2IrisValidatorErrorOffcenter(Exception):
"""Pupil2IrisValidatorErrorOffcenter error class."""

pass


class GeometryEstimationError(Exception):
"""GeometryEstimation module Error class."""

Expand Down Expand Up @@ -136,4 +140,3 @@ class IRISPipelineError(Exception):
"""IRIS Pipeline module Error class."""

pass

5 changes: 4 additions & 1 deletion src/iris/nodes/encoder/iris_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ def run(self, response: IrisFilterResponse) -> IrisTemplate:
for iris_response, mask_response in zip(response.iris_responses, response.mask_responses):

iris_code = np.stack([iris_response.real > 0, iris_response.imag > 0], axis=-1)
mask_code = np.stack([mask_response.real >= self.params.mask_threshold, mask_response.imag >= self.params.mask_threshold], axis=-1)
mask_code = np.stack(
[mask_response.real >= self.params.mask_threshold, mask_response.imag >= self.params.mask_threshold],
axis=-1,
)

iris_codes.append(iris_code)
mask_codes.append(mask_code)
Expand Down
2 changes: 1 addition & 1 deletion src/iris/nodes/iris_response/conv_filter_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def _convolve(
# Perform convolution at [i,j] probed pixel position.
iris_response[i][j] = (iris_patch * img_filter.kernel_values).sum() / non_padded_k_rows / k_cols
mask_response[i][j] = 0 if iris_response[i][j] == 0 else (mask_patch.sum() / non_padded_k_rows / k_cols)

iris_response.real = iris_response.real / img_filter.kernel_norm.real
iris_response.imag = iris_response.imag / img_filter.kernel_norm.imag
mask_response.imag = mask_response.real
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ def __init__(self, **kwargs: Any) -> None:
"""Init function."""
super().__init__(**kwargs)
self.__kernel_values = self.compute_kernel_values()
self.__kernel_norm = np.linalg.norm(self.__kernel_values.real, ord="fro") + np.linalg.norm(self.__kernel_values.imag, ord="fro")*1j
self.__kernel_norm = (
np.linalg.norm(self.__kernel_values.real, ord="fro")
+ np.linalg.norm(self.__kernel_values.imag, ord="fro") * 1j
)

@property
def kernel_norm(self) -> float:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FragileBitRefinement(Algorithm):
"""

class Parameters(Algorithm.Parameters):
"""RegularProbeSchema parameters."""
"""FragileBitRefinement parameters."""

value_threshold: Tuple[confloat(ge=0), confloat(ge=0), confloat(ge=0)]
fragile_type: FragileType
Expand All @@ -46,6 +46,7 @@ def __init__(
calculated in cartesian or polar coordinates. In the first, the values
of value_threshold denote to x and y axis, in the case of polar coordinates,
the values denote to radius and angle. Defaults to FragileType.polar.
callbacks(List[Callback]): List of callbacks. Defaults to [].
"""
super().__init__(value_threshold=value_threshold, fragile_type=fragile_type, callbacks=callbacks)

Expand Down
30 changes: 18 additions & 12 deletions src/iris/nodes/matcher/hamming_distance_matcher.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from typing import List, Literal, Optional
from typing import List, Optional

import numpy as np
from pydantic import confloat

from iris.io.dataclasses import IrisTemplate
from iris.nodes.matcher.utils import hamming_distance
from iris.nodes.matcher.hamming_distance_matcher_interface import Matcher
from iris.nodes.matcher.utils import hamming_distance


class HammingDistanceMatcher(Matcher):
"""Hamming distance Matcher.
"""Hamming distance Matcher with additional optional features.
Algorithm steps:
1) Calculate counts of nonmatch irisbits (IB_Counts) in common unmasked region and the counts of common maskbits (MB_Counts) in common unmasked region for both upper and lower half of iris, respectively.
Expand All @@ -22,32 +22,38 @@ class HammingDistanceMatcher(Matcher):
"""

class Parameters(Matcher.Parameters):
"""IrisMatcherParameters parameters."""
"""HammingDistanceMatcher parameters."""

normalise: bool
nm_dist: confloat(ge=0, le=1, strict=True)
nm_type: Literal["linear", "sqrt"]
separate_half_matching: bool
weights: Optional[List[np.ndarray]]

__parameters_type__ = Parameters

def __init__(
self,
rotation_shift: int = 15,
normalise: bool = False,
normalise: bool = True,
nm_dist: confloat(ge=0, le=1, strict=True) = 0.45,
nm_type: Literal["linear", "sqrt"] = "sqrt",
separate_half_matching: bool = True,
weights: Optional[List[np.ndarray]] = None,
) -> None:
"""Assign parameters.
Args:
rotation_shift (int): rotations allowed in matching, experessed in iris code columns. Defaults to 15.
nm_dist (Optional[confloat(ge=0, le = 1, strict=True)]): nonmatch distance used for normalized HD. Optional paremeter for normalized HD. Defaults to None.
weights (Optional[List[np.ndarray]]): list of weights table. Optional paremeter for weighted HD. Defaults to None.
rotation_shift (int): Rotation shifts allowed in matching (in columns). Defaults to 15.
normalise (bool, optional): Flag to normalize HD. Defaults to True.
nm_dist (Optional[confloat(ge=0, le = 1, strict=True)], optional): Nonmatch distance used for normalized HD. Optional paremeter for normalized HD. Defaults to 0.45.
separate_half_matching (bool, optional): Separate the upper and lower halves for matching. Defaults to True.
weights (Optional[List[np.ndarray]], optional): list of weights table. Optional paremeter for weighted HD. Defaults to None.
"""
super().__init__(
rotation_shift=rotation_shift, normalise=normalise, nm_dist=nm_dist, nm_type=nm_type, weights=weights
rotation_shift=rotation_shift,
normalise=normalise,
nm_dist=nm_dist,
separate_half_matching=separate_half_matching,
weights=weights,
)

def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> float:
Expand All @@ -66,7 +72,7 @@ def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> f
self.params.rotation_shift,
self.params.normalise,
self.params.nm_dist,
self.params.nm_type,
self.params.separate_half_matching,
self.params.weights,
)

Expand Down
3 changes: 2 additions & 1 deletion src/iris/nodes/matcher/hamming_distance_matcher_interface.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import abc
from typing import Any, List

from pydantic import conint

from iris.io.class_configs import ImmutableModel
from iris.io.dataclasses import IrisTemplate
from pydantic import conint


class Matcher(abc.ABC):
Expand Down
Binary file not shown.
27 changes: 10 additions & 17 deletions src/iris/nodes/matcher/simple_hamming_distance_matcher.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
from pydantic import confloat, conint
from pydantic import confloat

from iris.io.dataclasses import IrisTemplate
from iris.nodes.matcher.utils import simple_hamming_distance
from iris.nodes.matcher.hamming_distance_matcher_interface import Matcher
from iris.nodes.matcher.utils import simple_hamming_distance


class SimpleHammingDistanceMatcher(Matcher):
"""Hamming distance Matcher, without the bells and whistles.
"""Simple Hamming distance Matcher without the bells and whistles.
Algorithm steps:
1) Calculate counts of nonmatch irisbits (IB_Counts) in common unmasked region and the counts of common maskbits (MB_Counts) in common unmasked region.
2) Calculate Hamming distance (HD) based on IB_Counts and MB_Counts.
3) If parameter `normalise` is True, normalize Hamming distance based on parameter `norm_mean` and parameter `norm_nb_bits`.
3) If parameter `normalise` is True, normalize Hamming distance based on parameter `norm_mean`.
4) If parameter rotation_shift is > 0, repeat the above steps for additional rotations of the iriscode.
5) Return the minimium distance from above calculations.
"""

class Parameters(Matcher.Parameters):
"""IrisMatcherParameters parameters."""
"""SimpleHammingDistanceMatcher parameters."""

normalise: bool
norm_mean: confloat(ge=0, le=1)
norm_nb_bits: conint(gt=0)

__parameters_type__ = Parameters

Expand All @@ -30,20 +29,15 @@ def __init__(
rotation_shift: int = 15,
normalise: bool = False,
norm_mean: float = 0.45,
norm_nb_bits: float = 12288,
) -> None:
"""Assign parameters.
Args:
rotation_shift (int = 15): rotation allowed in matching, converted to columns. Defaults to 15.
normalise (bool = False): Flag to normalize HD. Defaults to False.
norm_mean (float = 0.45): Peak of the non-match distribution. Defaults to 0.45.
norm_nb_bits (float = 12288): 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).
rotation_shift (int, optional): Rotation shifts allowed in matching (in columns). Defaults to 15.
normalise (bool, optional): Flag to normalize HD. Defaults to False.
norm_mean (float, optional): Peak of the non-match distribution. Defaults to 0.45.
"""
super().__init__(
rotation_shift=rotation_shift, normalise=normalise, norm_mean=norm_mean, norm_nb_bits=norm_nb_bits
)
super().__init__(rotation_shift=rotation_shift, normalise=normalise, norm_mean=norm_mean)

def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> float:
"""Match iris templates using Hamming distance.
Expand All @@ -53,14 +47,13 @@ def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> f
template_gallery (IrisTemplate): Iris template from gallery.
Returns:
float: matching distance.
float: Matching distance.
"""
score, _ = simple_hamming_distance(
template_probe=template_probe,
template_gallery=template_gallery,
rotation_shift=self.params.rotation_shift,
normalise=self.params.normalise,
norm_mean=self.params.norm_mean,
norm_nb_bits=self.params.norm_nb_bits,
)
return score
Loading

0 comments on commit b9a771b

Please sign in to comment.