| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """Functions to generate a list of feature maps based on image features. |
| |
| Provides several feature map generators that can be used to build object |
| detection feature extractors. |
| |
| Object detection feature extractors usually are built by stacking two components |
| - A base feature extractor such as Inception V3 and a feature map generator. |
| Feature map generators build on the base feature extractors and produce a list |
| of final feature maps. |
| """ |
| import collections |
| import functools |
| import tensorflow as tf |
| from object_detection.utils import ops |
| slim = tf.contrib.slim |
|
|
| |
| |
| |
| ACTIVATION_BOUND = 6.0 |
|
|
|
|
| def get_depth_fn(depth_multiplier, min_depth): |
| """Builds a callable to compute depth (output channels) of conv filters. |
| |
| Args: |
| depth_multiplier: a multiplier for the nominal depth. |
| min_depth: a lower bound on the depth of filters. |
| |
| Returns: |
| A callable that takes in a nominal depth and returns the depth to use. |
| """ |
| def multiply_depth(depth): |
| new_depth = int(depth * depth_multiplier) |
| return max(new_depth, min_depth) |
| return multiply_depth |
|
|
|
|
| class KerasMultiResolutionFeatureMaps(tf.keras.Model): |
| """Generates multi resolution feature maps from input image features. |
| |
| A Keras model that generates multi-scale feature maps for detection as in the |
| SSD papers by Liu et al: https://arxiv.org/pdf/1512.02325v2.pdf, See Sec 2.1. |
| |
| More specifically, when called on inputs it performs the following two tasks: |
| 1) If a layer name is provided in the configuration, returns that layer as a |
| feature map. |
| 2) If a layer name is left as an empty string, constructs a new feature map |
| based on the spatial shape and depth configuration. Note that the current |
| implementation only supports generating new layers using convolution of |
| stride 2 resulting in a spatial resolution reduction by a factor of 2. |
| By default convolution kernel size is set to 3, and it can be customized |
| by caller. |
| |
| An example of the configuration for Inception V3: |
| { |
| 'from_layer': ['Mixed_5d', 'Mixed_6e', 'Mixed_7c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| |
| When this feature generator object is called on input image_features: |
| Args: |
| image_features: A dictionary of handles to activation tensors from the |
| base feature extractor. |
| |
| Returns: |
| feature_maps: an OrderedDict mapping keys (feature map names) to |
| tensors where each tensor has shape [batch, height_i, width_i, depth_i]. |
| """ |
|
|
| def __init__(self, |
| feature_map_layout, |
| depth_multiplier, |
| min_depth, |
| insert_1x1_conv, |
| is_training, |
| conv_hyperparams, |
| freeze_batchnorm, |
| name=None): |
| """Constructor. |
| |
| Args: |
| feature_map_layout: Dictionary of specifications for the feature map |
| layouts in the following format (Inception V2/V3 respectively): |
| { |
| 'from_layer': ['Mixed_3c', 'Mixed_4c', 'Mixed_5c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| or |
| { |
| 'from_layer': ['Mixed_5d', 'Mixed_6e', 'Mixed_7c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| If 'from_layer' is specified, the specified feature map is directly used |
| as a box predictor layer, and the layer_depth is directly infered from |
| the feature map (instead of using the provided 'layer_depth' parameter). |
| In this case, our convention is to set 'layer_depth' to -1 for clarity. |
| Otherwise, if 'from_layer' is an empty string, then the box predictor |
| layer will be built from the previous layer using convolution |
| operations. Note that the current implementation only supports |
| generating new layers using convolutions of stride 2 (resulting in a |
| spatial resolution reduction by a factor of 2), and will be extended to |
| a more flexible design. Convolution kernel size is set to 3 by default, |
| and can be customized by 'conv_kernel_size' parameter (similarily, |
| 'conv_kernel_size' should be set to -1 if 'from_layer' is specified). |
| The created convolution operation will be a normal 2D convolution by |
| default, and a depthwise convolution followed by 1x1 convolution if |
| 'use_depthwise' is set to True. |
| depth_multiplier: Depth multiplier for convolutional layers. |
| min_depth: Minimum depth for convolutional layers. |
| insert_1x1_conv: A boolean indicating whether an additional 1x1 |
| convolution should be inserted before shrinking the feature map. |
| is_training: Indicates whether the feature generator is in training mode. |
| conv_hyperparams: A `hyperparams_builder.KerasLayerHyperparams` object |
| containing hyperparameters for convolution ops. |
| freeze_batchnorm: Bool. Whether to freeze batch norm parameters during |
| training or not. When training with a small batch size (e.g. 1), it is |
| desirable to freeze batch norm update and use pretrained batch norm |
| params. |
| name: A string name scope to assign to the model. If 'None', Keras |
| will auto-generate one from the class name. |
| """ |
| super(KerasMultiResolutionFeatureMaps, self).__init__(name=name) |
|
|
| self.feature_map_layout = feature_map_layout |
| self.convolutions = [] |
|
|
| depth_fn = get_depth_fn(depth_multiplier, min_depth) |
|
|
| base_from_layer = '' |
| use_explicit_padding = False |
| if 'use_explicit_padding' in feature_map_layout: |
| use_explicit_padding = feature_map_layout['use_explicit_padding'] |
| use_depthwise = False |
| if 'use_depthwise' in feature_map_layout: |
| use_depthwise = feature_map_layout['use_depthwise'] |
| for index, from_layer in enumerate(feature_map_layout['from_layer']): |
| net = [] |
| layer_depth = feature_map_layout['layer_depth'][index] |
| conv_kernel_size = 3 |
| if 'conv_kernel_size' in feature_map_layout: |
| conv_kernel_size = feature_map_layout['conv_kernel_size'][index] |
| if from_layer: |
| base_from_layer = from_layer |
| else: |
| if insert_1x1_conv: |
| layer_name = '{}_1_Conv2d_{}_1x1_{}'.format( |
| base_from_layer, index, depth_fn(layer_depth / 2)) |
| net.append(tf.keras.layers.Conv2D(depth_fn(layer_depth / 2), |
| [1, 1], |
| padding='SAME', |
| strides=1, |
| name=layer_name + '_conv', |
| **conv_hyperparams.params())) |
| net.append( |
| conv_hyperparams.build_batch_norm( |
| training=(is_training and not freeze_batchnorm), |
| name=layer_name + '_batchnorm')) |
| net.append( |
| conv_hyperparams.build_activation_layer( |
| name=layer_name)) |
|
|
| layer_name = '{}_2_Conv2d_{}_{}x{}_s2_{}'.format( |
| base_from_layer, index, conv_kernel_size, conv_kernel_size, |
| depth_fn(layer_depth)) |
| stride = 2 |
| padding = 'SAME' |
| if use_explicit_padding: |
| padding = 'VALID' |
| |
| |
| |
| def fixed_padding(features, kernel_size=conv_kernel_size): |
| return ops.fixed_padding(features, kernel_size) |
| net.append(tf.keras.layers.Lambda(fixed_padding)) |
| |
| |
| if use_depthwise: |
| net.append(tf.keras.layers.DepthwiseConv2D( |
| [conv_kernel_size, conv_kernel_size], |
| depth_multiplier=1, |
| padding=padding, |
| strides=stride, |
| name=layer_name + '_depthwise_conv', |
| **conv_hyperparams.params())) |
| net.append( |
| conv_hyperparams.build_batch_norm( |
| training=(is_training and not freeze_batchnorm), |
| name=layer_name + '_depthwise_batchnorm')) |
| net.append( |
| conv_hyperparams.build_activation_layer( |
| name=layer_name + '_depthwise')) |
|
|
| net.append(tf.keras.layers.Conv2D(depth_fn(layer_depth), [1, 1], |
| padding='SAME', |
| strides=1, |
| name=layer_name + '_conv', |
| **conv_hyperparams.params())) |
| net.append( |
| conv_hyperparams.build_batch_norm( |
| training=(is_training and not freeze_batchnorm), |
| name=layer_name + '_batchnorm')) |
| net.append( |
| conv_hyperparams.build_activation_layer( |
| name=layer_name)) |
|
|
| else: |
| net.append(tf.keras.layers.Conv2D( |
| depth_fn(layer_depth), |
| [conv_kernel_size, conv_kernel_size], |
| padding=padding, |
| strides=stride, |
| name=layer_name + '_conv', |
| **conv_hyperparams.params())) |
| net.append( |
| conv_hyperparams.build_batch_norm( |
| training=(is_training and not freeze_batchnorm), |
| name=layer_name + '_batchnorm')) |
| net.append( |
| conv_hyperparams.build_activation_layer( |
| name=layer_name)) |
|
|
| |
| |
| self.convolutions.append(net) |
|
|
| def call(self, image_features): |
| """Generate the multi-resolution feature maps. |
| |
| Executed when calling the `.__call__` method on input. |
| |
| Args: |
| image_features: A dictionary of handles to activation tensors from the |
| base feature extractor. |
| |
| Returns: |
| feature_maps: an OrderedDict mapping keys (feature map names) to |
| tensors where each tensor has shape [batch, height_i, width_i, depth_i]. |
| """ |
| feature_maps = [] |
| feature_map_keys = [] |
|
|
| for index, from_layer in enumerate(self.feature_map_layout['from_layer']): |
| if from_layer: |
| feature_map = image_features[from_layer] |
| feature_map_keys.append(from_layer) |
| else: |
| feature_map = feature_maps[-1] |
| for layer in self.convolutions[index]: |
| feature_map = layer(feature_map) |
| layer_name = self.convolutions[index][-1].name |
| feature_map_keys.append(layer_name) |
| feature_maps.append(feature_map) |
| return collections.OrderedDict( |
| [(x, y) for (x, y) in zip(feature_map_keys, feature_maps)]) |
|
|
|
|
| def multi_resolution_feature_maps(feature_map_layout, depth_multiplier, |
| min_depth, insert_1x1_conv, image_features, |
| pool_residual=False): |
| """Generates multi resolution feature maps from input image features. |
| |
| Generates multi-scale feature maps for detection as in the SSD papers by |
| Liu et al: https://arxiv.org/pdf/1512.02325v2.pdf, See Sec 2.1. |
| |
| More specifically, it performs the following two tasks: |
| 1) If a layer name is provided in the configuration, returns that layer as a |
| feature map. |
| 2) If a layer name is left as an empty string, constructs a new feature map |
| based on the spatial shape and depth configuration. Note that the current |
| implementation only supports generating new layers using convolution of |
| stride 2 resulting in a spatial resolution reduction by a factor of 2. |
| By default convolution kernel size is set to 3, and it can be customized |
| by caller. |
| |
| An example of the configuration for Inception V3: |
| { |
| 'from_layer': ['Mixed_5d', 'Mixed_6e', 'Mixed_7c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| |
| Args: |
| feature_map_layout: Dictionary of specifications for the feature map |
| layouts in the following format (Inception V2/V3 respectively): |
| { |
| 'from_layer': ['Mixed_3c', 'Mixed_4c', 'Mixed_5c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| or |
| { |
| 'from_layer': ['Mixed_5d', 'Mixed_6e', 'Mixed_7c', '', '', ''], |
| 'layer_depth': [-1, -1, -1, 512, 256, 128] |
| } |
| If 'from_layer' is specified, the specified feature map is directly used |
| as a box predictor layer, and the layer_depth is directly infered from the |
| feature map (instead of using the provided 'layer_depth' parameter). In |
| this case, our convention is to set 'layer_depth' to -1 for clarity. |
| Otherwise, if 'from_layer' is an empty string, then the box predictor |
| layer will be built from the previous layer using convolution operations. |
| Note that the current implementation only supports generating new layers |
| using convolutions of stride 2 (resulting in a spatial resolution |
| reduction by a factor of 2), and will be extended to a more flexible |
| design. Convolution kernel size is set to 3 by default, and can be |
| customized by 'conv_kernel_size' parameter (similarily, 'conv_kernel_size' |
| should be set to -1 if 'from_layer' is specified). The created convolution |
| operation will be a normal 2D convolution by default, and a depthwise |
| convolution followed by 1x1 convolution if 'use_depthwise' is set to True. |
| depth_multiplier: Depth multiplier for convolutional layers. |
| min_depth: Minimum depth for convolutional layers. |
| insert_1x1_conv: A boolean indicating whether an additional 1x1 convolution |
| should be inserted before shrinking the feature map. |
| image_features: A dictionary of handles to activation tensors from the |
| base feature extractor. |
| pool_residual: Whether to add an average pooling layer followed by a |
| residual connection between subsequent feature maps when the channel |
| depth match. For example, with option 'layer_depth': [-1, 512, 256, 256], |
| a pooling and residual layer is added between the third and forth feature |
| map. This option is better used with Weight Shared Convolution Box |
| Predictor when all feature maps have the same channel depth to encourage |
| more consistent features across multi-scale feature maps. |
| |
| Returns: |
| feature_maps: an OrderedDict mapping keys (feature map names) to |
| tensors where each tensor has shape [batch, height_i, width_i, depth_i]. |
| |
| Raises: |
| ValueError: if the number entries in 'from_layer' and |
| 'layer_depth' do not match. |
| ValueError: if the generated layer does not have the same resolution |
| as specified. |
| """ |
| depth_fn = get_depth_fn(depth_multiplier, min_depth) |
|
|
| feature_map_keys = [] |
| feature_maps = [] |
| base_from_layer = '' |
| use_explicit_padding = False |
| if 'use_explicit_padding' in feature_map_layout: |
| use_explicit_padding = feature_map_layout['use_explicit_padding'] |
| use_depthwise = False |
| if 'use_depthwise' in feature_map_layout: |
| use_depthwise = feature_map_layout['use_depthwise'] |
| for index, from_layer in enumerate(feature_map_layout['from_layer']): |
| layer_depth = feature_map_layout['layer_depth'][index] |
| conv_kernel_size = 3 |
| if 'conv_kernel_size' in feature_map_layout: |
| conv_kernel_size = feature_map_layout['conv_kernel_size'][index] |
| if from_layer: |
| feature_map = image_features[from_layer] |
| base_from_layer = from_layer |
| feature_map_keys.append(from_layer) |
| else: |
| pre_layer = feature_maps[-1] |
| pre_layer_depth = pre_layer.get_shape().as_list()[3] |
| intermediate_layer = pre_layer |
| if insert_1x1_conv: |
| layer_name = '{}_1_Conv2d_{}_1x1_{}'.format( |
| base_from_layer, index, depth_fn(layer_depth / 2)) |
| intermediate_layer = slim.conv2d( |
| pre_layer, |
| depth_fn(layer_depth / 2), [1, 1], |
| padding='SAME', |
| stride=1, |
| scope=layer_name) |
| layer_name = '{}_2_Conv2d_{}_{}x{}_s2_{}'.format( |
| base_from_layer, index, conv_kernel_size, conv_kernel_size, |
| depth_fn(layer_depth)) |
| stride = 2 |
| padding = 'SAME' |
| if use_explicit_padding: |
| padding = 'VALID' |
| intermediate_layer = ops.fixed_padding( |
| intermediate_layer, conv_kernel_size) |
| if use_depthwise: |
| feature_map = slim.separable_conv2d( |
| intermediate_layer, |
| None, [conv_kernel_size, conv_kernel_size], |
| depth_multiplier=1, |
| padding=padding, |
| stride=stride, |
| scope=layer_name + '_depthwise') |
| feature_map = slim.conv2d( |
| feature_map, |
| depth_fn(layer_depth), [1, 1], |
| padding='SAME', |
| stride=1, |
| scope=layer_name) |
| if pool_residual and pre_layer_depth == depth_fn(layer_depth): |
| feature_map += slim.avg_pool2d( |
| pre_layer, [3, 3], |
| padding='SAME', |
| stride=2, |
| scope=layer_name + '_pool') |
| else: |
| feature_map = slim.conv2d( |
| intermediate_layer, |
| depth_fn(layer_depth), [conv_kernel_size, conv_kernel_size], |
| padding=padding, |
| stride=stride, |
| scope=layer_name) |
| feature_map_keys.append(layer_name) |
| feature_maps.append(feature_map) |
| return collections.OrderedDict( |
| [(x, y) for (x, y) in zip(feature_map_keys, feature_maps)]) |
|
|
|
|
| def fpn_top_down_feature_maps(image_features, |
| depth, |
| use_depthwise=False, |
| use_explicit_padding=False, |
| use_bounded_activations=False, |
| scope=None, |
| use_native_resize_op=False): |
| """Generates `top-down` feature maps for Feature Pyramid Networks. |
| |
| See https://arxiv.org/abs/1612.03144 for details. |
| |
| Args: |
| image_features: list of tuples of (tensor_name, image_feature_tensor). |
| Spatial resolutions of succesive tensors must reduce exactly by a factor |
| of 2. |
| depth: depth of output feature maps. |
| use_depthwise: whether to use depthwise separable conv instead of regular |
| conv. |
| use_explicit_padding: whether to use explicit padding. |
| use_bounded_activations: Whether or not to clip activations to range |
| [-ACTIVATION_BOUND, ACTIVATION_BOUND]. Bounded activations better lend |
| themselves to quantized inference. |
| scope: A scope name to wrap this op under. |
| use_native_resize_op: If True, uses tf.image.resize_nearest_neighbor op for |
| the upsampling process instead of reshape and broadcasting implementation. |
| |
| Returns: |
| feature_maps: an OrderedDict mapping keys (feature map names) to |
| tensors where each tensor has shape [batch, height_i, width_i, depth_i]. |
| """ |
| with tf.name_scope(scope, 'top_down'): |
| num_levels = len(image_features) |
| output_feature_maps_list = [] |
| output_feature_map_keys = [] |
| padding = 'VALID' if use_explicit_padding else 'SAME' |
| kernel_size = 3 |
| with slim.arg_scope( |
| [slim.conv2d, slim.separable_conv2d], padding=padding, stride=1): |
| top_down = slim.conv2d( |
| image_features[-1][1], |
| depth, [1, 1], activation_fn=None, normalizer_fn=None, |
| scope='projection_%d' % num_levels) |
| if use_bounded_activations: |
| top_down = tf.clip_by_value(top_down, -ACTIVATION_BOUND, |
| ACTIVATION_BOUND) |
| output_feature_maps_list.append(top_down) |
| output_feature_map_keys.append( |
| 'top_down_%s' % image_features[-1][0]) |
|
|
| for level in reversed(range(num_levels - 1)): |
| if use_native_resize_op: |
| with tf.name_scope('nearest_neighbor_upsampling'): |
| top_down_shape = top_down.shape.as_list() |
| top_down = tf.image.resize_nearest_neighbor( |
| top_down, [top_down_shape[1] * 2, top_down_shape[2] * 2]) |
| else: |
| top_down = ops.nearest_neighbor_upsampling(top_down, scale=2) |
| residual = slim.conv2d( |
| image_features[level][1], depth, [1, 1], |
| activation_fn=None, normalizer_fn=None, |
| scope='projection_%d' % (level + 1)) |
| if use_bounded_activations: |
| residual = tf.clip_by_value(residual, -ACTIVATION_BOUND, |
| ACTIVATION_BOUND) |
| if use_explicit_padding: |
| |
| residual_shape = tf.shape(residual) |
| top_down = top_down[:, :residual_shape[1], :residual_shape[2], :] |
| top_down += residual |
| if use_bounded_activations: |
| top_down = tf.clip_by_value(top_down, -ACTIVATION_BOUND, |
| ACTIVATION_BOUND) |
| if use_depthwise: |
| conv_op = functools.partial(slim.separable_conv2d, depth_multiplier=1) |
| else: |
| conv_op = slim.conv2d |
| if use_explicit_padding: |
| top_down = ops.fixed_padding(top_down, kernel_size) |
| output_feature_maps_list.append(conv_op( |
| top_down, |
| depth, [kernel_size, kernel_size], |
| scope='smoothing_%d' % (level + 1))) |
| output_feature_map_keys.append('top_down_%s' % image_features[level][0]) |
| return collections.OrderedDict(reversed( |
| list(zip(output_feature_map_keys, output_feature_maps_list)))) |
|
|
|
|
| def pooling_pyramid_feature_maps(base_feature_map_depth, num_layers, |
| image_features, replace_pool_with_conv=False): |
| """Generates pooling pyramid feature maps. |
| |
| The pooling pyramid feature maps is motivated by |
| multi_resolution_feature_maps. The main difference are that it is simpler and |
| reduces the number of free parameters. |
| |
| More specifically: |
| - Instead of using convolutions to shrink the feature map, it uses max |
| pooling, therefore totally gets rid of the parameters in convolution. |
| - By pooling feature from larger map up to a single cell, it generates |
| features in the same feature space. |
| - Instead of independently making box predictions from individual maps, it |
| shares the same classifier across different feature maps, therefore reduces |
| the "mis-calibration" across different scales. |
| |
| See go/ppn-detection for more details. |
| |
| Args: |
| base_feature_map_depth: Depth of the base feature before the max pooling. |
| num_layers: Number of layers used to make predictions. They are pooled |
| from the base feature. |
| image_features: A dictionary of handles to activation tensors from the |
| feature extractor. |
| replace_pool_with_conv: Whether or not to replace pooling operations with |
| convolutions in the PPN. Default is False. |
| |
| Returns: |
| feature_maps: an OrderedDict mapping keys (feature map names) to |
| tensors where each tensor has shape [batch, height_i, width_i, depth_i]. |
| Raises: |
| ValueError: image_features does not contain exactly one entry |
| """ |
| if len(image_features) != 1: |
| raise ValueError('image_features should be a dictionary of length 1.') |
| image_features = image_features[image_features.keys()[0]] |
|
|
| feature_map_keys = [] |
| feature_maps = [] |
| feature_map_key = 'Base_Conv2d_1x1_%d' % base_feature_map_depth |
| if base_feature_map_depth > 0: |
| image_features = slim.conv2d( |
| image_features, |
| base_feature_map_depth, |
| [1, 1], |
| padding='SAME', stride=1, scope=feature_map_key) |
| |
| |
| |
| |
| image_features = slim.max_pool2d( |
| image_features, [1, 1], padding='SAME', stride=1, scope=feature_map_key) |
| feature_map_keys.append(feature_map_key) |
| feature_maps.append(image_features) |
| feature_map = image_features |
| if replace_pool_with_conv: |
| with slim.arg_scope([slim.conv2d], padding='SAME', stride=2): |
| for i in range(num_layers - 1): |
| feature_map_key = 'Conv2d_{}_3x3_s2_{}'.format(i, |
| base_feature_map_depth) |
| feature_map = slim.conv2d( |
| feature_map, base_feature_map_depth, [3, 3], scope=feature_map_key) |
| feature_map_keys.append(feature_map_key) |
| feature_maps.append(feature_map) |
| else: |
| with slim.arg_scope([slim.max_pool2d], padding='SAME', stride=2): |
| for i in range(num_layers - 1): |
| feature_map_key = 'MaxPool2d_%d_2x2' % i |
| feature_map = slim.max_pool2d( |
| feature_map, [2, 2], padding='SAME', scope=feature_map_key) |
| feature_map_keys.append(feature_map_key) |
| feature_maps.append(feature_map) |
| return collections.OrderedDict( |
| [(x, y) for (x, y) in zip(feature_map_keys, feature_maps)]) |
|
|