|
|
- # Copyright 2017 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 COCO metrics."""
- import numpy as np
- import tensorflow as tf
-
- from object_detection.core import standard_fields
- from object_detection.metrics import coco_tools
- from object_detection.utils import json_utils
- from object_detection.utils import object_detection_evaluation
-
-
- class CocoDetectionEvaluator(object_detection_evaluation.DetectionEvaluator):
- """Class to evaluate COCO detection metrics."""
-
- def __init__(self,
- categories,
- include_metrics_per_category=False,
- all_metrics_per_category=False):
- """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'.
- include_metrics_per_category: If True, include metrics for each category.
- all_metrics_per_category: Whether to include all the summary metrics for
- each category in per_category_ap. Be careful with setting it to true if
- you have more than handful of categories, because it will pollute
- your mldash.
- """
- super(CocoDetectionEvaluator, self).__init__(categories)
- # _image_ids is a dictionary that maps unique image ids to Booleans which
- # indicate whether a corresponding detection has been added.
- self._image_ids = {}
- self._groundtruth_list = []
- self._detection_boxes_list = []
- self._category_id_set = set([cat['id'] for cat in self._categories])
- self._annotation_id = 1
- self._metrics = None
- self._include_metrics_per_category = include_metrics_per_category
- self._all_metrics_per_category = all_metrics_per_category
-
- def clear(self):
- """Clears the state to prepare for a fresh evaluation."""
- self._image_ids.clear()
- self._groundtruth_list = []
- self._detection_boxes_list = []
-
- def add_single_ground_truth_image_info(self,
- image_id,
- groundtruth_dict):
- """Adds groundtruth for a single image to be used for evaluation.
-
- If the image has already been added, a warning is logged, and groundtruth is
- ignored.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- groundtruth_dict: A dictionary containing -
- InputDataFields.groundtruth_boxes: float32 numpy array of shape
- [num_boxes, 4] containing `num_boxes` groundtruth boxes of the format
- [ymin, xmin, ymax, xmax] in absolute image coordinates.
- InputDataFields.groundtruth_classes: integer numpy array of shape
- [num_boxes] containing 1-indexed groundtruth classes for the boxes.
- InputDataFields.groundtruth_is_crowd (optional): integer numpy array of
- shape [num_boxes] containing iscrowd flag for groundtruth boxes.
- """
- if image_id in self._image_ids:
- tf.logging.warning('Ignoring ground truth with image id %s since it was '
- 'previously added', image_id)
- return
-
- groundtruth_is_crowd = groundtruth_dict.get(
- standard_fields.InputDataFields.groundtruth_is_crowd)
- # Drop groundtruth_is_crowd if empty tensor.
- if groundtruth_is_crowd is not None and not groundtruth_is_crowd.shape[0]:
- groundtruth_is_crowd = None
-
- self._groundtruth_list.extend(
- coco_tools.ExportSingleImageGroundtruthToCoco(
- image_id=image_id,
- next_annotation_id=self._annotation_id,
- category_id_set=self._category_id_set,
- groundtruth_boxes=groundtruth_dict[
- standard_fields.InputDataFields.groundtruth_boxes],
- groundtruth_classes=groundtruth_dict[
- standard_fields.InputDataFields.groundtruth_classes],
- groundtruth_is_crowd=groundtruth_is_crowd))
- self._annotation_id += groundtruth_dict[standard_fields.InputDataFields.
- groundtruth_boxes].shape[0]
- # Boolean to indicate whether a detection has been added for this image.
- self._image_ids[image_id] = False
-
- def add_single_detected_image_info(self,
- image_id,
- detections_dict):
- """Adds detections for a single image to be used for evaluation.
-
- If a detection has already been added for this image id, a warning is
- logged, and the detection is skipped.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- detections_dict: A dictionary containing -
- DetectionResultFields.detection_boxes: float32 numpy array of shape
- [num_boxes, 4] containing `num_boxes` detection boxes of the format
- [ymin, xmin, ymax, xmax] in absolute image coordinates.
- DetectionResultFields.detection_scores: float32 numpy array of shape
- [num_boxes] containing detection scores for the boxes.
- DetectionResultFields.detection_classes: integer numpy array of shape
- [num_boxes] containing 1-indexed detection classes for the boxes.
-
- Raises:
- ValueError: If groundtruth for the image_id is not available.
- """
- if image_id not in self._image_ids:
- raise ValueError('Missing groundtruth for image id: {}'.format(image_id))
-
- if self._image_ids[image_id]:
- tf.logging.warning('Ignoring detection with image id %s since it was '
- 'previously added', image_id)
- return
-
- self._detection_boxes_list.extend(
- coco_tools.ExportSingleImageDetectionBoxesToCoco(
- image_id=image_id,
- category_id_set=self._category_id_set,
- detection_boxes=detections_dict[standard_fields.
- DetectionResultFields
- .detection_boxes],
- detection_scores=detections_dict[standard_fields.
- DetectionResultFields.
- detection_scores],
- detection_classes=detections_dict[standard_fields.
- DetectionResultFields.
- detection_classes]))
- self._image_ids[image_id] = True
-
- def dump_detections_to_json_file(self, json_output_path):
- """Saves the detections into json_output_path in the format used by MS COCO.
-
- Args:
- json_output_path: String containing the output file's path. It can be also
- None. In that case nothing will be written to the output file.
- """
- if json_output_path and json_output_path is not None:
- with tf.gfile.GFile(json_output_path, 'w') as fid:
- tf.logging.info('Dumping detections to output json file.')
- json_utils.Dump(
- obj=self._detection_boxes_list, fid=fid, float_digits=4, indent=2)
-
- def evaluate(self):
- """Evaluates the detection boxes and returns a dictionary of coco metrics.
-
- Returns:
- A dictionary holding -
-
- 1. summary_metrics:
- 'DetectionBoxes_Precision/mAP': mean average precision over classes
- averaged over IOU thresholds ranging from .5 to .95 with .05
- increments.
- 'DetectionBoxes_Precision/mAP@.50IOU': mean average precision at 50% IOU
- 'DetectionBoxes_Precision/mAP@.75IOU': mean average precision at 75% IOU
- 'DetectionBoxes_Precision/mAP (small)': mean average precision for small
- objects (area < 32^2 pixels).
- 'DetectionBoxes_Precision/mAP (medium)': mean average precision for
- medium sized objects (32^2 pixels < area < 96^2 pixels).
- 'DetectionBoxes_Precision/mAP (large)': mean average precision for large
- objects (96^2 pixels < area < 10000^2 pixels).
- 'DetectionBoxes_Recall/AR@1': average recall with 1 detection.
- 'DetectionBoxes_Recall/AR@10': average recall with 10 detections.
- 'DetectionBoxes_Recall/AR@100': average recall with 100 detections.
- 'DetectionBoxes_Recall/AR@100 (small)': average recall for small objects
- with 100.
- 'DetectionBoxes_Recall/AR@100 (medium)': average recall for medium objects
- with 100.
- 'DetectionBoxes_Recall/AR@100 (large)': average recall for large objects
- with 100 detections.
-
- 2. per_category_ap: if include_metrics_per_category is True, category
- specific results with keys of the form:
- 'Precision mAP ByCategory/category' (without the supercategory part if
- no supercategories exist). For backward compatibility
- 'PerformanceByCategory' is included in the output regardless of
- all_metrics_per_category.
- """
- groundtruth_dict = {
- 'annotations': self._groundtruth_list,
- 'images': [{'id': image_id} for image_id in self._image_ids],
- 'categories': self._categories
- }
- coco_wrapped_groundtruth = coco_tools.COCOWrapper(groundtruth_dict)
- coco_wrapped_detections = coco_wrapped_groundtruth.LoadAnnotations(
- self._detection_boxes_list)
- box_evaluator = coco_tools.COCOEvalWrapper(
- coco_wrapped_groundtruth, coco_wrapped_detections, agnostic_mode=False)
- box_metrics, box_per_category_ap = box_evaluator.ComputeMetrics(
- include_metrics_per_category=self._include_metrics_per_category,
- all_metrics_per_category=self._all_metrics_per_category)
- box_metrics.update(box_per_category_ap)
- box_metrics = {'DetectionBoxes_'+ key: value
- for key, value in iter(box_metrics.items())}
- return box_metrics
-
- 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.
- """
- def update_op(
- image_id_batched,
- groundtruth_boxes_batched,
- groundtruth_classes_batched,
- groundtruth_is_crowd_batched,
- num_gt_boxes_per_image,
- detection_boxes_batched,
- detection_scores_batched,
- detection_classes_batched,
- num_det_boxes_per_image,
- is_annotated_batched):
- """Update operation for adding batch of images to Coco evaluator."""
-
- for (image_id, gt_box, gt_class, gt_is_crowd, num_gt_box, det_box,
- det_score, det_class, num_det_box, is_annotated) in zip(
- image_id_batched, groundtruth_boxes_batched,
- groundtruth_classes_batched, groundtruth_is_crowd_batched,
- num_gt_boxes_per_image,
- detection_boxes_batched, detection_scores_batched,
- detection_classes_batched, num_det_boxes_per_image,
- is_annotated_batched):
- if is_annotated:
- self.add_single_ground_truth_image_info(
- image_id, {
- 'groundtruth_boxes': gt_box[:num_gt_box],
- 'groundtruth_classes': gt_class[:num_gt_box],
- 'groundtruth_is_crowd': gt_is_crowd[:num_gt_box]
- })
- self.add_single_detected_image_info(
- image_id,
- {'detection_boxes': det_box[:num_det_box],
- 'detection_scores': det_score[:num_det_box],
- 'detection_classes': det_class[:num_det_box]})
-
- # 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]
- groundtruth_is_crowd = eval_dict.get(
- input_data_fields.groundtruth_is_crowd, None)
- 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 = eval_dict.get('is_annotated', None)
-
- if groundtruth_is_crowd is None:
- groundtruth_is_crowd = tf.zeros_like(groundtruth_classes, dtype=tf.bool)
- 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)
- groundtruth_is_crowd = tf.expand_dims(groundtruth_is_crowd, 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 is None:
- is_annotated = tf.constant([True])
- else:
- is_annotated = tf.expand_dims(is_annotated, 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 is None:
- is_annotated = tf.ones_like(image_id, dtype=tf.bool)
-
- update_op = tf.py_func(update_op, [image_id,
- groundtruth_boxes,
- groundtruth_classes,
- groundtruth_is_crowd,
- num_gt_boxes_per_image,
- detection_boxes,
- detection_scores,
- detection_classes,
- num_det_boxes_per_image,
- is_annotated], [])
- metric_names = ['DetectionBoxes_Precision/mAP',
- 'DetectionBoxes_Precision/mAP@.50IOU',
- 'DetectionBoxes_Precision/mAP@.75IOU',
- 'DetectionBoxes_Precision/mAP (large)',
- 'DetectionBoxes_Precision/mAP (medium)',
- 'DetectionBoxes_Precision/mAP (small)',
- 'DetectionBoxes_Recall/AR@1',
- 'DetectionBoxes_Recall/AR@10',
- 'DetectionBoxes_Recall/AR@100',
- 'DetectionBoxes_Recall/AR@100 (large)',
- 'DetectionBoxes_Recall/AR@100 (medium)',
- 'DetectionBoxes_Recall/AR@100 (small)']
- if self._include_metrics_per_category:
- for category_dict in self._categories:
- metric_names.append('DetectionBoxes_PerformanceByCategory/mAP/' +
- category_dict['name'])
-
- def first_value_func():
- self._metrics = self.evaluate()
- self.clear()
- return np.float32(self._metrics[metric_names[0]])
-
- def value_func_factory(metric_name):
- def value_func():
- return np.float32(self._metrics[metric_name])
- return value_func
-
- # Ensure that the metrics are only evaluated once.
- first_value_op = tf.py_func(first_value_func, [], tf.float32)
- eval_metric_ops = {metric_names[0]: (first_value_op, update_op)}
- with tf.control_dependencies([first_value_op]):
- for metric_name in metric_names[1:]:
- eval_metric_ops[metric_name] = (tf.py_func(
- value_func_factory(metric_name), [], np.float32), update_op)
- return eval_metric_ops
-
-
- def _check_mask_type_and_value(array_name, masks):
- """Checks whether mask dtype is uint8 and the values are either 0 or 1."""
- if masks.dtype != np.uint8:
- raise ValueError('{} must be of type np.uint8. Found {}.'.format(
- array_name, masks.dtype))
- if np.any(np.logical_and(masks != 0, masks != 1)):
- raise ValueError('{} elements can only be either 0 or 1.'.format(
- array_name))
-
-
- class CocoMaskEvaluator(object_detection_evaluation.DetectionEvaluator):
- """Class to evaluate COCO detection metrics."""
-
- def __init__(self, categories, include_metrics_per_category=False):
- """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'.
- include_metrics_per_category: If True, include metrics for each category.
- """
- super(CocoMaskEvaluator, self).__init__(categories)
- self._image_id_to_mask_shape_map = {}
- self._image_ids_with_detections = set([])
- self._groundtruth_list = []
- self._detection_masks_list = []
- self._category_id_set = set([cat['id'] for cat in self._categories])
- self._annotation_id = 1
- self._include_metrics_per_category = include_metrics_per_category
-
- def clear(self):
- """Clears the state to prepare for a fresh evaluation."""
- self._image_id_to_mask_shape_map.clear()
- self._image_ids_with_detections.clear()
- self._groundtruth_list = []
- self._detection_masks_list = []
-
- def add_single_ground_truth_image_info(self,
- image_id,
- groundtruth_dict):
- """Adds groundtruth for a single image to be used for evaluation.
-
- If the image has already been added, a warning is logged, and groundtruth is
- ignored.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- groundtruth_dict: A dictionary containing -
- InputDataFields.groundtruth_boxes: float32 numpy array of shape
- [num_boxes, 4] containing `num_boxes` groundtruth boxes of the format
- [ymin, xmin, ymax, xmax] in absolute image coordinates.
- InputDataFields.groundtruth_classes: integer numpy array of shape
- [num_boxes] containing 1-indexed groundtruth classes for the boxes.
- InputDataFields.groundtruth_instance_masks: uint8 numpy array of shape
- [num_boxes, image_height, image_width] containing groundtruth masks
- corresponding to the boxes. The elements of the array must be in
- {0, 1}.
- """
- if image_id in self._image_id_to_mask_shape_map:
- tf.logging.warning('Ignoring ground truth with image id %s since it was '
- 'previously added', image_id)
- return
-
- groundtruth_instance_masks = groundtruth_dict[
- standard_fields.InputDataFields.groundtruth_instance_masks]
- _check_mask_type_and_value(standard_fields.InputDataFields.
- groundtruth_instance_masks,
- groundtruth_instance_masks)
- self._groundtruth_list.extend(
- coco_tools.
- ExportSingleImageGroundtruthToCoco(
- image_id=image_id,
- next_annotation_id=self._annotation_id,
- category_id_set=self._category_id_set,
- groundtruth_boxes=groundtruth_dict[standard_fields.InputDataFields.
- groundtruth_boxes],
- groundtruth_classes=groundtruth_dict[standard_fields.
- InputDataFields.
- groundtruth_classes],
- groundtruth_masks=groundtruth_instance_masks))
- self._annotation_id += groundtruth_dict[standard_fields.InputDataFields.
- groundtruth_boxes].shape[0]
- self._image_id_to_mask_shape_map[image_id] = groundtruth_dict[
- standard_fields.InputDataFields.groundtruth_instance_masks].shape
-
- def add_single_detected_image_info(self,
- image_id,
- detections_dict):
- """Adds detections for a single image to be used for evaluation.
-
- If a detection has already been added for this image id, a warning is
- logged, and the detection is skipped.
-
- Args:
- image_id: A unique string/integer identifier for the image.
- detections_dict: A dictionary containing -
- DetectionResultFields.detection_scores: float32 numpy array of shape
- [num_boxes] containing detection scores for the boxes.
- DetectionResultFields.detection_classes: integer numpy array of shape
- [num_boxes] containing 1-indexed detection classes for the boxes.
- DetectionResultFields.detection_masks: optional uint8 numpy array of
- shape [num_boxes, image_height, image_width] containing instance
- masks corresponding to the boxes. The elements of the array must be
- in {0, 1}.
-
- Raises:
- ValueError: If groundtruth for the image_id is not available or if
- spatial shapes of groundtruth_instance_masks and detection_masks are
- incompatible.
- """
- if image_id not in self._image_id_to_mask_shape_map:
- raise ValueError('Missing groundtruth for image id: {}'.format(image_id))
-
- if image_id in self._image_ids_with_detections:
- tf.logging.warning('Ignoring detection with image id %s since it was '
- 'previously added', image_id)
- return
-
- groundtruth_masks_shape = self._image_id_to_mask_shape_map[image_id]
- detection_masks = detections_dict[standard_fields.DetectionResultFields.
- detection_masks]
- if groundtruth_masks_shape[1:] != detection_masks.shape[1:]:
- raise ValueError('Spatial shape of groundtruth masks and detection masks '
- 'are incompatible: {} vs {}'.format(
- groundtruth_masks_shape,
- detection_masks.shape))
- _check_mask_type_and_value(standard_fields.DetectionResultFields.
- detection_masks,
- detection_masks)
- self._detection_masks_list.extend(
- coco_tools.ExportSingleImageDetectionMasksToCoco(
- image_id=image_id,
- category_id_set=self._category_id_set,
- detection_masks=detection_masks,
- detection_scores=detections_dict[standard_fields.
- DetectionResultFields.
- detection_scores],
- detection_classes=detections_dict[standard_fields.
- DetectionResultFields.
- detection_classes]))
- self._image_ids_with_detections.update([image_id])
-
- def dump_detections_to_json_file(self, json_output_path):
- """Saves the detections into json_output_path in the format used by MS COCO.
-
- Args:
- json_output_path: String containing the output file's path. It can be also
- None. In that case nothing will be written to the output file.
- """
- if json_output_path and json_output_path is not None:
- tf.logging.info('Dumping detections to output json file.')
- with tf.gfile.GFile(json_output_path, 'w') as fid:
- json_utils.Dump(
- obj=self._detection_masks_list, fid=fid, float_digits=4, indent=2)
-
- def evaluate(self):
- """Evaluates the detection masks and returns a dictionary of coco metrics.
-
- Returns:
- A dictionary holding -
-
- 1. summary_metrics:
- 'DetectionMasks_Precision/mAP': mean average precision over classes
- averaged over IOU thresholds ranging from .5 to .95 with .05 increments.
- 'DetectionMasks_Precision/mAP@.50IOU': mean average precision at 50% IOU.
- 'DetectionMasks_Precision/mAP@.75IOU': mean average precision at 75% IOU.
- 'DetectionMasks_Precision/mAP (small)': mean average precision for small
- objects (area < 32^2 pixels).
- 'DetectionMasks_Precision/mAP (medium)': mean average precision for medium
- sized objects (32^2 pixels < area < 96^2 pixels).
- 'DetectionMasks_Precision/mAP (large)': mean average precision for large
- objects (96^2 pixels < area < 10000^2 pixels).
- 'DetectionMasks_Recall/AR@1': average recall with 1 detection.
- 'DetectionMasks_Recall/AR@10': average recall with 10 detections.
- 'DetectionMasks_Recall/AR@100': average recall with 100 detections.
- 'DetectionMasks_Recall/AR@100 (small)': average recall for small objects
- with 100 detections.
- 'DetectionMasks_Recall/AR@100 (medium)': average recall for medium objects
- with 100 detections.
- 'DetectionMasks_Recall/AR@100 (large)': average recall for large objects
- with 100 detections.
-
- 2. per_category_ap: if include_metrics_per_category is True, category
- specific results with keys of the form:
- 'Precision mAP ByCategory/category' (without the supercategory part if
- no supercategories exist). For backward compatibility
- 'PerformanceByCategory' is included in the output regardless of
- all_metrics_per_category.
- """
- groundtruth_dict = {
- 'annotations': self._groundtruth_list,
- 'images': [{'id': image_id, 'height': shape[1], 'width': shape[2]}
- for image_id, shape in self._image_id_to_mask_shape_map.
- items()],
- 'categories': self._categories
- }
- coco_wrapped_groundtruth = coco_tools.COCOWrapper(
- groundtruth_dict, detection_type='segmentation')
- coco_wrapped_detection_masks = coco_wrapped_groundtruth.LoadAnnotations(
- self._detection_masks_list)
- mask_evaluator = coco_tools.COCOEvalWrapper(
- coco_wrapped_groundtruth, coco_wrapped_detection_masks,
- agnostic_mode=False, iou_type='segm')
- mask_metrics, mask_per_category_ap = mask_evaluator.ComputeMetrics(
- include_metrics_per_category=self._include_metrics_per_category)
- mask_metrics.update(mask_per_category_ap)
- mask_metrics = {'DetectionMasks_'+ key: value
- for key, value in mask_metrics.items()}
- return mask_metrics
-
- 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.
-
- 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.
- """
-
- def update_op(image_id_batched, groundtruth_boxes_batched,
- groundtruth_classes_batched,
- groundtruth_instance_masks_batched,
- groundtruth_is_crowd_batched, num_gt_boxes_per_image,
- detection_scores_batched, detection_classes_batched,
- detection_masks_batched, num_det_boxes_per_image):
- """Update op for metrics."""
-
- for (image_id, groundtruth_boxes, groundtruth_classes,
- groundtruth_instance_masks, groundtruth_is_crowd, num_gt_box,
- detection_scores, detection_classes,
- detection_masks, num_det_box) in zip(
- image_id_batched, groundtruth_boxes_batched,
- groundtruth_classes_batched, groundtruth_instance_masks_batched,
- groundtruth_is_crowd_batched, num_gt_boxes_per_image,
- detection_scores_batched, detection_classes_batched,
- detection_masks_batched, num_det_boxes_per_image):
- self.add_single_ground_truth_image_info(
- image_id, {
- 'groundtruth_boxes':
- groundtruth_boxes[:num_gt_box],
- 'groundtruth_classes':
- groundtruth_classes[:num_gt_box],
- 'groundtruth_instance_masks':
- groundtruth_instance_masks[:num_gt_box],
- 'groundtruth_is_crowd':
- groundtruth_is_crowd[:num_gt_box]
- })
- self.add_single_detected_image_info(
- image_id, {
- 'detection_scores': detection_scores[:num_det_box],
- 'detection_classes': detection_classes[:num_det_box],
- 'detection_masks': detection_masks[:num_det_box]
- })
-
- # 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]
- groundtruth_instance_masks = eval_dict[
- input_data_fields.groundtruth_instance_masks]
- groundtruth_is_crowd = eval_dict.get(
- input_data_fields.groundtruth_is_crowd, None)
- num_gt_boxes_per_image = eval_dict.get(
- input_data_fields.num_groundtruth_boxes, None)
- detection_scores = eval_dict[detection_fields.detection_scores]
- detection_classes = eval_dict[detection_fields.detection_classes]
- detection_masks = eval_dict[detection_fields.detection_masks]
- num_det_boxes_per_image = eval_dict.get(detection_fields.num_detections,
- None)
-
- if groundtruth_is_crowd is None:
- groundtruth_is_crowd = tf.zeros_like(groundtruth_classes, dtype=tf.bool)
-
- 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)
- groundtruth_instance_masks = tf.expand_dims(groundtruth_instance_masks, 0)
- groundtruth_is_crowd = tf.expand_dims(groundtruth_is_crowd, 0)
- detection_scores = tf.expand_dims(detection_scores, 0)
- detection_classes = tf.expand_dims(detection_classes, 0)
- detection_masks = tf.expand_dims(detection_masks, 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_scores)[1:2]
- else:
- num_det_boxes_per_image = tf.expand_dims(num_det_boxes_per_image, 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_scores)[1:2],
- multiples=tf.shape(detection_scores)[0:1])
-
- update_op = tf.py_func(update_op, [
- image_id, groundtruth_boxes, groundtruth_classes,
- groundtruth_instance_masks, groundtruth_is_crowd,
- num_gt_boxes_per_image, detection_scores, detection_classes,
- detection_masks, num_det_boxes_per_image
- ], [])
-
- metric_names = ['DetectionMasks_Precision/mAP',
- 'DetectionMasks_Precision/mAP@.50IOU',
- 'DetectionMasks_Precision/mAP@.75IOU',
- 'DetectionMasks_Precision/mAP (large)',
- 'DetectionMasks_Precision/mAP (medium)',
- 'DetectionMasks_Precision/mAP (small)',
- 'DetectionMasks_Recall/AR@1',
- 'DetectionMasks_Recall/AR@10',
- 'DetectionMasks_Recall/AR@100',
- 'DetectionMasks_Recall/AR@100 (large)',
- 'DetectionMasks_Recall/AR@100 (medium)',
- 'DetectionMasks_Recall/AR@100 (small)']
- if self._include_metrics_per_category:
- for category_dict in self._categories:
- metric_names.append('DetectionMasks_PerformanceByCategory/mAP/' +
- category_dict['name'])
-
- def first_value_func():
- self._metrics = self.evaluate()
- self.clear()
- return np.float32(self._metrics[metric_names[0]])
-
- def value_func_factory(metric_name):
- def value_func():
- return np.float32(self._metrics[metric_name])
- return value_func
-
- # Ensure that the metrics are only evaluated once.
- first_value_op = tf.py_func(first_value_func, [], tf.float32)
- eval_metric_ops = {metric_names[0]: (first_value_op, update_op)}
- with tf.control_dependencies([first_value_op]):
- for metric_name in metric_names[1:]:
- eval_metric_ops[metric_name] = (tf.py_func(
- value_func_factory(metric_name), [], np.float32), update_op)
- return eval_metric_ops
|