Coverage for src/bob/fusion/base/script/boundary.py: 92%

92 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-14 22:15 +0100

1"""Plots the decision boundaries of fusion algorithms. 

2""" 

3import logging 

4 

5import click 

6import numpy as np 

7 

8from clapper.click import verbosity_option 

9 

10from bob.bio.base.score import load_score 

11 

12from ..algorithm import Algorithm 

13from ..tools import ( 

14 check_consistency, 

15 get_gza_from_lines_list, 

16 get_scores, 

17 grouping, 

18 remove_nan, 

19) 

20 

21logger = logging.getLogger(__name__) 

22 

23 

24def plot_boundary_decision( 

25 algorithm, 

26 scores, 

27 score_labels, 

28 threshold, 

29 thres_system1=None, 

30 thres_system2=None, 

31 do_grouping=False, 

32 resolution=2000, 

33 alpha=0.75, 

34 legends=None, 

35 i1=0, 

36 i2=1, 

37 x_label=None, 

38 y_label=None, 

39 **kwargs, 

40): 

41 if legends is None: 

42 legends = ["Zero Effort Impostor", "Presentation Attack", "Genuine"] 

43 markers = ["x", "o", "s"] 

44 

45 if scores.shape[1] > 2: 

46 raise NotImplementedError( 

47 "Currently plotting the decision boundary for more than two " 

48 "systems is not supported." 

49 ) 

50 

51 import matplotlib 

52 import matplotlib.pyplot as plt 

53 

54 plt.gca() # this is necessary for subplots to work. 

55 

56 X = scores[:, [i1, i2]] 

57 Y = score_labels 

58 x_pad = (X[:, i1].max() - X[:, i1].min()) * 0.1 

59 y_pad = (X[:, i2].max() - X[:, i2].min()) * 0.1 

60 x_min, x_max = X[:, i1].min() - x_pad, X[:, i1].max() + x_pad 

61 y_min, y_max = X[:, i2].min() - y_pad, X[:, i2].max() + y_pad 

62 xx, yy = np.meshgrid( 

63 np.linspace(x_min, x_max, resolution), 

64 np.linspace(y_min, y_max, resolution), 

65 ) 

66 

67 contourf = None 

68 if algorithm is not None: 

69 temp = np.c_[xx.ravel(), yy.ravel()] 

70 temp = algorithm.preprocess(temp) 

71 Z = (algorithm.fuse(temp) > threshold).reshape(xx.shape) 

72 

73 contourf = plt.contour(xx, yy, Z, 1, alpha=1, cmap=plt.cm.gray) 

74 

75 if do_grouping: 

76 gen = grouping(X[Y == 0, :], **kwargs) 

77 zei = grouping(X[Y == 1, :], **kwargs) 

78 atk = grouping(X[Y == 2, :], **kwargs) 

79 else: 

80 gen = X[Y == 0, :] 

81 zei = X[Y == 1, :] 

82 atk = X[Y == 2, :] 

83 for i, (X, color) in enumerate(((zei, "C0"), (atk, "C1"), (gen, "C2"))): 

84 if X.size == 0: 

85 continue 

86 try: 

87 plt.scatter( 

88 X[:, 0], 

89 X[:, 1], 

90 marker=markers[i], 

91 alpha=alpha, 

92 c=color, 

93 label=legends[i], 

94 ) 

95 except Exception as e: 

96 raise RuntimeError( 

97 f"matplotlib backend: {matplotlib.get_backend()}" 

98 ) from e 

99 

100 plt.legend( 

101 bbox_to_anchor=(-0.05, 1.02, 1.05, 0.102), 

102 loc=3, 

103 ncol=3, 

104 mode="expand", 

105 borderaxespad=0.0, 

106 fontsize=14, 

107 ) 

108 

109 if thres_system1 is not None: 

110 plt.axvline(thres_system1, color="red") 

111 plt.axhline(thres_system2, color="red") 

112 

113 plt.xlim([x_min, x_max]) 

114 plt.ylim([y_min, y_max]) 

115 plt.grid(True) 

116 

117 plt.xlabel(x_label) 

118 plt.ylabel(y_label) 

119 

120 return contourf 

121 

122 

