deeplab2 / evaluation /coco_instance_ap.py
akhaliq3
spaces demo
506da10
# coding=utf-8
# Copyright 2021 The Deeplab2 Authors.
#
# 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.
"""COCO-style instance segmentation evaluation metrics.
Implements a Keras interface to COCO API.
COCO API: github.com/cocodataset/cocoapi/
"""
from typing import Any, Collection, Mapping, Optional
from absl import logging
import numpy as np
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
import tensorflow as tf
from deeplab2.utils import coco_tools
from deeplab2.utils import panoptic_instances
def _unwrap_segmentation(seg):
return {
'size': list(seg['size']),
'counts': seg['counts'],
}
_ANNOTATION_CONVERSION = {
'bbox': list,
'segmentation': _unwrap_segmentation,
}
def _unwrap_annotation(ann: Mapping[str, Any]) -> Mapping[str, Any]:
"""Unwraps the objects in an COCO-style annotation dictionary.
Logic within the Keras metric class wraps the objects within the ground-truth
and detection annotations in ListWrapper and DictWrapper classes. On the other
hand, the COCO API does strict type checking as part of determining which
branch to use in comparing detections and segmentations. We therefore have
to coerce the types from the wrapper to the built-in types that COCO is
expecting.
Args:
ann: A COCO-style annotation dictionary that may contain ListWrapper and
DictWrapper objects.
Returns:
The same annotation information, but with wrappers reduced to built-in
types.
"""
unwrapped_ann = {}
for k in ann:
if k in _ANNOTATION_CONVERSION:
unwrapped_ann[k] = _ANNOTATION_CONVERSION[k](ann[k])
else:
unwrapped_ann[k] = ann[k]
return unwrapped_ann
class InstanceAveragePrecision(tf.keras.metrics.Metric):
"""COCO evaluation metric class."""
def __init__(self, name: str = 'instance_ap', **kwargs):
"""Constructs COCO evaluation class."""
super(InstanceAveragePrecision, self).__init__(name=name, **kwargs)
self.reset_states()
def reset_states(self) -> None:
"""Reset COCO API object."""
self.detections = []
self.dataset = {
'images': [],
'annotations': [],
'categories': []
}
self.image_id = 1
self.next_groundtruth_annotation_id = 1
self.category_ids = set()
self.metric_values = None
def evaluate(self) -> np.ndarray:
"""Evaluates with detections from all images with COCO API.
Returns:
coco_metric: float numpy array with shape [12] representing the
coco-style evaluation metrics.
"""
self.dataset['categories'] = [{
'id': int(category_id)
} for category_id in self.category_ids]
# Creates "unwrapped" copies of COCO json-style objects.
dataset = {
'images': self.dataset['images'],
'categories': self.dataset['categories']
}
dataset['annotations'] = [
_unwrap_annotation(ann) for ann in self.dataset['annotations']
]
detections = [_unwrap_annotation(ann) for ann in self.detections]
logging.info('Creating COCO objects for AP eval...')
coco_gt = COCO()
coco_gt.dataset = dataset
coco_gt.createIndex()
coco_dt = coco_gt.loadRes(detections)
logging.info('Running COCO evaluation...')
coco_eval = COCOeval(coco_gt, coco_dt, iouType='segm')
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_metrics = coco_eval.stats
return np.array(coco_metrics, dtype=np.float32)
def result(self) -> np.ndarray:
"""Return the instance segmentation metric values, computing them if needed.
Returns:
A float vector of 12 elements. The meaning of each element is (in order):
0. AP @[ IoU=0.50:0.95 | area= all | maxDets=100 ]
1. AP @[ IoU=0.50 | area= all | maxDets=100 ]
2. AP @[ IoU=0.75 | area= all | maxDets=100 ]
3. AP @[ IoU=0.50:0.95 | area= small | maxDets=100 ]
4. AP @[ IoU=0.50:0.95 | area=medium | maxDets=100 ]
5. AP @[ IoU=0.50:0.95 | area= large | maxDets=100 ]
6. AR @[ IoU=0.50:0.95 | area= all | maxDets= 1 ]
7. AR @[ IoU=0.50:0.95 | area= all | maxDets= 10 ]
8. AR @[ IoU=0.50:0.95 | area= all | maxDets=100 ]
9. AR @[ IoU=0.50:0.95 | area= small | maxDets=100 ]
10. AR @[ IoU=0.50:0.95 | area=medium | maxDets=100 ]
11, AR @[ IoU=0.50:0.95 | area= large | maxDets=100 ]
Where: AP = Average Precision
AR = Average Recall
IoU = Intersection over Union. IoU=0.50:0.95 is the average of the
metric over thresholds of 0.5 to 0.95 with increments of 0.05.
The area thresholds mean that, for those entries, ground truth annotation
with area outside the range is ignored.
small: [0**2, 32**2],
medium: [32**2, 96**2]
large: [96**2, 1e5**2]
"""
if not self.metric_values:
self.metric_values = self.evaluate()
return self.metric_values
def update_state(self, groundtruth_boxes: tf.Tensor,
groundtruth_classes: tf.Tensor, groundtruth_masks: tf.Tensor,
groundtruth_is_crowd: tf.Tensor, detection_masks: tf.Tensor,
detection_scores: tf.Tensor,
detection_classes: tf.Tensor) -> None:
"""Update detection results and groundtruth data.
Append detection results to self.detections to the aggregate results from
all of the validation set. The groundtruth_data is parsed and added into a
dictionary with the same format as COCO dataset, which can be used for
evaluation.
Args:
groundtruth_boxes: tensor (float32) with shape [num_gt_annos, 4]
groundtruth_classes: tensor (int) with shape [num_gt_annos]
groundtruth_masks: tensor (uint8) with shape [num_gt_annos, image_height,
image_width]
groundtruth_is_crowd: tensor (bool) with shape [num_gt_annos]
detection_masks: tensor (uint8) with shape [num_detections, image_height,
image_width]
detection_scores: tensor (float32) with shape [num_detections]
detection_classes: tensor (int) with shape [num_detections]
"""
# Reset the caching of result values.
self.metric_values = None
# Update known category ids.
self.category_ids.update(groundtruth_classes.numpy())
self.category_ids.update(detection_classes.numpy())
# Add ground-truth annotations.
groundtruth_annotations = coco_tools.ExportSingleImageGroundtruthToCoco(
self.image_id,
self.next_groundtruth_annotation_id,
self.category_ids,
groundtruth_boxes.numpy(),
groundtruth_classes.numpy(),
groundtruth_masks=groundtruth_masks.numpy(),
groundtruth_is_crowd=groundtruth_is_crowd.numpy())
self.next_groundtruth_annotation_id += len(groundtruth_annotations)
# Add to set of images for which there are gt & detections
# Infers image size from groundtruth masks.
_, height, width = groundtruth_masks.shape
self.dataset['images'].append({
'id': self.image_id,
'height': height,
'width': width,
})
self.dataset['annotations'].extend(groundtruth_annotations)
# Add predictions/detections.
detection_annotations = coco_tools.ExportSingleImageDetectionMasksToCoco(
self.image_id, self.category_ids, detection_masks.numpy(),
detection_scores.numpy(), detection_classes.numpy())
self.detections.extend(detection_annotations)
self.image_id += 1
def _instance_masks(panoptic_label_map: tf.Tensor,
instance_panoptic_labels: tf.Tensor) -> tf.Tensor:
"""Constructs an array of masks for each instance in a panoptic label map.
Args:
panoptic_label_map: An integer tensor of shape `[image_height, image_width]`
specifying the panoptic label at each pixel.
instance_panoptic_labels: An integer tensor of shape `[num_instances]` that
gives the label for each unique instance for which to compute masks.
Returns:
A boolean tensor of shape `[num_instances, image_height, image_width]` where
each slice in the first dimension gives the mask for a single instance over
the entire image.
"""
return tf.math.equal(
tf.expand_dims(panoptic_label_map, 0),
tf.reshape(instance_panoptic_labels,
[tf.size(instance_panoptic_labels), 1, 1]))
class PanopticInstanceAveragePrecision(tf.keras.metrics.Metric):
"""Computes instance segmentation AP of panoptic segmentations.
Panoptic segmentation includes both "thing" and "stuff" classes. This class
ignores the "stuff" classes to report metrics on only the "thing" classes
that have discrete instances. It computes a series of AP-based metrics using
the COCO evaluation scripts.
"""
def __init__(self,
num_classes: int,
things_list: Collection[int],
label_divisor: int,
ignored_label: int,
name: str = 'panoptic_instance_ap',
**kwargs):
"""Constructs panoptic instance segmentation evaluation class."""
super(PanopticInstanceAveragePrecision, self).__init__(name=name, **kwargs)
self.num_classes = num_classes
self.stuff_list = set(range(num_classes)).difference(things_list)
self.label_divisor = label_divisor
self.ignored_label = ignored_label
self.detection_metric = InstanceAveragePrecision()
self.reset_states()
def reset_states(self) -> None:
self.detection_metric.reset_states()
def result(self) -> np.ndarray:
return self.detection_metric.result()
def update_state(self,
groundtruth_panoptic: tf.Tensor,
predicted_panoptic: tf.Tensor,
semantic_probability: tf.Tensor,
instance_score_map: tf.Tensor,
is_crowd_map: Optional[tf.Tensor] = None) -> None:
"""Adds the results from a new image to be computed by the metric.
Args:
groundtruth_panoptic: A 2D integer tensor, with the true panoptic label at
each pixel.
predicted_panoptic: 2D integer tensor with predicted panoptic labels to be
evaluated.
semantic_probability: An float tensor of shape `[image_height,
image_width, num_classes]`. Specifies at each pixel the estimated
probability distribution that that pixel belongs to each semantic class.
instance_score_map: A 2D float tensor, where the pixels for an instance
will have the probability of that being an instance.
is_crowd_map: A 2D boolean tensor. Where it is True, the instance in that
region is a "crowd" instance. It is assumed that all pixels in an
instance will have the same value in this map. If set to None (the
default), it will be assumed that none of the ground truth instances are
crowds.
"""
classes_to_ignore = tf.convert_to_tensor([self.ignored_label] +
list(self.stuff_list), tf.int32)
(gt_unique_labels,
gt_box_coords) = panoptic_instances.instance_boxes_from_masks(
groundtruth_panoptic, classes_to_ignore, self.label_divisor)
gt_classes = tf.math.floordiv(gt_unique_labels, self.label_divisor)
gt_masks = _instance_masks(groundtruth_panoptic, gt_unique_labels)
if is_crowd_map is None:
gt_is_crowd = tf.zeros(tf.shape(gt_classes), tf.bool)
else:
gt_is_crowd = panoptic_instances.per_instance_is_crowd(
is_crowd_map, groundtruth_panoptic, gt_unique_labels)
(pred_unique_labels,
pred_scores) = panoptic_instances.combined_instance_scores(
predicted_panoptic, semantic_probability, instance_score_map,
self.label_divisor, self.ignored_label)
# Filter out stuff and ignored label.
pred_classes = tf.math.floordiv(pred_unique_labels, self.label_divisor)
pred_class_is_ignored = tf.math.reduce_any(
tf.math.equal(
tf.expand_dims(pred_classes, 1),
tf.expand_dims(classes_to_ignore, 0)),
axis=1)
pred_class_is_kept = tf.math.logical_not(pred_class_is_ignored)
pred_unique_labels = tf.boolean_mask(pred_unique_labels, pred_class_is_kept)
pred_scores = tf.boolean_mask(pred_scores, pred_class_is_kept)
# Recompute class labels after the filtering.
pred_classes = tf.math.floordiv(pred_unique_labels, self.label_divisor)
pred_masks = _instance_masks(predicted_panoptic, pred_unique_labels)
self.detection_metric.update_state(gt_box_coords, gt_classes, gt_masks,
gt_is_crowd, pred_masks, pred_scores,
pred_classes)