Coverage for src/bob/bio/base/script/vuln_commands.py: 87%

173 statements  

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

1"""The click-based vulnerability analysis commands. 

2""" 

3 

4import csv 

5import functools 

6import logging 

7import os 

8 

9import click 

10 

11from clapper.click import verbosity_option 

12from click.types import FLOAT 

13from numpy import random 

14 

15from bob.bio.base.score.load import split_csv_vuln 

16from bob.io.base import create_directories_safe 

17from bob.measure.script import common_options 

18 

19from . import vuln_figure as figure 

20 

21logger = logging.getLogger(__name__) 

22 

23 

24def vuln_plot_options( 

25 docstring, 

26 plot_output_default="vuln_plot.pdf", 

27 legend_loc_default="best", 

28 axes_lim_default=None, 

29 figsize_default="4,3", 

30 force_eval=False, 

31 x_label_rotation_default=0, 

32): 

33 def custom_options_command(func): 

34 func.__doc__ = docstring 

35 

36 def eval_if_not_forced(force_eval): 

37 def decorator(f): 

38 if not force_eval: 

39 return common_options.eval_option()( 

40 common_options.sep_dev_eval_option()( 

41 common_options.hide_dev_option()(f) 

42 ) 

43 ) 

44 else: 

45 return f 

46 

47 return decorator 

48 

49 @click.command() 

50 @common_options.scores_argument( 

51 min_arg=1, force_eval=force_eval, nargs=-1 

52 ) 

53 @eval_if_not_forced(force_eval) 

54 @common_options.legends_option() 

55 @common_options.no_legend_option() 

56 @common_options.legend_ncols_option() 

57 @common_options.legend_loc_option(dflt=legend_loc_default) 

58 @common_options.output_plot_file_option(default_out=plot_output_default) 

59 @common_options.lines_at_option(dflt=" ") 

60 @common_options.axes_val_option(dflt=axes_lim_default) 

61 @common_options.x_rotation_option(dflt=x_label_rotation_default) 

62 @common_options.x_label_option() 

63 @common_options.y_label_option() 

64 @common_options.points_curve_option() 

65 @common_options.const_layout_option() 

66 @common_options.figsize_option(dflt=figsize_default) 

67 @common_options.style_option() 

68 @common_options.linestyles_option() 

69 @common_options.alpha_option() 

70 @verbosity_option(logger=logger) 

71 @click.pass_context 

72 @functools.wraps(func) 

73 def wrapper(*args, **kwds): 

74 return func(*args, **kwds) 

75 

76 return wrapper 

77 

78 return custom_options_command 

79 

80 

81def real_data_option(**kwargs): 

82 """Option to choose if input data is real or generated""" 

83 return common_options.bool_option( 

84 name="real-data", 

85 short_name="R", 

86 desc="If False, will annotate the plots hypothetically, instead " 

87 "of with real data values of the calculated error rates.", 

88 dflt=True, 

89 **kwargs, 

90 ) 

91 

92 

93def fnmr_at_option(dflt=" ", **kwargs): 

94 """Get option to draw const FNMR lines""" 

95 return common_options.list_float_option( 

96 name="fnmr", 

97 short_name="fnmr", 

98 desc="If given, draw horizontal lines at the given FNMR position. " 

99 "Your values must be separated with a comma (,) without space. " 

100 "This option works in ROC and DET curves.", 

101 nitems=None, 

102 dflt=dflt, 

103 **kwargs, 

104 ) 

105 

106 

107def gen_score_distr( 

108 mean_gen, 

109 mean_zei, 

110 mean_pa, 

111 sigma_gen=1, 

112 sigma_zei=1, 

113 sigma_pa=1, 

114 num_gen=5000, 

115 num_zei=5000, 

116 num_pa=5000, 

117): 

118 # initialise the random number generator 

119 mt = random.RandomState(0) 

120 

121 genuine_scores = mt.normal(loc=mean_gen, scale=sigma_gen, size=num_gen) 

