diff --git a/bob/bio/face/annotator/Base.py b/bob/bio/face/annotator/Base.py index 003ed72b29b1840e2d8112c33c93c05d927881e4..e99dde1eb3be35d795368265546b070317bc3038 100644 --- a/bob/bio/face/annotator/Base.py +++ b/bob/bio/face/annotator/Base.py @@ -1,21 +1,20 @@ -from bob.bio.base import read_original_data as base_read +from bob.bio.base.annotator import Base as __Base -class Base(object): - """Base class for all annotators""" +class Base(__Base): + """Base class for all face annotators""" - def __init__(self, read_original_data=None, **kwargs): + def __init__(self, **kwargs): super(Base, self).__init__(**kwargs) - self.read_original_data = read_original_data or base_read - def annotate(self, image, **kwargs): + def annotate(self, sample, **kwargs): """Annotates an image and returns annotations in a dictionary. All annotator should return at least the topleft and bottomright coordinates. Parameters ---------- - image : array + sample : numpy.ndarray The image should be a Bob format (#Channels, Height, Width) RGB image. **kwargs @@ -24,5 +23,5 @@ class Base(object): raise NotImplementedError() # Alisa call to annotate - def __call__(self, image, **kwargs): - return self.annotate(image, **kwargs) + def __call__(self, sample, **kwargs): + return self.annotate(sample, **kwargs) diff --git a/bob/bio/face/annotator/FailSafe.py b/bob/bio/face/annotator/FailSafe.py deleted file mode 100644 index 4b9b587865772eae7d920e6b69ae260e2d6301ba..0000000000000000000000000000000000000000 --- a/bob/bio/face/annotator/FailSafe.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -from . import Base - -logger = logging.getLogger(__name__) - - -class FailSafe(Base): - """A fail-safe annotator. - This annotator takes a list of annotator and tries them until you get your - annotations. - The annotations of previous annotator is passed to the next one. - """ - - def __init__(self, annotators, required_keys, **kwargs): - super(FailSafe, self).__init__(**kwargs) - self.annotators = list(annotators) - self.required_keys = list(required_keys) - - def annotate(self, image, **kwargs): - if 'annotations' not in kwargs or kwargs['annotations'] is None: - kwargs['annotations'] = {} - for annotator in self.annotators: - try: - annotations = annotator(image, **kwargs) - except Exception: - logger.warning( - "The annotator `%s' failed to annotate!", annotator, - exc_info=True) - annotations = {} - if not annotations: - logger.debug( - "Annotator `%s' returned empty annotations.", annotator) - kwargs['annotations'].update(annotations) - # check if we have all the required annotations - if all(key in kwargs['annotations'] for key in self.required_keys): - break - return kwargs['annotations'] diff --git a/bob/bio/face/annotator/__init__.py b/bob/bio/face/annotator/__init__.py index 9f64b622c89211a701dac65cbc97095e6dda1a59..83adf1d8d9d1ae3ab7d5572edeb72abc38e85553 100644 --- a/bob/bio/face/annotator/__init__.py +++ b/bob/bio/face/annotator/__init__.py @@ -1,5 +1,5 @@ +from bob.ip.facedetect import bounding_box_from_annotation from .Base import Base -from .FailSafe import FailSafe def bounding_box_to_annotations(bbx): @@ -8,3 +8,50 @@ def bounding_box_to_annotations(bbx): 'bottomright': bbx.bottomright, } return landmarks + + +def min_face_size_validator(annotations, min_face_size=(32, 32)): + """Validates annotations based on face's minimal size. + + Parameters + ---------- + annotations : dict + The annotations in dictionary format. + min_face_size : (int, int), optional + The minimal size of a face. + + Returns + ------- + bool + True, if the face is large enough. + """ + bbx = bounding_box_from_annotation(source='direct', **annotations) + if bbx.size < min_face_size: + return False + return True + + +# 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__( + Base, +) + +__all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/bio/face/annotator/bobipdlib.py b/bob/bio/face/annotator/bobipdlib.py new file mode 100644 index 0000000000000000000000000000000000000000..d1c6a759f03a28a8c4e3805c3f8ac7b6223fcc71 --- /dev/null +++ b/bob/bio/face/annotator/bobipdlib.py @@ -0,0 +1,19 @@ +from . import Base, bounding_box_to_annotations +from bob.ip.facedetect import bounding_box_from_annotation +from bob.ip.dlib import DlibLandmarkExtraction + + +class BobIpDlib(Base): + """Annotator using bob.ip.dlib""" + + def __init__(self, **kwargs): + super(BobIpDlib, self).__init__(**kwargs) + self.detector = DlibLandmarkExtraction(bob_landmark_format=True) + + def annotate(self, image, **kwargs): + landmarks = self.detector(image) + if not landmarks: + return {} + bounding_box = bounding_box_from_annotation(source='eyes', **landmarks) + landmarks.update(bounding_box_to_annotations(bounding_box)) + return landmarks diff --git a/bob/bio/face/script/annotate.py b/bob/bio/face/script/annotate.py deleted file mode 100644 index e8a50d838ed054f31f20ca14cefb885cfaefbee1..0000000000000000000000000000000000000000 --- a/bob/bio/face/script/annotate.py +++ /dev/null @@ -1,87 +0,0 @@ -"""A script to help annotate databases. -""" -import logging -import json -import click -from os.path import dirname, isfile -from bob.extension.scripts.click_helper import ( - verbosity_option, Command, Option) -from bob.io.base import create_directories_safe -from bob.bio.base.tools.grid import indices - -logger = logging.getLogger(__name__) - - -@click.command(entry_point_group='bob.bio.config', cls=Command) -@click.option('--database', '-d', required=True, cls=Option, - entry_point_group='bob.bio.database') -@click.option('--annotator', '-a', required=True, cls=Option, - entry_point_group='bob.bio.annotator') -@click.option('--output-dir', '-o', required=True, cls=Option) -@click.option('--force', '-f', is_flag=True, cls=Option) -@click.option('--jobs', type=click.INT, default=1,) -@verbosity_option(cls=Option) -def annotate(database, annotator, output_dir, force, jobs, **kwargs): - """Annotates a database. - The annotations are written in text file (json) format which can be read - back using :any:`bob.db.base.read_annotation_file` (annotation_type='json') - - \b - Parameters - ---------- - database : :any:`bob.bio.database` - The database that you want to annotate. Can be a ``bob.bio.database`` - entry point or a path to a Python file which contains a variable - named `database`. - annotator : callable - A function that takes the database and a sample (biofile) of the - database and returns the annotations in a dictionary. Can be a - ``bob.bio.annotator`` entry point or a path to a Python file which - contains a variable named `annotator`. - output_dir : str - The directory to save the annotations. - force : bool, optional - Wether to overwrite existing annotations. - jobs : int, optional - Use this option alongside gridtk to submit this script as an array job. - verbose : int, optional - Increases verbosity (see help for --verbose). - - \b - [CONFIG]... Configuration files. It is possible to pass one or - several Python files (or names of ``bob.bio.config`` - entry points) which contain the parameters listed - above as Python variables. The options through the - command-line (see below) will override the values of - configuration files. - """ - logger.debug('database: %s', database) - logger.debug('annotator: %s', annotator) - logger.debug('force: %s', force) - logger.debug('output_dir: %s', output_dir) - logger.debug('jobs: %s', jobs) - logger.debug('kwargs: %s', kwargs) - biofiles = database.objects(groups=None, protocol=database.protocol) - biofiles = sorted(biofiles) - if jobs > 1: - start, end = indices(biofiles, jobs) - biofiles = biofiles[start:end] - logger.info("Saving annotations in %s", output_dir) - total = len(biofiles) - logger.info("Annotating %d samples ...", total) - for i, biofile in enumerate(biofiles): - logger.info( - "Extracting annotations for sample %d out of %d", i + 1, total) - outpath = biofile.make_path(output_dir, '.json') - if isfile(outpath): - if force: - logger.debug("Overwriting the annotations file `%s'", outpath) - else: - logger.debug("The annotation `%s' already exists", outpath) - continue - data = annotator.read_original_data( - biofile, database.original_directory, database.original_extension) - annot = annotator(data) - create_directories_safe(dirname(outpath)) - with open(outpath, 'w') as f: - json.dump(annot, f, indent=1, allow_nan=False) diff --git a/bob/bio/face/test/test_annotators.py b/bob/bio/face/test/test_annotators.py new file mode 100644 index 0000000000000000000000000000000000000000..c71dbe4faa94dd0b87efebc8060cdad1a3896586 --- /dev/null +++ b/bob/bio/face/test/test_annotators.py @@ -0,0 +1,13 @@ +from __future__ import print_function +import bob.io.base +import bob.io.base.test_utils +import bob.io.image +import bob.ip.facedetect + +face_image = bob.io.base.load(bob.io.base.test_utils.datafile( + 'testimage.jpg', 'bob.ip.facedetect')) + + +def test_bob_ip_facedetect(): + from bob.bio.face.annotator.bobipfacedetect import BobIpFacedetect + annot = BobIpFacedetect(face_image) diff --git a/setup.py b/setup.py index 1fef71cafc4f0402b3845d77a5e923f2fae69f15..9cf6eb0382bf3eb8e359f1e12248485be20d5983 100644 --- a/setup.py +++ b/setup.py @@ -181,10 +181,6 @@ setup( 'bic-jets = bob.bio.face.config.algorithm.bic_jets:algorithm', # BIC on gabor jets ], - 'bob.bio.cli': [ - 'annotate = bob.bio.face.script.annotate:annotate' - ] - }, # Classifiers are important if you plan to distribute this package through