"""
Iris Template Combiner
This module implements a hybrid approach for combining multiple iris templates
from the same user to enhance recognition performance.
The algorithm combines:
1. Selective Bits Fusion - Using the most reliable bits
2. Majority Voting with Weight Templates - For consistent bits
3. Inconsistent Bit Analysis - To utilize the pattern of inconsistent bits
Author: Rostyslav Shevchenko
Date: May 22, 2025
"""
from typing import List, Tuple
import numpy as np
from pydantic import Field
from iris.callbacks.callback_interface import Callback
from iris.io.class_configs import Algorithm
from iris.io.dataclasses import IrisTemplate
[docs]
class MajorityVoteAggregation(Algorithm):
"""
Class for combining multiple iris templates from the same user.
The algorithm identifies consistent and inconsistent bits across templates,
uses majority voting for consistent bits, creates a weight template to
reflect bit reliability, and generates a combined mask representing
consensus of valid regions.
"""
[docs]
class Parameters(Algorithm.Parameters):
consistency_threshold: float = Field(default=0.75, ge=0.0, le=1.0)
mask_threshold: float = Field(default=0.01, ge=0.0, le=1.0)
use_inconsistent_bits: bool = Field(default=True)
inconsistent_bit_threshold: float = Field(default=0.4, ge=0.0, le=1.0)
__parameters_type__ = Parameters
def __init__(
self,
consistency_threshold: float = 0.75,
mask_threshold: float = 0.01,
use_inconsistent_bits: bool = True,
inconsistent_bit_threshold: float = 0.4,
callbacks: List[Callback] = [],
):
"""
Initialize the IrisTemplateCombiner.
Args:
consistency_threshold (float): Threshold for considering a bit consistent across templates
mask_threshold (float): Threshold for considering a mask bit valid in the combined template
use_inconsistent_bits (bool): Whether to use inconsistent bits information
inconsistent_bit_threshold (float): Threshold for identifying inconsistent bits
"""
super().__init__(
consistency_threshold=consistency_threshold,
mask_threshold=mask_threshold,
use_inconsistent_bits=use_inconsistent_bits,
inconsistent_bit_threshold=inconsistent_bit_threshold,
callbacks=callbacks,
)
[docs]
def run(self, templates: List[IrisTemplate]) -> Tuple[IrisTemplate, np.ndarray]:
"""
Combine multiple iris templates from the same user.
Args:
templates (List[IrisTemplate]): List of IrisTemplate objects from the same user
Returns:
combined_template (IrisTemplate): Combined IrisTemplate
weights (np.ndarray): Weight matrix reflecting bit reliability
"""
return self.combine_templates(templates)
[docs]
def combine_templates(self, templates: List[IrisTemplate]) -> Tuple[IrisTemplate, np.ndarray]:
"""
Combine multiple iris templates from the same user.
Args:
templates (List[IrisTemplate]): List of IrisTemplate objects from the same user
Returns:
combined_template (IrisTemplate): Combined IrisTemplate
weights (np.ndarray): Weight matrix reflecting bit reliability
"""
if not templates:
raise ValueError("No templates provided for combination")
if len(templates) == 1:
# If only one template, return it with uniform weights
weights = [np.ones_like(code) for code in templates[0].iris_codes]
return templates[0], weights
# Get the number of wavelets (filter responses)
num_wavelets = len(templates[0].iris_codes)
# Initialize lists for combined iris codes and mask codes
combined_iris_codes = []
combined_mask_codes = []
weights = []
# Process each wavelet separately
for wavelet_idx in range(num_wavelets):
# Extract iris codes and mask codes for this wavelet from all templates
iris_codes_wavelet = [template.iris_codes[wavelet_idx] for template in templates]
mask_codes_wavelet = [template.mask_codes[wavelet_idx] for template in templates]
# Combine iris codes and mask codes for this wavelet
combined_iris_code, combined_mask_code, weight = self._combine_wavelet_codes(
iris_codes_wavelet, mask_codes_wavelet
)
combined_iris_codes.append(combined_iris_code)
combined_mask_codes.append(combined_mask_code)
weights.append(weight)
# Create combined template
combined_template = IrisTemplate(
iris_codes=combined_iris_codes,
mask_codes=combined_mask_codes,
iris_code_version=templates[0].iris_code_version,
)
return combined_template, weights
def _combine_wavelet_codes(
self, iris_codes: List[np.ndarray], mask_codes: List[np.ndarray]
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Combine iris codes and mask codes for a single wavelet.
Args:
iris_codes (List[np.ndarray]): List of iris codes for a single wavelet from multiple templates
mask_codes (List[np.ndarray]): List of mask codes for a single wavelet from multiple templates
Returns:
combined_iris_code (np.ndarray): Combined iris code for this wavelet
combined_mask_code (np.ndarray): Combined mask code for this wavelet
weight (np.ndarray): Weight matrix reflecting bit reliability
"""
num_templates = len(iris_codes)
shape = iris_codes[0].shape
# Initialize arrays for counting votes and valid masks
vote_counts = np.zeros(shape, dtype=float)
valid_mask_counts = np.zeros(shape, dtype=float)
# Count votes and valid masks
for i in range(num_templates):
# Count votes for iris code bits (1s)
vote_counts += iris_codes[i] * mask_codes[i]
# Count valid mask bits
valid_mask_counts += mask_codes[i]
# Calculate the fraction of votes for each bit
vote_fractions = vote_counts / np.maximum(valid_mask_counts, 1)
# Fraction of templates in which the bit is valid
valid_mask_fraction = valid_mask_counts / num_templates
# Calculate consistency (how far from 0.5 the vote fraction is)
# Values close to 0 or 1 are more consistent, values close to 0.5 are less consistent
consistency = np.abs(vote_fractions - 0.5) * 2 # Scale to [0, 1]
# Create combined iris code using majority voting
combined_iris_code = (vote_fractions > 0.5).astype(bool)
# Create combined mask code
# A bit is considered valid if enough templates have it as valid
combined_mask_code = ((valid_mask_counts / num_templates) >= self.params.mask_threshold).astype(bool)
# Create weight matrix based on consistency
# More consistent bits get higher weights
weight = np.where(
consistency >= self.params.consistency_threshold,
consistency,
self.params.inconsistent_bit_threshold if self.params.use_inconsistent_bits else 0,
)
# take into account the fraction of templates in which the bit is valid
weight = weight * valid_mask_fraction
return combined_iris_code, combined_mask_code, weight