|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| """Computes evaluation metrics on groundtruth and predictions in COCO format.
|
|
|
| The Common Objects in Context (COCO) dataset defines a format for specifying
|
| combined semantic and instance segmentations as "panoptic" segmentations. This
|
| is done with the combination of JSON and image files as specified at:
|
| http://cocodataset.org/#format-results
|
| where the JSON file specifies the overall structure of the result,
|
| including the categories for each annotation, and the images specify the image
|
| region for each annotation in that image by its ID.
|
|
|
| This script computes additional metrics such as Parsing Covering on datasets and
|
| predictions in this format. An implementation of Panoptic Quality is also
|
| provided for convenience.
|
| """
|
|
|
| from __future__ import absolute_import
|
| from __future__ import division
|
| from __future__ import print_function
|
|
|
| import collections
|
| import json
|
| import multiprocessing
|
| import os
|
|
|
| from absl import app
|
| from absl import flags
|
| from absl import logging
|
| import numpy as np
|
| from PIL import Image
|
| import utils as panopticapi_utils
|
| import six
|
|
|
| from deeplab.evaluation import panoptic_quality
|
| from deeplab.evaluation import parsing_covering
|
|
|
| FLAGS = flags.FLAGS
|
|
|
| flags.DEFINE_string(
|
| 'gt_json_file', None,
|
| ' Path to a JSON file giving ground-truth annotations in COCO format.')
|
| flags.DEFINE_string('pred_json_file', None,
|
| 'Path to a JSON file for the predictions to evaluate.')
|
| flags.DEFINE_string(
|
| 'gt_folder', None,
|
| 'Folder containing panoptic-format ID images to match ground-truth '
|
| 'annotations to image regions.')
|
| flags.DEFINE_string('pred_folder', None,
|
| 'Folder containing ID images for predictions.')
|
| flags.DEFINE_enum(
|
| 'metric', 'pq', ['pq', 'pc'], 'Shorthand name of a metric to compute. '
|
| 'Supported values are:\n'
|
| 'Panoptic Quality (pq)\n'
|
| 'Parsing Covering (pc)')
|
| flags.DEFINE_integer(
|
| 'num_categories', 201,
|
| 'The number of segmentation categories (or "classes") in the dataset.')
|
| flags.DEFINE_integer(
|
| 'ignored_label', 0,
|
| 'A category id that is ignored in evaluation, e.g. the void label as '
|
| 'defined in COCO panoptic segmentation dataset.')
|
| flags.DEFINE_integer(
|
| 'max_instances_per_category', 256,
|
| 'The maximum number of instances for each category. Used in ensuring '
|
| 'unique instance labels.')
|
| flags.DEFINE_integer('intersection_offset', None,
|
| 'The maximum number of unique labels.')
|
| flags.DEFINE_bool(
|
| 'normalize_by_image_size', True,
|
| 'Whether to normalize groundtruth instance region areas by image size. If '
|
| 'True, groundtruth instance areas and weighted IoUs will be divided by the '
|
| 'size of the corresponding image before accumulated across the dataset. '
|
| 'Only used for Parsing Covering (pc) evaluation.')
|
| flags.DEFINE_integer(
|
| 'num_workers', 0, 'If set to a positive number, will spawn child processes '
|
| 'to compute parts of the metric in parallel by splitting '
|
| 'the images between the workers. If set to -1, will use '
|
| 'the value of multiprocessing.cpu_count().')
|
| flags.DEFINE_integer('print_digits', 3,
|
| 'Number of significant digits to print in metrics.')
|
|
|
|
|
| def _build_metric(metric,
|
| num_categories,
|
| ignored_label,
|
| max_instances_per_category,
|
| intersection_offset=None,
|
| normalize_by_image_size=True):
|
| """Creates a metric aggregator objet of the given name."""
|
| if metric == 'pq':
|
| logging.warning('One should check Panoptic Quality results against the '
|
| 'official COCO API code. Small numerical differences '
|
| '(< 0.1%) can be magnified by rounding.')
|
| return panoptic_quality.PanopticQuality(num_categories, ignored_label,
|
| max_instances_per_category,
|
| intersection_offset)
|
| elif metric == 'pc':
|
| return parsing_covering.ParsingCovering(
|
| num_categories, ignored_label, max_instances_per_category,
|
| intersection_offset, normalize_by_image_size)
|
| else:
|
| raise ValueError('No implementation for metric "%s"' % metric)
|
|
|
|
|
| def _matched_annotations(gt_json, pred_json):
|
| """Yields a set of (groundtruth, prediction) image annotation pairs.."""
|
| image_id_to_pred_ann = {
|
| annotation['image_id']: annotation
|
| for annotation in pred_json['annotations']
|
| }
|
| for gt_ann in gt_json['annotations']:
|
| image_id = gt_ann['image_id']
|
| pred_ann = image_id_to_pred_ann[image_id]
|
| yield gt_ann, pred_ann
|
|
|
|
|
| def _open_panoptic_id_image(image_path):
|
| """Loads a COCO-format panoptic ID image from file."""
|
| return panopticapi_utils.rgb2id(
|
| np.array(Image.open(image_path), dtype=np.uint32))
|
|
|
|
|
| def _split_panoptic(ann_json, id_array, ignored_label, allow_crowds):
|
| """Given the COCO JSON and ID map, splits into categories and instances."""
|
| category = np.zeros(id_array.shape, np.uint16)
|
| instance = np.zeros(id_array.shape, np.uint16)
|
| next_instance_id = collections.defaultdict(int)
|
|
|
| next_instance_id[ignored_label] = 1
|
| for segment_info in ann_json['segments_info']:
|
| if allow_crowds and segment_info['iscrowd']:
|
| category_id = ignored_label
|
| else:
|
| category_id = segment_info['category_id']
|
| mask = np.equal(id_array, segment_info['id'])
|
| category[mask] = category_id
|
| instance[mask] = next_instance_id[category_id]
|
| next_instance_id[category_id] += 1
|
| return category, instance
|
|
|
|
|
| def _category_and_instance_from_annotation(ann_json, folder, ignored_label,
|
| allow_crowds):
|
| """Given the COCO JSON annotations, finds maps of categories and instances."""
|
| panoptic_id_image = _open_panoptic_id_image(
|
| os.path.join(folder, ann_json['file_name']))
|
| return _split_panoptic(ann_json, panoptic_id_image, ignored_label,
|
| allow_crowds)
|
|
|
|
|
| def _compute_metric(metric_aggregator, gt_folder, pred_folder,
|
| annotation_pairs):
|
| """Iterates over matched annotation pairs and computes a metric over them."""
|
| for gt_ann, pred_ann in annotation_pairs:
|
|
|
|
|
| gt_category, gt_instance = _category_and_instance_from_annotation(
|
| gt_ann, gt_folder, metric_aggregator.ignored_label, True)
|
| pred_category, pred_instance = _category_and_instance_from_annotation(
|
| pred_ann, pred_folder, metric_aggregator.ignored_label, False)
|
|
|
| metric_aggregator.compare_and_accumulate(gt_category, gt_instance,
|
| pred_category, pred_instance)
|
| return metric_aggregator
|
|
|
|
|
| def _iterate_work_queue(work_queue):
|
| """Creates an iterable that retrieves items from a queue until one is None."""
|
| task = work_queue.get(block=True)
|
| while task is not None:
|
| yield task
|
| task = work_queue.get(block=True)
|
|
|
|
|
| def _run_metrics_worker(metric_aggregator, gt_folder, pred_folder, work_queue,
|
| result_queue):
|
| result = _compute_metric(metric_aggregator, gt_folder, pred_folder,
|
| _iterate_work_queue(work_queue))
|
| result_queue.put(result, block=True)
|
|
|
|
|
| def _is_thing_array(categories_json, ignored_label):
|
| """is_thing[category_id] is a bool on if category is "thing" or "stuff"."""
|
| is_thing_dict = {}
|
| for category_json in categories_json:
|
| is_thing_dict[category_json['id']] = bool(category_json['isthing'])
|
|
|
|
|
|
|
|
|
| max_category_id = max(six.iterkeys(is_thing_dict))
|
| if len(is_thing_dict) != max_category_id + 1:
|
| seen_ids = six.viewkeys(is_thing_dict)
|
| all_ids = set(six.moves.range(max_category_id + 1))
|
| unseen_ids = all_ids.difference(seen_ids)
|
| if unseen_ids != {ignored_label}:
|
| logging.warning(
|
| 'Nonconsecutive category ids or no category JSON specified for ids: '
|
| '%s', unseen_ids)
|
|
|
| is_thing_array = np.zeros(max_category_id + 1)
|
| for category_id, is_thing in six.iteritems(is_thing_dict):
|
| is_thing_array[category_id] = is_thing
|
|
|
| return is_thing_array
|
|
|
|
|
| def eval_coco_format(gt_json_file,
|
| pred_json_file,
|
| gt_folder=None,
|
| pred_folder=None,
|
| metric='pq',
|
| num_categories=201,
|
| ignored_label=0,
|
| max_instances_per_category=256,
|
| intersection_offset=None,
|
| normalize_by_image_size=True,
|
| num_workers=0,
|
| print_digits=3):
|
| """Top-level code to compute metrics on a COCO-format result.
|
|
|
| Note that the default values are set for COCO panoptic segmentation dataset,
|
| and thus the users may want to change it for their own dataset evaluation.
|
|
|
| Args:
|
| gt_json_file: Path to a JSON file giving ground-truth annotations in COCO
|
| format.
|
| pred_json_file: Path to a JSON file for the predictions to evaluate.
|
| gt_folder: Folder containing panoptic-format ID images to match ground-truth
|
| annotations to image regions.
|
| pred_folder: Folder containing ID images for predictions.
|
| metric: Name of a metric to compute.
|
| num_categories: The number of segmentation categories (or "classes") in the
|
| dataset.
|
| ignored_label: A category id that is ignored in evaluation, e.g. the "void"
|
| label as defined in the COCO panoptic segmentation dataset.
|
| max_instances_per_category: The maximum number of instances for each
|
| category. Used in ensuring unique instance labels.
|
| intersection_offset: The maximum number of unique labels.
|
| normalize_by_image_size: Whether to normalize groundtruth instance region
|
| areas by image size. If True, groundtruth instance areas and weighted IoUs
|
| will be divided by the size of the corresponding image before accumulated
|
| across the dataset. Only used for Parsing Covering (pc) evaluation.
|
| num_workers: If set to a positive number, will spawn child processes to
|
| compute parts of the metric in parallel by splitting the images between
|
| the workers. If set to -1, will use the value of
|
| multiprocessing.cpu_count().
|
| print_digits: Number of significant digits to print in summary of computed
|
| metrics.
|
|
|
| Returns:
|
| The computed result of the metric as a float scalar.
|
| """
|
| with open(gt_json_file, 'r') as gt_json_fo:
|
| gt_json = json.load(gt_json_fo)
|
| with open(pred_json_file, 'r') as pred_json_fo:
|
| pred_json = json.load(pred_json_fo)
|
| if gt_folder is None:
|
| gt_folder = gt_json_file.replace('.json', '')
|
| if pred_folder is None:
|
| pred_folder = pred_json_file.replace('.json', '')
|
| if intersection_offset is None:
|
| intersection_offset = (num_categories + 1) * max_instances_per_category
|
|
|
| metric_aggregator = _build_metric(
|
| metric, num_categories, ignored_label, max_instances_per_category,
|
| intersection_offset, normalize_by_image_size)
|
|
|
| if num_workers == -1:
|
| logging.info('Attempting to get the CPU count to set # workers.')
|
| num_workers = multiprocessing.cpu_count()
|
|
|
| if num_workers > 0:
|
| logging.info('Computing metric in parallel with %d workers.', num_workers)
|
| work_queue = multiprocessing.Queue()
|
| result_queue = multiprocessing.Queue()
|
| workers = []
|
| worker_args = (metric_aggregator, gt_folder, pred_folder, work_queue,
|
| result_queue)
|
| for _ in six.moves.range(num_workers):
|
| workers.append(
|
| multiprocessing.Process(target=_run_metrics_worker, args=worker_args))
|
| for worker in workers:
|
| worker.start()
|
| for ann_pair in _matched_annotations(gt_json, pred_json):
|
| work_queue.put(ann_pair, block=True)
|
|
|
|
|
|
|
| for _ in six.moves.range(num_workers):
|
| work_queue.put(None, block=True)
|
|
|
|
|
| for _ in six.moves.range(num_workers):
|
| metric_aggregator.merge(result_queue.get(block=True))
|
|
|
| for worker in workers:
|
| worker.join()
|
| else:
|
| logging.info('Computing metric in a single process.')
|
| annotation_pairs = _matched_annotations(gt_json, pred_json)
|
| _compute_metric(metric_aggregator, gt_folder, pred_folder, annotation_pairs)
|
|
|
| is_thing = _is_thing_array(gt_json['categories'], ignored_label)
|
| metric_aggregator.print_detailed_results(
|
| is_thing=is_thing, print_digits=print_digits)
|
| return metric_aggregator.detailed_results(is_thing=is_thing)
|
|
|
|
|
| def main(argv):
|
| if len(argv) > 1:
|
| raise app.UsageError('Too many command-line arguments.')
|
|
|
| eval_coco_format(FLAGS.gt_json_file, FLAGS.pred_json_file, FLAGS.gt_folder,
|
| FLAGS.pred_folder, FLAGS.metric, FLAGS.num_categories,
|
| FLAGS.ignored_label, FLAGS.max_instances_per_category,
|
| FLAGS.intersection_offset, FLAGS.normalize_by_image_size,
|
| FLAGS.num_workers, FLAGS.print_digits)
|
|
|
|
|
| if __name__ == '__main__':
|
| flags.mark_flags_as_required(
|
| ['gt_json_file', 'gt_folder', 'pred_json_file', 'pred_folder'])
|
| app.run(main)
|
|
|