Commit 85db25fc authored by Olegs NIKISINS's avatar Olegs NIKISINS
Browse files

Added a quality assessment script and config for CelebA quality

parent c243d2ce
Pipeline #20561 passed with stage
in 13 minutes and 34 seconds
......@@ -3,3 +3,5 @@ recursive-include bob/pad/face/lists *.*
recursive-include bob/pad/face/config/preprocessor/dictionaries *.hdf5
recursive-include doc *.py *.rst *.ico *.png
recursive-include bob/pad/face/test/data *.hdf5 *.png
recursive-include bob/pad/face/config *.xml
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Quality assessment configuration file for the CelebA database to be used
with quality assessment script.
Note: this config checks the quality of the preprocessed(!) data. Here the
preprocessed data is sored in ``.hdf5`` files, as a frame container with
one frame. Frame contains a BW image of the facial regions of the size
64x64 pixels.
The config file MUST contain at least the following functions:
``load_datafile(file_name)`` - returns the ``data`` given ``file_name``, and
``assess_quality(data, **assess_quality_kwargs)`` - returns ``True`` for good
quality ``data``, and ``False`` for low quality data, and
``assess_quality_kwargs`` - a dictionary with kwargs for ``assess_quality()``
function.
@author: Olegs Nikisins
"""
# =============================================================================
# Import here:
import pkg_resources
import cv2
from bob.bio.video.preprocessor import Wrapper
import numpy as np
# =============================================================================
def detect_eyes_in_bw_image(image):
"""
Detect eyes in the image using OpenCV.
**Parameters:**
``image`` : 2D :py:class:`numpy.ndarray`
A BW image to detect the eyes in.
**Returns:**
``eyes`` : 2D :py:class:`numpy.ndarray`
An array containing coordinates of the bounding boxes of detected eyes.
The dimensionality of the array:
``num_of_detected_eyes x coordinates_of_bbx``
"""
eye_model = pkg_resources.resource_filename('bob.pad.face.config',
'quality_assessment/models/eye_detector.xml')
eye_cascade = cv2.CascadeClassifier(eye_model)
eyes = eye_cascade.detectMultiScale(image)
return eyes
# =============================================================================
def load_datafile(file_name):
"""
Load data from file given filename. Here the data file is an hdf5 file
containing a framecontainer with one frame. The data in the frame is
a BW image of the facial region.
**Parameters:**
``file_name`` : str
Absolute name of the file.
**Returns:**
``data`` : 2D :py:class:`numpy.ndarray`
Data array containing the image of the facial region.
"""
frame_container = Wrapper().read_data(file_name)
data = frame_container[0][1]
return data
# =============================================================================
face_size = 64
eyes_distance=((face_size + 1) / 2.)
eyes_center=(face_size / 4., (face_size - 0.5) / 2.)
eyes_expected = [[eyes_center[0], eyes_center[1]-eyes_distance/2.],
[eyes_center[0], eyes_center[1]+eyes_distance/2.]]
assess_quality_kwargs = {}
assess_quality_kwargs["eyes_expected"] = eyes_expected
assess_quality_kwargs["threshold"] = 7
# =============================================================================
def assess_quality(data, eyes_expected, threshold):
"""
Assess the quality of the data sample, which in this case is an image of
the face of the size 64x64 pixels. The quality assessment is based on the
eye detection. If two eyes are detected, and they are located in the
pre-defined positions, then quality is good, otherwise the quality is low.
**Parameters:**
``data`` : 2D :py:class:`numpy.ndarray`
Data array containing the image of the facial region. The size of the
image is 64x64.
``eyes_expected`` : list
A list containing expected coordinates of the eyes. The format is
as follows:
[ [left_y, left_x], [right_y, right_x] ]
``threshold`` : int
A maximum allowed distance between expected and detected centers of
the eyes.
**Returns:**
``quality_flag`` : bool
``True`` for good quality data, ``False`` otherwise.
"""
quality_flag = False
eyes = detect_eyes_in_bw_image(data)
if isinstance(eyes, np.ndarray):
if eyes.shape[0] == 2: # only consider the images with two eyes detected
# coordinates of detected centers of the eyes: [ [left_y, left_x], [right_y, right_x] ]:
eyes_detected = []
for (ex,ey,ew,eh) in eyes:
eyes_detected.append( [ey + eh/2., ex + ew/2.] )
dists = [] # dits between detected and expected:
for a, b in zip(eyes_detected, eyes_expected):
dists.append( np.linalg.norm(np.array(a)-np.array(b)) )
max_dist = np.max(dists)
if max_dist < threshold:
quality_flag = True
return quality_flag
This diff is collapsed.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
This script is designed to assess the quality of the data(images, videos, etc.)
in the user-defined folder and split/copy the data into two folders
according to their quality.
Good quality data will be copied to ``<save_path>/good_quality_data/`` folder,
respectively "low" quality to ``<save_path>/low_quality_data/`` folder.
The function determining the quality of the data must be implemented in the
config file.
@author: Olegs Nikisins
"""
# =============================================================================
# Import here:
import argparse
import importlib
import os
from shutil import copyfile
# =============================================================================
def parse_arguments(cmd_params=None):
"""
Parse command line arguments.
**Parameters:**
``cmd_params``: []
An optional list of command line arguments. Default: None.
**Returns:**
``data_folder``: py:class:`string`
A directory containing the data to be used in quality assessment.
``save_folder``: py:class:`string`
A directory to save the results to. Two sub-folders will be created
here: ``good_quality_data``, and ``low_quality_data``.
``file_extension``: py:class:`string`
An extension of the data files.
Default: ``.hdf5``.
``relative_mod_name``: py:class:`string`
Relative name of the module to import configurations from.
Default: ``celeb_a/quality_assessment_config.py``.
``config_group``: py:class:`string`
Group/package name containing the configuration file.
Default: ``bob.pad.face.config.quality_assessment``.
``verbosity``: py:class:`int`
Verbosity level.
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("data_folder", type=str,
help="A directory containing the data to be used in quality assessment.")
parser.add_argument("save_folder", type=str,
help="A directory to save the results to. "
"Two sub-folders will be created here: good_quality_data, and low_quality_data.")
parser.add_argument("-e", "--file-extension", type=str, help="An extension of the data files.",
default = ".hdf5")
parser.add_argument("-c", "--config-file", type=str, help="Relative name of the config file containing "
"quality assessment function, and data loading function.",
default = "celeb_a/quality_assessment_config.py")
parser.add_argument("-cg", "--config-group", type=str, help="Name of the group, where config file is stored.",
default = "bob.pad.face.config.quality_assessment")
parser.add_argument("-v", "--verbosity", action="count", default=0,
help="Currently not used.")
if cmd_params is not None:
args = parser.parse_args(cmd_params)
else:
args = parser.parse_args()
data_folder = args.data_folder
save_folder = args.save_folder
file_extension = args.file_extension
config_file = args.config_file
config_group = args.config_group
verbosity = args.verbosity
relative_mod_name = '.' + os.path.splitext(config_file)[0].replace(os.path.sep, '.')
return data_folder, save_folder, file_extension, relative_mod_name, config_group, verbosity
# =============================================================================
def get_all_filenames_for_path_and_extension(path, extension):
"""
Get all filenames with specific extension in all subdirectories of the
given path
**Parameters:**
``path`` : str
String containing the path to directory with files.
``extension`` : str
Extension of the files.
**Returns:**
``all_filenames`` : [str]
A list of selected absolute filenames.
"""
all_filenames = []
for root, dirs, files in os.walk(path):
for file in files:
if file.endswith(extension):
all_filenames.append(os.path.join(root, file))
return all_filenames
# =============================================================================
def copy_file(file_name, data_folder, save_folder):
"""
Copy the file from from source to destanation.
**Parameters:**
``file_name`` : str
Absolute name of the file to be copied.
``data_folder`` : str
Folder containing all data files.
``save_folder`` : str
Folder to copy the results to.
"""
# absolute name of the destanation file:
save_filename = os.path.join(save_folder ,file_name.replace( data_folder, "" ))
# make the folders to save the file to:
dst_folder = os.path.split(save_filename)[0]
if not os.path.exists(dst_folder):
os.makedirs(dst_folder)
copyfile(file_name, save_filename)
# =============================================================================
def main(cmd_params=None):
"""
The following steps are performed in this function:
1. The command line arguments are first parsed.
2. Folder to save the results to is created.
3. Configuration file specifying the quality function, and data loading
functionality, is loaded.
4. All files in data folder with specified extension are obtained.
"""
# Parse the command line arguments:
data_folder, save_folder, file_extension, relative_mod_name, config_group, verbosity = \
parse_arguments(cmd_params = cmd_params)
# Create the directories:
good_quality_folder = os.path.join(save_folder, "good_quality_data")
low_quality_folder = os.path.join(save_folder, "low_quality_data")
if not os.path.exists(good_quality_folder):
os.makedirs(good_quality_folder)
if not os.path.exists(low_quality_folder):
os.makedirs(low_quality_folder)
# Load the configuretion file:
config_module = importlib.import_module(relative_mod_name, config_group)
# Obtain a list of data files:
all_filenames = get_all_filenames_for_path_and_extension(data_folder,
file_extension)
if verbosity > 0:
print( "The number of files to process: {}".format( len( all_filenames ) ) )
for idx, file_name in enumerate(all_filenames):
data = config_module.load_datafile(file_name)
quality_flag = config_module.assess_quality(data, **config_module.assess_quality_kwargs)
if quality_flag:
copy_file(file_name, data_folder, good_quality_folder)
if verbosity > 0:
print("Good quality sample copied. {} out of {} samples processed.".format(idx, len(all_filenames)))
else:
copy_file(file_name, data_folder, low_quality_folder)
if verbosity > 0:
print("Bad quality sample copied. {} out of {} samples processed.".format(idx, len(all_filenames)))
if verbosity > 0:
print("Done!")
......@@ -56,6 +56,11 @@ setup(
# the version of bob.
entry_points={
# scripts should be declared using this entry:
'console_scripts': [
'quality-assessment.py = bob.pad.face.script.quality_assessment:main',
],
# registered databases:
'bob.pad.database': [
'replay-attack = bob.pad.face.config.replay_attack:database',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment