Spaces:
Running
Running
# 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) | |