From abf0e758f1d32c3fbf40cf4986f5686b56f22b55 Mon Sep 17 00:00:00 2001 From: Manuel Guenther <manuel.guenther@idiap.ch> Date: Fri, 19 Jun 2015 16:17:19 +0200 Subject: [PATCH] Added collect_results script --- bob/bio/base/script/collect_results.py | 217 +++++++++++++++++++++++++ bob/bio/base/test/test_scripts.py | 16 +- doc/conf.py | 10 +- setup.py | 1 + 4 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 bob/bio/base/script/collect_results.py diff --git a/bob/bio/base/script/collect_results.py b/bob/bio/base/script/collect_results.py new file mode 100644 index 00000000..b9438c0e --- /dev/null +++ b/bob/bio/base/script/collect_results.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Manuel Guenther <manuel.guenther@idiap.ch> +# Tue Jul 2 14:52:49 CEST 2013 +# +# Copyright (C) 2011-2013 Idiap Research Institute, Martigny, Switzerland +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from __future__ import print_function + +""" +This script parses through the given directory, collects all results of +verification experiments that are stored in file with the given file name. +It supports the split into development and test set of the data, as well as +ZT-normalized scores. + +All result files are parsed and evaluated. For each directory, the following +information are given in columns: + + * The Equal Error Rate of the development set + * The Equal Error Rate of the development set after ZT-Normalization + * The Half Total Error Rate of the evaluation set + * The Half Total Error Rate of the evaluation set after ZT-Normalization + * The sub-directory where the scores can be found + +The measure type of the development set can be changed to compute "HTER" or +"FAR" thresholds instead, using the --criterion option. +""" + + +import sys, os, glob +import argparse + +import bob.measure +import bob.core +logger = bob.core.log.setup("bob.bio.base") + +def command_line_arguments(command_line_parameters): + """Parse the program options""" + + # set up command line parser + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-d', '--devel-name', dest="dev", default="scores-dev", help = "Name of the file containing the development scores") + parser.add_argument('-e', '--eval-name', dest="eval", default="scores-eval", help = "Name of the file containing the evaluation scores") + parser.add_argument('-D', '--directory', default=".", help = "The directory where the results should be collected from; might include search patterns as '*'.") + parser.add_argument('-n', '--nonorm-dir', dest="nonorm", default="nonorm", help = "Directory where the unnormalized scores are found") + parser.add_argument('-z', '--ztnorm-dir', dest="ztnorm", default = "ztnorm", help = "Directory where the normalized scores are found") + parser.add_argument('-s', '--sort', action='store_true', help = "Sort the results") + parser.add_argument('-k', '--sort-key', dest='key', default = 'nonorm-dev', choices= ('nonorm-dev','nonorm-eval','ztnorm-dev','ztnorm-eval','dir'), + help = "Sort the results according to the given key") + parser.add_argument('-c', '--criterion', dest='criterion', default = 'EER', choices = ('EER', 'HTER', 'FAR'), + help = "Minimize the threshold on the development set according to the given criterion") + + parser.add_argument('-o', '--output', help = "Name of the output file that will contain the EER/HTER scores") + parser.add_argument('--parser', default = '4column', choices = ('4column', '5column'), help="The style of the resulting score files; rarely changed") + + parser.add_argument('--self-test', action='store_true', help=argparse.SUPPRESS) + + bob.core.log.add_command_line_option(parser) + + # parse arguments + args = parser.parse_args(command_line_parameters) + + bob.core.log.set_verbosity_level(logger, args.verbose) + + # assign the score file parser + args.parser = {'4column' : bob.measure.load.split_four_column, '5column' : bob.measure.load.split_five_column}[args.parser] + + return args + +class Result: + """Class for collecting the results of one experiment.""" + def __init__(self, dir, args): + self.dir = dir + self.m_args = args + self.nonorm_dev = None + self.nonorm_eval = None + self.ztnorm_dev = None + self.ztnorm_eval = None + + def _calculate(self, dev_file, eval_file = None): + """Calculates the EER and HTER or FRR based on the threshold criterion.""" + dev_neg, dev_pos = self.m_args.parser(dev_file) + + # switch which threshold function to use; + # THIS f***ing piece of code really is what python authors propose: + threshold = { + 'EER' : bob.measure.eer_threshold, + 'HTER' : bob.measure.min_hter_threshold, + 'FAR' : bob.measure.far_threshold + } [self.m_args.criterion](dev_neg, dev_pos) + + # compute far and frr for the given threshold + dev_far, dev_frr = bob.measure.farfrr(dev_neg, dev_pos, threshold) + dev_hter = (dev_far + dev_frr)/2.0 + + if eval_file: + eval_neg, eval_pos = self.m_args.parser(eval_file) + eval_far, eval_frr = bob.measure.farfrr(eval_neg, eval_pos, threshold) + eval_hter = (eval_far + eval_frr)/2.0 + else: + eval_hter = None + eval_frr = None + + if self.m_args.criterion == 'FAR': + return (dev_frr, eval_frr) + else: + return (dev_hter, eval_hter) + + def nonorm(self, dev_file, eval_file = None): + self.nonorm_dev, self.nonorm_eval = self._calculate(dev_file, eval_file) + + def ztnorm(self, dev_file, eval_file = None): + self.ztnorm_dev, self.ztnorm_eval = self._calculate(dev_file, eval_file) + + def __str__(self): + str = "" + for v in [self.nonorm_dev, self.ztnorm_dev, self.nonorm_eval, self.ztnorm_eval]: + if v: + val = "% 2.3f%%"%(v*100) + else: + val = "None" + cnt = 16-len(val) + str += " "*cnt + val + str += " %s"%self.dir + return str[5:] + + +results = [] + +def add_results(args, nonorm, ztnorm = None): + """Adds results of the given nonorm and ztnorm directories.""" + r = Result(os.path.dirname(nonorm).replace(args.directory+"/", ""), args) + logger.info("Adding results from directory '%s'", r.dir) + + # check if the results files are there + dev_file = os.path.join(nonorm, args.dev) + eval_file = os.path.join(nonorm, args.eval) + if os.path.isfile(dev_file): + if os.path.isfile(eval_file): + r.nonorm(dev_file, eval_file) + else: + r.nonorm(dev_file) + + if ztnorm: + dev_file = os.path.join(ztnorm, args.dev) + eval_file = os.path.join(ztnorm, args.eval) + if os.path.isfile(dev_file): + if os.path.isfile(eval_file): + r.ztnorm(dev_file, eval_file) + else: + r.ztnorm(dev_file) + + results.append(r) + + +def recurse(args, path): + """Recurse the directory structure and collect all results that are stored in the desired file names.""" + dir_list = os.listdir(path) + + # check if the score directories are included in the current path + if args.nonorm in dir_list: + if args.ztnorm in dir_list: + add_results(args, os.path.join(path, args.nonorm), os.path.join(path, args.ztnorm)) + else: + add_results(args, os.path.join(path, args.nonorm)) + + for e in dir_list: + real_path = os.path.join(path, e) + if os.path.isdir(real_path): + recurse(args, real_path) + + +def table(): + """Generates a table containing all results in a nice format.""" + A = " "*2 + 'dev nonorm'+ " "*5 + 'dev ztnorm' + " "*6 + 'eval nonorm' + " "*4 + 'eval ztnorm' + " "*12 + 'directory\n' + A += "-"*100+"\n" + for r in results: + A += str(r) + "\n" + return A + + +def main(command_line_parameters = None): + """Iterates through the desired directory and collects all result files.""" + args = command_line_arguments(command_line_parameters) + + # collect results + directories = glob.glob(args.directory) + for directory in directories: + recurse(args, directory) + + # sort results if desired + if args.sort: + import operator + results.sort(key=operator.attrgetter(args.key.replace('-','_'))) + + # print the results + if args.self_test: + table() + elif args.output: + f = open(args.output, "w") + f.writelines(table()) + f.close() + else: + print (table()) diff --git a/bob/bio/base/test/test_scripts.py b/bob/bio/base/test/test_scripts.py index 7d40bc34..94c13f6e 100644 --- a/bob/bio/base/test/test_scripts.py +++ b/bob/bio/base/test/test_scripts.py @@ -278,18 +278,16 @@ def test_evaluate(): os.rmdir(test_dir) - - - -""" -def test16_collect_results(self): +def test_collect_results(): # simply test that the collect_results script works test_dir = tempfile.mkdtemp(prefix='bobtest_') - from facereclib.script.collect_results import main - main(['--directory', test_dir, '--sort', '--sort-key', 'dir', '--criterion', 'FAR', '--self-test']) - os.rmdir(test_dir) + try: + from facereclib.script.collect_results import main + main(['--directory', test_dir, '--sort', '--sort-key', 'dir', '--criterion', 'FAR', '--self-test']) + finally: + if os.path.exists(test_dir): + os.rmdir(test_dir) -""" @utils.grid_available def test_grid_search(): diff --git a/doc/conf.py b/doc/conf.py index c667df32..d102e79c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,7 +58,7 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Bob Example Project' +project = u'Bobs interface for running biometric recognition experiments' import time copyright = u'%s, Idiap Research Institute' % time.strftime('%Y') @@ -187,7 +187,7 @@ html_favicon = '' #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'bob_example_project_doc' +htmlhelp_basename = 'bob_bio_base_doc' # -- Options for LaTeX output -------------------------------------------------- @@ -201,7 +201,7 @@ latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'bob_example_project.tex', u'Bob', + ('index', 'bob_bio_base.tex', u'Bob', u'Biometrics Group, Idiap Research Institute', 'manual'), ] @@ -236,7 +236,7 @@ rst_epilog = '' # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'bob.example.project', u'Bob Example Project Documentation', [u'Idiap Research Institute'], 1) + ('index', 'bob.bio.base', u'Base tools to run biometric recognition experiments', [u'Idiap Research Institute'], 1) ] # Default processing flags for sphinx @@ -246,7 +246,7 @@ autodoc_default_flags = ['members', 'undoc-members', 'inherited-members', 'show- # For inter-documentation mapping: from bob.extension.utils import link_documentation -intersphinx_mapping = link_documentation(['python', 'numpy', 'bob.io.base', 'bob.db.verification.utils']) +intersphinx_mapping = link_documentation(['python', 'numpy', 'bob.io.base', 'bob.db.verification.utils', 'bob.bio.face', 'bob.bio.speaker', 'bob.bio.gmm', 'bob.bio.video', 'bob.bio.csu']) def setup(app): diff --git a/setup.py b/setup.py index 3746ff5e..6811620e 100644 --- a/setup.py +++ b/setup.py @@ -106,6 +106,7 @@ setup( 'verify.py = bob.bio.base.script.verify:main', 'resources.py = bob.bio.base.script.resources:main', 'evaluate.py = bob.bio.base.script.evaluate:main', + 'collect_results.py = bob.bio.base.script.collect_results:main', 'grid_search.py = bob.bio.base.script.grid_search:main', 'preprocess.py = bob.bio.base.script.preprocess:main', 'extract.py = bob.bio.base.script.extract:main', -- GitLab