122 zei_scores = mt.normal(loc=mean_zei, scale=sigma_zei, size=num_zei) 

123 pa_scores = mt.normal(loc=mean_pa, scale=sigma_pa, size=num_pa) 

124 

125 return genuine_scores, zei_scores, pa_scores 

126 

127 

128def write_scores_to_file(neg_licit, pos_licit, spoof, filename): 

129 """Writes score distributions into a CSV score file. For the format of 

130 the score files, please refer to Bob's documentation. 

131 

132 Parameters 

133 ---------- 

134 neg : array_like 

135 Scores for negative samples. 

136 pos : array_like 

137 Scores for positive samples. 

138 filename : str 

139 The path to write the score to. 

140 """ 

141 logger.info(f"Creating score file '{filename}'") 

142 create_directories_safe(os.path.dirname(filename)) 

143 with open(filename, "wt") as f: 

144 csv_writer = csv.writer(f) 

145 # Write the header 

146 csv_writer.writerow( 

147 [ 

148 "bio_ref_subject_id", 

149 "probe_subject_id", 

150 "probe_key", 

151 "probe_attack_type", 

152 "score", 

153 ] 

154 ) 

155 for score in neg_licit: 

156 csv_writer.writerow(["x", "y", "0", None, score]) 

157 for score in pos_licit: 

158 csv_writer.writerow(["x", "x", "0", None, score]) 

159 for score in spoof: 

160 csv_writer.writerow(["x", "y", "0", "pai", score]) 

161 

162 

163@click.command() 

164@click.argument("outdir") 

165@click.option("-mg", "--mean-gen", default=7, type=FLOAT, show_default=True) 

166@click.option("-mz", "--mean-zei", default=3, type=FLOAT, show_default=True) 

167@click.option("-mp", "--mean-pa", default=5, type=FLOAT, show_default=True) 

168@verbosity_option(logger=logger) 

169def gen(outdir, mean_gen, mean_zei, mean_pa, **kwargs): 

170 """Generate random scores. 

171 Generates random scores for three types of verification attempts: 

172 genuine users, zero-effort impostors and spoofing attacks and writes them 

173 into CSV score files for so called licit and spoof scenario. The 

174 scores are generated using Gaussian distribution whose mean is an input 

175 parameter. The generated scores can be used as hypothetical datasets. 

176 """ 

177 # Generate the data 

178 genuine_dev, zei_dev, pa_dev = gen_score_distr(mean_gen, mean_zei, mean_pa) 

179 genuine_eval, zei_eval, pa_eval = gen_score_distr( 

180 mean_gen, mean_zei, mean_pa 

181 ) 

182 

183 # Write the data into files 

184 write_scores_to_file( 

185 zei_dev, genuine_dev, pa_dev, os.path.join(outdir, "scores-dev.csv") 

186 ) 

187 write_scores_to_file( 

188 zei_eval, genuine_eval, pa_eval, os.path.join(outdir, "scores-eval.csv") 

189 ) 

190 

191 

192@common_options.metrics_command( 

193 docstring="""Extracts different statistical values from scores distributions 

194 

195 Prints a table that contains different metrics to assess the performance of a 

196 biometric system against zero-effort impostor and presentation attacks. 

197 

198 The CSV score files must contain an `attack-type` column, in addition to the 

199 "regular" biometric scores columns (`bio_ref_subject_id`, 

200 `probe_subject_id`, and `score`). 

201 

202 Examples: 

203 

204 $ bob vuln metrics -v scores-dev.csv 

205 

206 $ bob vuln metrics -v -e scores-{dev,eval}.csv 

207 """ 

208) 

209@common_options.cost_option() 

210def metrics(ctx, scores, evaluation, **kwargs): 

211 process = figure.Metrics(ctx, scores, evaluation, split_csv_vuln) 

212 process.run() 

213 

214 

