diff --git a/bob/pad/face/config/oulunpu.py b/bob/pad/face/config/oulunpu.py
new file mode 100644
index 0000000000000000000000000000000000000000..69bedd1fe6fa8a1caaf8865c72fa533f7982eb18
--- /dev/null
+++ b/bob/pad/face/config/oulunpu.py
@@ -0,0 +1,25 @@
+"""The `OULU-NPU`_ Database.
+A mobile face presentation attack database with real-world variations database.
+
+To configure the location of the database on your computer, run::
+
+    bob config set bob.db.oulunpu.directory /path/to/oulunpu/database
+
+
+If you use this database, please cite the following publication::
+
+    @INPROCEEDINGS{OULU_NPU_2017,
+             author = {Boulkenafet, Z. and Komulainen, J. and Li, Lei. and Feng, X. and Hadid, A.},
+           keywords = {biometrics, face recognition, anti-spoofing, presentation attack, generalization, colour texture},
+              month = May,
+              title = {{OULU-NPU}: A mobile face presentation attack database with real-world variations},
+            journal = {IEEE International Conference on Automatic Face and Gesture Recognition},
+               year = {2017},
+    }
+
+
+.. include:: links.rst
+"""
+from bob.pad.face.database import OulunpuPadDatabase
+
+database = OulunpuPadDatabase()
diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py
index 082597cb446ca2962c26cbf15020d81cea568937..b842785c16887e19c0bc016b5ff249b53ab98a19 100644
--- a/bob/pad/face/database/__init__.py
+++ b/bob/pad/face/database/__init__.py
@@ -6,6 +6,7 @@ from .maskattack import MaskAttackPadDatabase
 from .replay_attack import ReplayAttackPadDatabase
 from .replay_mobile import ReplayMobilePadDatabase
 from .swan import SwanPadDatabase
+from .oulunpu import OulunpuPadDatabase
 
 
 # gets sphinx autodoc done right - don't remove it
@@ -32,6 +33,7 @@ __appropriate__(
     CasiaSurfPadDatabase,
     CasiaFasdPadDatabase,
     SwanPadDatabase,
+    OulunpuPadDatabase,
 )
 
 __all__ = [_ for _ in dir() if not _.startswith('_')]
