diff --git a/bob/bio/face/script/display_face_annotations.py b/bob/bio/face/script/display_face_annotations.py index cc2f40dfc102b380d32ec2594f86743522a3e1d5..79efc5771fc25e0c4c44a1fcfc86709a6516dc4b 100644 --- a/bob/bio/face/script/display_face_annotations.py +++ b/bob/bio/face/script/display_face_annotations.py @@ -1,7 +1,6 @@ #!../bin/python -"""This script displays the iamges with annotations provided by any face database. -Basically, anything that can be used as a --database for verify.py can be specified here as well, including configuration files and ``database`` resources: ``resources.py -d database``. +"""This script displays the images with annotations provided by any face database. By default, all images and their corresponding annotations are displayed, and you have to press ``Enter`` after each image. If the database does not include annotations, or you want to display a different set of annotations, you can specify the ``--annotation-directory`` (and if required modify the ``--annotation-file-extension`` and ``--annotation-file-type``. @@ -13,130 +12,241 @@ Note that this script can only be used with face image databases, not with video from __future__ import print_function import os -import argparse +import click import sys - -import bob.bio.base - -import bob.core -logger = bob.core.log.setup("bob.bio.face") - -def command_line_arguments(command_line_parameters): - """Defines the command line parameters that are accepted.""" - - # create parser - parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - # add parameters - parser.add_argument('-d', '--database', nargs = '+', help = 'Select the database for which the images plus annotations should be shown.') - parser.add_argument('-V', '--video', action = 'store_true', help = 'Provide this flag if your database is a video database. For video databases, the annotations for the first frame is shown.') - parser.add_argument('-f', '--file-ids', nargs = '+', help = 'If given, only the images of the --database with the given file id are shown (non-existing IDs will be silently skipped).') - parser.add_argument('-a', '--annotation-directory', help = 'If given, use the annotations stored in the given annotation directory (this might be required for some databases).') - parser.add_argument('-x', '--annotation-file-extension', default = '.pos', help = 'Annotation files have the given filename extension.') - parser.add_argument('-t', '--annotation-file-type', default = 'named', help = 'Select the annotation file style, see bob.db.base documentation for valid types.') - parser.add_argument('-n', '--annotate-names', action = 'store_true', help = 'Plot the names of the annotations, too.') - parser.add_argument('-m', '--marker-style', default='rx', help = 'Select the marker style') - parser.add_argument('-M', '--marker-size', type=float, default=10., help = 'Select the marker size') - parser.add_argument('-F', '--font-size', type=int, default=16, help = 'Select the font size for the annotation names') - parser.add_argument('-C', '--font-color', default = 'b', help = 'Select the color for the annotation names') - parser.add_argument('--database-directories-file', metavar = 'FILE', default = "%s/.bob_bio_databases.txt" % os.environ["HOME"], help = 'An optional file, where database directories are stored (to avoid changing the database configurations)') - parser.add_argument('-o', '--output', help = "If given, it will save the plots in this output file instead of showing them. This option is useful when you don't have a display server (ssh).") - - parser.add_argument('--self-test', action='store_true', help=argparse.SUPPRESS) - - bob.core.log.add_command_line_option(parser) - args = parser.parse_args(command_line_parameters) - bob.core.log.set_verbosity_level(logger, args.verbose) - - return args - - -def main(command_line_parameters=None): - - args = command_line_arguments(command_line_parameters) - - # load database - database = bob.bio.base.load_resource("".join(args.database), "database") - # replace directories - if isinstance(database, bob.bio.base.database.BioDatabase): - database.replace_directories(args.database_directories_file) - - # get all files - if bob.bio.base.utils.is_argument_available('flat', database.all_files): - files = database.all_files(flat=True) - else: - files = database.all_files() - - # filter file ids; convert them to str first - if args.file_ids is not None: - files = [f for f in files if str(f.id) in args.file_ids] - - # open figure - if not args.self_test: +import logging +import json + +from bob.extension.scripts.click_helper import ( + verbosity_option, + ConfigCommand, + ResourceOption, +) + +import bob.ip.color + +logger = logging.getLogger(__name__) + +@click.command( + entry_point_group="bob.bio.config", + cls=ConfigCommand, + epilog="""\b +Examples: + + $ bob bio display-face-annotations -vvv -d <database> -a <annot_dir> +""", +) +@click.option( + "-d", + "--database", + required=True, + cls=ResourceOption, + entry_point_group="bob.bio.database", + help="Select the database for which the images plus annotations should be shown.", +) +@click.option( + "-V", + "--video", + "is_video", + is_flag=True, + help="Provide this flag if your database is a video database. " + "For video databases, the annotations for the first frame is shown.", +) +@click.option( + "-a", + "--annotations-dir", + help="If given, use the annotations stored in this directory " + "(when annotated with `$ bob bio annnotate` for example).", +) +@click.option( + "-x", + "--annotations-extension", + default = ".json", + show_default=True, + help="Annotations files have the given filename extension.", +) +@click.option( + "-n", + "--display-names", + is_flag=True, + help="Plot the names of the annotations, too.", +) +@click.option( + "-m", + "--marker-style", + default="rx", + show_default=True, + help="Select the marker style", +) +@click.option( + "-M", + "--marker-size", + type=float, + default=10., + show_default=True, + help="Select the marker size", +) +@click.option( + "-C", + "--font-color", + default="b", + show_default=True, + help="Select the color for the annotations names", +) +@click.option( + "-F", + "--font-size", + type=int, + default=16, + show_default=True, + help="Select the font size for the annotations names", +) +@click.option( + "-o", + "--output-dir", + help="If given, it will save the plots in this output file instead of showing them. This option is useful when you don't have a display server (ssh).", +) +@click.option( + "-k", + "--keep-all", + is_flag=True, + help="When -o is given: keeps every annotated samples instead of just one.", +) +@click.option( + "--self-test", + is_flag=True, + help="Prevents outputing to the screen and waiting for user input.", +) +@click.option( + "--groups", + "-g", + multiple=True, + default=["dev", "eval"], + show_default=True, + help="Biometric Database group that will be displayed.", +) +@verbosity_option(cls=ResourceOption) +def display_face_annotations( + database, + is_video, + annotations_dir, + annotations_extension, + marker_style, + marker_size, + display_names, + font_color, + font_size, + output_dir, + keep_all, + self_test, + groups, + **kwargs +): + """ + Plots annotations on the corresponding face picture. + """ + logger.debug("Retrieving background model samples from database.") + background_model_samples = database.background_model_samples() + + logger.debug("Retrieving references and probes samples from database.") + references_samplesets = [] + probes_samplesets = [] + for group in groups: + references_samplesets.extend(database.references(group=group)) + probes_samplesets.extend(database.probes(group=group)) + + # Unravels all samples in one list (no SampleSets) + samples = background_model_samples + samples.extend([ + sample + for r in references_samplesets + for sample in r.samples + ]) + samples.extend([ + sample + for p in probes_samplesets + for sample in p.samples + ]) + + logger.debug(f"{len(samples)} samples loaded from database.") + + # open figure from matplotlib import pyplot - if args.output is None: - pyplot.ion() - pyplot.show() + if not self_test and not output_dir: + pyplot.ion() + pyplot.show() else: - pyplot.ioff() + pyplot.ioff() pyplot.figure() - for f in files: - # load image - logger.info("loading image for file %s", f.id) - image = f.load(database.original_directory, database.original_extension) - if args.video: - frame_id, image, _ = image[0] - # convert to color if it is not - if image.ndim == 2: - image = bob.ip.color.gray_to_rgb(image) - - # get annotations - annotations = {} - if args.annotation_directory is not None: - # load annotation file - annotation_file = f.make_path(args.annotation_directory, args.annotation_file_extension) - if os.path.exists(annotation_file): - logger.info("Loading annotations from file %s", annotation_file) - annotations = bob.db.base.read_annotation_file(annotation_file, args.annotation_file_type) - else: - logger.warn("Could not find annotation file %s", annotation_file) - else: - # get annotations from database - annotations = database.annotations(f) - - if not annotations: - logger.warn("Could not find annotations for file %s", f.id) - - if args.video: - assert frame_id in annotations, annotations - annotations = annotations[frame_id] - - if not args.self_test: - pyplot.clf() - pyplot.imshow(image.transpose(1,2,0)) - - global_annotation = [] - for n,a in annotations.items(): - if isinstance(a, (list,tuple)) and len(a) == 2: - pyplot.plot(a[1], a[0], args.marker_style, ms = args.marker_size, mew = args.marker_size / 5.) - if args.annotate_names: - pyplot.annotate(n, (a[1], a[0]), color=args.font_color, fontsize=args.font_size) + for sample in samples: + # load image + logger.info("loading image for sample %s", sample.key) + image = sample.data + if is_video: + frame_id, image, _ = image[0] + # convert to color if it is not + if image.ndim == 2: + image = bob.ip.color.gray_to_rgb(image) + + # get annotations + annotations = {} + if annotations_dir is not None: + # Loads the corresponding annotations file + annotations_file = os.path.join(annotations_dir, sample.key + annotations_extension) + if os.path.exists(annotations_file): + logger.info("Loading annotations from file %s", annotations_file) + with open(annotations_file) as f: # TODO remove and use bob.db.base.read_annotations_file + annotations=json.load(f) + # annotations = bob.db.base.read_annotation_file(annotations_file, args.annotations_type) + else: + logger.warn("Could not find annotation file %s", annotations_file) else: - global_annotation.append("%s=%s"%(n,a)) - - # plot all global annotations, at the top center of the image - pyplot.annotate(";".join(global_annotation), (image.shape[-1]/2, 0), color=args.font_color, fontsize=args.font_size, ha='center', va='baseline') - - pyplot.gca().set_aspect("equal") - pyplot.gca().autoscale(tight=True) - if args.output is None: - pyplot.pause(0.001) - else: - pyplot.savefig(args.output) - - input_text = "Press Enter to continue to the next image (or Ctrl-C + Enter to exit)" - if sys.version_info >= (3, 0): - input(input_text) - else: - raw_input(input_text) + # get annotations from database + annotations = database.annotations(sample) + + if not annotations: + logger.warn("Could not find annotations for file %s", sample.key) + continue + + if is_video: + assert frame_id in annotations, annotations + annotations = annotations[frame_id] + + pyplot.clf() + pyplot.imshow(image.transpose(1,2,0)) + + global_annotation = [] + for n,a in annotations.items(): + if isinstance(a, (list,tuple)) and len(a) == 2: + pyplot.plot(a[1], a[0], marker_style, ms = marker_size, mew = marker_size / 5.) + if display_names: + pyplot.annotate(n, (a[1], a[0]), color=font_color, fontsize=font_size) + else: + global_annotation.append("%s=%s"%(n,a)) + + # plot all global annotations, at the top center of the image + pyplot.annotate(";".join(global_annotation), (image.shape[-1]/2, 0), color=font_color, fontsize=font_size, ha='center', va='baseline') + + pyplot.gca().set_aspect("equal") + pyplot.gca().autoscale(tight=True) + + if output_dir is None: + if self_test: + raise RuntimeError("Do not run self_test without --output_dir.") + pyplot.pause(0.001) + else: + if keep_all: + output_path = os.path.join(output_dir, sample.key + ".png") + else: + output_path = os.path.join(output_dir, "annotated.png") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + pyplot.savefig(output_path) + + if not self_test: + input_text = "Press Enter to continue to the next image (or Ctrl-C to exit)" + if sys.version_info >= (3, 0): + input(input_text) + else: + raw_input(input_text) + diff --git a/bob/bio/face/test/data/annotations/s1/1.json b/bob/bio/face/test/data/annotations/s1/1.json new file mode 100644 index 0000000000000000000000000000000000000000..09c8d47128fa13f9c317b47bdbe180e7c546c80e --- /dev/null +++ b/bob/bio/face/test/data/annotations/s1/1.json @@ -0,0 +1 @@ +{"topleft": [5.476375102996826, 11.880301475524902], "bottomright": [78.71327209472656, 111.0], "reye": [27.06552505493164, 52.629173278808594], "leye": [61.69809341430664, 51.46814727783203], "nose": [45.439552307128906, 72.81857299804688], "mouthright": [30.882678985595703, 89.54910278320312], "mouthleft": [59.577816009521484, 89.41582489013672], "quality": 0.999476969242096} \ No newline at end of file diff --git a/bob/bio/face/test/test_scripts.py b/bob/bio/face/test/test_scripts.py index 37dcdd1d4d24e6e58996c0644937f34a3aa714e3..516258e01cfde4b3e3181189acb582b19fedd218 100644 --- a/bob/bio/face/test/test_scripts.py +++ b/bob/bio/face/test/test_scripts.py @@ -1,13 +1,41 @@ -import bob.bio.base.test.utils -import bob.bio.face +from click.testing import CliRunner +import pkg_resources +import tempfile +import shutil +import os -# TODO: Disabling this test until we have bob.bio.base#146 -""" def test_display_annotations(): - from bob.bio.face.script.display_face_annotations import main + from bob.bio.face.script.display_face_annotations import display_face_annotations + try: + tmp_dir = tempfile.mkdtemp(prefix="bobtest_") + annotations_dir = pkg_resources.resource_filename( + "bob.bio.face.test", + "data/annotations/" + ) + runner = CliRunner() + result = runner.invoke( + display_face_annotations, + args=( + '--database', 'dummy', + '--groups', 'dev', + '--annotations-dir', annotations_dir, + '--output-dir', tmp_dir, '--keep-all', + '--self-test', + ) + ) + assertion_error_message = ( + 'Command exited with this output: `{}\' \n' + 'If the output is empty, you can run this script locally to see ' + 'what is wrong:\n' + '$ bob bio display_face_annotations -vvv -d dummy -g dev -a ./annotations/ -o /tmp/temp_annotated' + ''.format(result.output)) + assert result.exit_code == 0, assertion_error_message - with bob.bio.base.test.utils.Quiet(): - parameters = ['-d', 'dummy', '-a', '/very/unlikely/directory', '--self-test'] - main(parameters) -""" + # Checks if an annotated sample exists + sample_1_path = os.path.join(tmp_dir, "s1","1.png") + assertion_error_message = "File '{}' not created.".format(sample_1_path) + assert os.path.isfile(sample_1_path), assertion_error_message + + finally: + shutil.rmtree(tmp_dir) diff --git a/setup.py b/setup.py index a5f1e4147cd022eea0bd58f2dd9850dbcc7dc8f5..b73abd8794d355a220e290140320ccef311e74ba 100644 --- a/setup.py +++ b/setup.py @@ -195,6 +195,10 @@ setup( 'fargo = bob.bio.face.config.database.fargo', ], + 'bob.bio.cli': [ + 'display-face-annotations = bob.bio.face.script.display_face_annotations:display_face_annotations', + ], + },