215@vuln_plot_options( 

216 docstring="""Plots the ROC for vulnerability analysis 

217 

218 You need to provide 1 or 2 (with `--eval`) score 

219 files for each vulnerability system in this order: 

220 

221 \b 

222 * dev scores 

223 * [eval scores] 

224 

225 The CSV score files must contain an `attack-type` column, in addition to the 

226 "regular" biometric scores columns (`bio_ref_subject_id`, 

227 `probe_subject_id`, and `score`). 

228 

229 Examples: 

230 

231 $ bob vuln roc -v -o roc.pdf scores.csv 

232 

233 $ bob vuln roc -v -e scores-{dev,eval}.csv 

234 """, 

235 plot_output_default="vuln_roc.pdf", 

236) 

237@common_options.title_option() 

238@common_options.min_far_option() 

239@common_options.tpr_option(dflt=True) 

240@common_options.semilogx_option(dflt=True) 

241@fnmr_at_option() 

242@real_data_option() 

243def roc(ctx, scores, evaluation, real_data, **kwargs): 

244 process = figure.RocVuln( 

245 ctx, scores, evaluation, split_csv_vuln, real_data, False 

246 ) 

247 process.run() 

248 

249 

250@vuln_plot_options( 

251 docstring="""Plots the DET for vulnerability analysis 

252 

253 You need to provide 1 or 2 (with `--eval`) score 

254 files for each vulnerability system in this order: 

255 

256 \b 

257 * dev scores 

258 * [eval scores] 

259 

260 The CSV score files must contain an `attack-type` column, in addition to the 

261 "regular" biometric scores columns (`bio_ref_subject_id`, 

262 `probe_subject_id`, and `score`). 

263 

264 See :ref:`bob.bio.base.vulnerability` in the documentation for a guide on 

265 vulnerability analysis. 

266 

267 Examples: 

268 

269 $ bob vuln det -v -o det.pdf scores.csv 

270 

271 $ bob vuln det -v -e scores-{dev,eval}.csv 

272 """, 

273 plot_output_default="vuln_det.pdf", 

274 legend_loc_default="upper-right", 

275 axes_lim_default="0.01,95,0.01,95", 

276 figsize_default="6,4", 

277 x_label_rotation_default=45, 

278) 

279@common_options.title_option() 

280@real_data_option() 

281@fnmr_at_option() 

282def det(ctx, scores, evaluation, real_data, **kwargs): 

283 process = figure.DetVuln( 

284 ctx, scores, evaluation, split_csv_vuln, real_data, False 

285 ) 

286 process.run() 

287 

288 

289@vuln_plot_options( 

290 docstring="""Plots the EPC for vulnerability analysis 

291 

292 You need to provide 2 score files for each vulnerability system in this order: 

293 

294 \b 

295 * dev scores 

296 * eval scores 

297 

298 The CSV score files must contain an `attack-type` column, in addition to the 

299 "regular" biometric scores columns (`bio_ref_subject_id`, 

300 `probe_subject_id`, and `score`). 

301 

302 See :ref:`bob.bio.base.vulnerability` in the documentation for a guide on 

303 vulnerability analysis. 

304 

305 Examples: 

306 

307 $ bob vuln epc -v scores-dev.csv scores-eval.csv 

308 

309 $ bob vuln epc -v -o epc.pdf scores-{dev,eval}.csv 

310 """, 

311 plot_output_default="vuln_epc.pdf", 

312 force_eval=True, 

313 legend_loc_default="upper-center", 

314) 

315@common_options.title_option() 

316@common_options.bool_option( 

317 "iapmr", "I", "Whether to plot the IAPMR related lines or not.", True 

318) 

319def epc(ctx, scores, **kwargs): 

320 process = figure.Epc(ctx, scores, True, split_csv_vuln) 

321 process.run() 

322 

323 

