Coverage for src/bob/bio/base/algorithm/distance.py: 61%
51 statements
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-14 21:41 +0100
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-14 21:41 +0100
1import numpy as np
3from scipy.spatial.distance import cdist
5from ..pipelines import BioAlgorithm
8class Distance(BioAlgorithm):
9 """A distance algorithm to compare feature vectors.
10 Many biometric algorithms are based on comparing feature vectors that
11 are usually extracted by using deep neural networks.
12 The most common distance function is the cosine similarity, which is
13 the default in this class.
14 """
16 def __init__(
17 self,
18 distance_function="cosine",
19 factor=-1,
20 average_on_enroll=True,
21 average_probes=False,
22 probes_score_fusion="max",
23 enrolls_score_fusion="max",
24 **kwargs,
25 ):
26 """
27 Parameters
28 ----------
29 distance_function : str or :py:class:`function`, optional
30 function to be used to measure the distance of probe and model
31 features compatible with :any:`scipy.spatial.distance.cdist`. If the
32 function exists in scipy.spatial.distance, provide its string name
33 as scipy will run an optimized version.
35 factor : float
36 A coefficient which is multiplied to distance (after
37 distance_function) to find score between probe and model features.
38 In bob.bio.base, the scores should be similarity scores (higher
39 score for a genuine pair) so use this factor to make sure you are
40 using similarity scores.
42 average_on_enroll : bool
43 Some database protocols contain multiple samples (e.g. face images)
44 to create one enrollment template. This option is useful in case of
45 those databases. If True, the algorithm will average the enroll
46 features to create a single template. If False, the algorithm will
47 use the enroll features as is and will compare the probe template
48 against all features. The final score will be computed based on the
49 ``enrolls_score_fusion`` option.
51 average_probes : bool
52 Some database protocols contain multiple samples (e.g. face images)
53 to create one probe template. This option is useful in case of those
54 databases. If True, the algorithm will average the probe features to
55 create a single template. If False, the algorithm will use the probe
56 features as is and will compare the enrollment template against all
57 features. The final score will be computed based on the
58 ``probes_score_fusion`` option.
60 probes_score_fusion : str
61 How to fuse the scores of the probes if average_probes is False and
62 the database contains multiple probe samples.
64 enrolls_score_fusion : str
65 How to fuse the scores of the enrolls if average_on_enroll is False
66 and the database contains multiple enroll samples.
67 """
68 super().__init__(
69 probes_score_fusion=probes_score_fusion,
70 enrolls_score_fusion=enrolls_score_fusion,
71 **kwargs,
72 )
73 self.distance_function = distance_function
74 self.factor = factor
75 self.average_on_enroll = average_on_enroll
76 self.average_probes = average_probes
78 def create_templates(self, list_of_feature_sets, enroll):
79 """Creates templates from the given feature sets.
80 Will make sure the features are 2 dimensional before creating templates.
81 Will average features over samples if ``average_on_enroll`` is True or
82 ``average_probes`` is True.
83 """
84 list_of_feature_sets = [
85 self._make_2d(data) for data in list_of_feature_sets
86 ]
87 # shape of list_of_feature_sets is Nx?xD
88 if (enroll and self.average_on_enroll) or (
89 not enroll and self.average_probes
90 ):
91 # we cannot call np.mean(list_of_feature_sets, axis=1) because the size of
92 # axis 1 is diffent for each feature set.
93 # output will be NxD
94 return np.array(
95 [np.mean(feat, axis=0) for feat in list_of_feature_sets]
96 )
97 # output shape is Nx?xD
98 return list_of_feature_sets
100 def _make_2d(self, X):
101 """Makes sure that the features are 2 dimensional before creating enroll
102 and probe templates.
104 For instance, when the source is `VideoLikeContainer` the input of
105 ``create_templates`` is [`VideoLikeContainer`, ....]. The concatenation
106 of them makes and array of `ZxNxD`. Hence we need to stack them in `Z`.
107 """
108 if not len(X):
109 return [[]]
110 if X[0].ndim == 2:
111 X = np.vstack(X)
112 return np.atleast_2d(X)
114 def compare(self, enroll_templates, probe_templates):
115 """Compares the probe templates to the enroll templates.
117 Depending on the ``average_on_enroll`` and ``average_probes`` options,
118 the templates have different shapes.
119 """
120 # returns scores NxM where N is the number of enroll templates and M is
121 # the number of probe templates
122 if self.average_on_enroll and self.average_probes:
123 # enroll_templates is NxD
124 enroll_templates = np.asarray(enroll_templates)
125 # probe_templates is MxD
126 probe_templates = np.asarray(probe_templates)
127 return self.factor * cdist(
128 enroll_templates, probe_templates, self.distance_function
129 )
130 elif self.average_on_enroll:
131 # enroll_templates is NxD
132 enroll_templates = np.asarray(enroll_templates)
133 # probe_templates is Mx?xD
134 scores = []
135 for probe in probe_templates:
136 s = self.factor * cdist(
137 enroll_templates, probe, self.distance_function
138 )
139 # s is Nx?, we want s to be N
140 s = self.fuse_probe_scores(s, axis=1)
141 scores.append(s)
142 return np.array(scores).T
143 elif self.average_probes:
144 # enroll_templates is Nx?xD
145 # probe_templates is MxD
146 probe_templates = np.asarray(probe_templates)
147 scores = []
148 for enroll in enroll_templates:
149 s = self.factor * cdist(
150 enroll, probe_templates, self.distance_function
151 )
152 # s is ?xM, we want s to be M
153 s = self.fuse_enroll_scores(s, axis=0)
154 scores.append(s)
155 return np.array(scores)
156 else:
157 # enroll_templates is Nx?1xD
158 # probe_templates is Mx?2xD
159 scores = []
160 for enroll in enroll_templates:
161 scores.append([])
162 for probe in probe_templates:
163 s = self.factor * cdist(
164 enroll, probe, self.distance_function
165 )
166 # s is ?1x?2, we want s to be scalar
167 s = self.fuse_probe_scores(s, axis=1)
168 s = self.fuse_enroll_scores(s, axis=0)
169 scores[-1].append(s)
170 return np.array(scores)