diff --git a/bob/rppg/cvpr14/script/extract_pulses.py b/bob/rppg/cvpr14/script/extract_pulses.py new file mode 100644 index 0000000000000000000000000000000000000000..88ba6bb703e63922c6c169e6f2ba3e91c47017e8 --- /dev/null +++ b/bob/rppg/cvpr14/script/extract_pulses.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Extract pulses according to Li's CVPR 2014 algorithm + +Usage: + %(prog)s <configuration> + [--protocol=<string>] [--subset=<string> ...] + [--dbdir=<path>] [--facedir=<path>] [--bgdir=<path>] + [--npoints=<int>] [--indent=<int>] [--quality=<float>] [--distance=<int>] + [--wholeface] [--overwrite] [--verbose ...] [--plot] [--gridcount] + + %(prog)s (--help | -h) + %(prog)s (--version | -V) + + +Options: + -h, --help Show this screen + -V, --version Show version + -p, --protocol=<string> Protocol [default: all]. + -s, --subset=<string> Data subset to load. If nothing is provided + all the sets will be loaded. + -f, --facedir=<path> The path to the directory where signal extracted + from the face area will be stored [default: face] + -b, --bgdir=<path> The path to the directory where signal extracted + from the background area will be stored [default: background] + -n, --npoints=<int> Number of good features to track [default: 40] + -i, --indent=<int> Indent (in percent of the face width) to apply to + keypoints to get the mask [default: 10] + -q, --quality=<float> Quality level of the good features to track + [default: 0.01] + -e, --distance=<int> Minimum distance between detected good features to + track [default: 10] + -O, --overwrite By default, we don't overwrite existing files. The + processing will skip those so as to go faster. If you + still would like me to overwrite them, set this flag. + -v, --verbose Increases the verbosity (may appear multiple times) + -P, --plot Set this flag if you'd like to follow-up the algorithm + execution graphically. We'll plot some interactions. + -g, --gridcount Prints the number of objects to process and exits. + -w, --wholeface Consider the whole face region instead of the mask. + + +Example: + + To run the pulse extractor: + + $ %(prog)s config.py -v + +See '%(prog)s --help' for more information. + +""" + +from __future__ import print_function + +import os +import sys +import pkg_resources + +from bob.core.log import setup +logger = setup("bob.rppg.base") + +from docopt import docopt + +from bob.extension.config import load + +version = pkg_resources.require('bob.rppg.base')[0].version + +import numpy +import bob.io.base +import bob.ip.facedetect + +from ...base.utils import get_parameter +from ...base.utils import crop_face + +from ..extract_utils import kp66_to_mask +from ..extract_utils import get_good_features_to_track +from ..extract_utils import track_features +from ..extract_utils import find_transformation +from ..extract_utils import get_current_mask_points +from ..extract_utils import get_mask +from ..extract_utils import compute_average_colors_mask +from ..extract_utils import compute_average_colors_wholeface + +def main(user_input=None): + + # Parse the command-line arguments + if user_input is not None: + arguments = user_input + else: + arguments = sys.argv[1:] + + prog = os.path.basename(sys.argv[0]) + completions = dict(prog=prog, version=version,) + args = docopt(__doc__ % completions, argv=arguments, version='Signal extractor for videos (%s)' % version,) + + # if the user wants more verbosity, lowers the logging level + from bob.core.log import set_verbosity_level + set_verbosity_level(logger, args['--verbose']) + + # load configuration file + configuration = load([os.path.join(args['<configuration>'])]) + + # get various parameters, either from config file or command-line + protocol = get_parameter(args, configuration, 'protocol', 'all') + subset = get_parameter(args, configuration, 'subset', '') + facedir = get_parameter(args, configuration, 'facedir', 'face') + bgdir = get_parameter(args, configuration, 'bgdir', 'bg') + npoints = get_parameter(args, configuration, 'npoints', 40) + indent = get_parameter(args, configuration, 'indent', 10) + quality = get_parameter(args, configuration, 'quality', 0.01) + distance = get_parameter(args, configuration, 'distance', 10) + overwrite = get_parameter(args, configuration, 'overwrite', False) + plot = get_parameter(args, configuration, 'plot', False) + gridcount = get_parameter(args, configuration, 'gridcount', False) + wholeface = get_parameter(args, configuration, 'wholeface', False) + verbosity_level = get_parameter(args, configuration, 'verbose', 0) + + # TODO: find a way to check protocol names - Guillaume HEUSCH, 22-06-2018 + if hasattr(configuration, 'database'): + objects = configuration.database.objects(args['--protocol'], args['--subset']) + else: + logger.error("Please provide a database in your configuration file !") + sys.exit() + + # if we are on a grid environment, just find what I have to process. + sge = False + try: + sge = os.environ.has_key('SGE_TASK_ID') # python2 + except AttributeError: + sge = 'SGE_TASK_ID' in os.environ # python3 + + if sge: + pos = int(os.environ['SGE_TASK_ID']) - 1 + if pos >= len(objects): + raise RuntimeError("Grid request for job {} on a setup with {} jobs".format(pos, len(objects))) + objects = [objects[pos]] + + if args['--gridcount']: + print(len(objects)) + sys.exit() + + # does the actual work - for every video in the available dataset, + # extract the signals and dumps the results to the corresponding directory + for obj in objects: + + # expected output file + output_face = obj.make_path(args['--facedir'], '.hdf5') + output_bg = obj.make_path(args['--bgdir'], '.hdf5') + + # if output exists and not overwriting, skip this file + if (os.path.exists(output_face) and os.path.exists(output_bg)) and not args['--overwrite']: + logger.info("Skipping output file `%s': already exists, use --overwrite to force an overwrite", output_face) + continue + + # load video + video = obj.load_video(configuration.dbdir) + logger.info("Processing input video from `%s'...", video.filename) + + # load the result of face detection + bounding_boxes = obj.load_face_detection() + + # average green color in the mask area + face_color = numpy.zeros(len(video), dtype='float64') + # average green color in the background area + bg_color = numpy.zeros(len(video), dtype='float64') + + # loop on video frames + for i, frame in enumerate(video): + logger.debug("Processing frame %d/%d...", i+1, len(video)) + + if i == 0: + # first frame: + # -> load the keypoints detected by DMRF + # -> infer the mask from the keypoints + # -> detect the face + # -> get "good features" inside the face + if not bool(args['--wholeface']): + kpts = obj.load_drmf_keypoints() + mask_points, mask = kp66_to_mask(frame, kpts, int(args['--indent']), bool(args['--plot'])) + + try: + bbox = bounding_boxes[i] + except NameError: + bbox, quality = bob.ip.facedetect.detect_single_face(frame) + + # define the face width for the whole sequence + facewidth = bbox.size[1] + face = crop_face(frame, bbox, facewidth) + + if not bool(args['--wholeface']): + good_features = get_good_features_to_track(face,int(args['--npoints']), + float(args['--quality']), int(args['--distance']), bool(args['--plot'])) + else: + # subsequent frames: + # -> crop the face with the bounding_boxes of the previous frame (so + # that faces are of the same size) + # -> get the projection of the corners detected in the previous frame + # -> find the (affine) transformation relating previous corners with + # current corners + # -> apply this transformation to the mask + face = crop_face(frame, prev_bb, facewidth) + if not bool(args['--wholeface']): + good_features = track_features(prev_face, face, prev_features, bool(args['--plot'])) + project = find_transformation(prev_features, good_features) + if project is None: + logger.warn("Sequence {0}, frame {1} : No projection was found" + " between previous and current frame, mask from previous frame will be used" + .format(obj.path, i)) + else: + mask_points = get_current_mask_points(mask_points, project) + + # update stuff for the next frame: + # -> the previous face is the face in this frame, with its bbox (and not + # with the previous one) + # -> the features to be tracked on the next frame are re-detected + try: + prev_bb = bounding_boxes[i] + except NameError: + bb, quality = bob.ip.facedetect.detect_single_face(frame) + prev_bb = bb + + + if not bool(args['--wholeface']): + prev_face = crop_face(frame, prev_bb, facewidth) + prev_features = get_good_features_to_track(face, int(args['--npoints']), + float(args['--quality']), int(args['--distance']), + bool(args['--plot'])) + if prev_features is None: + logger.warn("Sequence {0}, frame {1} No features to track" + " detected in the current frame, using the previous ones" + .format(obj.path, i)) + prev_features = good_features + + # get the bottom face region average colors + face_mask = get_mask(frame, mask_points) + face_color[i] = compute_average_colors_mask(frame, face_mask, bool(args['--plot'])) + else: + face_color[i] = compute_average_colors_wholeface(face, bool(args['--plot'])) + + # get the background region average colors + bg_mask = numpy.zeros((frame.shape[1], frame.shape[2]), dtype=bool) + bg_mask[:100, :100] = True + bg_color[i] = compute_average_colors_mask(frame, bg_mask, bool(args['--plot'])) + + # saves the data into an HDF5 file with a '.hdf5' extension + out_facedir = os.path.dirname(output_face) + if not os.path.exists(out_facedir): bob.io.base.create_directories_safe(out_facedir) + bob.io.base.save(face_color, output_face) + logger.info("Output file saved to `%s'...", output_face) + + out_bgdir = os.path.dirname(output_bg) + if not os.path.exists(out_bgdir): bob.io.base.create_directories_safe(out_bgdir) + bob.io.base.save(bg_color, output_bg) + logger.info("Output file saved to `%s'...", output_bg) diff --git a/setup.py b/setup.py index 9615800068d88cbd8103f87d939c579113188266..0867f4233a5191f1853fbb4d149fe672d89359e4 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ setup( entry_points={ 'console_scripts': [ + 'cvpr14_extract_pulses.py = bob.rppg.cvpr14.script.extract_pulses:main', 'cvpr14_extract_signals.py = bob.rppg.cvpr14.script.extract_signals:main', 'cvpr14_video2skin.py = bob.rppg.cvpr14.script.video2skin:main', 'cvpr14_illumination.py = bob.rppg.cvpr14.script.illumination_rectification:main',