diff --git a/bob/pad/face/database/oulunpu.py b/bob/pad/face/database/oulunpu.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1b56a023ee882fa8c252860648db0f541597a6f
--- /dev/null
+++ b/bob/pad/face/database/oulunpu.py
@@ -0,0 +1,55 @@
+import logging
+
+from bob.extension import rc
+from bob.extension.download import get_file
+from bob.pad.base.database import FileListPadDatabase
+from bob.pad.face.database import VideoPadSample
+
+logger = logging.getLogger(__name__)
+
+
+def OulunpuPadDatabase(
+    protocol="Protocol_1",
+    selection_style=None,
+    max_number_of_frames=None,
+    step_size=None,
+    annotation_directory=None,
+    annotation_type=None,
+    fixed_positions=None,
+    **kwargs,
+):
+    name = "pad-face-oulunpu-75221078.tar.gz"
+    dataset_protocols_path = get_file(
+        name,
+        [f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{name}"],
+        cache_subdir="protocols",
+        file_hash="75221078",
+    )
+
+    if annotation_directory is None:
+        name = "annotations-oulunpu-mtcnn-3bee9b6d.tar.xz"
+        annotation_directory = get_file(
+            name,
+            [f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{name}"],
+            cache_subdir="annotations",
+            file_hash="3bee9b6d",
+        )
+        annotation_type = "eyes-center"
+
+    transformer = VideoPadSample(
+        original_directory=rc.get("bob.db.oulunpu.directory"),
+        annotation_directory=annotation_directory,
+        selection_style=selection_style,
+        max_number_of_frames=max_number_of_frames,
+        step_size=step_size,
+    )
+
+    database = FileListPadDatabase(
+        dataset_protocols_path,
+        protocol,
+        transformer=transformer,
+        **kwargs,
+    )
+    database.annotation_type = annotation_type
+    database.fixed_positions = fixed_positions
+    return database
diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py
index c1162b248d5bbfa42c06b807befa7e0a62dbb068..3c5fba90acc6ebcace6565f44155d21a0fb55af9 100644
--- a/bob/pad/face/test/test_databases.py
+++ b/bob/pad/face/test/test_databases.py
@@ -39,7 +39,9 @@ def test_replayattack():
 
     sample = database.sort(database.samples())[0]
     try:
-        assert sample.annotations["0"] == {
+        annot = dict(sample.annotations["0"])
+        assert annot["leye"][1] > annot["reye"][1], annot
+        assert annot == {
             "bottomright": [213, 219],
             "topleft": [58, 108],
             "leye": [118, 190],
@@ -76,7 +78,9 @@ def test_replaymobile():
 
     sample = database.sort(database.samples())[0]
     try:
-        assert sample.annotations["0"] == {
+        annot = dict(sample.annotations["0"])
+        assert annot["leye"][1] > annot["reye"][1], annot
+        assert annot == {
             "bottomright": [760, 498],
             "topleft": [374, 209],
             "leye": [518, 417],
@@ -211,7 +215,9 @@ def test_swan():
 
     sample = database.sort(database.samples())[0]
     try:
-        assert dict(sample.annotations["0"]) == {
+        annot = dict(sample.annotations["0"])
+        assert annot["leye"][1] > annot["reye"][1], annot
+        assert annot == {
             "bottomright": [849, 564],
             "leye": [511, 453],
             "mouthleft": [709, 271],
@@ -224,3 +230,61 @@ def test_swan():
         assert sample.data[0][0, 0, 0] == 87
     except RuntimeError as e:
         raise SkipTest(e)
+
+
+def test_oulunpu():
+    database = bob.bio.base.load_resource(
+        "oulunpu",
+        "database",
+        preferred_package="bob.pad.face",
+        package_prefix="bob.pad.",
+    )
+
+    assert database.protocols() == [
+        "Protocol_1",
+        "Protocol_1_2",
+        "Protocol_1_3",
+        "Protocol_2",
+        "Protocol_3_1",
+        "Protocol_3_2",
+        "Protocol_3_3",
+        "Protocol_3_4",
+        "Protocol_3_5",
+        "Protocol_3_6",
+        "Protocol_4_1",
+        "Protocol_4_2",
+        "Protocol_4_3",
+        "Protocol_4_4",
+        "Protocol_4_5",
+        "Protocol_4_6",
+    ]
+    assert database.groups() == ["dev", "eval", "train"]
+    assert len(database.samples(groups=["train", "dev", "eval"])) == 1200 + 900 + 600
+    assert len(database.samples(groups=["train", "dev"])) == 1200 + 900
+    assert len(database.samples(groups=["train"])) == 1200
+    assert (
+        len(database.samples(groups=["train", "dev", "eval"], purposes="real"))
+        == 240 + 180 + 120
+    )
+    assert (
+        len(database.samples(groups=["train", "dev", "eval"], purposes="attack"))
+        == 960 + 720 + 480
+    )
+
+    sample = database.sort(database.samples())[0]
+    try:
+        annot = dict(sample.annotations["0"])
+        assert annot["leye"][1] > annot["reye"][1], annot
+        assert annot == {
+            "bottomright": [1124, 773],
+            "leye": [818, 638],
+            "mouthleft": [1005, 489],
+            "mouthright": [1000, 634],
+            "nose": [906, 546],
+            "reye": [821, 470],
+            "topleft": [632, 394],
+        }
+        assert sample.data.shape == (20, 3, 1920, 1080)
+        assert sample.data[0][0, 0, 0] == 195
+    except RuntimeError as e:
+        raise SkipTest(e)
diff --git a/doc/links.rst b/doc/links.rst
index 09f5c2d7d96c5e751167f21500362038c7539c23..5c51ec6cc7d0bb7cb2a4e4034d8220206c431ef6 100644
--- a/doc/links.rst
+++ b/doc/links.rst
@@ -15,3 +15,4 @@
 .. _MIFS: http://www.antitza.com/makeup-datasets.html
 .. _CELEBA: http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html
 .. _Swan: https://www.idiap.ch/dataset/swan
+.. _oulu-npu: https://sites.google.com/site/oulunpudatabase/
diff --git a/setup.py b/setup.py
index 10450c19bf771fcfeab77ac8fdfebb013ac778c9..f14cecf4057cc447451a8941a3699920d5311012 100644
--- a/setup.py
+++ b/setup.py
@@ -63,6 +63,7 @@ setup(
             "casiasurf-color = bob.pad.face.config.casiasurf_color:database",
             "casiasurf = bob.pad.face.config.casiasurf:database",
             "swan = bob.pad.face.config.swan:database",
+            "oulunpu = bob.pad.face.config.oulunpu:database",
         ],
         # registered configurations:
         "bob.pad.config": [
@@ -74,6 +75,7 @@ setup(
             "casiasurf-color = bob.pad.face.config.casiasurf_color",
             "casiasurf = bob.pad.face.config.casiasurf",
             "swan = bob.pad.face.config.swan",
+            "oulunpu = bob.pad.face.config.oulunpu",
             # LBPs
             "lbp = bob.pad.face.config.lbp_64",
             # quality measure