|
|
- # 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.
- # ==============================================================================
- """Class for evaluating object detections with calibration metrics."""
-
- from __future__ import absolute_import
- from __future__ import division
- from __future__ import print_function
-
- import tensorflow as tf
-
- from object_detection.box_coders import mean_stddev_box_coder
- from object_detection.core import box_list
- from object_detection.core import region_similarity_calculator
- from object_detection.core import standard_fields
- from object_detection.core import target_assigner
- from object_detection.matchers import argmax_matcher
- from object_detection.metrics import calibration_metrics
- from object_detection.utils import object_detection_evaluation
-
-
- # TODO(zbeaver): Implement metrics per category.
- class CalibrationDetectionEvaluator(
- object_detection_evaluation.DetectionEvaluator):
- """Class to evaluate calibration detection metrics."""
-
- def __init__(self,
- categories,
- iou_threshold=0.5):
- """Constructor.
-
- Args:
- categories: A list of dicts, each of which has the following keys -
- 'id': (required) an integer id uniquely identifying this category.
- 'name': (required) string representing category name e.g., 'cat', 'dog'.
- iou_threshold: Threshold above which to consider a box as matched during
- evaluation.
- """
- super(CalibrationDetectionEvaluator, self).__init__(categories)
-
- # Constructing target_assigner to match detections to groundtruth.
- similarity_calc = region_similarity_calculator.IouSimilarity()
- matcher = argmax_matcher.ArgMaxMatcher(
- matched_threshold=iou_threshold, unmatched_threshold=iou_threshold)
- box_coder = mean_stddev_box_coder.MeanStddevBoxCoder(stddev=0.1)
- self._target_assigner = target_assigner.TargetAssigner(
- similarity_calc, matcher, box_coder)
-
- def match_single_image_info(self, image_info):
- """Match detections to groundtruth for a single image.
-
- Detections are matched to available groundtruth in the image based on the
- IOU threshold from the constructor. The classes of the detections and
- groundtruth matches are then compared. Detections that do not have IOU above
- the required threshold or have different classes from their match are
- considered negative matches. All inputs in `image_info` originate or are
- inferred from the eval_dict passed to class method
- `get_estimator_eval_metric_ops`.
-
- Args:
- image_info: a tuple or list containing the following (in order):
- - gt_boxes: tf.float32 tensor of groundtruth boxes.
- - gt_classes: tf.int64 tensor of groundtruth classes associated with
- groundtruth boxes.
- - num_gt_box: scalar indicating the number of groundtruth boxes per
- image.
- - det_boxes: tf.float32 tensor of detection boxes.
- - det_classes: tf.int64 tensor of detection classes associated with
- detection boxes.
- - num_det_box: scalar indicating the number of detection boxes per
- image.
- Returns:
- is_class_matched: tf.int64 tensor identical in shape to det_boxes,
- indicating whether detection boxes matched with and had the same
- class as groundtruth annotations.
- """
- (gt_boxes, gt_classes, num_gt_box, det_boxes, det_classes,
- num_det_box) = image_info
- detection_boxes = det_boxes[:num_det_box]
- detection_classes = det_classes[:num_det_box]
- groundtruth_boxes = gt_boxes[:num_gt_box]
- groundtruth_classes = gt_classes[:num_gt_box]
- det_boxlist = box_list.BoxList(detection_boxes)
- gt_boxlist = box_list.BoxList(groundtruth_boxes)
-
- # Target assigner requires classes in one-hot format. An additional
- # dimension is required since gt_classes are 1-indexed; the zero index is
- # provided to all non-matches.
- one_hot_depth = tf.cast(tf.add(tf.reduce_max(groundtruth_classes), 1),
- dtype=tf.int32)
- gt_classes_one_hot = tf.one_hot(
- groundtruth_classes, one_hot_depth, dtype=tf.float32)
- one_hot_cls_targets, _, _, _, _ = self._target_assigner.assign(
- det_boxlist,
- gt_boxlist,
- gt_classes_one_hot,
- unmatched_class_label=tf.zeros(shape=one_hot_depth, dtype=tf.float32))
- # Transform from one-hot back to indexes.
- cls_targets = tf.argmax(one_hot_cls_targets, axis=1)
- is_class_matched = tf.cast(
- tf.equal(tf.cast(cls_targets, tf.int64), detection_classes),
- dtype=tf.int64)
- return is_class_matched
-
- def get_estimator_eval_metric_ops(self, eval_dict):
- """Returns a dictionary of eval metric ops.
-
- Note that once value_op is called, the detections and groundtruth added via
- update_op are cleared.
-
- This function can take in groundtruth and detections for a batch of images,
- or for a single image. For the latter case, the batch dimension for input
- tensors need not be present.
-
- Args:
- eval_dict: A dictionary that holds tensors for evaluating object detection
- performance. For single-image evaluation, this dictionary may be
- produced from eval_util.result_dict_for_single_example(). If multi-image
- evaluation, `eval_dict` should contain the fields
- 'num_groundtruth_boxes_per_image' and 'num_det_boxes_per_image' to
- properly unpad the tensors from the batch.
-
- Returns:
- a dictionary of metric names to tuple of value_op and update_op that can
- be used as eval metric ops in tf.estimator.EstimatorSpec. Note that all
- update ops must be run together and similarly all value ops must be run
- together to guarantee correct behaviour.
- """
- # Unpack items from the evaluation dictionary.
- input_data_fields = standard_fields.InputDataFields
- detection_fields = standard_fields.DetectionResultFields
- image_id = eval_dict[input_data_fields.key]
- groundtruth_boxes = eval_dict[input_data_fields.groundtruth_boxes]
- groundtruth_classes = eval_dict[input_data_fields.groundtruth_classes]
- detection_boxes = eval_dict[detection_fields.detection_boxes]
- detection_scores = eval_dict[detection_fields.detection_scores]
- detection_classes = eval_dict[detection_fields.detection_classes]
- num_gt_boxes_per_image = eval_dict.get(
- 'num_groundtruth_boxes_per_image', None)
- num_det_boxes_per_image = eval_dict.get('num_det_boxes_per_image', None)
- is_annotated_batched = eval_dict.get('is_annotated', None)
-
- if not image_id.shape.as_list():
- # Apply a batch dimension to all tensors.
- image_id = tf.expand_dims(image_id, 0)
- groundtruth_boxes = tf.expand_dims(groundtruth_boxes, 0)
- groundtruth_classes = tf.expand_dims(groundtruth_classes, 0)
- detection_boxes = tf.expand_dims(detection_boxes, 0)
- detection_scores = tf.expand_dims(detection_scores, 0)
- detection_classes = tf.expand_dims(detection_classes, 0)
-
- if num_gt_boxes_per_image is None:
- num_gt_boxes_per_image = tf.shape(groundtruth_boxes)[1:2]
- else:
- num_gt_boxes_per_image = tf.expand_dims(num_gt_boxes_per_image, 0)
-
- if num_det_boxes_per_image is None:
- num_det_boxes_per_image = tf.shape(detection_boxes)[1:2]
- else:
- num_det_boxes_per_image = tf.expand_dims(num_det_boxes_per_image, 0)
-
- if is_annotated_batched is None:
- is_annotated_batched = tf.constant([True])
- else:
- is_annotated_batched = tf.expand_dims(is_annotated_batched, 0)
- else:
- if num_gt_boxes_per_image is None:
- num_gt_boxes_per_image = tf.tile(
- tf.shape(groundtruth_boxes)[1:2],
- multiples=tf.shape(groundtruth_boxes)[0:1])
- if num_det_boxes_per_image is None:
- num_det_boxes_per_image = tf.tile(
- tf.shape(detection_boxes)[1:2],
- multiples=tf.shape(detection_boxes)[0:1])
- if is_annotated_batched is None:
- is_annotated_batched = tf.ones_like(image_id, dtype=tf.bool)
-
- # Filter images based on is_annotated_batched and match detections.
- image_info = [tf.boolean_mask(tensor, is_annotated_batched) for tensor in
- [groundtruth_boxes, groundtruth_classes,
- num_gt_boxes_per_image, detection_boxes, detection_classes,
- num_det_boxes_per_image]]
- is_class_matched = tf.map_fn(
- self.match_single_image_info, image_info, dtype=tf.int64)
- y_true = tf.squeeze(is_class_matched)
- y_pred = tf.squeeze(tf.boolean_mask(detection_scores, is_annotated_batched))
- ece, update_op = calibration_metrics.expected_calibration_error(
- y_true, y_pred)
- return {'CalibrationError/ExpectedCalibrationError': (ece, update_op)}
-
- def add_single_ground_truth_image_info(self, image_id, groundtruth_dict):
- """Adds groundtruth for a single image to be used for evaluation.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- groundtruth_dict: A dictionary of groundtruth numpy arrays required
- for evaluations.
- """
- raise NotImplementedError
-
- def add_single_detected_image_info(self, image_id, detections_dict):
- """Adds detections for a single image to be used for evaluation.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- detections_dict: A dictionary of detection numpy arrays required for
- evaluation.
- """
- raise NotImplementedError
-
- def evaluate(self):
- """Evaluates detections and returns a dictionary of metrics."""
- raise NotImplementedError
-
- def clear(self):
- """Clears the state to prepare for a fresh evaluation."""
- raise NotImplementedError
|