diff --git a/bob/pad/face/test/test_utils.py b/bob/pad/face/test/test_utils.py
index 539aba19ea0216a6ed61c8fa05bced35a5a0cafa..ade5e9a64803b95af736b789a9c4de2702fbabf4 100644
--- a/bob/pad/face/test/test_utils.py
+++ b/bob/pad/face/test/test_utils.py
@@ -9,6 +9,10 @@ image = padfile.load(Database().original_directory,
                      Database().original_extension)[0][1]
 
 
+def dummy_cropper(frame, annotations=None):
+    return frame
+
+
 def _annotations(self, padfile):
     return {'0': {'topleft': (0, 0), 'bottomright': self.frame_shape}}
 
@@ -24,7 +28,7 @@ def test_yield_frames():
 @raises(ValueError)
 def test_yield_faces_1():
     database = Database()
-    for face in yield_faces(database, padfile):
+    for face in yield_faces(database, padfile, dummy_cropper):
         pass
 
 
@@ -32,7 +36,8 @@ def test_yield_faces_2():
     database = Database()
     database.annotations = MethodType(
         _annotations, database)
-    for face in yield_faces(database, padfile):
+    assert len(list(yield_faces(database, padfile, dummy_cropper)))
+    for face in yield_faces(database, padfile, dummy_cropper):
         assert face.ndim == 2
         assert face.shape == database.frame_shape
 
diff --git a/bob/pad/face/utils/__init__.py b/bob/pad/face/utils/__init__.py
index fe90985c44f031beee8771dcc8ec134836f5907b..4274408459b2d3c87d8eb51a54c47817510b813a 100644
--- a/bob/pad/face/utils/__init__.py
+++ b/bob/pad/face/utils/__init__.py
@@ -1,5 +1,7 @@
-from .load_utils import (frames, number_of_frames, yield_frames,
-                         normalize_detections, yield_faces, scale_face, blocks)
+from .load_utils import (
+    frames, number_of_frames, yield_frames, yield_faces, scale_face, blocks,
+    bbx_cropper, min_face_size_normalizer, color_augmentation,
+    the_giant_video_loader)
 
 # gets sphinx autodoc done right - don't remove it
 __all__ = [_ for _ in dir() if not _.startswith('_')]
diff --git a/bob/pad/face/utils/load_utils.py b/bob/pad/face/utils/load_utils.py
index 64e42e964da4e0f47d8c2780538fd6427f9e41af..69725e07e47f75530e5783eea5f9b4438c46c233 100644
--- a/bob/pad/face/utils/load_utils.py
+++ b/bob/pad/face/utils/load_utils.py
@@ -1,6 +1,10 @@
+from bob.bio.face.annotator import min_face_size_validator
+from bob.bio.video.annotator import normalize_annotations
 from bob.io.video import reader
 from bob.ip.base import scale, block, block_output_shape
+from bob.ip.color import rgb_to_yuv, rgb_to_hsv
 from bob.ip.facedetect import bounding_box_from_annotation
+from functools import partial
 import numpy
 import six
 
@@ -19,8 +23,7 @@ def frames(path):
         A frame of the video. The size is (3, 240, 320).
     """
     video = reader(path)
-    for frame in video:
-        yield frame
+    return iter(video)
 
 
 def number_of_frames(path):
@@ -56,55 +59,21 @@ def yield_frames(paddb, padfile):
     :any:`numpy.array`
         Frames of the PAD file one by one.
     """
-    frames = paddb.frames(padfile)
-    for image in frames:
-        yield image
+    return paddb.frames(padfile)
 
 
-def normalize_detections(detections, nframes, max_age=-1, faceSizeFilter=0):
-    """Calculates a list of "nframes" with the best possible detections taking
-    into consideration the ages of the last valid detection on the detections
-    list.
+def bbx_cropper(frame, annotations):
+    bbx = bounding_box_from_annotation(**annotations)
+    return frame[..., bbx.top:bbx.bottom, bbx.left:bbx.right]
 
-    Parameters
-    ----------
-    detections : dict
-        A dictionary containing keys that indicate the frame number of the
-        detection and a value which is a BoundingBox object.
-
-    nframes : int
-        An integer indicating how many frames has the video that will be
-        analyzed.
-
-    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
-
-    faceSizeFilter : :obj:`int`, optional
-        The minimum required size of face height (in pixels)
-
-    Yields
-    ------
-    object
-        The bounding box or None.
-    """
-    curr = None
-    age = 0
 
-    for k in range(nframes):
-        if detections and k in detections and \
-                (detections[k].size[0] > faceSizeFilter):
-            curr = detections[k]
-            age = 0
-        elif max_age < 0 or age < max_age:
-            age += 1
-        else:  # no detections and age is larger than maximum allowed
-            curr = None
+def min_face_size_normalizer(annotations, max_age=15, **kwargs):
+    return normalize_annotations(annotations,
+                                 partial(min_face_size_validator, **kwargs),
+                                 max_age=max_age)
 
-        yield curr
 