123@click.command( 

124 epilog="""\b 

125Examples: 

126$ bob fusion boundary -vvv {sys1,sys2}/scores-eval -m /path/to/Model.pkl 

127""" 

128) 

129@click.argument("scores", nargs=-1, required=True, type=click.Path(exists=True)) 

130@click.option( 

131 "-m", 

132 "--model-file", 

133 required=False, 

134 help="The path to where the algorithm will be loaded from.", 

135) 

136@click.option( 

137 "-t", 

138 "--threshold", 

139 type=click.FLOAT, 

140 required=False, 

141 help="The threshold to classify scores after fusion. Usually " 

142 "calculated from fused development set.", 

143) 

144@click.option( 

145 "-g", 

146 "--group", 

147 type=click.INT, 

148 default=0, 

149 show_default=True, 

150 help="If given scores will be grouped into N samples.", 

151) 

152@click.option( 

153 "-G", 

154 "--grouping", 

155 type=click.Choice(("random", "kmeans")), 

156 default="kmeans", 

157 show_default=True, 

158 help="The gouping algorithm to be used.", 

159) 

160@click.option( 

161 "-o", 

162 "--output", 

163 default="scatter.pdf", 

164 show_default=True, 

165 type=click.Path(writable=True), 

166 help="The path to the saved plot.", 

167) 

168@click.option( 

169 "-X", 

170 "--x-label", 

171 default="Recognition scores", 

172 show_default=True, 

173 help="The label for the first system.", 

174) 

175@click.option( 

176 "-Y", 

177 "--y-label", 

178 default="PAD scores", 

179 show_default=True, 

180 help="The label for the second system.", 

181) 

182@click.option( 

183 "--skip-check", 

184 is_flag=True, 

185 show_default=True, 

186 help="If True, it will skip checking for the consistency " 

187 "between scores.", 

188) 

189@verbosity_option(logger) 

190def boundary( 

191 scores, 

192 model_file, 

193 threshold, 

194 group, 

195 grouping, 

196 output, 

197 x_label, 

198 y_label, 

199 skip_check, 

200 **kwargs, 

201): 

202 """Plots the decision boundaries of fusion algorithms. 

203 

204 The script takes several scores (usually eval scores) from different 

205 biometric and pad systems and a trained algorithm and plots the decision 

206 boundary. 

207 

208 You need to provide two score files from two systems. System 1 will be 

209 plotted on the x-axis. 

210 """ 

211 # load the algorithm 

212 algorithm = None 

213 if model_file: 

214 algorithm = Algorithm().load(model_file) 

215 assert ( 

216 threshold is not None 

217 ), "threshold must be provided with the model" 

218 

219 # load the scores 

220 score_lines_list_eval = [load_score(path) for path in scores] 

221 

222 # genuine, zero effort impostor, and attack list 

223 idx1, gen_le, zei_le, atk_le = get_gza_from_lines_list( 

224 score_lines_list_eval 

225 ) 

226 

227 # check if score lines are consistent 

228 if not skip_check: 

229 check_consistency(gen_le, zei_le, atk_le) 

230 

231 # concatenate the scores and create the labels 

232 scores = get_scores(gen_le, zei_le, atk_le) 

233 score_labels = np.zeros((scores.shape[0],)) 

234 gensize = gen_le[0].shape[0] 

235 zeisize = zei_le[0].shape[0] 

236 score_labels[:gensize] = 0 

237 score_labels[gensize : gensize + zeisize] = 1 

238 score_labels[gensize + zeisize :] = 2 

239 found_nan, nan_idx, scores = remove_nan(scores, False) 

240 score_labels = score_labels[~nan_idx] 

241 

242 if found_nan: 

243 logger.warn("{} nan values were removed.".format(np.sum(nan_idx))) 

244 

245 # plot the decision boundary 

246 do_grouping = True 

247 if group < 1: 

248 do_grouping = False 

249 

250 import matplotlib.pyplot as plt 

251 

252 plot_boundary_decision( 

253 algorithm, 

254 scores, 

255 score_labels, 

256 threshold, 

257 do_grouping=do_grouping, 

258 npoints=group, 

259 seed=0, 

260 gformat=grouping, 

261 x_label=x_label, 

262 y_label=y_label, 

263 ) 

264 plt.savefig(output, transparent=True) 

265 plt.close()