|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| """Implementation of the Panoptic Quality metric.
|
|
|
| Panoptic Quality is an instance-based metric for evaluating the task of
|
| image parsing, aka panoptic segmentation.
|
|
|
| Please see the paper for details:
|
| "Panoptic Segmentation", Alexander Kirillov, Kaiming He, Ross Girshick,
|
| Carsten Rother and Piotr Dollar. arXiv:1801.00868, 2018.
|
|
|
| Note that this metric class is branched from
|
| https://github.com/tensorflow/models/blob/master/research/deeplab/evaluation/panoptic_quality.py
|
| """
|
|
|
| import collections
|
| from typing import Any, Dict, Optional, Tuple, Union
|
|
|
| import numpy as np
|
| import tensorflow as tf, tf_keras
|
|
|
| from official.vision.ops import box_ops
|
|
|
| _EPSILON = 1e-10
|
|
|
|
|
| def realdiv_maybe_zero(x, y):
|
| """Element-wise x / y where y may contain zeros, for those returns 0 too."""
|
| return np.where(
|
| np.less(np.abs(y), _EPSILON), np.zeros_like(x), np.divide(x, y))
|
|
|
|
|
| def _ids_to_counts(id_array):
|
| """Given a numpy array, a mapping from each unique entry to its count."""
|
| ids, counts = np.unique(id_array, return_counts=True)
|
| return dict(zip(ids, counts))
|
|
|
|
|
| class PanopticQuality:
|
| """Metric class for Panoptic Quality.
|
|
|
| "Panoptic Segmentation" by Alexander Kirillov, Kaiming He, Ross Girshick,
|
| Carsten Rother, Piotr Dollar.
|
| https://arxiv.org/abs/1801.00868
|
| """
|
|
|
| def __init__(self, num_categories, ignored_label, max_instances_per_category,
|
| offset):
|
| """Initialization for PanopticQualityMetric.
|
|
|
| Args:
|
| 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 COCO panoptic segmentation dataset.
|
| max_instances_per_category: The maximum number of instances for each
|
| category. Used in ensuring unique instance labels.
|
| offset: The maximum number of unique labels. This is used, by multiplying
|
| the ground-truth labels, to generate unique ids for individual regions
|
| of overlap between ground-truth and predicted segments.
|
| """
|
| self.num_categories = num_categories
|
| self.ignored_label = ignored_label
|
| self.max_instances_per_category = max_instances_per_category
|
| self.offset = offset
|
| self.reset()
|
|
|
| def _naively_combine_labels(self, category_mask, instance_mask):
|
| """Naively creates a combined label array from categories and instances."""
|
| return (category_mask.astype(np.uint32) * self.max_instances_per_category +
|
| instance_mask.astype(np.uint32))
|
|
|
| def compare_and_accumulate(self, groundtruths, predictions):
|
| """Compares predictions with ground-truths, and accumulates the metrics.
|
|
|
| It is not assumed that instance ids are unique across different categories.
|
| See for example combine_semantic_and_instance_predictions.py in official
|
| PanopticAPI evaluation code for issues to consider when fusing category
|
| and instance labels.
|
|
|
| Instances ids of the ignored category have the meaning that id 0 is "void"
|
| and remaining ones are crowd instances.
|
|
|
| Args:
|
| groundtruths: A dictionary contains ground-truth labels. It should contain
|
| the following fields.
|
| - category_mask: A 2D numpy uint16 array of ground-truth per-pixel
|
| category labels.
|
| - instance_mask: A 2D numpy uint16 array of ground-truth per-pixel
|
| instance labels.
|
| predictions: A dictionary contains the model outputs. It should contain
|
| the following fields.
|
| - category_array: A 2D numpy uint16 array of predicted per-pixel
|
| category labels.
|
| - instance_array: A 2D numpy uint16 array of predicted instance labels.
|
| """
|
| groundtruth_category_mask = groundtruths['category_mask']
|
| groundtruth_instance_mask = groundtruths['instance_mask']
|
| predicted_category_mask = predictions['category_mask']
|
| predicted_instance_mask = predictions['instance_mask']
|
|
|
|
|
|
|
| pred_segment_id = self._naively_combine_labels(predicted_category_mask,
|
| predicted_instance_mask)
|
| gt_segment_id = self._naively_combine_labels(groundtruth_category_mask,
|
| groundtruth_instance_mask)
|
|
|
|
|
| gt_segment_areas = _ids_to_counts(gt_segment_id)
|
| pred_segment_areas = _ids_to_counts(pred_segment_id)
|
|
|
|
|
| void_segment_id = self.ignored_label * self.max_instances_per_category
|
|
|
|
|
|
|
|
|
| ignored_segment_ids = {
|
| gt_segment_id for gt_segment_id in gt_segment_areas
|
| if (gt_segment_id //
|
| self.max_instances_per_category) == self.ignored_label
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| intersection_id_array = (
|
| gt_segment_id.astype(np.uint64) * self.offset +
|
| pred_segment_id.astype(np.uint64))
|
|
|
|
|
|
|
|
|
| intersection_areas = _ids_to_counts(intersection_id_array)
|
|
|
|
|
|
|
| def prediction_void_overlap(pred_segment_id):
|
| void_intersection_id = void_segment_id * self.offset + pred_segment_id
|
| return intersection_areas.get(void_intersection_id, 0)
|
|
|
|
|
| def prediction_ignored_overlap(pred_segment_id):
|
| total_ignored_overlap = 0
|
| for ignored_segment_id in ignored_segment_ids:
|
| intersection_id = ignored_segment_id * self.offset + pred_segment_id
|
| total_ignored_overlap += intersection_areas.get(intersection_id, 0)
|
| return total_ignored_overlap
|
|
|
|
|
|
|
|
|
| gt_matched = set()
|
| pred_matched = set()
|
|
|
|
|
| for intersection_id, intersection_area in intersection_areas.items():
|
| gt_segment_id = int(intersection_id // self.offset)
|
| pred_segment_id = int(intersection_id % self.offset)
|
|
|
| gt_category = int(gt_segment_id // self.max_instances_per_category)
|
| pred_category = int(pred_segment_id // self.max_instances_per_category)
|
| if gt_category != pred_category:
|
| continue
|
|
|
|
|
|
|
|
|
| union = (
|
| gt_segment_areas[gt_segment_id] +
|
| pred_segment_areas[pred_segment_id] - intersection_area -
|
| prediction_void_overlap(pred_segment_id))
|
| iou = intersection_area / union
|
| if iou > 0.5:
|
| self.tp_per_class[gt_category] += 1
|
| self.iou_per_class[gt_category] += iou
|
| gt_matched.add(gt_segment_id)
|
| pred_matched.add(pred_segment_id)
|
|
|
|
|
| for gt_segment_id in gt_segment_areas:
|
| if gt_segment_id in gt_matched:
|
| continue
|
| category = gt_segment_id // self.max_instances_per_category
|
|
|
| if category == self.ignored_label:
|
| continue
|
| self.fn_per_class[category] += 1
|
|
|
|
|
| for pred_segment_id in pred_segment_areas:
|
| if pred_segment_id in pred_matched:
|
| continue
|
|
|
|
|
| if (prediction_ignored_overlap(pred_segment_id) /
|
| pred_segment_areas[pred_segment_id]) > 0.5:
|
| continue
|
| category = pred_segment_id // self.max_instances_per_category
|
| self.fp_per_class[category] += 1
|
|
|
| def _valid_categories(self):
|
| """Categories with a "valid" value for the metric, have > 0 instances.
|
|
|
| We will ignore the `ignore_label` class and other classes which have
|
| `tp + fn + fp = 0`.
|
|
|
| Returns:
|
| Boolean array of shape `[num_categories]`.
|
| """
|
| valid_categories = np.not_equal(
|
| self.tp_per_class + self.fn_per_class + self.fp_per_class, 0)
|
| if self.ignored_label >= 0 and self.ignored_label < self.num_categories:
|
| valid_categories[self.ignored_label] = False
|
| return valid_categories
|
|
|
| def result_per_category(self):
|
| """For supported metrics, return individual per-category metric values.
|
|
|
| Returns:
|
| A dictionary contains all per-class metrics, each metrics is a numpy array
|
| of shape `[self.num_categories]`, where index `i` is the metrics value
|
| over only that category.
|
| """
|
| sq_per_class = realdiv_maybe_zero(self.iou_per_class, self.tp_per_class)
|
| rq_per_class = realdiv_maybe_zero(
|
| self.tp_per_class,
|
| self.tp_per_class + 0.5 * self.fn_per_class + 0.5 * self.fp_per_class)
|
| return {
|
| 'sq_per_class': sq_per_class,
|
| 'rq_per_class': rq_per_class,
|
| 'pq_per_class': np.multiply(sq_per_class, rq_per_class)
|
| }
|
|
|
| def result(self, is_thing=None):
|
| """Computes and returns the detailed metric results over all comparisons.
|
|
|
| Args:
|
| is_thing: A boolean array of length `num_categories`. The entry
|
| `is_thing[category_id]` is True iff that category is a "thing" category
|
| instead of "stuff."
|
|
|
| Returns:
|
| A dictionary with a breakdown of metrics and/or metric factors by things,
|
| stuff, and all categories.
|
| """
|
| results = self.result_per_category()
|
| valid_categories = self._valid_categories()
|
|
|
| category_sets = collections.OrderedDict()
|
| category_sets['All'] = valid_categories
|
| if is_thing is not None:
|
| category_sets['Things'] = np.logical_and(valid_categories, is_thing)
|
| category_sets['Stuff'] = np.logical_and(valid_categories,
|
| np.logical_not(is_thing))
|
|
|
| for category_set_name, in_category_set in category_sets.items():
|
| if np.any(in_category_set):
|
| results.update({
|
| f'{category_set_name}_pq':
|
| np.mean(results['pq_per_class'][in_category_set]),
|
| f'{category_set_name}_sq':
|
| np.mean(results['sq_per_class'][in_category_set]),
|
| f'{category_set_name}_rq':
|
| np.mean(results['rq_per_class'][in_category_set]),
|
|
|
| f'{category_set_name}_num_categories':
|
| np.sum(in_category_set.astype(np.int32)),
|
| })
|
| else:
|
| results.update({
|
| f'{category_set_name}_pq': 0.,
|
| f'{category_set_name}_sq': 0.,
|
| f'{category_set_name}_rq': 0.,
|
| f'{category_set_name}_num_categories': 0
|
| })
|
|
|
| return results
|
|
|
| def reset(self):
|
| """Resets the accumulation to the metric class's state at initialization."""
|
| self.iou_per_class = np.zeros(self.num_categories, dtype=np.float64)
|
| self.tp_per_class = np.zeros(self.num_categories, dtype=np.float64)
|
| self.fn_per_class = np.zeros(self.num_categories, dtype=np.float64)
|
| self.fp_per_class = np.zeros(self.num_categories, dtype=np.float64)
|
|
|
|
|
| def _get_instance_class_ids(
|
| category_mask: tf.Tensor,
|
| instance_mask: tf.Tensor,
|
| max_num_instances: int,
|
| ignored_label: int,
|
| ) -> tf.Tensor:
|
| """Get the class id of each instance (index starts from 1)."""
|
|
|
| instance_mask = tf.where(
|
| (instance_mask == 0) | (category_mask == ignored_label), -1, instance_mask
|
| )
|
|
|
| instance_binary_mask = tf.one_hot(
|
| instance_mask, max_num_instances + 1, dtype=tf.int32
|
| )
|
|
|
| result = tf.reduce_max(
|
| instance_binary_mask * category_mask[..., tf.newaxis], axis=[1, 2]
|
| )
|
|
|
| return tf.where(result == 0, -1, result)
|
|
|
|
|
| class PanopticQualityV2(tf_keras.metrics.Metric):
|
| """Panoptic quality metrics with vectorized implementation.
|
|
|
| This implementation is supported on TPU.
|
|
|
| "Panoptic Segmentation" by Alexander Kirillov, Kaiming He, Ross Girshick,
|
| Carsten Rother, Piotr Dollar.
|
| https://arxiv.org/abs/1801.00868
|
| """
|
|
|
| def __init__(
|
| self,
|
| num_categories: int,
|
| is_thing: Optional[Tuple[bool, ...]] = None,
|
| max_num_instances: int = 255,
|
| ignored_label: int = 255,
|
| rescale_predictions: bool = False,
|
| name: Optional[str] = None,
|
| dtype: Optional[Union[str, tf.dtypes.DType]] = tf.float32,
|
| ):
|
| """Initialization for PanopticQualityV2.
|
|
|
| Args:
|
| num_categories: the number of categories.
|
| is_thing: a boolean array of length `num_categories`. The entry
|
| `is_thing[category_id]` is True iff that category is a "thing" category
|
| instead of "stuff". Default to `None`, and it means categories are not
|
| classified into these two categories.
|
| max_num_instances: the maximum number of instances in an image.
|
| ignored_label: a category id that is ignored in evaluation, e.g. the void
|
| label as defined in COCO panoptic segmentation dataset.
|
| rescale_predictions: whether to scale back prediction to original image
|
| sizes. If True, the image_info of the groundtruth is used to rescale
|
| predictions.
|
| name: string name of the metric instance.
|
| dtype: data type of the metric result.
|
| """
|
| super().__init__(name=name, dtype=dtype)
|
|
|
| self._num_categories = num_categories
|
| if is_thing is not None:
|
| self._is_thing = is_thing
|
| else:
|
| self._is_thing = [True] * self._num_categories
|
| self._max_num_instances = max_num_instances
|
| self._ignored_label = ignored_label
|
| self._rescale_predictions = rescale_predictions
|
|
|
|
|
| self.tp_count = self.add_weight(
|
| 'tp_count',
|
| shape=[self._num_categories],
|
| initializer='zeros',
|
| dtype=tf.float32,
|
| )
|
| self.fp_count = self.add_weight(
|
| 'fp_count',
|
| shape=[self._num_categories],
|
| initializer='zeros',
|
| dtype=tf.float32,
|
| )
|
| self.fn_count = self.add_weight(
|
| 'fn_count',
|
| shape=[self._num_categories],
|
| initializer='zeros',
|
| dtype=tf.float32,
|
| )
|
| self.tp_iou_sum = self.add_weight(
|
| 'tp_iou_sum',
|
| shape=[self._num_categories],
|
| initializer='zeros',
|
| dtype=tf.float32,
|
| )
|
|
|
| def get_config(self) -> Dict[str, Any]:
|
| """Returns the serializable config of the metric."""
|
| return {
|
| 'num_categories': self._num_categories,
|
| 'is_thing': self._is_thing,
|
| 'max_num_instances': self._max_num_instances,
|
| 'ignored_label': self._ignored_label,
|
| 'rescale_predictions': self._rescale_predictions,
|
| 'name': self.name,
|
| 'dtype': self.dtype,
|
| }
|
|
|
| def reset_state(self):
|
| """Resets all of the metric state variables."""
|
| self.tp_count.assign(tf.zeros_like(self.tp_count))
|
| self.fp_count.assign(tf.zeros_like(self.fp_count))
|
| self.fn_count.assign(tf.zeros_like(self.fn_count))
|
| self.tp_iou_sum.assign(tf.zeros_like(self.tp_iou_sum))
|
|
|
| def update_state(
|
| self, y_true: Dict[str, tf.Tensor], y_pred: Dict[str, tf.Tensor]
|
| ):
|
| category_mask = tf.convert_to_tensor(y_pred['category_mask'], tf.int32)
|
| instance_mask = tf.convert_to_tensor(y_pred['instance_mask'], tf.int32)
|
| gt_category_mask = tf.convert_to_tensor(y_true['category_mask'], tf.int32)
|
| gt_instance_mask = tf.convert_to_tensor(y_true['instance_mask'], tf.int32)
|
|
|
| if self._rescale_predictions:
|
| _, height, width = gt_category_mask.get_shape().as_list()
|
|
|
|
|
|
|
| image_shape = tf.cast(y_true['image_info'][:, 0, :], tf.int32)
|
|
|
| y0_x0 = tf.broadcast_to(
|
| tf.constant([[0, 0]], dtype=tf.int32), tf.shape(image_shape)
|
| )
|
|
|
| image_shape_bbox = tf.concat([y0_x0, image_shape], axis=1)
|
|
|
| image_shape_masks = box_ops.bbox2mask(
|
| bbox=image_shape_bbox,
|
| image_height=height,
|
| image_width=width,
|
| dtype=tf.bool,
|
| )
|
|
|
| category_mask = tf.where(
|
| image_shape_masks, category_mask, self._ignored_label
|
| )
|
| instance_mask = tf.where(image_shape_masks, instance_mask, 0)
|
| gt_category_mask = tf.where(
|
| image_shape_masks, gt_category_mask, self._ignored_label
|
| )
|
| gt_instance_mask = tf.where(image_shape_masks, gt_instance_mask, 0)
|
|
|
| self._update_thing_classes(
|
| category_mask, instance_mask, gt_category_mask, gt_instance_mask
|
| )
|
| self._update_stuff_classes(category_mask, gt_category_mask)
|
|
|
| def _update_thing_classes(
|
| self,
|
| category_mask: tf.Tensor,
|
| instance_mask: tf.Tensor,
|
| gt_category_mask: tf.Tensor,
|
| gt_instance_mask: tf.Tensor,
|
| ):
|
| _, height, width = category_mask.get_shape().as_list()
|
|
|
|
|
| instance_class_ids = _get_instance_class_ids(
|
| category_mask,
|
| instance_mask,
|
| self._max_num_instances,
|
| self._ignored_label,
|
| )
|
|
|
| gt_instance_class_ids = _get_instance_class_ids(
|
| gt_category_mask,
|
| gt_instance_mask,
|
| self._max_num_instances,
|
| self._ignored_label,
|
| )
|
|
|
|
|
| valid_mask = gt_category_mask != self._ignored_label
|
|
|
|
|
| instance_binary_masks = tf.one_hot(
|
| tf.where(instance_mask > 0, instance_mask, -1),
|
| self._max_num_instances + 1,
|
| on_value=True,
|
| off_value=False,
|
| )
|
|
|
| gt_instance_binary_masks = tf.one_hot(
|
| tf.where(gt_instance_mask > 0, gt_instance_mask, -1),
|
| self._max_num_instances + 1,
|
| on_value=True,
|
| off_value=False,
|
| )
|
|
|
|
|
| flattened_binary_masks = tf.reshape(
|
| instance_binary_masks & valid_mask[..., tf.newaxis],
|
| [-1, height * width, self._max_num_instances + 1],
|
| )
|
|
|
| flattened_gt_binary_masks = tf.reshape(
|
| gt_instance_binary_masks & valid_mask[..., tf.newaxis],
|
| [-1, height * width, self._max_num_instances + 1],
|
| )
|
|
|
| flattened_binary_masks = tf.transpose(flattened_binary_masks, [0, 2, 1])
|
|
|
| intersection = tf.matmul(
|
| tf.cast(flattened_binary_masks, tf.float32),
|
| tf.cast(flattened_gt_binary_masks, tf.float32),
|
| )
|
| union = (
|
| tf.math.count_nonzero(
|
| flattened_binary_masks, axis=-1, keepdims=True, dtype=tf.float32
|
| )
|
| + tf.math.count_nonzero(
|
| flattened_gt_binary_masks, axis=-2, keepdims=True, dtype=tf.float32
|
| )
|
| - intersection
|
| )
|
|
|
| detection_to_gt_ious = tf.math.divide_no_nan(intersection, union)
|
| detection_matches_gt = (
|
| (detection_to_gt_ious > 0.5)
|
| & (
|
| instance_class_ids[:, :, tf.newaxis]
|
| == gt_instance_class_ids[:, tf.newaxis, :]
|
| )
|
| & (gt_instance_class_ids[:, tf.newaxis, :] > 0)
|
| )
|
|
|
|
|
| is_tp = tf.reduce_any(detection_matches_gt, axis=1)
|
|
|
| tp_iou = tf.reduce_max(
|
| tf.where(detection_matches_gt, detection_to_gt_ious, 0), axis=1
|
| )
|
|
|
|
|
| is_fp = tf.reduce_any(instance_binary_masks, axis=[1, 2]) & ~tf.reduce_any(
|
| detection_matches_gt, axis=2
|
| )
|
|
|
| fp_binary_mask = is_fp[:, tf.newaxis, tf.newaxis, :] & instance_binary_masks
|
|
|
| fp_area = tf.math.count_nonzero(
|
| fp_binary_mask, axis=[1, 2], dtype=tf.float32
|
| )
|
|
|
| fp_crowd_or_ignored_area = tf.math.count_nonzero(
|
| fp_binary_mask
|
| & (
|
| (
|
|
|
|
|
|
|
|
|
| (instance_mask > 0)
|
| & (category_mask > 0)
|
| & (gt_category_mask == category_mask)
|
| & (gt_instance_mask == 0)
|
| )
|
| | (gt_category_mask == self._ignored_label)
|
| )[..., tf.newaxis],
|
| axis=[1, 2],
|
| dtype=tf.float32,
|
| )
|
|
|
|
|
|
|
|
|
| is_fp &= tf.math.divide_no_nan(fp_crowd_or_ignored_area, fp_area) <= 0.5
|
|
|
|
|
| detection_by_class = tf.one_hot(
|
| instance_class_ids, self._num_categories, on_value=True, off_value=False
|
| )
|
|
|
| gt_by_class = tf.one_hot(
|
| gt_instance_class_ids,
|
| self._num_categories,
|
| on_value=True,
|
| off_value=False,
|
| )
|
|
|
|
|
| gt_count = tf.math.count_nonzero(gt_by_class, axis=[0, 1], dtype=tf.float32)
|
| tp_count = tf.math.count_nonzero(
|
| is_tp[..., tf.newaxis] & gt_by_class, axis=[0, 1], dtype=tf.float32
|
| )
|
| fn_count = gt_count - tp_count
|
| fp_count = tf.math.count_nonzero(
|
| is_fp[..., tf.newaxis] & detection_by_class,
|
| axis=[0, 1],
|
| dtype=tf.float32,
|
| )
|
| tp_iou_sum = tf.reduce_sum(
|
| tf.cast(gt_by_class, tf.float32) * tp_iou[..., tf.newaxis], axis=[0, 1]
|
| )
|
|
|
| self.tp_count.assign_add(tp_count)
|
| self.fn_count.assign_add(fn_count)
|
| self.fp_count.assign_add(fp_count)
|
| self.tp_iou_sum.assign_add(tp_iou_sum)
|
|
|
| def _update_stuff_classes(
|
| self, category_mask: tf.Tensor, gt_category_mask: tf.Tensor
|
| ):
|
|
|
| category_binary_mask = tf.one_hot(
|
| category_mask, self._num_categories, on_value=True, off_value=False
|
| )
|
| gt_category_binary_mask = tf.one_hot(
|
| gt_category_mask, self._num_categories, on_value=True, off_value=False
|
| )
|
|
|
|
|
| valid_mask = gt_category_mask != self._ignored_label
|
|
|
|
|
| intersection = tf.math.count_nonzero(
|
| category_binary_mask
|
| & gt_category_binary_mask
|
| & valid_mask[..., tf.newaxis],
|
| axis=[1, 2],
|
| dtype=tf.float32,
|
| )
|
| union = tf.math.count_nonzero(
|
| (category_binary_mask | gt_category_binary_mask)
|
| & valid_mask[..., tf.newaxis],
|
| axis=[1, 2],
|
| dtype=tf.float32,
|
| )
|
| iou = tf.math.divide_no_nan(intersection, union)
|
|
|
| is_thing = tf.constant(self._is_thing, dtype=tf.bool)
|
|
|
| is_tp = (iou > 0.5) & ~is_thing
|
| is_fn = (
|
| tf.reduce_any(gt_category_binary_mask, axis=[1, 2]) & ~is_thing & ~is_tp
|
| )
|
| is_fp = (
|
| tf.reduce_any(category_binary_mask, axis=[1, 2]) & ~is_thing & ~is_tp
|
| )
|
|
|
|
|
| fp_binary_mask = is_fp[:, tf.newaxis, tf.newaxis, :] & category_binary_mask
|
|
|
| fp_area = tf.math.count_nonzero(
|
| fp_binary_mask, axis=[1, 2], dtype=tf.float32
|
| )
|
| fp_ignored_area = tf.math.count_nonzero(
|
| fp_binary_mask
|
| & (gt_category_mask == self._ignored_label)[..., tf.newaxis],
|
| axis=[1, 2],
|
| dtype=tf.float32,
|
| )
|
|
|
|
|
| is_fp &= tf.math.divide_no_nan(fp_ignored_area, fp_area) <= 0.5
|
|
|
|
|
| tp_count = tf.math.count_nonzero(is_tp, axis=0, dtype=tf.float32)
|
| fn_count = tf.math.count_nonzero(is_fn, axis=0, dtype=tf.float32)
|
| fp_count = tf.math.count_nonzero(is_fp, axis=0, dtype=tf.float32)
|
| tp_iou_sum = tf.reduce_sum(tf.cast(is_tp, tf.float32) * iou, axis=0)
|
|
|
| self.tp_count.assign_add(tp_count)
|
| self.fn_count.assign_add(fn_count)
|
| self.fp_count.assign_add(fp_count)
|
| self.tp_iou_sum.assign_add(tp_iou_sum)
|
|
|
| def result(self) -> Dict[str, tf.Tensor]:
|
| """Returns the metrics values as a dict."""
|
|
|
| tp_fn_fp_count = self.tp_count + self.fn_count + self.fp_count
|
| is_ignore_label = tf.one_hot(
|
| self._ignored_label,
|
| self._num_categories,
|
| on_value=True,
|
| off_value=False,
|
| )
|
|
|
| sq_per_class = tf.math.divide_no_nan(
|
| self.tp_iou_sum, self.tp_count
|
| ) * tf.cast(~is_ignore_label, tf.float32)
|
| rq_per_class = tf.math.divide_no_nan(
|
| self.tp_count, self.tp_count + 0.5 * self.fp_count + 0.5 * self.fn_count
|
| ) * tf.cast(~is_ignore_label, tf.float32)
|
| pq_per_class = sq_per_class * rq_per_class
|
| is_thing = tf.constant(self._is_thing, dtype=tf.bool)
|
|
|
| result = {
|
|
|
| 'valid_thing_classes': (
|
| (tp_fn_fp_count > 0) & is_thing & ~is_ignore_label
|
| ),
|
|
|
| 'valid_stuff_classes': (
|
| (tp_fn_fp_count > 0) & ~is_thing & ~is_ignore_label
|
| ),
|
|
|
| 'sq_per_class': sq_per_class,
|
|
|
| 'rq_per_class': rq_per_class,
|
|
|
| 'pq_per_class': pq_per_class,
|
| }
|
| return result
|
|
|