diff --git a/bob/bio/face/__init__.py b/bob/bio/face/__init__.py
index 5064b9bc683a1d816728b6aec7a63fb1cdfcd9ea..4f3858c5fbc180e24a8368de4c55b4e90e73e3c1 100644
--- a/bob/bio/face/__init__.py
+++ b/bob/bio/face/__init__.py
@@ -3,6 +3,7 @@ from . import extractor
 from . import algorithm
 from . import script
 from . import database
+from . import annotator
 
 from . import test
 
diff --git a/bob/bio/face/annotator/Base.py b/bob/bio/face/annotator/Base.py
new file mode 100644
index 0000000000000000000000000000000000000000..a31efd46e09d8d9d23e5d55788f502b1131d502e
--- /dev/null
+++ b/bob/bio/face/annotator/Base.py
@@ -0,0 +1,25 @@
+from bob.bio.base import read_original_data as base_read
+
+
+class Base(object):
+    """Base class for all annotators"""
+
+    def __init__(self, read_original_data=None, **kwargs):
+        super(Base, self).__init__(**kwargs)
+        self.read_original_data = read_original_data or base_read
+
+    def annotate(self, image, **kwargs):
+        """Annotates an image and returns annotations in a dictionary
+
+        Parameters
+        ----------
+        image : object
+            The image is what comes out of ``read_original_data``.
+        **kwargs
+            The extra arguments that may be passed.
+        """
+        raise NotImplementedError()
+
+    # Alisa call to annotate
+    __call__ = annotate
+    __call__.__doc__ = annotate.__doc__
diff --git a/bob/bio/face/annotator/FailSafe.py b/bob/bio/face/annotator/FailSafe.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6fa02023490b7be06dcccfdfc549e17c44a60b4
--- /dev/null
+++ b/bob/bio/face/annotator/FailSafe.py
@@ -0,0 +1,34 @@
+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:
+            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 = {}
+            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
new file mode 100644
index 0000000000000000000000000000000000000000..7f91254ab71b4a618fa7ce11884a4e2d68f7828b
--- /dev/null
+++ b/bob/bio/face/annotator/__init__.py
@@ -0,0 +1,69 @@
+from .Base import Base
+from bob.ip.facedetect import bounding_box_from_annotation
+
+
+def bounding_box_to_annotations(bbx):
+    landmarks = {}
+    landmarks['topleft'] = bbx.topleft_f
+    landmarks['bottomright'] = bbx.bottomright_f
+    return landmarks
+
+
+def normalize_annotations(annotations, validator, max_age=-1):
+    """Normalizes the annotations of one video sequence. It fills the
+    annotations for frames from previous ones if the annotation for the current
+    frame is not valid.
+
+    Parameters
+    ----------
+    annotations : dict
+        A dict of dict where the keys to the first dict are frame indices as
+        strings (starting from 0). The inside dicts contain annotations for
+        that frame.
+    validator : callable
+        Takes a dict (annotations) and returns True if the annotations are
+        valid. This can be check based on minimal face size for example.
+    max_age : :obj:`int`, optional
+        An integer indicating for a how many frames a detected face is valid if
+        no detection occurs after such frame. A value of -1 == forever
+
+    Yields
+    ------
+    dict
+        The corrected annotations of frames.
+    """
+    # the annotations for the current frame
+    current = {}
+    age = 0
+
+    for k, annot in annotations.items():
+        if validator(annot):
+            current = annot
+            age = 0
+        elif max_age < 0 or age < max_age:
+            age += 1
+        else:  # no detections and age is larger than maximum allowed
+            current = {}
+
+        yield current
+
+
+def min_face_size_validator(annotations, min_face_size=32):
+    """Validates annotations based on face's minimal size.
+
+    Parameters
+    ----------
+    annotations : dict
+        The annotations in dictionary format.
+    min_face_size : int, optional
+        The minimal size of a face.
+
+    Returns
+    -------
+    bool
+        True, if the face is large enough.
+    """
+    bbx = bounding_box_from_annotation(**annotations)
+    if bbx.size < 32:
+        return False
+    return True
diff --git a/bob/bio/face/annotator/bobipfacedetect.py b/bob/bio/face/annotator/bobipfacedetect.py
new file mode 100644
index 0000000000000000000000000000000000000000..a648c3d174492c9380a237dfb33d3402a972bb52
--- /dev/null
+++ b/bob/bio/face/annotator/bobipfacedetect.py
@@ -0,0 +1,49 @@
+import math
+from bob.io.base import HDF5File
+from bob.ip.facedetect import (
+    detect_single_face, Sampler, default_cascade, Cascade,
+    expected_eye_positions)
+from . import Base, bounding_box_to_annotations
+
+
+class BobIpFacedetect(Base):
+    """Annotator using bob.ip.facedetect"""
+
+    def __init__(self, cascade=None,
+                 detection_overlap=0.2, distance=2,
+                 scale_base=math.pow(2., -1. / 16.), lowest_scale=0.125,
+                 **kwargs):
+        super(BobIpFacedetect, self).__init__(**kwargs)
+        self.sampler = Sampler(
+            scale_factor=scale_base, lowest_scale=lowest_scale,
+            distance=distance)
+        if cascade is None:
+            self.cascade = default_cascade()
+        else:
+            self.cascade = Cascade(HDF5File(cascade))
+        self.detection_overlap = detection_overlap
+
+    def annotate(self, image, **kwargs):
+        """Return topleft and bottomright and expected eye positions
+
+        Parameters
+        ----------
+        image : array
+            Image gray scale.
+        **kwargs
+            Ignored.
+
+        Returns
+        -------
+        dict
+            The annotations in a dictionary. The keys are topleft, bottomright,
+            quality, leye, reye.
+        """
+        if image.ndim != 2:
+            raise ValueError("The image must be gray scale (two dimensions).")
+        bounding_box, quality = detect_single_face(
+            image, self.cascade, self.sampler, self.detection_overlap)
+        landmarks = expected_eye_positions(bounding_box)
+        landmarks.update(bounding_box_to_annotations(bounding_box))
+        landmarks['quality'] = quality
+        return landmarks
diff --git a/bob/bio/face/annotator/bobipflandmark.py b/bob/bio/face/annotator/bobipflandmark.py
new file mode 100644
index 0000000000000000000000000000000000000000..414c2d66b1fca3f2009d3fd3ace4f34a1fdb4058
--- /dev/null
+++ b/bob/bio/face/annotator/bobipflandmark.py
@@ -0,0 +1,46 @@
+from . import Base
+from bob.ip.flandmark import Flandmark
+
+
+class BobIpFlandmark(Base):
+    """Annotator using bob.ip.flandmark.
+    This annotator needs the topleft and bottomright annotations provided."""
+
+    def __init__(self, **kwargs):
+        super(BobIpFlandmark, self).__init__(**kwargs)
+        self.flandmark = Flandmark()
+
+    def annotate(self, image, annotations, **kwargs):
+        """Annotates a gray-scale image
+
+        Parameters
+        ----------
+        image : array
+            Image in gray-scale.
+        annotations : dict
+            The topleft and bottomright annotations are required.
+        **kwargs
+            Ignored.
+
+        Returns
+        -------
+        dict
+            Annotations with reye and leye keys or an empty dict if it fails.
+        """
+        top, left = annotations['topleft']
+        top, left = max(top, 0), max(left, 0)
+        height = annotations['bottomright'][0] - top
+        width = annotations['bottomright'][1] - left
+        height, width = min(height, image.shape[0]), min(width, image.shape[1])
+
+        landmarks = self.flandmark.locate(image, top, left, height, width)
+
+        if landmarks is not None and len(landmarks):
+            return {
+                'reye': ((landmarks[1][0] + landmarks[5][0]) / 2.,
+                         (landmarks[1][1] + landmarks[5][1]) / 2.),
+                'leye': ((landmarks[2][0] + landmarks[6][0]) / 2.,
+                         (landmarks[2][1] + landmarks[6][1]) / 2.)
+            }
+        else:
+            return {}
diff --git a/bob/bio/face/annotator/bobipmtcnn.py b/bob/bio/face/annotator/bobipmtcnn.py
new file mode 100644
index 0000000000000000000000000000000000000000..32669d953367955d67168bdceb63e680ddd733f5
--- /dev/null
+++ b/bob/bio/face/annotator/bobipmtcnn.py
@@ -0,0 +1,15 @@
+from . import Base, bounding_box_to_annotations
+from bob.ip.mtcnn import FaceDetector
+
+
+class BobIpMTCNN(Base):
+    """Annotator using bob.ip.mtcnn"""
+
+    def __init__(self, **kwargs):
+        super(BobIpMTCNN, self).__init__(**kwargs)
+        self.detector = FaceDetector()
+
+    def annotate(self, image, **kwargs):
+        bounding_box, landmarks = self.detector.detect_single_face(image)
+        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
new file mode 100644
index 0000000000000000000000000000000000000000..92daef2fa1c7fb22f97c798a1bc601f1a716300c
--- /dev/null
+++ b/bob/bio/face/script/annotate.py
@@ -0,0 +1,80 @@
+"""A script to help annotate databases.
+"""
+import logging
+import json
+import click
+from os.path import dirname
+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.all_files(groups=None)
+    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("Processing %d files ...", total)
+    for i, biofile in enumerate(biofiles):
+        logger.info(
+            "Extracting annotations for file %d out of %d", i + 1, total)
+        data = annotator.read_original_data(
+            biofile, database.original_directory, database.original_extension)
+        annot = annotator(data, annotations=database.annotations(biofile))
+        outpath = biofile.make_path(output_dir, '.json')
+        create_directories_safe(dirname(outpath))
+        with open(outpath, 'w') as f:
+            json.dump(annot, f)
diff --git a/setup.py b/setup.py
index e0e5768d9cc0d6326553634cf47a3e230eea933d..1fef71cafc4f0402b3845d77a5e923f2fae69f15 100644
--- a/setup.py
+++ b/setup.py
@@ -180,6 +180,11 @@ setup(
             'histogram         = bob.bio.face.config.algorithm.histogram:algorithm',  # LGBPHS histograms
             '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