|
|
- # 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.
- # ==============================================================================
-
- """Bounding Box List operations for Numpy BoxLists.
-
- Example box operations that are supported:
- * Areas: compute bounding box areas
- * IOU: pairwise intersection-over-union scores
- """
- import numpy as np
-
- from object_detection.utils import np_box_list
- from object_detection.utils import np_box_ops
-
-
- class SortOrder(object):
- """Enum class for sort order.
-
- Attributes:
- ascend: ascend order.
- descend: descend order.
- """
- ASCEND = 1
- DESCEND = 2
-
-
- def area(boxlist):
- """Computes area of boxes.
-
- Args:
- boxlist: BoxList holding N boxes
-
- Returns:
- a numpy array with shape [N*1] representing box areas
- """
- y_min, x_min, y_max, x_max = boxlist.get_coordinates()
- return (y_max - y_min) * (x_max - x_min)
-
-
- def intersection(boxlist1, boxlist2):
- """Compute pairwise intersection areas between boxes.
-
- Args:
- boxlist1: BoxList holding N boxes
- boxlist2: BoxList holding M boxes
-
- Returns:
- a numpy array with shape [N*M] representing pairwise intersection area
- """
- return np_box_ops.intersection(boxlist1.get(), boxlist2.get())
-
-
- def iou(boxlist1, boxlist2):
- """Computes pairwise intersection-over-union between box collections.
-
- Args:
- boxlist1: BoxList holding N boxes
- boxlist2: BoxList holding M boxes
-
- Returns:
- a numpy array with shape [N, M] representing pairwise iou scores.
- """
- return np_box_ops.iou(boxlist1.get(), boxlist2.get())
-
-
- def ioa(boxlist1, boxlist2):
- """Computes pairwise intersection-over-area between box collections.
-
- Intersection-over-area (ioa) between two boxes box1 and box2 is defined as
- their intersection area over box2's area. Note that ioa is not symmetric,
- that is, IOA(box1, box2) != IOA(box2, box1).
-
- Args:
- boxlist1: BoxList holding N boxes
- boxlist2: BoxList holding M boxes
-
- Returns:
- a numpy array with shape [N, M] representing pairwise ioa scores.
- """
- return np_box_ops.ioa(boxlist1.get(), boxlist2.get())
-
-
- def gather(boxlist, indices, fields=None):
- """Gather boxes from BoxList according to indices and return new BoxList.
-
- By default, gather returns boxes corresponding to the input index list, as
- well as all additional fields stored in the boxlist (indexing into the
- first dimension). However one can optionally only gather from a
- subset of fields.
-
- Args:
- boxlist: BoxList holding N boxes
- indices: a 1-d numpy array of type int_
- fields: (optional) list of fields to also gather from. If None (default),
- all fields are gathered from. Pass an empty fields list to only gather
- the box coordinates.
-
- Returns:
- subboxlist: a BoxList corresponding to the subset of the input BoxList
- specified by indices
-
- Raises:
- ValueError: if specified field is not contained in boxlist or if the
- indices are not of type int_
- """
- if indices.size:
- if np.amax(indices) >= boxlist.num_boxes() or np.amin(indices) < 0:
- raise ValueError('indices are out of valid range.')
- subboxlist = np_box_list.BoxList(boxlist.get()[indices, :])
- if fields is None:
- fields = boxlist.get_extra_fields()
- for field in fields:
- extra_field_data = boxlist.get_field(field)
- subboxlist.add_field(field, extra_field_data[indices, ...])
- return subboxlist
-
-
- def sort_by_field(boxlist, field, order=SortOrder.DESCEND):
- """Sort boxes and associated fields according to a scalar field.
-
- A common use case is reordering the boxes according to descending scores.
-
- Args:
- boxlist: BoxList holding N boxes.
- field: A BoxList field for sorting and reordering the BoxList.
- order: (Optional) 'descend' or 'ascend'. Default is descend.
-
- Returns:
- sorted_boxlist: A sorted BoxList with the field in the specified order.
-
- Raises:
- ValueError: if specified field does not exist or is not of single dimension.
- ValueError: if the order is not either descend or ascend.
- """
- if not boxlist.has_field(field):
- raise ValueError('Field ' + field + ' does not exist')
- if len(boxlist.get_field(field).shape) != 1:
- raise ValueError('Field ' + field + 'should be single dimension.')
- if order != SortOrder.DESCEND and order != SortOrder.ASCEND:
- raise ValueError('Invalid sort order')
-
- field_to_sort = boxlist.get_field(field)
- sorted_indices = np.argsort(field_to_sort)
- if order == SortOrder.DESCEND:
- sorted_indices = sorted_indices[::-1]
- return gather(boxlist, sorted_indices)
-
-
- def non_max_suppression(boxlist,
- max_output_size=10000,
- iou_threshold=1.0,
- score_threshold=-10.0):
- """Non maximum suppression.
-
- This op greedily selects a subset of detection bounding boxes, pruning
- away boxes that have high IOU (intersection over union) overlap (> thresh)
- with already selected boxes. In each iteration, the detected bounding box with
- highest score in the available pool is selected.
-
- Args:
- boxlist: BoxList holding N boxes. Must contain a 'scores' field
- representing detection scores. All scores belong to the same class.
- max_output_size: maximum number of retained boxes
- iou_threshold: intersection over union threshold.
- score_threshold: minimum score threshold. Remove the boxes with scores
- less than this value. Default value is set to -10. A very
- low threshold to pass pretty much all the boxes, unless
- the user sets a different score threshold.
-
- Returns:
- a BoxList holding M boxes where M <= max_output_size
- Raises:
- ValueError: if 'scores' field does not exist
- ValueError: if threshold is not in [0, 1]
- ValueError: if max_output_size < 0
- """
- if not boxlist.has_field('scores'):
- raise ValueError('Field scores does not exist')
- if iou_threshold < 0. or iou_threshold > 1.0:
- raise ValueError('IOU threshold must be in [0, 1]')
- if max_output_size < 0:
- raise ValueError('max_output_size must be bigger than 0.')
-
- boxlist = filter_scores_greater_than(boxlist, score_threshold)
- if boxlist.num_boxes() == 0:
- return boxlist
-
- boxlist = sort_by_field(boxlist, 'scores')
-
- # Prevent further computation if NMS is disabled.
- if iou_threshold == 1.0:
- if boxlist.num_boxes() > max_output_size:
- selected_indices = np.arange(max_output_size)
- return gather(boxlist, selected_indices)
- else:
- return boxlist
-
- boxes = boxlist.get()
- num_boxes = boxlist.num_boxes()
- # is_index_valid is True only for all remaining valid boxes,
- is_index_valid = np.full(num_boxes, 1, dtype=bool)
- selected_indices = []
- num_output = 0
- for i in range(num_boxes):
- if num_output < max_output_size:
- if is_index_valid[i]:
- num_output += 1
- selected_indices.append(i)
- is_index_valid[i] = False
- valid_indices = np.where(is_index_valid)[0]
- if valid_indices.size == 0:
- break
-
- intersect_over_union = np_box_ops.iou(
- np.expand_dims(boxes[i, :], axis=0), boxes[valid_indices, :])
- intersect_over_union = np.squeeze(intersect_over_union, axis=0)
- is_index_valid[valid_indices] = np.logical_and(
- is_index_valid[valid_indices],
- intersect_over_union <= iou_threshold)
- return gather(boxlist, np.array(selected_indices))
-
-
- def multi_class_non_max_suppression(boxlist, score_thresh, iou_thresh,
- max_output_size):
- """Multi-class version of non maximum suppression.
-
- This op greedily selects a subset of detection bounding boxes, pruning
- away boxes that have high IOU (intersection over union) overlap (> thresh)
- with already selected boxes. It operates independently for each class for
- which scores are provided (via the scores field of the input box_list),
- pruning boxes with score less than a provided threshold prior to
- applying NMS.
-
- Args:
- boxlist: BoxList holding N boxes. Must contain a 'scores' field
- representing detection scores. This scores field is a tensor that can
- be 1 dimensional (in the case of a single class) or 2-dimensional, which
- which case we assume that it takes the shape [num_boxes, num_classes].
- We further assume that this rank is known statically and that
- scores.shape[1] is also known (i.e., the number of classes is fixed
- and known at graph construction time).
- score_thresh: scalar threshold for score (low scoring boxes are removed).
- iou_thresh: scalar threshold for IOU (boxes that that high IOU overlap
- with previously selected boxes are removed).
- max_output_size: maximum number of retained boxes per class.
-
- Returns:
- a BoxList holding M boxes with a rank-1 scores field representing
- corresponding scores for each box with scores sorted in decreasing order
- and a rank-1 classes field representing a class label for each box.
- Raises:
- ValueError: if iou_thresh is not in [0, 1] or if input boxlist does not have
- a valid scores field.
- """
- if not 0 <= iou_thresh <= 1.0:
- raise ValueError('thresh must be between 0 and 1')
- if not isinstance(boxlist, np_box_list.BoxList):
- raise ValueError('boxlist must be a BoxList')
- if not boxlist.has_field('scores'):
- raise ValueError('input boxlist must have \'scores\' field')
- scores = boxlist.get_field('scores')
- if len(scores.shape) == 1:
- scores = np.reshape(scores, [-1, 1])
- elif len(scores.shape) == 2:
- if scores.shape[1] is None:
- raise ValueError('scores field must have statically defined second '
- 'dimension')
- else:
- raise ValueError('scores field must be of rank 1 or 2')
- num_boxes = boxlist.num_boxes()
- num_scores = scores.shape[0]
- num_classes = scores.shape[1]
-
- if num_boxes != num_scores:
- raise ValueError('Incorrect scores field length: actual vs expected.')
-
- selected_boxes_list = []
- for class_idx in range(num_classes):
- boxlist_and_class_scores = np_box_list.BoxList(boxlist.get())
- class_scores = np.reshape(scores[0:num_scores, class_idx], [-1])
- boxlist_and_class_scores.add_field('scores', class_scores)
- boxlist_filt = filter_scores_greater_than(boxlist_and_class_scores,
- score_thresh)
- nms_result = non_max_suppression(boxlist_filt,
- max_output_size=max_output_size,
- iou_threshold=iou_thresh,
- score_threshold=score_thresh)
- nms_result.add_field(
- 'classes', np.zeros_like(nms_result.get_field('scores')) + class_idx)
- selected_boxes_list.append(nms_result)
- selected_boxes = concatenate(selected_boxes_list)
- sorted_boxes = sort_by_field(selected_boxes, 'scores')
- return sorted_boxes
-
-
- def scale(boxlist, y_scale, x_scale):
- """Scale box coordinates in x and y dimensions.
-
- Args:
- boxlist: BoxList holding N boxes
- y_scale: float
- x_scale: float
-
- Returns:
- boxlist: BoxList holding N boxes
- """
- y_min, x_min, y_max, x_max = np.array_split(boxlist.get(), 4, axis=1)
- y_min = y_scale * y_min
- y_max = y_scale * y_max
- x_min = x_scale * x_min
- x_max = x_scale * x_max
- scaled_boxlist = np_box_list.BoxList(np.hstack([y_min, x_min, y_max, x_max]))
-
- fields = boxlist.get_extra_fields()
- for field in fields:
- extra_field_data = boxlist.get_field(field)
- scaled_boxlist.add_field(field, extra_field_data)
-
- return scaled_boxlist
-
-
- def clip_to_window(boxlist, window):
- """Clip bounding boxes to a window.
-
- This op clips input bounding boxes (represented by bounding box
- corners) to a window, optionally filtering out boxes that do not
- overlap at all with the window.
-
- Args:
- boxlist: BoxList holding M_in boxes
- window: a numpy array of shape [4] representing the
- [y_min, x_min, y_max, x_max] window to which the op
- should clip boxes.
-
- Returns:
- a BoxList holding M_out boxes where M_out <= M_in
- """
- y_min, x_min, y_max, x_max = np.array_split(boxlist.get(), 4, axis=1)
- win_y_min = window[0]
- win_x_min = window[1]
- win_y_max = window[2]
- win_x_max = window[3]
- y_min_clipped = np.fmax(np.fmin(y_min, win_y_max), win_y_min)
- y_max_clipped = np.fmax(np.fmin(y_max, win_y_max), win_y_min)
- x_min_clipped = np.fmax(np.fmin(x_min, win_x_max), win_x_min)
- x_max_clipped = np.fmax(np.fmin(x_max, win_x_max), win_x_min)
- clipped = np_box_list.BoxList(
- np.hstack([y_min_clipped, x_min_clipped, y_max_clipped, x_max_clipped]))
- clipped = _copy_extra_fields(clipped, boxlist)
- areas = area(clipped)
- nonzero_area_indices = np.reshape(np.nonzero(np.greater(areas, 0.0)),
- [-1]).astype(np.int32)
- return gather(clipped, nonzero_area_indices)
-
-
- def prune_non_overlapping_boxes(boxlist1, boxlist2, minoverlap=0.0):
- """Prunes the boxes in boxlist1 that overlap less than thresh with boxlist2.
-
- For each box in boxlist1, we want its IOA to be more than minoverlap with
- at least one of the boxes in boxlist2. If it does not, we remove it.
-
- Args:
- boxlist1: BoxList holding N boxes.
- boxlist2: BoxList holding M boxes.
- minoverlap: Minimum required overlap between boxes, to count them as
- overlapping.
-
- Returns:
- A pruned boxlist with size [N', 4].
- """
- intersection_over_area = ioa(boxlist2, boxlist1) # [M, N] tensor
- intersection_over_area = np.amax(intersection_over_area, axis=0) # [N] tensor
- keep_bool = np.greater_equal(intersection_over_area, np.array(minoverlap))
- keep_inds = np.nonzero(keep_bool)[0]
- new_boxlist1 = gather(boxlist1, keep_inds)
- return new_boxlist1
-
-
- def prune_outside_window(boxlist, window):
- """Prunes bounding boxes that fall outside a given window.
-
- This function prunes bounding boxes that even partially fall outside the given
- window. See also ClipToWindow which only prunes bounding boxes that fall
- completely outside the window, and clips any bounding boxes that partially
- overflow.
-
- Args:
- boxlist: a BoxList holding M_in boxes.
- window: a numpy array of size 4, representing [ymin, xmin, ymax, xmax]
- of the window.
-
- Returns:
- pruned_corners: a tensor with shape [M_out, 4] where M_out <= M_in.
- valid_indices: a tensor with shape [M_out] indexing the valid bounding boxes
- in the input tensor.
- """
-
- y_min, x_min, y_max, x_max = np.array_split(boxlist.get(), 4, axis=1)
- win_y_min = window[0]
- win_x_min = window[1]
- win_y_max = window[2]
- win_x_max = window[3]
- coordinate_violations = np.hstack([np.less(y_min, win_y_min),
- np.less(x_min, win_x_min),
- np.greater(y_max, win_y_max),
- np.greater(x_max, win_x_max)])
- valid_indices = np.reshape(
- np.where(np.logical_not(np.max(coordinate_violations, axis=1))), [-1])
- return gather(boxlist, valid_indices), valid_indices
-
-
- def concatenate(boxlists, fields=None):
- """Concatenate list of BoxLists.
-
- This op concatenates a list of input BoxLists into a larger BoxList. It also
- handles concatenation of BoxList fields as long as the field tensor shapes
- are equal except for the first dimension.
-
- Args:
- boxlists: list of BoxList objects
- fields: optional list of fields to also concatenate. By default, all
- fields from the first BoxList in the list are included in the
- concatenation.
-
- Returns:
- a BoxList with number of boxes equal to
- sum([boxlist.num_boxes() for boxlist in BoxList])
- Raises:
- ValueError: if boxlists is invalid (i.e., is not a list, is empty, or
- contains non BoxList objects), or if requested fields are not contained in
- all boxlists
- """
- if not isinstance(boxlists, list):
- raise ValueError('boxlists should be a list')
- if not boxlists:
- raise ValueError('boxlists should have nonzero length')
- for boxlist in boxlists:
- if not isinstance(boxlist, np_box_list.BoxList):
- raise ValueError('all elements of boxlists should be BoxList objects')
- concatenated = np_box_list.BoxList(
- np.vstack([boxlist.get() for boxlist in boxlists]))
- if fields is None:
- fields = boxlists[0].get_extra_fields()
- for field in fields:
- first_field_shape = boxlists[0].get_field(field).shape
- first_field_shape = first_field_shape[1:]
- for boxlist in boxlists:
- if not boxlist.has_field(field):
- raise ValueError('boxlist must contain all requested fields')
- field_shape = boxlist.get_field(field).shape
- field_shape = field_shape[1:]
- if field_shape != first_field_shape:
- raise ValueError('field %s must have same shape for all boxlists '
- 'except for the 0th dimension.' % field)
- concatenated_field = np.concatenate(
- [boxlist.get_field(field) for boxlist in boxlists], axis=0)
- concatenated.add_field(field, concatenated_field)
- return concatenated
-
-
- def filter_scores_greater_than(boxlist, thresh):
- """Filter to keep only boxes with score exceeding a given threshold.
-
- This op keeps the collection of boxes whose corresponding scores are
- greater than the input threshold.
-
- Args:
- boxlist: BoxList holding N boxes. Must contain a 'scores' field
- representing detection scores.
- thresh: scalar threshold
-
- Returns:
- a BoxList holding M boxes where M <= N
-
- Raises:
- ValueError: if boxlist not a BoxList object or if it does not
- have a scores field
- """
- if not isinstance(boxlist, np_box_list.BoxList):
- raise ValueError('boxlist must be a BoxList')
- if not boxlist.has_field('scores'):
- raise ValueError('input boxlist must have \'scores\' field')
- scores = boxlist.get_field('scores')
- if len(scores.shape) > 2:
- raise ValueError('Scores should have rank 1 or 2')
- if len(scores.shape) == 2 and scores.shape[1] != 1:
- raise ValueError('Scores should have rank 1 or have shape '
- 'consistent with [None, 1]')
- high_score_indices = np.reshape(np.where(np.greater(scores, thresh)),
- [-1]).astype(np.int32)
- return gather(boxlist, high_score_indices)
-
-
- def change_coordinate_frame(boxlist, window):
- """Change coordinate frame of the boxlist to be relative to window's frame.
-
- Given a window of the form [ymin, xmin, ymax, xmax],
- changes bounding box coordinates from boxlist to be relative to this window
- (e.g., the min corner maps to (0,0) and the max corner maps to (1,1)).
-
- An example use case is data augmentation: where we are given groundtruth
- boxes (boxlist) and would like to randomly crop the image to some
- window (window). In this case we need to change the coordinate frame of
- each groundtruth box to be relative to this new window.
-
- Args:
- boxlist: A BoxList object holding N boxes.
- window: a size 4 1-D numpy array.
-
- Returns:
- Returns a BoxList object with N boxes.
- """
- win_height = window[2] - window[0]
- win_width = window[3] - window[1]
- boxlist_new = scale(
- np_box_list.BoxList(boxlist.get() -
- [window[0], window[1], window[0], window[1]]),
- 1.0 / win_height, 1.0 / win_width)
- _copy_extra_fields(boxlist_new, boxlist)
-
- return boxlist_new
-
-
- def _copy_extra_fields(boxlist_to_copy_to, boxlist_to_copy_from):
- """Copies the extra fields of boxlist_to_copy_from to boxlist_to_copy_to.
-
- Args:
- boxlist_to_copy_to: BoxList to which extra fields are copied.
- boxlist_to_copy_from: BoxList from which fields are copied.
-
- Returns:
- boxlist_to_copy_to with extra fields.
- """
- for field in boxlist_to_copy_from.get_extra_fields():
- boxlist_to_copy_to.add_field(field, boxlist_to_copy_from.get_field(field))
- return boxlist_to_copy_to
-
-
- def _update_valid_indices_by_removing_high_iou_boxes(
- selected_indices, is_index_valid, intersect_over_union, threshold):
- max_iou = np.max(intersect_over_union[:, selected_indices], axis=1)
- return np.logical_and(is_index_valid, max_iou <= threshold)
|