From 76bde45994710ca4995368ebbee8c04829043b5e Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Wed, 19 Jun 2024 12:44:20 +0000 Subject: [PATCH 01/11] remove upper bound on matcher's rotation parameter --- src/iris/nodes/matcher/hamming_distance_matcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iris/nodes/matcher/hamming_distance_matcher.py b/src/iris/nodes/matcher/hamming_distance_matcher.py index 0eeca45..40a608d 100644 --- a/src/iris/nodes/matcher/hamming_distance_matcher.py +++ b/src/iris/nodes/matcher/hamming_distance_matcher.py @@ -24,7 +24,7 @@ class HammingDistanceMatcher(Algorithm): class Parameters(Algorithm.Parameters): """Parameters class for HammingDistanceMatcher.""" - rotation_shift: conint(ge=0, le=180, strict=True) + rotation_shift: conint(ge=0, strict=True) nm_dist: Optional[confloat(ge=0, le=1, strict=True)] weights: Optional[List[np.ndarray]] @@ -39,7 +39,7 @@ def __init__( """Assign parameters. Args: - rotation_shift (int): rotations allowed in matching, converted to shifts in columns. Defaults to 15. + 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. """ From dbb79b74857ff44b9c581417714e91bf55c7ace8 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 20 Jun 2024 11:06:35 +0000 Subject: [PATCH 02/11] Add Fix unit test --- tests/unit_tests/nodes/matcher/test_hamming_distance_matcher.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit_tests/nodes/matcher/test_hamming_distance_matcher.py b/tests/unit_tests/nodes/matcher/test_hamming_distance_matcher.py index a6e8cd6..09a3ee4 100644 --- a/tests/unit_tests/nodes/matcher/test_hamming_distance_matcher.py +++ b/tests/unit_tests/nodes/matcher/test_hamming_distance_matcher.py @@ -12,7 +12,6 @@ [ pytest.param(-0.5, 0.45), pytest.param(1.5, None), - pytest.param(200, 0.3), pytest.param(200, "a"), pytest.param(100, -0.2), pytest.param(10, 1.3), @@ -20,7 +19,6 @@ ids=[ "rotation_shift should not be negative", "rotation_shift should not be floating points", - "rotation_shift should not be larger than 180", "nm_dist should be float", "nm_dist should not be negative", "nm_dist should not be more than 1", From 736d65547e1f8ab5fa919edd2772a8ecab2d84a6 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:55:16 +0000 Subject: [PATCH 03/11] Add a new normalisation option for + new parameter --- src/iris/nodes/matcher/utils.py | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/iris/nodes/matcher/utils.py b/src/iris/nodes/matcher/utils.py index 3580a72..26f22e1 100644 --- a/src/iris/nodes/matcher/utils.py +++ b/src/iris/nodes/matcher/utils.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Literal, Optional, Tuple import numpy as np @@ -109,7 +109,9 @@ def hamming_distance( template_probe: IrisTemplate, template_gallery: IrisTemplate, rotation_shift: int, - nm_dist: Optional[float] = None, + 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. @@ -118,7 +120,9 @@ def hamming_distance( template_probe (IrisTemplate): Iris template from probe. template_gallery (IrisTemplate): Iris template from gallery. rotation_shift (int): rotation allowed in matching, converted to columns. - nm_dist (Optional[float] = None): nonmatch distance, Optional paremeter for normalized HD. Defaults to None. + 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: @@ -138,7 +142,7 @@ def hamming_distance( if probe_code.shape != w.shape: raise MatcherError("weights table and iris codes are of different sizes") - if nm_dist: + 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 @@ -174,7 +178,7 @@ def hamming_distance( if maskbitcount == 0: continue - if nm_dist: + if normalise: normdist_top = ( normalized_HD(irisbitcount_top, maskbitcount_top, sqrt_totalbitcount_top, nm_dist) if maskbitcount_top > 0 @@ -185,12 +189,22 @@ def hamming_distance( if maskbitcount_bot > 0 else 1 ) - 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 - ) + 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 From 9542395e259f4e027238ef447945356d1b147ae1 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:55:50 +0000 Subject: [PATCH 04/11] Adapt HammingDistanceMatcher to new parameter --- .../nodes/matcher/hamming_distance_matcher.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/iris/nodes/matcher/hamming_distance_matcher.py b/src/iris/nodes/matcher/hamming_distance_matcher.py index 40a608d..3d5be3b 100644 --- a/src/iris/nodes/matcher/hamming_distance_matcher.py +++ b/src/iris/nodes/matcher/hamming_distance_matcher.py @@ -1,14 +1,14 @@ -from typing import List, Optional +from typing import List, Literal, Optional import numpy as np -from pydantic import confloat, conint +from pydantic import confloat -from iris.io.class_configs import Algorithm from iris.io.dataclasses import IrisTemplate from iris.nodes.matcher.utils import hamming_distance +from iris.nodes.matcher.hamming_distance_matcher_interface import Matcher -class HammingDistanceMatcher(Algorithm): +class HammingDistanceMatcher(Matcher): """Hamming distance Matcher. Algorithm steps: @@ -18,14 +18,15 @@ class HammingDistanceMatcher(Algorithm): 4) If parameters nm_dist and weights are both defined, calculate weighted normalized Hamming distance (WNHD) based on IB_Counts, MB_Counts, nm_dist and weights. 5) Otherwise, calculate Hamming distance (HD) based on IB_Counts and MB_Counts. 6) If parameter rotation_shift is > 0, repeat the above steps for additional rotations of the iriscode. - 7) Return the minimium distance from above calculations and its correpsonding rotation angle. + 7) Return the minimium distance from above calculations. """ - class Parameters(Algorithm.Parameters): - """Parameters class for HammingDistanceMatcher.""" + class Parameters(Matcher.Parameters): + """IrisMatcherParameters parameters.""" - rotation_shift: conint(ge=0, strict=True) - nm_dist: Optional[confloat(ge=0, le=1, strict=True)] + normalise: bool + nm_dist: confloat(ge=0, le=1, strict=True) + nm_type: Literal["linear", "sqrt"] weights: Optional[List[np.ndarray]] __parameters_type__ = Parameters @@ -33,7 +34,9 @@ class Parameters(Algorithm.Parameters): def __init__( self, rotation_shift: int = 15, - nm_dist: Optional[confloat(ge=0, le=1, strict=True)] = None, + normalise: bool = False, + nm_dist: confloat(ge=0, le=1, strict=True) = 0.45, + nm_type: Literal["linear", "sqrt"] = "sqrt", weights: Optional[List[np.ndarray]] = None, ) -> None: """Assign parameters. @@ -43,7 +46,9 @@ def __init__( 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. """ - super().__init__(rotation_shift=rotation_shift, nm_dist=nm_dist, weights=weights) + super().__init__( + rotation_shift=rotation_shift, normalise=normalise, nm_dist=nm_dist, nm_type=nm_type, weights=weights + ) def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> float: """Match iris templates using Hamming distance. @@ -59,7 +64,9 @@ def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> f template_probe, template_gallery, self.params.rotation_shift, + self.params.normalise, self.params.nm_dist, + self.params.nm_type, self.params.weights, ) From cc80c0301af4ea087e22efc0a64fc99169adb5aa Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:56:28 +0000 Subject: [PATCH 05/11] Add Matcher and BatchMatcher interfaces --- .../hamming_distance_matcher_interface.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/iris/nodes/matcher/hamming_distance_matcher_interface.py diff --git a/src/iris/nodes/matcher/hamming_distance_matcher_interface.py b/src/iris/nodes/matcher/hamming_distance_matcher_interface.py new file mode 100644 index 0000000..8d483b3 --- /dev/null +++ b/src/iris/nodes/matcher/hamming_distance_matcher_interface.py @@ -0,0 +1,84 @@ +import abc +from typing import Any, List + +from iris.io.class_configs import ImmutableModel +from iris.io.dataclasses import IrisTemplate +from pydantic import conint + + +class Matcher(abc.ABC): + """Parent Abstract class for 1-to-1 matchers.""" + + class Parameters(ImmutableModel): + """IrisMatcherParameters parameters.""" + + rotation_shift: conint(ge=0, strict=True) + + __parameters_type__ = Parameters + + def __init__(self, **kwargs) -> None: + """Assign parameters. + + Args: + rotation_shift (int = 15): rotation allowed in matching, converted to columns. Defaults to 15. + """ + self.params = self.__parameters_type__(**kwargs) + + @abc.abstractmethod + def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> float: + """Match iris templates using Hamming distance. + + Args: + template_probe (IrisTemplate): Iris template from probe. + template_gallery (IrisTemplate): Iris template from gallery. + + Returns: + float: matching distance. + """ + pass + + +class BatchMatcher(abc.ABC): + """Parent Abstract class for 1-to-N matchers.""" + + class Parameters(ImmutableModel): + """IrisMatcherParameters parameters.""" + + rotation_shift: conint(ge=0, strict=True) + + __parameters_type__ = Parameters + + def __init__(self, **kwargs: Any) -> None: + """Assign parameters. + + Args: + rotation_shift (int = 15): rotation allowed in matching, converted to columns. Defaults to 15. + """ + self.params = self.__parameters_type__(**kwargs) + + @abc.abstractmethod + def intra_gallery(self, template_gallery: List[IrisTemplate]) -> List[List[float]]: + """Match iris templates using Hamming distance. + + Args: + template_gallery (List[IrisTemplate]): Iris template gallery. + + Returns: + List[List[float]]: matching distances. + """ + pass + + @abc.abstractmethod + def gallery_to_gallery( + self, template_gallery_1: List[IrisTemplate], template_gallery_2: List[IrisTemplate] + ) -> List[List[float]]: + """Match iris templates using Hamming distance. + + Args: + template_gallery_1 (List[IrisTemplate]): Iris template gallery. + template_gallery_2 (List[IrisTemplate]): Iris template gallery. + + Returns: + List[List[float]]: matching distances. + """ + pass From ac9e28171b4ded81cac00bb9b1e943a246e3130d Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:57:34 +0000 Subject: [PATCH 06/11] Update __init__ --- src/iris/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/iris/__init__.py b/src/iris/__init__.py index 880d1f5..0eeb2d5 100644 --- a/src/iris/__init__.py +++ b/src/iris/__init__.py @@ -65,6 +65,7 @@ from iris.nodes.iris_response.probe_schemas.regular_probe_schema import RegularProbeSchema from iris.nodes.iris_response_refinement.fragile_bits_refinement import FragileBitRefinement from iris.nodes.matcher.hamming_distance_matcher import HammingDistanceMatcher +from iris.nodes.matcher.hamming_distance_matcher_interface import BatchMatcher, Matcher from iris.nodes.normalization.linear_normalization import LinearNormalization from iris.nodes.normalization.nonlinear_normalization import NonlinearNormalization from iris.nodes.normalization.perspective_normalization import PerspectiveNormalization From 3934f629cd91c9e221872292b551060ac8995754 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:58:26 +0000 Subject: [PATCH 07/11] Add , a HD matcher without bells and whistles --- src/iris/__init__.py | 1 + .../simple_hamming_distance_matcher.py | 66 +++++++++++++++++++ src/iris/nodes/matcher/utils.py | 53 +++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 src/iris/nodes/matcher/simple_hamming_distance_matcher.py diff --git a/src/iris/__init__.py b/src/iris/__init__.py index 0eeb2d5..f5b518a 100644 --- a/src/iris/__init__.py +++ b/src/iris/__init__.py @@ -65,6 +65,7 @@ from iris.nodes.iris_response.probe_schemas.regular_probe_schema import RegularProbeSchema from iris.nodes.iris_response_refinement.fragile_bits_refinement import FragileBitRefinement from iris.nodes.matcher.hamming_distance_matcher import HammingDistanceMatcher +from iris.nodes.matcher.simple_hamming_distance_matcher import SimpleHammingDistanceMatcher from iris.nodes.matcher.hamming_distance_matcher_interface import BatchMatcher, Matcher from iris.nodes.normalization.linear_normalization import LinearNormalization from iris.nodes.normalization.nonlinear_normalization import NonlinearNormalization diff --git a/src/iris/nodes/matcher/simple_hamming_distance_matcher.py b/src/iris/nodes/matcher/simple_hamming_distance_matcher.py new file mode 100644 index 0000000..e2d55e8 --- /dev/null +++ b/src/iris/nodes/matcher/simple_hamming_distance_matcher.py @@ -0,0 +1,66 @@ +from pydantic import confloat, conint + +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 + + +class SimpleHammingDistanceMatcher(Matcher): + """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`. + 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.""" + + normalise: bool + norm_mean: confloat(ge=0, le=1) + norm_nb_bits: conint(gt=0) + + __parameters_type__ = Parameters + + def __init__( + self, + 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). + + """ + super().__init__( + rotation_shift=rotation_shift, normalise=normalise, norm_mean=norm_mean, norm_nb_bits=norm_nb_bits + ) + + def run(self, template_probe: IrisTemplate, template_gallery: IrisTemplate) -> float: + """Match iris templates using Hamming distance. + + Args: + template_probe (IrisTemplate): Iris template from probe. + template_gallery (IrisTemplate): Iris template from gallery. + + Returns: + 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 diff --git a/src/iris/nodes/matcher/utils.py b/src/iris/nodes/matcher/utils.py index 26f22e1..a337477 100644 --- a/src/iris/nodes/matcher/utils.py +++ b/src/iris/nodes/matcher/utils.py @@ -6,6 +6,59 @@ from iris.io.errors import MatcherError +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 + + def normalized_HD(irisbitcount: int, maskbitcount: int, sqrt_totalbitcount: float, nm_dist: float) -> float: """Perform normalized HD calculation. From d4eed91dec3bd8431a5100debee7ef900cb70881 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Thu, 4 Jul 2024 16:58:47 +0000 Subject: [PATCH 08/11] Adapt existing HD unit tests --- .../test_e2e_hamming_distance_matcher.py | 26 ++++++++------ .../nodes/matcher/test_matcher_utils.py | 34 ++++++++++++++++--- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/tests/e2e_tests/nodes/matcher/test_e2e_hamming_distance_matcher.py b/tests/e2e_tests/nodes/matcher/test_e2e_hamming_distance_matcher.py index ffc4dac..4380d06 100644 --- a/tests/e2e_tests/nodes/matcher/test_e2e_hamming_distance_matcher.py +++ b/tests/e2e_tests/nodes/matcher/test_e2e_hamming_distance_matcher.py @@ -18,16 +18,16 @@ def load_mock_pickle(name: str) -> Any: @pytest.mark.parametrize( - "rotation_shift,nm_dist,weights,expected_result", + "rotation_shift,normalise,nm_dist,weights,expected_result", [ - pytest.param(10, None, None, 0.0), - pytest.param(15, None, None, 0.0), - pytest.param(10, 0.45, None, 0.0123), - pytest.param(15, 0.45, None, 0.0123), - pytest.param(10, None, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0), - pytest.param(15, None, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0), - pytest.param(10, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0492), - pytest.param(15, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0492), + pytest.param(10, False, 0.45, None, 0.0), + pytest.param(15, False, 0.45, None, 0.0), + pytest.param(10, True, 0.45, None, 0.0123), + pytest.param(15, True, 0.45, None, 0.0123), + pytest.param(10, False, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0), + pytest.param(15, False, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0), + pytest.param(10, True, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0492), + pytest.param(15, True, 0.45, [np.ones([16, 256, 2]), np.ones([16, 256, 2])], 0.0492), ], ids=[ "regular1", @@ -42,6 +42,7 @@ def load_mock_pickle(name: str) -> Any: ) def test_e2e_iris_matcher( rotation_shift: int, + normalise: bool, nm_dist: float, weights: Optional[List[np.ndarray]], expected_result: float, @@ -49,7 +50,12 @@ def test_e2e_iris_matcher( first_template = load_mock_pickle("iris_template") second_template = deepcopy(first_template) - matcher = HammingDistanceMatcher(rotation_shift, nm_dist, weights) + matcher = HammingDistanceMatcher( + rotation_shift=rotation_shift, + normalise=normalise, + nm_dist=nm_dist, + weights=weights, + ) result = matcher.run(first_template, second_template) assert round(result, 4) == expected_result diff --git a/tests/unit_tests/nodes/matcher/test_matcher_utils.py b/tests/unit_tests/nodes/matcher/test_matcher_utils.py index 2032973..3768377 100644 --- a/tests/unit_tests/nodes/matcher/test_matcher_utils.py +++ b/tests/unit_tests/nodes/matcher/test_matcher_utils.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( - "template_probe, template_gallery, rotation_shift, nm_dist, expected_result", + "template_probe, template_gallery, rotation_shift, normalise, nm_dist, expected_result", [ ( IrisTemplate( @@ -24,6 +24,7 @@ iris_code_version="v2.1", ), 1, + False, None, (0, 0), ), @@ -39,6 +40,7 @@ iris_code_version="v2.1", ), 1, + False, None, (0.25, -1), ), @@ -54,6 +56,7 @@ iris_code_version="v2.1", ), 1, + False, None, (1, 0), ), @@ -69,6 +72,7 @@ iris_code_version="v2.1", ), 1, + False, None, (0.8, -1), ), @@ -84,6 +88,7 @@ iris_code_version="v2.1", ), 0, + False, None, (1, 0), ), @@ -99,6 +104,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0, 0), ), @@ -114,6 +120,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0.2867006838144548, -1), ), @@ -129,6 +136,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (1, 0), ), @@ -144,6 +152,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0.7703179042939132, -1), ), @@ -159,6 +168,7 @@ iris_code_version="v2.1", ), -1, + True, 0.45, (1, 0), ), @@ -174,6 +184,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0.7703179042939132, -1), ), @@ -189,6 +200,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0.6349365679618759, 0), ), @@ -204,6 +216,7 @@ iris_code_version="v2.1", ), 1, + True, 0.45, (0.48484617125293383, -1), ), @@ -228,16 +241,17 @@ def test_hamming_distance( template_probe: IrisTemplate, template_gallery: IrisTemplate, rotation_shift: int, + normalise: bool, nm_dist: float, expected_result: Tuple[float, ...], ) -> None: - result = hamming_distance(template_probe, template_gallery, rotation_shift, nm_dist) + result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist) assert math.isclose(result[0], expected_result[0], rel_tol=1e-05, abs_tol=1e-05) assert result[1] == expected_result[1] @pytest.mark.parametrize( - "template_probe, template_gallery, rotation_shift, nm_dist, weights, expected_result", + "template_probe, template_gallery, rotation_shift, normalise, nm_dist, weights, expected_result", [ ( IrisTemplate( @@ -251,6 +265,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + False, None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0, 0), @@ -267,6 +282,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + False, None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.14285714285714285, -1), @@ -283,6 +299,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + False, None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), @@ -299,6 +316,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + False, None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8888888888888888, -1), @@ -315,6 +333,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 0, + False, None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), @@ -331,6 +350,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0, 0), @@ -347,6 +367,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.22967674843904062, -1), @@ -363,6 +384,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8512211593818028, 0), @@ -379,6 +401,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), 1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8020834362167217, -1), @@ -395,6 +418,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), -1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), @@ -423,6 +447,7 @@ def test_hamming_distance( iris_code_version="v2.1", ), -1, + True, 0.45, [np.array([[3, 1], [1, 2]]), np.array([[3, 1, 4, 2], [1, 2, 5, 4]])], (1, 0), @@ -446,11 +471,12 @@ def test_hamming_distance_with_weights( template_probe: IrisTemplate, template_gallery: IrisTemplate, rotation_shift: int, + normalise: bool, nm_dist: float, weights: np.ndarray, expected_result: Tuple[float, ...], ) -> None: - result = hamming_distance(template_probe, template_gallery, rotation_shift, nm_dist, weights) + result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist, weights=weights) assert math.isclose(result[0], expected_result[0], rel_tol=1e-05, abs_tol=1e-05) assert result[1] == expected_result[1] From 9a002be8b4fab8cfaad13f3fbcd3394d4073421c Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Fri, 5 Jul 2024 11:22:52 +0000 Subject: [PATCH 09/11] Extend matcher unit tests --- .../nodes/matcher/test_matcher_utils.py | 137 ++++++++++++++++-- 1 file changed, 128 insertions(+), 9 deletions(-) diff --git a/tests/unit_tests/nodes/matcher/test_matcher_utils.py b/tests/unit_tests/nodes/matcher/test_matcher_utils.py index 3768377..ae56ccd 100644 --- a/tests/unit_tests/nodes/matcher/test_matcher_utils.py +++ b/tests/unit_tests/nodes/matcher/test_matcher_utils.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( - "template_probe, template_gallery, rotation_shift, normalise, nm_dist, expected_result", + "template_probe, template_gallery, rotation_shift, normalise, nm_dist, nm_type, expected_result", [ ( IrisTemplate( @@ -26,6 +26,7 @@ 1, False, None, + None, (0, 0), ), ( @@ -42,6 +43,7 @@ 1, False, None, + None, (0.25, -1), ), ( @@ -58,6 +60,7 @@ 1, False, None, + None, (1, 0), ), ( @@ -74,6 +77,7 @@ 1, False, None, + None, (0.8, -1), ), ( @@ -90,6 +94,7 @@ 0, False, None, + None, (1, 0), ), ( @@ -106,6 +111,7 @@ 1, True, 0.45, + "sqrt", (0, 0), ), ( @@ -122,6 +128,7 @@ 1, True, 0.45, + "sqrt", (0.2867006838144548, -1), ), ( @@ -138,6 +145,7 @@ 1, True, 0.45, + "sqrt", (1, 0), ), ( @@ -154,8 +162,26 @@ 1, True, 0.45, + "sqrt", (0.7703179042939132, -1), ), + ( + IrisTemplate( + iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, False], [True, True]])], + iris_code_version="v2.1", + ), + IrisTemplate( + iris_codes=[np.array([[False, False], [True, False]]), np.array([[False, True], [False, False]])], + mask_codes=[np.array([[True, False], [False, True]]), np.array([[True, True], [True, True]])], + iris_code_version="v2.1", + ), + 1, + True, + 0.45, + "linear", + (0.7645670365077234, -1), + ), ( IrisTemplate( iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], @@ -170,6 +196,7 @@ -1, True, 0.45, + "sqrt", (1, 0), ), ( @@ -186,8 +213,26 @@ 1, True, 0.45, + "sqrt", (0.7703179042939132, -1), ), + ( + IrisTemplate( + iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, False], [True, True]])], + iris_code_version="v2.1", + ), + IrisTemplate( + iris_codes=[np.array([[False, False], [True, False]]), np.array([[False, True], [False, False]])], + mask_codes=[np.array([[True, False], [False, True]]), np.array([[True, True], [True, True]])], + iris_code_version="v2.1", + ), + 1, + True, + 0.45, + "linear", + (0.7645670365077234, -1), + ), ( IrisTemplate( iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], @@ -202,6 +247,7 @@ 1, True, 0.45, + "sqrt", (0.6349365679618759, 0), ), ( @@ -218,6 +264,7 @@ 1, True, 0.45, + "sqrt", (0.48484617125293383, -1), ), ], @@ -230,9 +277,11 @@ "genuine0_norm", "genuine1_norm", "imposter0_norm", - "impostor1_norm", + "impostor1_norm with sqrt nm_type ", + "impostor1_norm with linear nm_type ", "impostor2_norm", - "impostor3_norm", + "impostor3_norm with sqrt type", + "impostor3_norm with linear type", "impostor4_lowerhalfnoinfo_norm", "impostor5_upperhalfnoinfo_norm", ], @@ -243,15 +292,16 @@ def test_hamming_distance( rotation_shift: int, normalise: bool, nm_dist: float, + nm_type: str, expected_result: Tuple[float, ...], ) -> None: - result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist) + result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist, nm_type) assert math.isclose(result[0], expected_result[0], rel_tol=1e-05, abs_tol=1e-05) assert result[1] == expected_result[1] @pytest.mark.parametrize( - "template_probe, template_gallery, rotation_shift, normalise, nm_dist, weights, expected_result", + "template_probe, template_gallery, rotation_shift, normalise, nm_dist, nm_type, weights, expected_result", [ ( IrisTemplate( @@ -267,6 +317,7 @@ def test_hamming_distance( 1, False, None, + None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0, 0), ), @@ -284,6 +335,7 @@ def test_hamming_distance( 1, False, None, + None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.14285714285714285, -1), ), @@ -301,6 +353,7 @@ def test_hamming_distance( 1, False, None, + None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), ), @@ -318,6 +371,7 @@ def test_hamming_distance( 1, False, None, + None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8888888888888888, -1), ), @@ -335,6 +389,7 @@ def test_hamming_distance( 0, False, None, + None, [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), ), @@ -352,6 +407,7 @@ def test_hamming_distance( 1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0, 0), ), @@ -369,9 +425,28 @@ def test_hamming_distance( 1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.22967674843904062, -1), ), + ( + IrisTemplate( + iris_codes=[np.array([[True, True], [False, True]]), np.array([[True, True], [True, False]])], + mask_codes=[np.array([[True, False], [True, False]]), np.array([[False, True], [False, True]])], + iris_code_version="v2.1", + ), + IrisTemplate( + iris_codes=[np.array([[True, True], [True, False]]), np.array([[True, True], [True, True]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, False], [True, True]])], + iris_code_version="v2.1", + ), + 1, + True, + 0.45, + "linear", + [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], + (0.23281720292127467, -1), + ), ( IrisTemplate( iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], @@ -386,9 +461,28 @@ def test_hamming_distance( 1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8512211593818028, 0), ), + ( + IrisTemplate( + iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + iris_code_version="v2.1", + ), + IrisTemplate( + iris_codes=[np.array([[False, False], [False, True]]), np.array([[False, False], [False, False]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + iris_code_version="v2.1", + ), + 1, + True, + 0.45, + "linear", + [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], + (0.8571428571428571, 0), + ), ( IrisTemplate( iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], @@ -403,9 +497,28 @@ def test_hamming_distance( 1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (0.8020834362167217, -1), ), + ( + IrisTemplate( + iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], + mask_codes=[np.array([[True, True], [True, True]]), np.array([[True, False], [True, True]])], + iris_code_version="v2.1", + ), + IrisTemplate( + iris_codes=[np.array([[False, False], [True, False]]), np.array([[False, True], [False, False]])], + mask_codes=[np.array([[True, False], [False, True]]), np.array([[True, True], [True, True]])], + iris_code_version="v2.1", + ), + 1, + True, + 0.45, + "linear", + [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], + (0.8011224867405204, -1), + ), ( IrisTemplate( iris_codes=[np.array([[True, True], [True, True]]), np.array([[True, True], [True, True]])], @@ -420,6 +533,7 @@ def test_hamming_distance( -1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1], [1, 2]])], (1, 0), ), @@ -449,6 +563,7 @@ def test_hamming_distance( -1, True, 0.45, + "sqrt", [np.array([[3, 1], [1, 2]]), np.array([[3, 1, 4, 2], [1, 2, 5, 4]])], (1, 0), ), @@ -461,9 +576,12 @@ def test_hamming_distance( "impostor2", "genuine0_norm", "genuine1_norm", - "imposter0_norm", - "impostor1_norm", - "impostor2_norm", + "imposter0_norm with nm_type sqrt", + "imposter0_norm with nm_type linear", + "impostor1_norm with nm_type sqrt", + "impostor1_norm with nm_type linear", + "impostor2_norm with nm_type sqrt", + "impostor2_norm with nm_type linear", "impostor3_norm different size", ], ) @@ -473,10 +591,11 @@ def test_hamming_distance_with_weights( rotation_shift: int, normalise: bool, nm_dist: float, + nm_type: str, weights: np.ndarray, expected_result: Tuple[float, ...], ) -> None: - result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist, weights=weights) + result = hamming_distance(template_probe, template_gallery, rotation_shift, normalise, nm_dist, nm_type, weights) assert math.isclose(result[0], expected_result[0], rel_tol=1e-05, abs_tol=1e-05) assert result[1] == expected_result[1] From cbfa80a10eb90cab62a3f6c0f52f890a296774a2 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Fri, 5 Jul 2024 11:23:43 +0000 Subject: [PATCH 10/11] Fix HD score normalisation bug --- src/iris/nodes/matcher/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/iris/nodes/matcher/utils.py b/src/iris/nodes/matcher/utils.py index a337477..87784e6 100644 --- a/src/iris/nodes/matcher/utils.py +++ b/src/iris/nodes/matcher/utils.py @@ -71,9 +71,7 @@ def normalized_HD(irisbitcount: int, maskbitcount: int, sqrt_totalbitcount: floa Returns: float: normalized Hamming distance. """ - norm_HD = max( - 0, nm_dist - (nm_dist - irisbitcount / maskbitcount) * min(1.0, np.sqrt(maskbitcount) / sqrt_totalbitcount) - ) + norm_HD = max(0, nm_dist - (nm_dist - irisbitcount / maskbitcount) * np.sqrt(maskbitcount) / sqrt_totalbitcount) return norm_HD From 919359f62318897d6b27bb24c3a84588171bba27 Mon Sep 17 00:00:00 2001 From: TanguyJeanneau Date: Fri, 5 Jul 2024 16:46:50 +0000 Subject: [PATCH 11/11] Bump version to 1.1.1 --- colab/ConfiguringCustomPipeline.ipynb | 10 +++++----- docs/source/examples/custom_pipeline.rst | 10 +++++----- src/iris/_version.py | 2 +- src/iris/pipelines/confs/pipeline.yaml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/colab/ConfiguringCustomPipeline.ipynb b/colab/ConfiguringCustomPipeline.ipynb index c22d3b9..4196edb 100644 --- a/colab/ConfiguringCustomPipeline.ipynb +++ b/colab/ConfiguringCustomPipeline.ipynb @@ -125,7 +125,7 @@ "```yaml\n", "metadata:\n", " pipeline_name: iris_pipeline\n", - " iris_version: 1.1.0\n", + " iris_version: 1.1.1\n", "```\n", "\n", "The top YAML file contains `IRISPipeline` metadata, used to both describe `IRISPipeline` and specify package parameters that are later used to verify compatibility between `iris` package version/release and later, specified in the `pipeline` YAML file section, pipeline's graph.\n", @@ -205,7 +205,7 @@ "outputs": [], "source": [ "default_pipeline_conf = {\n", - " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.0\"},\n", + " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.1\"},\n", " \"pipeline\": [\n", " {\n", " \"name\": \"segmentation\",\n", @@ -480,7 +480,7 @@ "outputs": [], "source": [ "new_pipeline_conf = {\n", - " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.0\"},\n", + " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.1\"},\n", " \"pipeline\": [\n", " {\n", " \"name\": \"segmentation\",\n", @@ -759,7 +759,7 @@ "outputs": [], "source": [ "default_pipeline_conf = {\n", - " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.0\"},\n", + " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.1\"},\n", " \"pipeline\": [\n", " {\n", " \"name\": \"segmentation\",\n", @@ -1034,7 +1034,7 @@ "outputs": [], "source": [ "new_pipeline_conf = {\n", - " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.0\"},\n", + " \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.1.1\"},\n", " \"pipeline\": [\n", " {\n", " \"name\": \"segmentation\",\n", diff --git a/docs/source/examples/custom_pipeline.rst b/docs/source/examples/custom_pipeline.rst index d82d383..f685b26 100644 --- a/docs/source/examples/custom_pipeline.rst +++ b/docs/source/examples/custom_pipeline.rst @@ -18,7 +18,7 @@ When the ``IRISPipeline`` pipeline is created with default parameters, it's grap metadata: pipeline_name: iris_pipeline - iris_version: 1.1.0 + iris_version: 1.1.1 The top YAML file contains ``IRISPipeline`` metadata, used to both describe ``IRISPipeline`` and specify package parameters that are later used to verify compatibility between ``iris`` package version/release and later, specified in the ``pipeline`` YAML file section, pipeline's graph. @@ -93,7 +93,7 @@ First let's intantiate ``IRISPipeline`` with default configuration and see ``iri .. code-block:: python default_pipeline_conf = { - "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.0"}, + "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.1"}, "pipeline": [ { "name": "segmentation", @@ -320,7 +320,7 @@ As expected all threshold values are set to default ``0.5`` value. Now, let's mo .. code-block:: python new_pipeline_conf = { - "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.0"}, + "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.1"}, "pipeline": [ { "name": "segmentation", @@ -552,7 +552,7 @@ First let's instantiate ``IRISPipeline`` with default configuration and see node .. code-block:: python default_pipeline_conf = { - "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.0"}, + "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.1"}, "pipeline": [ { "name": "segmentation", @@ -783,7 +783,7 @@ As expected, ``input_polygons`` argument of the ``run`` method is taken from the .. code-block:: python new_pipeline_conf = { - "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.0"}, + "metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.1.1"}, "pipeline": [ { "name": "segmentation", diff --git a/src/iris/_version.py b/src/iris/_version.py index 6849410..a82b376 100644 --- a/src/iris/_version.py +++ b/src/iris/_version.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" diff --git a/src/iris/pipelines/confs/pipeline.yaml b/src/iris/pipelines/confs/pipeline.yaml index b66f5f1..c26d803 100644 --- a/src/iris/pipelines/confs/pipeline.yaml +++ b/src/iris/pipelines/confs/pipeline.yaml @@ -1,6 +1,6 @@ metadata: pipeline_name: iris_pipeline - iris_version: 1.1.0 + iris_version: 1.1.1 pipeline: - name: segmentation