Commit 03d4ac4e authored by Anjith GEORGE's avatar Anjith GEORGE

Merge branch 'databases' into 'master'

Improve high level database interfaces

See merge request !104
parents 52dd9734 b3fb6d49
Pipeline #36881 passed with stages
in 20 minutes and 43 seconds
This diff is collapsed.
......@@ -8,6 +8,8 @@ from bob.pad.base.database import PadDatabase
from bob.pad.face.database import VideoPadFile
from bob.db.base.utils import (
check_parameter_for_validity, check_parameters_for_validity)
from bob.db.base.annotations import read_annotation_file
from bob.ip.facedetect import expected_eye_positions, BoundingBox
import numpy
import os
......@@ -20,7 +22,7 @@ class CasiaFasdPadFile(VideoPadFile):
A high level implementation of the File class for the CASIA_FASD database.
"""
def __init__(self, f, original_directory=None):
def __init__(self, f, original_directory=None, annotation_directory=None):
"""
Parameters
----------
......@@ -31,6 +33,7 @@ class CasiaFasdPadFile(VideoPadFile):
self.f = f
self.original_directory = original_directory
self.annotation_directory = annotation_directory
if f.is_real():
attack_type = None
......@@ -93,6 +96,10 @@ class CasiaFasdPadFile(VideoPadFile):
def annotations(self):
"""Reads the annotations
If the file object has an attribute of annotation_directory, it will read
annotations from there instead of loading annotations that are shipped with the
database.
Returns
-------
annotations : :py:class:`dict`
......@@ -103,6 +110,10 @@ class CasiaFasdPadFile(VideoPadFile):
is the dictionary defining the coordinates of the face bounding box
in frame N.
"""
if self.annotation_directory is not None:
path = self.make_path(self.annotation_directory, extension=".json")
return read_annotation_file(path, annotation_type="json")
annots = self.f.bbx()
annotations = {}
for i, v in enumerate(annots):
......@@ -110,6 +121,9 @@ class CasiaFasdPadFile(VideoPadFile):
bottomright = (v[2] + v[4], v[1] + v[3])
annotations[str(i)] = {'topleft': topleft,
'bottomright': bottomright}
size = (bottomright[0] - topleft[0], bottomright[1] - topleft[1])
bounding_box = BoundingBox(topleft, size)
annotations[str(i)].update(expected_eye_positions(bounding_box))
return annotations
def load(self, directory=None, extension='.avi',
......@@ -149,6 +163,7 @@ class CasiaFasdPadDatabase(PadDatabase):
# grandtest is the new modified protocol for this database
protocol='grandtest',
original_directory=rc['bob.db.casia_fasd.directory'],
annotation_directory=None,
**kwargs):
"""
Parameters
......@@ -169,6 +184,7 @@ class CasiaFasdPadDatabase(PadDatabase):
protocol=protocol,
original_directory=original_directory,
original_extension='.avi',
annotation_directory=annotation_directory,
training_depends_on_protocol=True,
**kwargs)
......@@ -280,7 +296,8 @@ class CasiaFasdPadDatabase(PadDatabase):
db_mappings[t + '_' + q])
files.append(CasiaFasdPadFile(
File(filename, c, grp),
self.original_directory))
original_directory=self.original_directory,
annotation_directory=self.annotation_directory))
return files
def annotations(self, padfile):
......
from bob.pad.base.database import PadFile
import bob.bio.video
import bob.io.video
from bob.db.base.annotations import read_annotation_file
class VideoPadFile(PadFile):
......@@ -9,14 +11,15 @@ class VideoPadFile(PadFile):
def __init__(self, attack_type, client_id, path, file_id=None):
super(VideoPadFile, self).__init__(
attack_type=attack_type,
client_id=client_id,
path=path,
file_id=file_id,
attack_type=attack_type, client_id=client_id, path=path, file_id=file_id
)
def load(self, directory=None, extension='.avi',
frame_selector=bob.bio.video.FrameSelector(selection_style='all')):
def load(
self,
directory=None,
extension=".avi",
frame_selector=bob.bio.video.FrameSelector(selection_style="all"),
):
"""Loads the video file and returns in a :any:`bob.bio.video.FrameContainer`.
Parameters
......@@ -34,3 +37,97 @@ class VideoPadFile(PadFile):
The loaded frames inside a frame container.
"""
return frame_selector(self.make_path(directory, extension))
def check_original_directory_and_extension(self):
if not hasattr(self, "original_directory"):
raise RuntimeError(
"Please set the original_directory attribute of files in your "
"database's object method."
)
if not hasattr(self, "original_extension"):
raise RuntimeError(
"Please set the original_extension attribute of files in your "
"database's object method."
)
@property
def frames(self):
"""Returns an iterator of frames in the video.
If your database video files need to be loaded in a special way, you need to
override this property.
Returns
-------
collection.Iterator
An iterator returning frames of the video.
Raises
------
RuntimeError
In your database implementation, the original_directory and
original_extension attributes of the files need to be set when database's
object method is called.
"""
self.check_original_directory_and_extension()
path = self.make_path(
directory=self.original_directory, extension=self.original_extension
)
return iter(bob.io.video.reader(path))
@property
def number_of_frames(self):
self.check_original_directory_and_extension()
path = self.make_path(
directory=self.original_directory, extension=self.original_extension
)
return bob.io.video.reader(path).number_of_frames
@property
def frame_shape(self):
"""Returns the size of each frame in this database.
This implementation assumes all videos and frames have the same shape.
It's best to override this method in your database implementation and return
a constant.
Returns
-------
(int, int, int)
The (Channels, Height, Width) sizes.
"""
self.check_original_directory_and_extension()
path = self.make_path(
directory=self.original_directory, extension=self.original_extension
)
frame = next(bob.io.video.reader(path))
return frame.shape
@property
def annotations(self):
"""Reads the annotations
For this property to work, you need to set ``annotation_directory``,
``annotation_extension``, and ``annotation_type`` attributes of the files when
database's object method is called.
Returns
-------
dict
The annotations as a dictionary.
"""
if not (
hasattr(self, "annotation_directory")
and hasattr(self, "annotation_extension")
and hasattr(self, "annotation_type")
):
raise RuntimeError(
"For this property to work, you need to set ``annotation_directory``, "
"``annotation_extension``, and ``annotation_type`` attributes of the "
"files when database's object method is called."
)
if self.annotation_directory is None:
return None
annotation_file = self.make_path(
directory=self.annotation_directory, extension=self.annotation_extension
)
return read_annotation_file(annotation_file, self.annotation_type)
......@@ -5,7 +5,7 @@ import os
import numpy as np
import bob.io.video
from bob.bio.video import FrameSelector, FrameContainer
from bob.pad.face.database import VideoPadFile
from bob.pad.face.database import VideoPadFile
from bob.pad.base.database import PadDatabase
class MaskAttackPadFile(VideoPadFile):
......@@ -17,18 +17,18 @@ class MaskAttackPadFile(VideoPadFile):
f : :py:class:`object`
An instance of the File class defined in the low level db interface
of the 3DMAD database, in the bob.db.maskattack.models.py file.
"""
def __init__(self, f):
"""Init function
Parameters
----------
f : :py:class:`object`
An instance of the File class defined in the low level db interface
of the 3DMAD database, in the bob.db.maskattack.models.py file.
"""
self.f = f
if f.is_real():
......@@ -48,10 +48,10 @@ class MaskAttackPadFile(VideoPadFile):
Parameters
----------
directory : :py:class:`str`
String containing the path to the 3DMAD database
String containing the path to the 3DMAD database
(generated sequences from original data).
extension : :py:class:`str`
Extension of the video files
Extension of the video files
frame_selector : :py:class:`bob.bio.video.FrameSelector`
The frame selector to use.
......@@ -59,7 +59,7 @@ class MaskAttackPadFile(VideoPadFile):
-------
video_data : :py:class:`bob.bio.video.utils.FrameContainer`
video data stored in a FrameContainer
"""
vfilename = self.make_path(directory, extension)
video = bob.io.video.reader(vfilename)
......@@ -69,7 +69,7 @@ class MaskAttackPadFile(VideoPadFile):
class MaskAttackPadDatabase(PadDatabase):
"""High level implementation of the Database class for the 3DMAD database.
Attributes
----------
db : :py:class:`bob.db.maskattack.Database`
......@@ -92,12 +92,12 @@ class MaskAttackPadDatabase(PadDatabase):
The directory where the original data of the database are stored.
original_extension : :py:class:`str`
The file name extension of the original data.
"""
from bob.db.maskattack import Database as LowLevelDatabase
self.db = LowLevelDatabase()
self.low_level_group_names = ('world', 'dev', 'test')
self.low_level_group_names = ('world', 'dev', 'test')
self.high_level_group_names = ('train', 'dev', 'eval')
super(MaskAttackPadDatabase, self).__init__(
......@@ -147,13 +147,13 @@ class MaskAttackPadDatabase(PadDatabase):
groups = self.convert_names_to_lowlevel(groups, self.low_level_group_names, self.high_level_group_names)
if groups is not None:
# for training
lowlevel_purposes = []
if 'world' in groups and purposes == 'real':
lowlevel_purposes.append('trainReal')
lowlevel_purposes.append('trainReal')
if 'world' in groups and purposes == 'attack':
lowlevel_purposes.append('trainMask')
lowlevel_purposes.append('trainMask')
# for dev and eval
if ('dev' in groups or 'test' in groups) and purposes == 'real':
......@@ -163,10 +163,14 @@ class MaskAttackPadDatabase(PadDatabase):
files = self.db.objects(sets=groups, purposes=lowlevel_purposes, **kwargs)
files = [MaskAttackPadFile(f) for f in files]
# set the attributes
for f in files:
f.original_directory = self.original_directory
f.original_extension = self.original_extension
return files
def annotations(self, file):
"""Return annotations for a given file object.
......
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#==============================================================================
# Used in ReplayMobilePadFile class
from bob.bio.video import FrameSelector, FrameContainer
from bob.pad.face.database import VideoPadFile # Used in MsuMfsdPadFile class
from bob.pad.base.database import PadDatabase
from bob.extension import rc
import os
import numpy as np
#==============================================================================
class MsuMfsdPadFile(VideoPadFile):
"""
A high level implementation of the File class for the MSU MFSD database.
......@@ -40,18 +34,20 @@ class MsuMfsdPadFile(VideoPadFile):
if f.is_real():
attack_type = None
else:
attack_type = 'attack'
attack_type = "attack"
# attack_type is a string and I decided to make it like this for this
# particular database. You can do whatever you want for your own database.
super(MsuMfsdPadFile, self).__init__(
client_id=f.client_id,
path=f.path,
attack_type=attack_type,
file_id=f.id)
#==========================================================================
def load(self, directory=None, extension=None, frame_selector=FrameSelector(selection_style='all')):
client_id=f.client_id, path=f.path, attack_type=attack_type, file_id=f.id
)
def load(
self,
directory=None,
extension=None,
frame_selector=FrameSelector(selection_style="all"),
):
"""
Overridden version of the load method defined in the ``VideoPadFile``.
......@@ -76,26 +72,27 @@ class MsuMfsdPadFile(VideoPadFile):
for further details.
"""
_, extension = os.path.splitext(
self.f.videofile()) # get file extension
_, extension = os.path.splitext(self.f.videofile()) # get file extension
video_data_array = self.f.load(
directory=directory, extension=extension)
video_data_array = self.f.load(directory=directory, extension=extension)
return frame_selector(video_data_array)
#==============================================================================
class MsuMfsdPadDatabase(PadDatabase):
"""
A high level implementation of the Database class for the MSU MFSD database.
"""
def __init__(
self,
protocol='grandtest', # grandtest is the default protocol for this database
original_directory=None,
original_extension=None,
**kwargs):
self,
protocol="grandtest", # grandtest is the default protocol for this database
original_directory=None,
original_extension=None,
annotation_directory=None,
annotation_extension='.json',
annotation_type='json',
**kwargs
):
"""
**Parameters:**
......@@ -119,19 +116,24 @@ class MsuMfsdPadDatabase(PadDatabase):
# Since the high level API expects different group names than what the low
# level API offers, you need to convert them when necessary
self.low_level_group_names = (
'train', 'devel',
'test') # group names in the low-level database interface
"train",
"devel",
"test",
) # group names in the low-level database interface
self.high_level_group_names = (
'train', 'dev',
'eval') # names are expected to be like that in objects() function
"train",
"dev",
"eval",
) # names are expected to be like that in objects() function
# Always use super to call parent class methods.
super(MsuMfsdPadDatabase, self).__init__(
name='msu-mfsd',
name="msu-mfsd",
protocol=protocol,
original_directory=original_directory,
original_extension=original_extension,
**kwargs)
**kwargs
)
@property
def original_directory(self):
......@@ -141,13 +143,9 @@ class MsuMfsdPadDatabase(PadDatabase):
def original_directory(self, value):
self.db.original_directory = value
#==========================================================================
def objects(self,
groups=None,
protocol=None,
purposes=None,
model_ids=None,
**kwargs):
def objects(
self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs
):
"""
This function returns lists of MsuMfsdPadFile objects, which fulfill the given restrictions.
......@@ -179,16 +177,21 @@ class MsuMfsdPadDatabase(PadDatabase):
# Convert group names to low-level group names here.
groups = self.convert_names_to_lowlevel(
groups, self.low_level_group_names, self.high_level_group_names)
groups, self.low_level_group_names, self.high_level_group_names
)
# Since this database was designed for PAD experiments, nothing special
# needs to be done here.
files = self.db.objects(group=groups, cls=purposes, **kwargs)
files = [MsuMfsdPadFile(f) for f in files]
for f in files:
f.original_directory = self.original_directory
f.annotation_directory = self.annotation_directory
f.annotation_extension = self.annotation_extension
f.annotation_type = self.annotation_type
return files
#==========================================================================
def annotations(self, f):
"""
Return annotations for a given file object ``f``, which is an instance
......@@ -220,12 +223,14 @@ class MsuMfsdPadDatabase(PadDatabase):
for frame_annots in annots:
topleft = (np.int(frame_annots[2]), np.int(frame_annots[1]))
bottomright = (np.int(frame_annots[2] + frame_annots[4]),
np.int(frame_annots[1] + frame_annots[3]))
bottomright = (
np.int(frame_annots[2] + frame_annots[4]),
np.int(frame_annots[1] + frame_annots[3]),
)
annotations[str(np.int(frame_annots[0]))] = {
'topleft': topleft,
'bottomright': bottomright
"topleft": topleft,
"bottomright": bottomright,
}
return annotations
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Used in ReplayMobilePadFile class
from bob.pad.base.database import PadDatabase
from bob.pad.base.database import PadDatabase # Used in ReplayMobilePadFile class
from bob.pad.face.database import VideoPadFile # Used in ReplayPadFile class
from bob.pad.face.utils import frames, number_of_frames
from bob.extension import rc
from bob.ip.facedetect import expected_eye_positions, BoundingBox
from bob.db.base.annotations import read_annotation_file
REPLAY_ATTACK_FRAME_SHAPE = (3, 240, 320)
class ReplayPadFile(VideoPadFile):
......@@ -35,16 +37,73 @@ class ReplayPadFile(VideoPadFile):
if f.is_real():
attack_type = None
else:
attack_type = 'attack'
attack_type = "attack"
# attack_type is a string and I decided to make it like this for this
# particular database. You can do whatever you want for your own
# database.
super(ReplayPadFile, self).__init__(
client_id=f.client_id,
path=f.path,
attack_type=attack_type,
file_id=f.id)
client_id=f.client_id, path=f.path, attack_type=attack_type, file_id=f.id
)
@property
def frame_shape(self):
"""Returns the size of each frame in this database.
Returns
-------
(int, int, int)
The (#Channels, Height, Width) which is (3, 240, 320).
"""
return REPLAY_ATTACK_FRAME_SHAPE
@property
def annotations(self):
"""
Return annotations as a dictionary of dictionaries.
If the file object has an attribute of annotation_directory, it will read
annotations from there instead of loading annotations that are shipped with the
database.
Returns
-------
annotations : :py:class:`dict`
A dictionary containing the annotations for each frame in the
video. Dictionary structure:
``annotations = {'1': frame1_dict, '2': frame1_dict, ...}``.Where
``frameN_dict = {'topleft': (row, col), 'bottomright': (row, col)}``
is the dictionary defining the coordinates of the face bounding box
in frame N.
"""
if (
hasattr(self, "annotation_directory")
and self.annotation_directory is not None
):
path = self.make_path(self.annotation_directory, extension=".json")
return read_annotation_file(path, annotation_type="json")
# numpy array containing the face bounding box data for each video
# frame, returned data format described in the f.bbx() method of the
# low level interface
annots = self.f.bbx(directory=self.original_directory)
annotations = {} # dictionary to return
for fn, frame_annots in enumerate(annots):
topleft = (frame_annots[2], frame_annots[1])
bottomright = (
frame_annots[2] + frame_annots[4],
frame_annots[1] + frame_annots[3],
)
annotations[str(fn)] = {"topleft": topleft, "bottomright": bottomright}
size = (bottomright[0] - topleft[0], bottomright[1] - topleft[1])
bounding_box = BoundingBox(topleft, size)
annotations[str(fn)].update(expected_eye_positions(bounding_box))
return annotations
class ReplayPadDatabase(PadDatabase):
......@@ -54,12 +113,14 @@ class ReplayPadDatabase(PadDatabase):
"""
def __init__(
self,
# grandtest is the default protocol for this database
protocol='grandtest',
original_directory=rc['bob.db.replay.directory'],
original_extension='.mov',
**kwargs):
self,
# grandtest is the default protocol for this database
protocol="grandtest",
original_directory=rc["bob.db.replay.directory"],
original_extension=".mov",
annotation_directory=None,
**kwargs
):
"""
Parameters
----------
......@@ -86,19 +147,25 @@ class ReplayPadDatabase(PadDatabase):
# Since the high level API expects different group names than what the
# low level API offers, you need to convert them when necessary
self.low_level_group_names = (
'train', 'devel',
'test') # group names in the low-level database interface
"train",
"devel",
"test",
) # group names in the low-level database interface
self.high_level_group_names = (
'train', 'dev',
'eval') # names are expected to be like that in objects() function
"train",
"dev",
"eval",
) # names are expected to be like that in objects() function
# Always use super to call parent class methods.
super(ReplayPadDatabase, self).__init__(
name='replay',
name="replay",
protocol=protocol,
original_directory=original_directory,
original_extension=original_extension,
**kwargs)
annotation_directory=annotation_directory,
**kwargs
)
@property
def original_directory(self):
......@@ -108,12 +175,9 @@ class ReplayPadDatabase(PadDatabase):
def original_directory(self, value):
self.db.original_directory = value
def objects(self,
groups=None,
protocol=None,
purposes=None,
model_ids=None,
**kwargs):
def objects(
self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs
):
"""
This function returns lists of ReplayPadFile objects, which fulfill the
given restrictions.
......@@ -146,12 +210,19 @@ class ReplayPadDatabase(PadDatabase):
# Convert group names to low-level group names here.
groups = self.convert_names_to_lowlevel(
groups, self.low_level_group_names, self.high_level_group_names)
groups, self.low_level_group_names, self.high_level_group_names
)
# Since this database was designed for PAD experiments, nothing special
# needs to be done here.
files = self.db.objects(
protocol=protocol, groups=groups, cls=purposes, **kwargs)
protocol=protocol, groups=groups, cls=purposes, **kwargs
)
files = [ReplayPadFile(f) for f in files]
# set the attributes
for f in files:
f.original_directory = self.original_directory
f.original_extension = self.original_extension
f.annotation_directory = self.annotation_directory
return files
def annotations(self, f):
......@@ -178,26 +249,7 @@ class ReplayPadDatabase(PadDatabase):
is the dictionary defining the coordinates of the face bounding box
in frame N.
"""
# numpy array containing the face bounding box data for each video
# frame, returned data format described in the f.bbx() method of the
# low level interface
annots = f.f.bbx(directory=self.original_directory)
annotations = {} # dictionary to return
for fn, frame_annots in enumerate(annots):
topleft = (frame_annots[2], frame_annots[1])
bottomright = (frame_annots[2] + frame_annots[4],
frame_annots[1] + frame_annots[3])
annotations[str(fn)] = {
'topleft': topleft,
'bottomright': bottomright
}
return annotations
return f.annotations
def frames(self, padfile):
"""Yields the frames of the padfile one by one.
......@@ -212,11 +264,7 @@ class ReplayPadDatabase(PadDatabase):
:any:`numpy.array`
A frame of the video. The size is (3, 240, 320).
"""
vfilename = padfile.make_path(
directory=self.original_directory,
extension=self.original_extension)
for retval in frames(vfilename):
yield retval
return padfile.frames
def number_of_frames(self, padfile):
"""Returns the number of frames in a video file.
......@@ -231,10 +279,7 @@ class ReplayPadDatabase(PadDatabase):
int
The number of frames.
"""
vfilename = padfile.make_path(
directory=self.original_directory,
extension=self.original_extension)