Coverage for src/bob/bio/base/algorithm/distance.py: 61%

51 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-12 22:34 +0200

1import numpy as np 

2 

3from scipy.spatial.distance import cdist 

4 

5from ..pipelines import BioAlgorithm 

6 

7 

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 """ 

15 

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. 

34 

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. 

41 

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. 

50 

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. 

59 

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. 

63 

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 

77 

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 

99 

100 def _make_2d(self, X): 

101 """Makes sure that the features are 2 dimensional before creating enroll 

102 and probe templates. 

103 

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) 

113 

114 def compare(self, enroll_templates, probe_templates): 

115 """Compares the probe templates to the enroll templates. 

116 

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)