324@vuln_plot_options( 

325 docstring="""Plots the EPSC for vulnerability analysis 

326 

327 Plots the Expected Performance Spoofing Curve. 

328 

329 Note that when using 3D plots with option ``--three-d``, you cannot plot 

330 both WER and IAPMR on the same figure (which is possible in 2D). 

331 

332 You need to provide 2 score files for each vulnerability system in this order: 

333 

334 \b 

335 * dev scores 

336 * eval scores 

337 

338 The CSV score files must contain an `attack-type` column, in addition to the 

339 "regular" biometric scores columns (`bio_ref_subject_id`, 

340 `probe_subject_id`, and `score`). 

341 

342 See :ref:`bob.bio.base.vulnerability` in the documentation for a guide on 

343 vulnerability analysis. 

344 

345 Examples: 

346 

347 $ bob vuln epsc -v scores-dev.csv scores-eval.csv 

348 

349 $ bob vuln epsc -v -o epsc.pdf scores-{dev,eval}.csv 

350 

351 $ bob vuln epsc -v -D -o epsc_3D.pdf scores-{dev,eval}.csv 

352 """, 

353 plot_output_default="vuln_epc.pdf", 

354 force_eval=True, 

355 figsize_default="5,3", 

356) 

357@common_options.titles_option() 

358@common_options.bool_option( 

359 "wer", "w", "Whether to plot the WER related lines or not.", True 

360) 

361@common_options.bool_option( 

362 "iapmr", "I", "Whether to plot the IAPMR related lines or not.", True 

363) 

364@common_options.bool_option( 

365 "three-d", 

366 "D", 

367 "If true, generate 3D plots. You need to turn off " 

368 "wer or iapmr when using this option.", 

369 False, 

370) 

371@click.option( 

372 "-c", 

373 "--criteria", 

374 default="eer", 

375 show_default=True, 

376 help="Criteria for threshold selection", 

377 type=click.Choice(("eer", "min-hter")), 

378) 

379@click.option( 

380 "-vp", 

381 "--var-param", 

382 default="omega", 

383 show_default=True, 

384 help="Name of the varying parameter", 

385 type=click.Choice(("omega", "beta")), 

386) 

387@common_options.list_float_option( 

388 name="fixed-params", 

389 short_name="fp", 

390 dflt="0.5", 

391 desc="Values of the fixed parameter, separated by commas", 

392) 

393@click.option( 

394 "-s", 

395 "--sampling", 

396 default=5, 

397 show_default=True, 

398 help="Sampling of the EPSC 3D surface", 

399 type=click.INT, 

400) 

401def epsc(ctx, scores, criteria, var_param, three_d, sampling, **kwargs): 

402 if three_d: 

403 if ctx.meta["wer"] and ctx.meta["iapmr"]: 

404 logger.info( 

405 "Cannot plot both WER and IAPMR in 3D. Will turn IAPMR off." 

406 ) 

407 ctx.meta["iapmr"] = False 

408 ctx.meta["sampling"] = sampling 

409 process = figure.Epsc3D( 

410 ctx, scores, split_csv_vuln, criteria, var_param, **kwargs 

411 ) 

412 else: 

413 process = figure.Epsc( 

414 ctx, scores, split_csv_vuln, criteria, var_param, **kwargs 

415 ) 

416 process.run() 

417 

418 

419@vuln_plot_options( 

420 docstring="""Vulnerability analysis score distribution histograms. 

421 

422 Plots the histogram of score distributions. You need to provide 1 or 2 score 

423 files for each biometric system in this order: 

424 

425 \b 

426 * development scores 

427 * [evaluation scores] 

428 

429 When evaluation scores are provided, you must use the ``--eval`` option. 

430 

431 The CSV score files must contain an `attack-type` column, in addition to the 

432 "regular" biometric scores columns (`bio_ref_subject_id`, 

433 `probe_subject_id`, and `score`). 

434 

435 See :ref:`bob.bio.base.vulnerability` in the documentation for a guide on 

436 vulnerability analysis. 

437 

438 When eval-scores are given, eval-scores histograms are displayed with the 

439 threshold line computed from dev-scores. 

440 

441 Examples: 

442 

443 $ bob vuln hist -v -o hist.pdf results/scores-dev.csv 

444 

445 $ bob vuln hist -e -v results/scores-dev.csv results/scores-eval.csv 

446 

447 $ bob vuln hist -e -v results/scores-{dev,eval}.csv 

448 """, 

449 plot_output_default="vuln_hist.pdf", 

450) 

