| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """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] |
|
|
| |
| 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] |
| """ |
| |
| self.metric_values = None |
|
|
| |
| self.category_ids.update(groundtruth_classes.numpy()) |
| self.category_ids.update(detection_classes.numpy()) |
|
|
| |
| 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) |
|
|
| |
| |
| _, height, width = groundtruth_masks.shape |
| self.dataset['images'].append({ |
| 'id': self.image_id, |
| 'height': height, |
| 'width': width, |
| }) |
| self.dataset['annotations'].extend(groundtruth_annotations) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|