Spaces:
Runtime error
Runtime error
| # Lint as: python2, python3 | |
| # Copyright 2019 The TensorFlow Authors All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # ============================================================================== | |
| """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) | |
| # Skip instance label 0 for ignored label. That is reserved for void. | |
| 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: | |
| # We only expect "iscrowd" to appear in the ground-truth, and not in model | |
| # output. In predicted JSON it is simply ignored, as done in official code. | |
| 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']) | |
| # Check our assumption that the category ids are consecutive. | |
| # Usually metrics should be able to handle this case, but adding a warning | |
| # here. | |
| 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) | |
| # Will cause each worker to return a result and terminate upon recieving a | |
| # None task. | |
| for _ in six.moves.range(num_workers): | |
| work_queue.put(None, block=True) | |
| # Retrieve results. | |
| 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) | |