-
-def yield_faces(database, padfile, **kwargs):
+def yield_faces(database, padfile, cropper, normalizer=None):
     """Yields face images of a padfile. It uses the annotations from the
     database. The annotations are further normalized.
 
@@ -115,8 +84,12 @@ def yield_faces(database, padfile, **kwargs):
         `frames` method.
     padfile : :any:`bob.pad.base.database.PadFile`
         The padfile to return the faces.
-    **kwargs
-        They are passed to :any:`normalize_detections`.
+    cropper : callable
+        A face image cropper that works with database's annotations.
+    normalizer : callable
+        If not None, it should be a function that takes all the annotations of
+        the whole video and yields normalized annotations frame by frame. It
+        should yield same as ``annotations.items()``.
 
     Yields
     ------
@@ -129,20 +102,25 @@ def yield_faces(database, padfile, **kwargs):
         If the database returns None for annotations.
     """
     frames_gen = database.frames(padfile)
-    nframes = database.number_of_frames(padfile)
+
     # read annotation
-    annots = database.annotations(padfile)
-    if annots is None:
+    annotations = database.annotations(padfile)
+    if annotations is None:
         raise ValueError("No annotations were returned.")
-    # normalize annotations
-    annots = {int(k): bounding_box_from_annotation(**v)
-              for k, v in six.iteritems(annots)}
-    bounding_boxes = normalize_detections(annots, nframes, **kwargs)
-    for frame, bbx in six.moves.zip(frames_gen, bounding_boxes):
-        if bbx is None:
+
+    if normalizer is None:
+        annotations_gen = annotations.items()
+    else:
+        annotations_gen = normalizer(annotations)
+
+    # normalize annotations and crop faces
+    for _, annot in annotations_gen:
+        frame = six.next(frames_gen)
+        if annot is None:
             continue
-        face = frame[..., bbx.top:bbx.bottom, bbx.left:bbx.right]
-        yield face
+        face = cropper(frame, annotations=annot)
+        if face is not None:
+            yield face
 
 
 def scale_face(face, face_height, face_width=None):
@@ -214,3 +192,75 @@ def blocks(data, block_size, block_overlap=(0, 0)):
     else:
         raise ValueError("Unknown data dimension {}".format(data.ndim))
     return output
+
+
+def color_augmentation(image, channels=('rgb',)):
+    """Converts an RGB image to different color channels.
+
+    Parameters
+    ----------
+    image : numpy.array
+        The image in RGB Bob format.
+    channels : :obj:`tuple`, optional
+        List of channels to convert the image to. It can be any of ``rgb``,
+        ``yuv``, ``hsv``.
+
+    Returns
+    -------
+    numpy.array
+        The image that contains several channels:
+        ``(3*len(channels), height, width)``.
+    """
+    final_image = []
+
+    if 'rgb' in channels:
+        final_image.append(image)
+
+    if 'yuv' in channels:
+        final_image.append(rgb_to_yuv(image))
+
+    if 'hsv' in channels:
+        final_image.append(rgb_to_hsv(image))
+
+    return numpy.concatenate(final_image, axis=0)
+
+
+def _random_sample(A, size):
+    return A[numpy.random.choice(A.shape[0], size, replace=False), ...]
+
+
+def the_giant_video_loader(paddb, padfile,
+                           region='whole', scaling_factor=None, cropper=None,
+                           normalizer=None, patches=False,
+                           block_size=(96, 96), block_overlap=(0, 0),
+                           random_patches_per_frame=None, augment=None,
+                           multiple_bonafide_patches=1):
+    if region == 'whole':
+        generator = yield_frames(paddb, padfile)
+    elif region == 'crop':
+        generator = yield_faces(
+            paddb, padfile, cropper=cropper, normalizer=normalizer)
+    else:
+        raise ValueError("Invalid region value: `{}'".format(region))
+
+    if scaling_factor is not None:
+        generator = (scale(frame, scaling_factor)
+                     for frame in generator)
+    if patches:
+        if random_patches_per_frame is None:
+            generator = (
+                patch for frame in generator
+                for patch in blocks(frame, block_size, block_overlap))
+        else:
+            if padfile.attack_type is None:
+                random_patches_per_frame *= multiple_bonafide_patches
+            generator = (
+                patch for frame in generator
+                for patch in _random_sample(
+                    blocks(frame, block_size, block_overlap),
+                    random_patches_per_frame))
+
+    if augment is not None:
+        generator = (augment(frame) for frame in generator)
+
+    return generator
diff --git a/doc/api.rst b/doc/api.rst
index e085abd029c89a2c9a42f2fe0096009d5b015e66..fe623bb207a004423bff797e55c1e5653bc886f3 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -71,11 +71,14 @@ Utilities
 ---------
 
 .. autosummary::
+   bob.pad.face.utils.bbx_cropper
    bob.pad.face.utils.blocks
+   bob.pad.face.utils.color_augmentation
    bob.pad.face.utils.frames
-   bob.pad.face.utils.normalize_detections
+   bob.pad.face.utils.min_face_size_normalizer
    bob.pad.face.utils.number_of_frames
    bob.pad.face.utils.scale_face
+   bob.pad.face.utils.the_giant_video_loader
    bob.pad.face.utils.yield_faces
    bob.pad.face.utils.yield_frames