diff --git a/bob/bio/base/__init__.py b/bob/bio/base/__init__.py index 9ef81253a184f70511b99f0c3ecda10d28de32a0..c69d5db39ea2a2281fe7a6e7b3994a17d42e0395 100644 --- a/bob/bio/base/__init__.py +++ b/bob/bio/base/__init__.py @@ -6,6 +6,7 @@ from . import algorithm from . import tools from . import grid # only one file, not complete directory from . import annotator +from . import baseline from . import script from . import test diff --git a/bob/bio/base/baseline/Baseline.py b/bob/bio/base/baseline/Baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..40459ccad63c444709e6092b37d20b29e9048d33 --- /dev/null +++ b/bob/bio/base/baseline/Baseline.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Tiago de Freitas Pereira <tiago.pereira@idiap.ch> +from .. import resource_keys, load_resource + + +def search_preprocessor(db_name, keys): + """ + Wrapper that searches for preprocessors for specific databases. + If not found, the default preprocessor is returned + """ + for k in keys: + if db_name.startswith(k): + return k + else: + return "default" + + +def get_available_databases(): + """ + Get all the available databases through the database entry-points + """ + + available_databases = dict() + all_databases = resource_keys('database', strip=[]) + for database in all_databases: + try: + database_entry_point = load_resource(database, 'database') + + available_databases[database] = dict() + + # Checking if the database has data for the ZT normalization + available_databases[database]["has_zt"] = hasattr(database_entry_point, "zobjects") and hasattr(database_entry_point, "tobjects") + available_databases[database]["groups"] = [] + # Searching for database groups + try: + groups = list(database_entry_point.groups()) + for g in ["dev", "eval"]: + available_databases[database]["groups"] += [g] if g in groups else [] + except Exception: + # In case the method groups is not implemented + available_databases[database]["groups"] = ["dev"] + except Exception: + pass + return available_databases + + +class Baseline(object): + """ + Base class to define baselines + + A Baseline is composed by the triplet + :any:`bob.bio.base.preprocessor.Preprocessor`, + :any:`bob.bio.base.extractor.Extractor`, and + :any:`bob.bio.base.algorithm.Algorithm` + + Attributes + ---------- + name : str + Name of the baseline. This name will be displayed in the command line + interface. + preprocessors : dict + Dictionary containing all possible preprocessors + extractor : str + Registered resource or a config file containing the feature extractor + algorithm : str + Registered resource or a config file containing the algorithm + """ + + def __init__(self, name, preprocessors, extractor, algorithm, **kwargs): + super(Baseline, self).__init__(**kwargs) + self.name = name + self.preprocessors = preprocessors + self.extractor = extractor + self.algorithm = algorithm diff --git a/bob/bio/base/baseline/__init__.py b/bob/bio/base/baseline/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..8e510bd3ea4dee434a12a3817a1c4769862d1fcd --- /dev/null +++ b/bob/bio/base/baseline/__init__.py @@ -0,0 +1,31 @@ +from .Baseline import Baseline, search_preprocessor, get_available_databases + + +def get_config(): + """Returns a string containing the configuration information. + """ + import bob.extension + return bob.extension.get_config(__name__) + + +# gets sphinx autodoc done right - don't remove it +def __appropriate__(*args): + """Says object was actually declared here, and not in the import module. + Fixing sphinx warnings of not being able to find classes, when path is + shortened. Parameters: + + *args: An iterable of objects to modify + + Resolves `Sphinx referencing issues + <https://github.com/sphinx-doc/sphinx/issues/3048>` + """ + + for obj in args: + obj.__module__ = __name__ + + +__appropriate__( + Baseline, +) + +__all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/bio/base/script/baseline.py b/bob/bio/base/script/baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..74c219446b86748e2f5b0ce343f46c1332236e3b --- /dev/null +++ b/bob/bio/base/script/baseline.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Tiago de Freitas Pereira <tiago.pereira@idiap.ch> + +""" +A script to run biometric recognition baselines +""" + + +from .. import load_resource +import os +from .verify import main as verify +from ..baseline import get_available_databases, search_preprocessor +from bob.extension.scripts.click_helper import verbosity_option +import click + + +@click.command(context_settings={'ignore_unknown_options': True, + 'allow_extra_args': True}) +@click.argument('baseline', required=True) +@click.argument('database', required=True) +@verbosity_option() +@click.pass_context +def baseline(ctx, baseline, database): + """Run a biometric recognition baseline. + + \b + Example: + $ bob bio baseline eigenface atnt -vvv + + which will run the eigenface baseline (from bob.bio.face) on the atnt + database. + + \b + Check out all baselines available by running: + `resource.py --types baseline` + and all available databases by running: + `resource.py --types database` + + This script accepts parameters accepted by verify.py as well. + See `verify.py --help` for the extra options that you can pass. + + Hint: pass `--grid demanding` to run the baseline on the SGE grid. + + Hint: pass `--temp-directory <dir>` to set the directory for temporary files + + Hint: pass `--result-directory <dir>` to set the directory for resulting score files + + """ + # Triggering training for each baseline/database + loaded_baseline = load_resource( + baseline, 'baseline', package_prefix="bob.bio.") + + # this is the default sub-directory that is used + sub_directory = os.path.join(database, baseline) + + # find the compatible preprocessor for this database + database_data = get_available_databases()[database] + db = search_preprocessor(database, loaded_baseline.preprocessors.keys()) + preprocessor = loaded_baseline.preprocessors[db] + + # call verify with all parameters + parameters = [ + '-p', preprocessor, + '-e', loaded_baseline.extractor, + '-d', database, + '-a', loaded_baseline.algorithm, + '--sub-directory', sub_directory + ] + ['-v'] * ctx.meta['verbosity'] + + parameters += ['--groups'] + database_data["groups"] + + verify(parameters + ctx.args) diff --git a/bob/bio/base/script/resources.py b/bob/bio/base/script/resources.py index c68d6f22202a1f14ece2ecd572ff2dffed071289..f8f564511382ae7360ab1415900be84d71564820 100644 --- a/bob/bio/base/script/resources.py +++ b/bob/bio/base/script/resources.py @@ -9,8 +9,8 @@ def resources(command_line_parameters = None): import argparse parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--types", '-t', nargs = '+', - choices = ('d', 'database', 'p', 'preprocessor', 'e', 'extractor', 'a', 'algorithm', 'g', 'grid', 'c', 'config', 'an', 'annotator'), - default = ('d', 'p', 'e', 'a', 'g', 'c', 'an'), + choices = ('d', 'database', 'p', 'preprocessor', 'e', 'extractor', 'a', 'algorithm', 'g', 'grid', 'c', 'config', 'an', 'annotator', 'b', 'baseline'), + default = ('d', 'p', 'e', 'a', 'g', 'c', 'an', 'b'), help = "Select the resource types that should be listed.") parser.add_argument("--details", '-d', action='store_true', help = "Prints the complete configuration for all resources") @@ -55,6 +55,10 @@ def resources(command_line_parameters = None): print ("\nList of registered annotators:") print (bob.bio.base.list_resources('annotator', **kwargs)) + if 'b' in args.types or 'baseline' in args.types: + print ("\nList of registered baseline:") + print (bob.bio.base.list_resources('baseline', **kwargs)) + print() def databases(command_line_parameters = None): diff --git a/bob/bio/base/test/dummy/baseline.py b/bob/bio/base/test/dummy/baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..e52d4b59992c160aa3a3a2b0c1874709968637a4 --- /dev/null +++ b/bob/bio/base/test/dummy/baseline.py @@ -0,0 +1,9 @@ +from bob.bio.base.baseline import Baseline +import pkg_resources +import os + +dummy_dir = pkg_resources.resource_filename('bob.bio.base', 'test/dummy') +baseline = Baseline(name="dummy", + preprocessors={"default": os.path.join(dummy_dir, 'preprocessor.py')}, + extractor=os.path.join(dummy_dir, 'extractor.py'), + algorithm=os.path.join(dummy_dir, 'algorithm.py')) diff --git a/bob/bio/base/test/test_baselines.py b/bob/bio/base/test/test_baselines.py new file mode 100644 index 0000000000000000000000000000000000000000..27dd5749b5eab5a7cbef8853753e25e41c218731 --- /dev/null +++ b/bob/bio/base/test/test_baselines.py @@ -0,0 +1,21 @@ +import tempfile +import shutil +from click.testing import CliRunner +from bob.bio.base.script.baseline import baseline + +def test_baselines(): + + try: + tmp_dir = tempfile.mkdtemp(prefix="bobtest_") + runner = CliRunner() + result = runner.invoke(baseline, args=('dummy', 'dummy', '-T', tmp_dir, '-R', tmp_dir)) + assertion_error_message = ( + 'Command exited with this output: `{}\' \n' + 'If the output is empty, you can run this script locally to see ' + 'what is wrong:\n' + 'bin/bob bio baseline -d dummy -a dummy -o /tmp/temp_annotations' + ''.format(result.output)) + assert result.exit_code == 0, assertion_error_message + + finally: + shutil.rmtree(tmp_dir) diff --git a/bob/bio/base/utils/resources.py b/bob/bio/base/utils/resources.py index fa5e3c42949e026c383fd475406f0471ad2d2770..57a5b5a294f5f67530c67b090e350df3cbfc0c2d 100644 --- a/bob/bio/base/utils/resources.py +++ b/bob/bio/base/utils/resources.py @@ -21,7 +21,7 @@ logger = logging.getLogger("bob.bio.base") #: Keywords for which resources are defined. -valid_keywords = ('database', 'preprocessor', 'extractor', 'algorithm', 'grid', 'config', 'annotator') +valid_keywords = ('database', 'preprocessor', 'extractor', 'algorithm', 'grid', 'config', 'annotator', 'baseline') def _collect_config(paths): diff --git a/doc/baseline.rst b/doc/baseline.rst new file mode 100644 index 0000000000000000000000000000000000000000..2e6da391465c2c217906ea450c3a1d415a407b6e --- /dev/null +++ b/doc/baseline.rst @@ -0,0 +1,86 @@ +.. _bob.bio.base.baseline: + +================== +Defining baselines +================== + + +Once you have a biometric system well established, tuned and working for a +particular database (or a particular set of databases), you may want to provide +**an easier to reproduce** way to share it. For this purpose, we defined +something called baseline. + +A baseline (:any:`bob.bio.base.baseline.Baseline`) is composed by the triplet +of :any:`bob.bio.base.preprocessor.Preprocessor`, +:any:`bob.bio.base.extractor.Extractor` and +:any:`bob.bio.base.algorithm.Algorithm`. + +First, check it out the baselines ready to be triggered in your environment by +doing: + +.. code-block:: sh + + $ bob bio baseline --help + +For example, if you run ``bob bio baseline -vvv eigenface atnt``, it will run +the eigenface face recognition baseline on the atnt database (assuming you have +installed ``bob.bio.face`` and ``bob.db.atnt``). + + +To create your own baseline, you just need to define it like in the recipe +below: + +.. code-block:: py + + from bob.bio.base.baseline import Baseline + + baseline = Baseline(name="my-baseline", + preprocessors={"default": 'my-preprocessor'}, + extractor='my-extractor'), + algorithm='my-algorithm')) + +Some databases may require some specific preprocessors depending on the type +of meta-informations provided. For instance, for some face recognition +databases, faces should be cropped in a particular way depending on the +annotations provided. To approach this issue, the preprocessors are defined in +a dictionary, with a generic preprocessor defined as **default** and the +database specific preprocessor defined by database name as in the example +below: + +.. code-block:: py + + self.preprocessors = dict() + self.preprocessors["default"] = 'my-preprocessor' + self.preprocessors["database_name"] = 'my-specific-preprocessor' + + +Follow below a full example on how to define a baseline with database specific +preprocessors. + +.. code-block:: py + + from bob.bio.base.baseline import Baseline + + preprocessors = {"default": 'my-preprocessor'} + preprocessors["database_name"] = 'my-specific-preprocessor' + baseline = Baseline(name="another-baseline", + preprocessors=preprocessors, + extractor='my-extractor'), + algorithm='my-algorithm')) + +.. note:: + + The triplet can be a resource or a configuration file. This works in the + same way as in :ref:`Running Experiments <running_part_1>`. + +.. note:: + + Baselines are also registered as resources under the keyword + `bob.bio.baseline`. + +You can find the list of readily available baselines using the ``resources.py`` +command: + +.. code-block:: sh + + $ resources.py --types baseline diff --git a/doc/implemented.rst b/doc/implemented.rst index 052e56deb9b1f7c0bb0f8fb12aafc59215c1cb82..80a4ab1ee8a35d73c98bafae3c2261ac66184d01 100644 --- a/doc/implemented.rst +++ b/doc/implemented.rst @@ -16,6 +16,7 @@ Base Classes bob.bio.base.algorithm.Algorithm bob.bio.base.grid.Grid bob.bio.base.annotator.Annotator + bob.bio.base.baseline.Baseline Implementations @@ -80,4 +81,10 @@ Annotators .. automodule:: bob.bio.base.annotator +Baselines +--------- + +.. automodule:: bob.bio.base.baseline + + .. include:: links.rst diff --git a/doc/index.rst b/doc/index.rst index 2b966a5d00794c070ff4578ccbe667253e593a4f..8e1e3a67bfc8117c13f1073338e3f209f2dc16f2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -75,6 +75,7 @@ Users Guide struct_bio_rec_sys experiments implementation + baseline filelist-guide more annotations diff --git a/setup.py b/setup.py index 58dbbfcf7880f7182c00812da011b0704b980d21..e488a1c3698b2554a2a5ff1d6ff8b2e35adce0d0 100644 --- a/setup.py +++ b/setup.py @@ -147,12 +147,19 @@ setup( 'dir = bob.bio.base.script.commands:dir', 'gen = bob.bio.base.script.gen:gen', 'evaluate = bob.bio.base.script.commands:evaluate', + 'baseline = bob.bio.base.script.baseline:baseline', ], # annotators 'bob.bio.annotator': [ 'dummy = bob.bio.base.test.dummy.annotator:annotator', ], + + #baselines + 'bob.bio.baseline':[ + 'dummy = bob.bio.base.test.dummy.baseline:baseline', + ], + }, # Classifiers are important if you plan to distribute this package through