451@common_options.titles_option() 

452@common_options.n_bins_option() 

453@common_options.thresholds_option() 

454@common_options.print_filenames_option(dflt=False) 

455@common_options.bool_option( 

456 "iapmr-line", "I", "Whether to plot the IAPMR related lines or not.", True 

457) 

458@real_data_option() 

459@common_options.subplot_option() 

460@common_options.criterion_option() 

461def hist(ctx, scores, evaluation, **kwargs): 

462 process = figure.HistVuln(ctx, scores, evaluation, split_csv_vuln) 

463 process.run() 

464 

465 

466@vuln_plot_options( 

467 docstring="""Plots the FMR vs IAPMR for vulnerability analysis 

468 

469 You need to provide 1 or 2 (with `--eval`) score 

470 files for each vulnerability system in this order: 

471 

472 \b 

473 * dev scores 

474 * [eval scores] 

475 

476 The CSV score files must contain an `attack-type` column, in addition to the 

477 "regular" biometric scores columns (`bio_ref_subject_id`, 

478 `probe_subject_id`, and `score`). 

479 

480 Examples: 

481 

482 $ bob vuln fmr_iapmr -v -o fmr_iapmr.pdf scores.csv 

483 

484 $ bob vuln fmr_iapmr -v -e scores-{dev,eval}.csv 

485 """, 

486 plot_output_default="vuln_roc.pdf", 

487 force_eval=True, 

488) 

489@common_options.title_option() 

490@common_options.semilogx_option() 

491def fmr_iapmr(ctx, scores, **kwargs): 

492 process = figure.FmrIapmr(ctx, scores, True, split_csv_vuln) 

493 process.run() 

494 

495 

496@common_options.evaluate_command( 

497 common_options.EVALUATE_HELP.format( 

498 score_format=( 

499 "Files must be in CSV format, with the `bio_ref_subject_id`, " 

500 "`probe_references_id`, `score`, and `attack_type` columns." 

501 ), 

502 command="bob vuln evaluate", 

503 ), 

504 criteria=("eer", "min-hter", "far"), 

505) 

506def evaluate(ctx, scores, evaluation, **kwargs): 

507 # open_mode is always 'write' in this command. 

508 ctx.meta["open_mode"] = "w" 

509 criterion = ctx.meta.get("criterion") 

510 if criterion is not None: 

511 click.echo(f"Computing metrics with {criterion}...") 

512 ctx.invoke(metrics, scores=scores, evaluation=evaluation) 

513 if ctx.meta.get("log") is not None: 

514 click.echo(f"[metrics] => {ctx.meta['log']}") 

515 

516 ctx.meta["lines_at"] = None 

517 

518 # Avoid closing pdf file before all figures are plotted 

519 ctx.meta["closef"] = False 

520 if evaluation: 

521 click.echo("Starting evaluate with dev and eval scores...") 

522 else: 

523 click.echo("Starting evaluate with dev scores only...") 

524 click.echo("Plotting FMR vs IAPMR for bob vuln evaluate...") 

525 ctx.forward(fmr_iapmr) # uses class defaults plot settings 

526 click.echo("Plotting ROC for bob vuln evaluate...") 

527 ctx.forward(roc) # uses class defaults plot settings 

528 click.echo("Plotting DET for bob vuln evaluate...") 

529 ctx.forward(det) # uses class defaults plot settings 

530 if evaluation: 

531 click.echo("Plotting EPSC for bob vuln evaluate...") 

532 ctx.forward(epsc) # uses class defaults plot settings 

533 # Mark the last plot to close the output file 

534 ctx.meta["closef"] = True 

535 click.echo("Plotting score histograms for bob vuln evaluate...") 

536 ctx.forward(hist) 

537 click.echo("Evaluate successfully completed!") 

538 click.echo(f"[plots] => {ctx.meta['output']}")