|
|
- # 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.
- # ==============================================================================
-
- """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 used for TPU v1. Activations will be clipped to
- # [-ACTIVATION_BOUND, ACTIVATION_BOUND] when training with
- # use_bounded_activations enabled.
- 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'
- # We define this function here while capturing the value of
- # conv_kernel_size, to avoid holding a reference to the loop variable
- # conv_kernel_size inside of a lambda function
- def fixed_padding(features, kernel_size=conv_kernel_size):
- return ops.fixed_padding(features, kernel_size)
- net.append(tf.keras.layers.Lambda(fixed_padding))
- # TODO(rathodv): Add some utilities to simplify the creation of
- # Depthwise & non-depthwise convolutions w/ normalization & activations
- 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))
-
- # Until certain bugs are fixed in checkpointable lists,
- # this net must be appended only once it's been filled with layers
- 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:
- # slice top_down to the same shape as residual
- 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], # kernel size
- padding='SAME', stride=1, scope=feature_map_key)
- # Add a 1x1 max-pooling node (a no op node) immediately after the conv2d for
- # TPU v1 compatibility. Without the following dummy op, TPU runtime
- # compiler will combine the convolution with one max-pooling below into a
- # single cycle, so getting the conv2d feature becomes impossible.
- 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)])
|