Files
openvino/model-optimizer/extensions/front/tf/ObjectDetectionAPI.py
T
Evgeny Lazarev d28ecc4f2f New approach for Proposal sub-graph conversion for TF OD API models (#5847)
* Draft version of new approach for Proposal sub-graph conversion for TF OD API models

* Added clip_after_nms = True for Proposal sub-graph being replaced with DetectionOutput

* Refactored code to insert DetectionOutput instead of Proposal operation

* Code cleanup

* Added separate function to insert DetectionOutput instead of Proposal operation

* Updated transformation configuration files for the TF OD API models with Proposal transformation

* Code refactoring

* Code refactoring

* Fix for the condition

* Fixed transformation

* Fixed transformation. One more time

* Updated document about conversion of the TF OD API models

* Update code comments
2021-08-01 10:17:22 +03:00

1673 lines
100 KiB
Python

# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import logging as log
from math import sqrt
import numpy as np
from extensions.front.Pack import Pack
from extensions.front.TransposeOrderNormalizer import TransposeOrderNormalizer
from extensions.front.split_normalizer import SqueezeAxis
from extensions.front.tf.CropAndResizeReplacement import CropAndResizeReplacement
from extensions.front.tf.FakeQuantWithMinMaxVars import FakeQuantWithMinMaxVarsToQuantize
from extensions.front.tf.MapFNTransformation import MapFNInputSlicing, MapFNOutputConcatenation
from extensions.front.tf.TFSliceToSlice import TFSliceToSliceReplacer
from extensions.front.tf.pad_tf_to_pad import PadTFToPad
from extensions.middle.InsertLayoutPropagationTransposes import mark_as_correct_data_layout, \
mark_input_as_in_correct_layout, mark_output_as_in_correct_layout
from extensions.ops.Cast import Cast
from extensions.ops.DetectionOutput import DetectionOutput
from extensions.ops.ReduceOps import ReduceMean
from extensions.ops.activation_ops import Sigmoid
from extensions.ops.elementwise import Mul, Sub, Add, Div
from extensions.ops.gather import Gather
from extensions.ops.parameter import Parameter
from extensions.ops.priorbox_clustered import PriorBoxClusteredOp
from extensions.ops.proposal import ProposalOp
from extensions.ops.psroipooling import PSROIPoolingOp
from extensions.ops.split import Split
from extensions.ops.transpose import Transpose
from mo.front.common.layout import get_batch_dim, get_height_dim, get_width_dim
from mo.front.common.partial_infer.utils import int64_array
from mo.front.common.replacement import FrontReplacementPattern
from mo.front.extractor import output_user_data_repack, add_output_ops
from mo.front.subgraph_matcher import SubgraphMatch
from mo.front.tf.custom_subgraph_call import skip_nodes_by_condition
from mo.front.tf.graph_utils import add_activation_function_after_node, add_convolution_to_swap_xy_coordinates, \
mark_squeeze_reshape_concat_before_detection_output, add_fake_background_loc, create_op_node_with_second_input, \
create_op_with_const_inputs
from mo.front.tf.replacement import FrontReplacementFromConfigFileSubGraph, FrontReplacementFromConfigFileGeneral
from mo.graph.graph import Graph, Node
from mo.middle.passes.convert_data_type import data_type_str_to_np
from mo.ops.clamp import AttributedClamp
from mo.ops.concat import Concat
from mo.ops.const import Const
from mo.ops.crop import Crop
from mo.ops.op import PermuteAttrs
from mo.ops.reshape import Reshape
from mo.ops.result import Result
from mo.ops.roipooling import ROIPooling
from mo.ops.shape import Shape
from mo.ops.softmax import Softmax
from mo.ops.squeeze import Squeeze
from mo.ops.tile import Tile
from mo.utils.error import Error
from mo.utils.graph import backward_bfs_for_operation, bfs_search, clear_tensor_names_info, sub_graph_between_nodes
from mo.utils.pipeline_config import PipelineConfig
from mo.utils.shape import node_to_get_shape_value_of_indices
missing_param_error = 'To convert the model specify path to the pipeline configuration file which was used to ' \
'generate the model. Please use "--tensorflow_object_detection_api_pipeline_config" option:\n' \
'--tensorflow_object_detection_api_pipeline_config "<path_to_pipeline.config>"\nIf you have ' \
'downloaded the model file from the Object Detection Model zoo repository then this file is ' \
'located in the archive with frozen model and called "pipeline.config".\nIf you did not use ' \
'this command line parameter before that means that you are using currently deprecated ' \
'TensorFlow* Object Detection API models conversion mechanism.'
def _value_or_raise(match: SubgraphMatch, pipeline_config: PipelineConfig, key: str):
"""
Returns value from the 'custom_attributes' of the 'match' object or pipeline_config associated with a key 'key'.
If the value doesn't exist then raise error.
:param match: SubgraphMatch object containing 'custom_attributes'.
:param pipeline_config: PipelineConfig object with parsed values.
:param key: key to search for.
:return: the requested value.
"""
if match and key in match.custom_replacement_desc.custom_attributes:
return match.custom_replacement_desc.custom_attributes[key]
value = pipeline_config.get_param(key)
if value is None:
raise Error('The sub-graph replacer "[REPLACEMENT_ID]" was not able to find the value for key "{}" in the '
'pipeline configuration file specified with the --tensorflow_object_detection_api_pipeline_config '
'command line parameter. Update the sub-graph replacement configuration file specified with the '
'--tensorflow_use_custom_operations_config command line parameter by adding key "{}" with required '
'value to the "custom_attributes" dictionary of the "[REPLACEMENT_ID]" replacer.'.format(key, key))
return value
def _find_ssd_head_node(graph: Graph, ssd_head_index: int, head_type: str):
"""
Finds the SSD head node with index 'ssd_head_index' in the topology. The parameter 'head_type' specifies what type
of the head is requested: with box predictions or class predictions.
:param graph: graph with the topology.
:param ssd_head_index: index of the SSD head.
:param head_type: either 'box' or 'class' string specifying type of the SSD head node.
:return: the requested Node or None if node is not found.
"""
if head_type == 'box':
possible_node_names = ['BoxPredictor_%d/BoxEncodingPredictor/BiasAdd' % ssd_head_index,
'WeightSharedConvolutionalBoxPredictor/BoxPredictor/BiasAdd' if ssd_head_index == 0 else
'WeightSharedConvolutionalBoxPredictor_%d/BoxPredictor/BiasAdd' % ssd_head_index]
elif head_type == 'class':
possible_node_names = ['BoxPredictor_%d/ClassPredictor/BiasAdd' % ssd_head_index,
'WeightSharedConvolutionalBoxPredictor/ClassPredictor/BiasAdd' if ssd_head_index == 0
else 'WeightSharedConvolutionalBoxPredictor_%d/ClassPredictor/BiasAdd' % ssd_head_index]
else:
raise Error('SSD heads can be of type "box" and "class" only.')
head_node = None
for head_node_name in possible_node_names:
if head_node_name in graph.nodes():
assert (head_node is None) # only one of the possible node names should exist in the graph
head_node = Node(graph, head_node_name)
return head_node
def _variance_from_pipeline_config(pipeline_config: PipelineConfig):
"""
Generates a numpy array with variances values from the pipeline_config object. The order of the elements is the
following: variance x, variance y, variance box width, variance box height.
:param pipeline_config: pipeline_config object to get variances from.
:return: the numpy array with variances.
"""
return 1.0 / np.array([pipeline_config.get_param('frcnn_variance_x'),
pipeline_config.get_param('frcnn_variance_y'),
pipeline_config.get_param('frcnn_variance_width'),
pipeline_config.get_param('frcnn_variance_height')])
def _skip_node_of_type(node: Node, node_ops_to_skip: list):
"""
Skips nodes of specified ops starting from node 'node'.
:param node: node to start skipping Identity nodes.
:return: node of the op
"""
# skip the Identity node
while len(node.out_edges()) == 1 and node.op in node_ops_to_skip:
node = node.out_node()
return node
def _relax_reshape_nodes(graph: Graph, pipeline_config: PipelineConfig):
"""
Finds the 'Reshape' operations following the SSD head nodes which have hard-coded output dimensions and replaces
them with new ones with one of the dimensions sizes equal to -1. This function is used to make TF OD API SSD models
reshape-able.
:param graph: graph with the topology.
:param pipeline_config: PipelineConfig object with parsed values.
:return: None
"""
num_classes = pipeline_config.get_param('num_classes')
num_layers = pipeline_config.get_param('ssd_anchor_generator_num_layers')
if num_layers is None:
num_layers = pipeline_config.get_param('multiscale_anchor_generator_max_level') - \
pipeline_config.get_param('multiscale_anchor_generator_min_level') + 1
for ssd_head_ind in range(num_layers):
input_node = _find_ssd_head_node(graph, ssd_head_ind, 'box')
assert (input_node is not None)
old_reshape_node = _skip_node_of_type(input_node.out_node(),
['Identity', 'FakeQuantWithMinMaxVars', 'FakeQuantize'])
assert old_reshape_node.op == 'Reshape'
reshape_size_node = Const(graph, {'value': int64_array([0, -1, 1, 4])}).create_node([])
new_reshape_op = Reshape(graph, {'name': input_node.id + '/Reshape'})
new_reshape_node = new_reshape_op.create_node([input_node, reshape_size_node])
old_reshape_node.replace_node(new_reshape_node)
# fix hard-coded value for the number of items in tensor produced by the convolution to make topology reshapable
input_node = _find_ssd_head_node(graph, ssd_head_ind, 'class')
assert (input_node is not None)
old_reshape_node = _skip_node_of_type(input_node.out_node(),
['Identity', 'FakeQuantWithMinMaxVars', 'FakeQuantize'])
assert old_reshape_node.op == 'Reshape'
reshape_size_node_2 = Const(graph, {'value': int64_array([0, -1, num_classes + 1])}).create_node([])
new_reshape_op_2 = Reshape(graph, {'name': input_node.id + '/Reshape'})
new_reshape_node_2 = new_reshape_op_2.create_node([input_node, reshape_size_node_2])
old_reshape_node.replace_node(new_reshape_node_2)
def _create_prior_boxes_node(graph: Graph, pipeline_config: PipelineConfig):
"""
The function creates one or several PriorBoxClustered nodes based on information from the pipeline configuration
files. The PriorBoxClustered nodes get input data from SSD 'heads' and from the placeholder node (just to get
input image size).
:param graph: graph with the topology.
:param pipeline_config: PipelineConfig object with parsed values.
:return: node generating prior boxes.
"""
min_scale = pipeline_config.get_param('ssd_anchor_generator_min_scale')
max_scale = pipeline_config.get_param('ssd_anchor_generator_max_scale')
num_layers = pipeline_config.get_param('ssd_anchor_generator_num_layers')
aspect_ratios = pipeline_config.get_param('ssd_anchor_generator_aspect_ratios')
if not isinstance(aspect_ratios, list):
aspect_ratios = [aspect_ratios]
# prior boxes have to be generated using the image size used for training
image_height = pipeline_config.get_param('resizer_image_height')
image_width = pipeline_config.get_param('resizer_image_width')
min_im_shape = min(image_height, image_width)
_base_anchor_height = pipeline_config.get_param('ssd_anchor_generator_base_anchor_height')
_base_anchor_width = pipeline_config.get_param('ssd_anchor_generator_base_anchor_width')
base_anchor_size = [min_im_shape / image_height * _base_anchor_height,
min_im_shape / image_width * _base_anchor_width]
reduce_boxes_in_lowest_layer = True
if pipeline_config.get_param('ssd_anchor_generator_reduce_lowest') is not None:
reduce_boxes_in_lowest_layer = pipeline_config.get_param('ssd_anchor_generator_reduce_lowest')
if pipeline_config.get_param('ssd_anchor_generator_scales') is not None:
scales = pipeline_config.get_param('ssd_anchor_generator_scales') + [1.0]
else:
scales = [min_scale + (max_scale - min_scale) * i / (num_layers - 1) for i in range(num_layers)] + [1.0]
prior_box_nodes = []
for ssd_head_ind in range(num_layers):
ssd_head_node = _find_ssd_head_node(graph, ssd_head_ind, 'box')
assert (ssd_head_node is not None)
if ssd_head_ind == 0 and reduce_boxes_in_lowest_layer:
widths = [0.1, min_scale * sqrt(2.0), min_scale * sqrt(0.5)]
heights = [0.1, min_scale / sqrt(2.0), min_scale / sqrt(0.5)]
else:
widths = [scales[ssd_head_ind] * sqrt(ar) for ar in aspect_ratios]
heights = [scales[ssd_head_ind] / sqrt(ar) for ar in aspect_ratios]
interpolated_scale_ar = pipeline_config.get_param('ssd_anchor_generator_interpolated_scale_aspect_ratio')
if interpolated_scale_ar > 0.0:
widths += [sqrt(scales[ssd_head_ind] * scales[ssd_head_ind + 1]) * interpolated_scale_ar]
heights += [sqrt(scales[ssd_head_ind] * scales[ssd_head_ind + 1]) / interpolated_scale_ar]
widths = [w * image_width * base_anchor_size[1] for w in widths]
heights = [h * image_height * base_anchor_size[0] for h in heights]
variance = _variance_from_pipeline_config(pipeline_config)
prior_box_op = PriorBoxClusteredOp(graph, {'width': np.array(widths), 'height': np.array(heights),
'clip': 0, 'flip': 0, 'variance': variance, 'offset': 0.5,
})
# connect the PriorBoxClustered node with the "Cast" node of the Placeholder node because the pass that removes
# Cast operations is executed in the middle phase and it will fail when there are several consumers of the
# Placeholder
input_node_name = 'image_tensor' if 'image_tensor' in graph.nodes else 'input_tensor'
prior_box_node = prior_box_op.create_node([ssd_head_node, Node(graph, input_node_name).out_node(0)],
{'name': 'PriorBoxClustered_{}'.format(ssd_head_ind)})
prior_box_nodes.append(prior_box_node)
if len(prior_box_nodes) == 1:
return prior_box_nodes[0]
else:
concat_prior_boxes_op = Concat(graph, {'axis': -1, 'in_ports_count': len(prior_box_nodes)})
return concat_prior_boxes_op.create_node(prior_box_nodes, {'name': 'ConcatPriorBoxesClustered'})
def _create_multiscale_prior_boxes_node(graph: Graph, pipeline_config: PipelineConfig):
"""
The function creates one or several PriorBoxClustered nodes based on information from the pipeline configuration
files. The PriorBoxClustered nodes get input data from SSD 'heads' and from the placeholder node (just to get
input image size).
:param graph: graph with the topology.
:param pipeline_config: PipelineConfig object with parsed values.
:return: node generating prior boxes.
"""
min_level = pipeline_config.get_param('multiscale_anchor_generator_min_level')
max_level = pipeline_config.get_param('multiscale_anchor_generator_max_level')
anchor_scale = pipeline_config.get_param('multiscale_anchor_generator_anchor_scale')
aspect_ratios = pipeline_config.get_param('multiscale_anchor_generator_aspect_ratios')
scales_per_octave = pipeline_config.get_param('multiscale_anchor_generator_scales_per_octave')
prior_box_nodes = []
scales = [2 ** (float(scale) / scales_per_octave) for scale in range(scales_per_octave)]
for level in range(min_level, max_level + 1):
base_anchor_size = 2 ** level * anchor_scale
ssd_head_ind = level - min_level
ssd_head_node = _find_ssd_head_node(graph, ssd_head_ind, 'box')
assert (ssd_head_node is not None)
widths = [base_anchor_size * scale * sqrt(ar) for ar in aspect_ratios for scale in scales]
heights = [base_anchor_size * scale / sqrt(ar) for ar in aspect_ratios for scale in scales]
variance = _variance_from_pipeline_config(pipeline_config)
prior_box_op = PriorBoxClusteredOp(graph, {'width': np.array(widths), 'height': np.array(heights),
'clip': 0, 'flip': 0, 'variance': variance,
'offset': 0.5,
})
# connect the PriorBoxClustered node with the "Cast" node of the Placeholder node because the pass that removes
# Cast operations is executed in the middle phase and it will fail when there are several consumers of the
# Placeholder
prior_box_node = prior_box_op.create_node([ssd_head_node, Node(graph, 'image_tensor').out_node(0)],
{'name': 'PriorBoxClustered_{}'.format(ssd_head_ind)})
prior_box_nodes.append(prior_box_node)
if len(prior_box_nodes) == 1:
return prior_box_nodes[0]
else:
concat_prior_boxes_op = Concat(graph, {'axis': -1, 'in_ports_count': len(prior_box_nodes)})
return concat_prior_boxes_op.create_node(prior_box_nodes, {'name': 'ConcatPriorBoxesClustered'})
def insert_weights_swap_xy_sub_graph(graph: Graph, connection):
"""
Inserts a sub-graph of operations which does the following:
1. Reshapes the input tensor (should be convolution weights/biases) to [-1, 2].
2. Swaps slices of data [:, 0] and [:, 1].
3. Reshapes tensor to the initial shape.
"""
weights_producer = connection.get_source()
name = weights_producer.node.soft_get('name', weights_producer.node.id)
# this Shape operation must be inferred and constant folded
origin_shape = Shape(graph, {'name': name + '/OriginShape', 'force_dead_node': True}).create_node()
origin_shape.in_port(0).connect(weights_producer)
reshaped = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 2]), {'name': name + '/Reshape2D'})
reshaped.in_port(0).connect(weights_producer)
swapped_weight = Gather(graph, {'name': name + '/SwappedWeights'}).create_node()
gather_indices = Const(graph,
{'name': swapped_weight.name + '/Indices', 'value': int64_array([1, 0])}).create_node()
gather_axis = Const(graph, {'name': swapped_weight.name + '/Axis', 'value': int64_array(1)}).create_node()
swapped_weight.in_port(0).connect(reshaped.out_port(0))
swapped_weight.in_port(1).connect(gather_indices.out_port(0))
swapped_weight.in_port(2).connect(gather_axis.out_port(0))
reshape_back = Reshape(graph, {'name': name + '/ReshapeBack'}).create_node()
reshape_back.in_port(0).connect(swapped_weight.out_port(0))
reshape_back.in_port(1).connect(origin_shape.out_port(0))
connection.set_source(reshape_back.out_port(0))
def swap_weights_xy(graph: Graph, nodes: list):
"""
The function changes weights of the nodes from the 'nodes' list which are used with calculations with coordinates of
some objects. The function should be used when it is necessary to virtually change the layout of data from XY to YX.
The node from the 'nodes' list should be some sort of convolution node or matrix multiplication.
The function also swaps weights in the following Add and BiasAdd operations.
:param graph: graph with the topology.
:param nodes: list of Node objects to change the weights in them.
:return: None
"""
producers_ports = set()
for node in nodes:
# need to skip the FakeQuantize node if it exists
weights_producer = node.in_port(1).get_source()
if weights_producer.node.soft_get('type') == 'FakeQuantize':
weights_producer = weights_producer.node.in_port(0).get_source()
producers_ports.add(weights_producer)
for producers_port in producers_ports:
log.debug('Swapping weights for node "{}"'.format(producers_port.node.name))
insert_weights_swap_xy_sub_graph(graph, producers_port.get_connection())
for node in nodes:
# swap biases
for m in [n.node for n in node.out_port(0).get_destinations()]:
if m.soft_get('type') in ['Add', 'BiasAdd']:
insert_weights_swap_xy_sub_graph(graph, m.in_port(1).get_connection())
def calculate_shape_keeping_aspect_ratio(height: int, width: int, min_size: int, max_size: int,
pad_to_max_dimension: bool = False):
"""
The function scales spatial sizes of the image keeping aspect ratio to satisfy provided requirements.
The behavior of this function is equivalent to the output shape calculation of the Preprocessor block of TensorFlow
Object Detection API models with keep aspect ratio resizer.
:param height: input height.
:param width: input width.
:param min_size: size limit.
:param max_size: size limit.
:param pad_to_max_dimension: scale the input image size to the maximum value specified
:return: the tuple with scaled image height, width.
"""
if pad_to_max_dimension:
return max_size, max_size
ratio_min = min_size / min(height, width)
ratio_max = max_size / max(height, width)
ratio = min(ratio_min, ratio_max)
return int(round(height * ratio)), int(round(width * ratio))
def calculate_placeholder_spatial_shape(graph: Graph, match: SubgraphMatch, pipeline_config: PipelineConfig):
"""
The function calculates the preprocessed shape of the input image for a TensorFlow Object Detection API model.
It uses various sources to calculate it:
1. The shape passed using the '--input_shape' command line parameter.
2. The values from the pipeline configuration file describing Preprocessor block of the topology:
a. If the fixed size resizer is used then size passed via '--input_shape' can override them, but Model Optimizer
prints warning. If the '--input_shape' is not defined then use values from the pipeline configuration file.
b. If the keep aspect ratio resizer is used then scale the size passed via '--input_shape' using the provided
limits. If the '--input_shape' is not defined then use shape as (min_dimension_size, min_dimension_size)
defined in the pipeline configuration file.
:param graph: graph with the topology.
:param match: the object containing matching sub-graph and custom attributes from the sub-graph replacement file.
:param pipeline_config: the object contain information from the pipeline configuration file.
:return: tuple (height, width) of the placeholder shape.
"""
height = None
width = None
user_shapes = graph.graph['user_shapes']
if match and ('preprocessed_image_height' in match.custom_replacement_desc.custom_attributes or
'preprocessed_image_width' in match.custom_replacement_desc.custom_attributes):
log.error('The "preprocessed_image_height" or "preprocessed_image_width" is specified in the sub-graph '
'replacement configuration file but they are ignored. Please, specify desired input shape using the '
'"--input_shape" command line parameter.', extra={'is_warning': True})
user_defined_height = None
user_defined_width = None
input_name = 'input_tensor' if 'input_tensor' in graph.nodes else 'image_tensor'
if user_shapes and input_name in user_shapes and user_shapes[input_name]:
user_defined_shape = user_shapes[input_name][0]['shape']
if user_defined_shape is not None:
user_defined_height = user_defined_shape[1]
user_defined_width = user_defined_shape[2]
resizer_height = pipeline_config.get_param('resizer_image_height')
resizer_width = pipeline_config.get_param('resizer_image_width')
if resizer_height and resizer_width:
log.debug('The model resizes image to a fixed shape: ({}, {})'.format(resizer_height, resizer_width))
resizer_min_dimension = pipeline_config.get_param('resizer_min_dimension')
resizer_max_dimension = pipeline_config.get_param('resizer_max_dimension')
if resizer_min_dimension and resizer_max_dimension:
log.debug('The model resizes image using keep aspect ratio with minimum size {}, maximum size {}.'.format(
resizer_min_dimension, resizer_max_dimension))
# if the model is created with an input image resizer to a fixed shape
if resizer_width and resizer_height:
if user_defined_height and user_defined_width:
if user_defined_width != resizer_width or user_defined_width != resizer_width:
log.error('The model expects that the input image is resized to a fixed shape ({}, {}), but the shape '
'provided with the "--input_shape" command line parameter is different ({}, {}).'.format(
resizer_height, resizer_width, user_defined_height, user_defined_width), extra={'is_warning': True})
height = user_defined_height
width = user_defined_width
else:
height = resizer_height
width = resizer_width
# if the model is created with an input image resizer keeping aspect ratio
if resizer_min_dimension and resizer_max_dimension:
pad_to_max_dimension = pipeline_config.get_param('pad_to_max_dimension')
print('[ WARNING ] Model Optimizer removes pre-processing block of the model which resizes image keeping '
'aspect ratio. The Inference Engine does not support dynamic image size so the Intermediate '
'Representation file is generated with the input image size of a fixed size.')
if user_defined_height and user_defined_width:
scaled_height, scaled_width = calculate_shape_keeping_aspect_ratio(user_defined_height,
user_defined_width,
resizer_min_dimension,
resizer_max_dimension,
pad_to_max_dimension)
if scaled_height != user_defined_height or scaled_width != user_defined_width:
log.error('The model resizes the input image keeping aspect ratio with min dimension {}, max '
'dimension {}. The provided input height {}, width {} is transformed to height {}, width '
'{}.'.format(resizer_min_dimension, resizer_max_dimension, user_defined_height,
user_defined_width, scaled_height, scaled_width), extra={'is_warning': True})
height = scaled_height
width = scaled_width
else:
if pad_to_max_dimension:
height = width = resizer_max_dimension
else:
height = width = resizer_min_dimension
print('Specify the "--input_shape" command line parameter to override the default shape which is equal to '
'({}, {}).'.format(height, width))
if height is None or width is None:
raise Error('Failed to determine the placeholder shape.')
return height, width
def update_parameter_shape(graph: Graph, match: [SubgraphMatch, None]):
"""
Updates the shape of the model Parameter node based on the user provided input shape or values provided in the
pipeline.config configuration file used for model training.
:param graph: model graph
:param match: Match object with information abouot matched sub-graph
:return: tupe with input node names and Parameter Node
"""
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
initial_input_node_name = 'input_tensor' if 'input_tensor' in graph.nodes else 'image_tensor'
if initial_input_node_name not in graph.nodes():
raise Error('Input node "{}" of the graph is not found. Do not run the Model Optimizer with '
'"--input" command line parameter.'.format(initial_input_node_name))
parameter_node = Node(graph, initial_input_node_name)
# set default value of the batch size to 1 if user didn't specify batch size and input shape
layout = graph.graph['layout']
batch_dim = get_batch_dim(layout, 4)
if argv.batch is None and parameter_node.shape[batch_dim] == -1:
parameter_node.shape[batch_dim] = 1
height, width = calculate_placeholder_spatial_shape(graph, match, pipeline_config)
parameter_node.shape[get_height_dim(layout, 4)] = height
parameter_node.shape[get_width_dim(layout, 4)] = width
# save the pre-processed image spatial sizes to be used in the other replacers
graph.graph['preprocessed_image_height'] = parameter_node.shape[get_height_dim(layout, 4)]
graph.graph['preprocessed_image_width'] = parameter_node.shape[get_width_dim(layout, 4)]
return initial_input_node_name, parameter_node
class ObjectDetectionAPITransformationsStart(FrontReplacementPattern):
"""
This is a anchor transformation which is used to distinguish TF OD API models related transformations.
"""
enabled = True
def run_after(self):
return [CropAndResizeReplacement, FakeQuantWithMinMaxVarsToQuantize]
def find_and_replace_pattern(self, graph: Graph):
pass
class ObjectDetectionAPITransformationsFinish(FrontReplacementPattern):
"""
This is a anchor transformation which is used to distinguish TF OD API models related transformations.
"""
enabled = True
# cleanup the graph after applying of TF OD API transformations to remove a lot of unconnected nodes to avoid issues
# with shape inference
force_clean_up = True
def run_before(self):
# PadTFToPad inserts Transpose ops for Pad ops inside the sub-graph corresponding to DetectionOutput.
# But the inputs corresponding to padding values is re-used as inputs for newly created Pad node. This input
# is removed during removing nodes from the DO sub-graph so the first input to Transpose is missing which
# results in TransposeOrderNormalizer transformation failure.
return [Pack, TransposeOrderNormalizer, PadTFToPad, SqueezeAxis, TFSliceToSliceReplacer,
MapFNOutputConcatenation, MapFNInputSlicing]
def find_and_replace_pattern(self, graph: Graph):
pass
def get_specific_ops_with_const_inputs(first_node: Node, allowed_ops: list, forward: bool = True):
"""
Returns the list with information about consecutive nodes of operation from "allowed_ops".
:param first_node: The first node (not included) to start looking for nodes from the "allowed_ops" list
:param allowed_ops: list of allowed operations
:param forward: flag specifying direction of search
:return: list of triplets (Node, const_port_index, const_value)
"""
node = first_node.out_port(0).get_destination().node if forward else first_node.in_port(0).get_source().node
result = [] # (Node, port # with constant input, value)
while node.soft_get('op') in allowed_ops:
num_in_ports = len(node.in_ports())
assert num_in_ports == 2, 'The node "{}" should have exactly 2 inputs, but it has only {}.' \
''.format(node.soft_get('name', node.id), num_in_ports)
for port in (0, 1):
if node.in_port(port).get_source().node.has_valid('value'): # this is a constant input to the node
result.append((node, port, node.in_port(port).get_source().node.value.copy()))
node = node.out_port(0).get_destination().node if forward else node.in_port(1 - port).get_source().node
break
return result
def get_preprocessing_ops(graph: Graph, start_node_id_suffix: str, end_node_id_suffix: str):
"""
Finds a sequence of pre-processing nodes (Sub, Mul, Div and Add) after the node with the id suffix
'end_node_id_suffix' or ending with the node with id suffix 'end_node_id_suffix'.
:param graph: graph to look for pre-processing ops
:param start_node_id_suffix: suffix of the start node name
:param end_node_id_suffix: suffix of the end node name
:return: the list with pre-processing nodes information and flag specifying nodes position
"""
start_node = None
end_node = None
for node in graph.get_op_nodes():
if node.id.endswith(start_node_id_suffix):
start_node = node
if node.id.endswith(end_node_id_suffix):
end_node = node
assert start_node is not None and end_node is not None, \
'Failed to find start/end nodes of the pre-processing block. The section of the transformation JSON ' \
'configuration file related to "ObjectDetectionAPIPreprocessor2Replacement" transformation should be updated ' \
'for this particular model.'
allowed_ops = ['Sub', 'Mul', 'Div', 'Add']
preprocessing_nodes = get_specific_ops_with_const_inputs(start_node, allowed_ops, False)
trailing = False # switch to apply newly created pre-processing nodes after/before start_node/end_node
if len(preprocessing_nodes) == 0:
preprocessing_nodes = get_specific_ops_with_const_inputs(end_node, allowed_ops, True)
trailing = True
return preprocessing_nodes, trailing
class ObjectDetectionAPIPreprocessorReplacement(FrontReplacementFromConfigFileSubGraph):
"""
The class replaces the "Preprocessor" block resizing input image and applying mean/scale values. Only nodes related
to applying mean/scaling values are kept.
"""
replacement_id = 'ObjectDetectionAPIPreprocessorReplacement'
run_not_recursively = True
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def run_after(self):
return [ObjectDetectionAPITransformationsStart]
def nodes_to_remove(self, graph: Graph, match: SubgraphMatch):
new_nodes_to_remove = match.matched_nodes_names()
# do not remove nodes that perform input image scaling and mean value subtraction
for node_to_keep in ('Preprocessor/sub', 'Preprocessor/sub/y', 'Preprocessor/mul', 'Preprocessor/mul/x'):
if node_to_keep in new_nodes_to_remove:
new_nodes_to_remove.remove(node_to_keep)
return new_nodes_to_remove
def is_preprocessing_applied_before_resize(self, to_float: Node, mul: Node, sub: Node):
"""
The function checks if the output of 'to_float' operation is consumed by 'mul' or 'sub'. If this is true then
the pre-processing (mean/scale) is applied before the image resize. The image resize was applied first in the
original version of the TF OD API models, but in the recent versions it is applied after.
:param to_float: the Cast node which converts the input tensor to Float
:param mul: the Mul node (can be None)
:param sub: the Sub node
:return: the result of the check
"""
assert sub is not None, 'The Sub node should not be None. Check the caller function.'
if mul is not None:
return any([port.node.id == mul.id for port in to_float.out_port(0).get_destinations()])
else:
return any([port.node.id == sub.id for port in to_float.out_port(0).get_destinations()])
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
sub_node = match.output_node(0)[0]
if sub_node.soft_get('op') != 'Sub':
raise Error('The output op of the Preprocessor sub-graph is not of type "Sub". Looks like the topology is '
'not created with TensorFlow Object Detection API.')
mul_node = None
if sub_node.in_port(0).get_source().node.soft_get('op') == 'Mul':
log.info('There is image scaling node in the Preprocessor block.')
mul_node = sub_node.in_port(0).get_source().node
initial_input_node_name, placeholder_node = update_parameter_shape(graph, match)
to_float_node = placeholder_node.out_port(0).get_destination().node
if to_float_node.soft_get('op') != 'Cast':
raise Error('The output of the node "{}" is not Cast operation. Cannot apply transformation.'.format(
initial_input_node_name))
if self.is_preprocessing_applied_before_resize(to_float_node, mul_node, sub_node):
# connect sub node directly to nodes which consume resized image
resize_output_node_id = 'Preprocessor/map/TensorArrayStack/TensorArrayGatherV3'
if resize_output_node_id not in graph.nodes:
raise Error('There is no expected node "{}" in the graph.'.format(resize_output_node_id))
resize_output = Node(graph, resize_output_node_id)
for dst_port in resize_output.out_port(0).get_destinations():
dst_port.get_connection().set_source(sub_node.out_port(0))
else:
# connect to_float_node directly with node performing scale on mean value subtraction
if mul_node is None:
to_float_node.out_port(0).connect(sub_node.in_port(0))
else:
to_float_node.out_port(0).connect(mul_node.in_port(1))
print('The Preprocessor block has been removed. Only nodes performing mean value subtraction and scaling (if'
' applicable) are kept.')
return {}
class ObjectDetectionAPIPreprocessor2Replacement(FrontReplacementFromConfigFileGeneral):
"""
The class replaces the "Preprocessor" block resizing input image and applying mean/scale values. Only nodes related
to applying mean/scaling values are kept. The transformation is used for TensorFlow 2.X models.
There are 6 possible cases:
1. ... -> Scale -> Start -> Resize -> End -> ...
2. ... -> Start -> Resize -> End -> Scale -> ...
3. ... -> Start -> Resize -> End -> ...
4. ... -> Start -> While (... -> Scale -> Resize -> ...) -> End -> ...
5. ... -> Start -> While (... -> Resize -> Scale -> ...) -> End -> ...
6. ... -> Start -> While (... -> Resize -> ...) -> End -> ...
Where:
- "Start" - is the node name specified in the transformation configuration file
- "End" - is the node name specified in the transformation configuration file
- "Scale" - a node or a sequence of element-wise nodes like Mul, Add, Sub or Div with Const input
- "While" (... nodes ... ) - a Loop operation with body nodes specified in parentheses
- "Resize" - the Resize sub-graph being removed
The transformation creates a new sub-graph of pre-processing nodes if in the original model it is inside the Loop,
or keeps the existing one if they are in the main graph originally.
"""
replacement_id = 'ObjectDetectionAPIPreprocessor2Replacement'
run_not_recursively = True
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def run_after(self):
return [ObjectDetectionAPITransformationsStart]
def transform_graph(self, graph: Graph, replacement_descriptions: dict):
update_parameter_shape(graph, None)
start_nodes = replacement_descriptions['start_nodes']
end_nodes = replacement_descriptions['end_nodes']
start_nodes = [node_id for node_id in start_nodes if node_id in graph.nodes]
end_nodes = [node_id for node_id in end_nodes if node_id in graph.nodes]
assert len(start_nodes) >= 1
start_node = Node(graph, start_nodes[0])
assert len(end_nodes) >= 1
end_node = Node(graph, end_nodes[0])
# determine nodes between specified input and output nodes to check if there is a Loop op among them
sub_graph_node_ids = sub_graph_between_nodes(graph, start_nodes, end_nodes, include_control_flow=False,
allow_non_reachable_end_nodes=True)
pre_processing_in_loop = False
# If the pre-processing block contains Loop operation then mean and scale value should be obtained from it using
# some pre-defined marker nodes existing for all pre-processing blocks.
# If there is no Loop then pre-processing nodes are in the main graph and they should be obtained from it
loop_nodes_ids = [node_id for node_id in sub_graph_node_ids if graph.node[node_id].get('op') == 'Loop']
if len(loop_nodes_ids):
assert len(loop_nodes_ids) == 1, 'There should be exactly one Loop node in the pre-processor block.'
pre_processing_in_loop = True
loop_node = Node(graph, loop_nodes_ids[0])
body_graph = loop_node.body
# we stick to the nodes with ids 'map/while/Preprocessor/unstack' and 'map/while/Preprocessor/stack' as they
# "wrap" nodes performing image resize. The scale/mean values nodes are located strictly before or after
# them
pre_processing_ops, trailing = get_preprocessing_ops(body_graph,
'map/while/Preprocessor/unstack',
'map/while/Preprocessor/stack')
else:
pre_processing_ops, trailing = get_preprocessing_ops(graph, start_node.id, end_node.id)
if len(pre_processing_ops):
# if the pre-processing is applied before the resize then reverse them to be in the topological order
if not trailing:
pre_processing_ops = list(reversed(pre_processing_ops))
if pre_processing_in_loop: # case 4 and 5
# build a sub-graph containing a sequence of pre_processing_ops if they came from the Loop
new_preprocessing_ops = []
ops_mapping = {'Add': Add, 'Div': Div, 'Mul': Mul, 'Sub': Sub}
for idx in range(len(pre_processing_ops)):
origin_node, const_port_ind, value = pre_processing_ops[idx]
new_node = create_op_with_const_inputs(graph, ops_mapping[origin_node.op], {const_port_ind: value})
if len(new_preprocessing_ops):
new_node.in_port(1 - const_port_ind).connect(new_preprocessing_ops[-1].out_port(0))
new_preprocessing_ops.append(new_node)
# replace sub-graph between start and end nodes (including them) with new_preprocessing_ops nodes
end_node.out_port(0).get_connection().set_source(new_preprocessing_ops[-1].out_port(0))
start_node.in_port(0).get_connection().set_destination(
new_preprocessing_ops[0].in_port(new_preprocessing_ops[0].is_in_port_connected(0)))
else:
if trailing: # case 2
# change output of the end_node to be produced with the start node producer
source_port = start_node.in_port(0).get_source()
source_port.disconnect()
end_node.out_port(0).get_connection().set_source(source_port)
else: # case 1
# change output of the end_node to be produced with the last preprocessing op
end_node.out_port(0).get_connection().set_source(pre_processing_ops[-1][0].out_port(0))
start_node.in_port(0).disconnect()
else: # simply remove the nodes in between start_node and end_node (including them). Case 3 and 6
end_node.out_port(0).get_connection().set_source(start_node.in_port(0).get_source())
print('The Preprocessor block has been removed. Only nodes performing mean value subtraction and scaling (if'
' applicable) are kept.')
class ObjectDetectionAPIDetectionOutputReplacement(FrontReplacementFromConfigFileSubGraph):
"""
Replaces the sub-graph that is equal to the DetectionOutput layer from Inference Engine. This replacer is used for
Faster R-CNN, R-FCN and Mask R-CNN topologies conversion.
The replacer uses a value of the custom attribute 'coordinates_swap_method' from the sub-graph replacement
configuration file to choose how to swap box coordinates of the 0-th input of the generated DetectionOutput layer.
Refer to the code for more details.
"""
replacement_id = 'ObjectDetectionAPIDetectionOutputReplacement'
run_not_recursively = True
def run_before(self):
return [ObjectDetectionAPIMaskRCNNROIPoolingSecondReplacement]
def run_after(self):
return [ObjectDetectionAPIProposalReplacement]
def nodes_to_remove(self, graph: Graph, match: SubgraphMatch):
new_nodes_to_remove = match.matched_nodes_names().copy()
outputs = ['detection_boxes', 'detection_scores', 'num_detections']
for output in outputs:
if output in graph.nodes:
children = Node(graph, output).out_nodes()
if len(children) != 1:
log.warning('Output {} has {} children. It should have only one output: with op==`Result`'
''.format(output, len(children)))
elif children[list(children.keys())[0]].op == 'Result':
new_nodes_to_remove.append(children[list(children.keys())[0]].id)
new_nodes_to_remove.extend(outputs)
return new_nodes_to_remove
def output_edges_match(self, graph: Graph, match: SubgraphMatch, new_sub_graph: dict):
# the DetectionOutput in IE produces single tensor, but in TF it produces four tensors, so we need to create
# only one output edge match
if match.outputs_count() >= 1:
return {match.output_node(0)[0].id: new_sub_graph['detection_output_node'].id}
else:
return {list(graph.graph.get("packed_outputs").keys())[0]: new_sub_graph['detection_output_node'].id}
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
custom_attributes = match.custom_replacement_desc.custom_attributes
num_classes = _value_or_raise(match, pipeline_config, 'num_classes')
max_proposals = _value_or_raise(match, pipeline_config, 'first_stage_max_proposals')
activation_function = _value_or_raise(match, pipeline_config, 'postprocessing_score_converter')
activation_conf_node = add_activation_function_after_node(graph, match.single_input_node(1)[0].in_node(0),
activation_function)
# IE DetectionOutput layer consumes flattened tensors so need add a Reshape layer.
# The batch value of the input tensor is not equal to the batch of the topology, so it is not possible to use
# "0" value in the Reshape layer attribute to refer to the batch size, but we know how to
# calculate the second dimension so the batch value will be deduced from it with help of "-1".
reshape_conf_node = create_op_node_with_second_input(graph, Reshape,
int64_array([-1, (num_classes + 1) * max_proposals]),
dict(name='do_reshape_conf'), activation_conf_node)
mark_as_correct_data_layout(reshape_conf_node)
# Workaround for TransposeForReshape pass.
# We looking for first not Reshape-typed node before match.single_input_node(0)[0].in_node(0).
# And add reshape_loc node after this first not Reshape-typed node.
current_node = skip_nodes_by_condition(match.single_input_node(0)[0].in_node(0),
lambda x: x['kind'] == 'op' and x.has_and_set('reinterp_shape'))
share_box_across_classes = _value_or_raise(match, pipeline_config, 'share_box_across_classes')
background_label_id = int(custom_attributes.get('background_label_id', 0))
if share_box_across_classes:
reshape_loc_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 1, 1, 4]),
dict(name='reshape_loc'), current_node)
else:
reshape_loc_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, num_classes, 1, 4]),
dict(name='reshape_loc'), current_node)
mark_as_correct_data_layout(reshape_loc_node)
# constant node with variances
variances_const_op = Const(graph, dict(value=_variance_from_pipeline_config(pipeline_config)))
variances_const_node = variances_const_op.create_node([])
if share_box_across_classes:
loc_node = reshape_loc_node
else:
# TF produces locations tensor without boxes for background.
# Inference Engine DetectionOutput layer requires background boxes so we generate them
loc_node = add_fake_background_loc(graph, reshape_loc_node)
PermuteAttrs.set_permutation(reshape_loc_node, loc_node, None)
# reshape locations tensor to 2D so it could be passed to Eltwise which will be converted to ScaleShift
reshape_loc_2d_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 4]),
dict(name='reshape_locs_2d'), loc_node)
mark_as_correct_data_layout(reshape_loc_2d_node)
# element-wise multiply locations with variances
eltwise_locs_op = Mul(graph, dict())
eltwise_locs_node = eltwise_locs_op.create_node([reshape_loc_2d_node, variances_const_node],
dict(name='scale_locs'))
# IE DetectionOutput layer consumes flattened tensors so need add a Reshape layer.
# The batch value of the input tensor is not equal to the batch of the topology, so it is not possible to use
# "0" value in the Reshape layer attribute to refer to the batch size, but we know how to
# calculate the second dimension so the batch value will be deduced from it with help of "-1".
reshape_loc_do_op = Reshape(graph, dict(name='do_reshape_locs'))
coordinates_swap_method = 'add_convolution'
if 'coordinates_swap_method' not in custom_attributes:
log.error('The ObjectDetectionAPIDetectionOutputReplacement sub-graph replacement configuration file '
'must contain "coordinates_swap_method" in the "custom_attributes" dictionary. Two values are '
'supported: "swap_weights" and "add_convolution". The first one should be used when there is '
'a MatMul or Conv2D node before the "SecondStagePostprocessor" block in the topology. With this '
'solution the weights of the MatMul or Conv2D nodes are permutted, simulating the swap of XY '
'coordinates in the tensor. The second could be used in any other cases but it is worse in terms '
'of performance because it adds the Conv2D node which performs permutting of data. Since the '
'attribute is not defined the second approach is used by default.')
else:
coordinates_swap_method = custom_attributes['coordinates_swap_method']
supported_swap_methods = ['swap_weights', 'add_convolution']
if coordinates_swap_method not in supported_swap_methods:
raise Error('Unsupported "coordinates_swap_method" defined in the sub-graph replacement configuration '
'file. Supported methods are: {}'.format(', '.join(supported_swap_methods)))
if coordinates_swap_method == 'add_convolution':
swapped_locs_node = add_convolution_to_swap_xy_coordinates(graph, eltwise_locs_node, 4)
reshape_loc_do_node = reshape_loc_do_op.create_node([swapped_locs_node])
else:
reshape_loc_do_node = reshape_loc_do_op.create_node([eltwise_locs_node])
if share_box_across_classes:
reshape_loc_do_dims = Const(graph, {'value': int64_array([-1, max_proposals * 4]),
'name': reshape_loc_do_node.name + '/Dim'}).create_node()
else:
reshape_loc_do_dims = Const(graph, {'value': int64_array([-1, (num_classes + 1) * max_proposals * 4]),
'name': reshape_loc_do_node.name + '/Dim'}).create_node()
reshape_loc_do_dims.out_port(0).connect(reshape_loc_do_node.in_port(1))
mark_as_correct_data_layout(reshape_loc_do_node)
# find Proposal output which has the data layout as in TF: YXYX coordinates without batch indices.
proposal_nodes_ids = [node_id for node_id, attrs in graph.nodes(data=True)
if 'name' in attrs and attrs['name'] == 'crop_proposals']
if len(proposal_nodes_ids) != 1:
raise Error("Found the following nodes '{}' with name 'crop_proposals' but there should be exactly 1. "
"Looks like ObjectDetectionAPIProposalReplacement replacement didn't work.".
format(proposal_nodes_ids))
proposal_node = Node(graph, proposal_nodes_ids[0])
# check whether it is necessary to permute proposals coordinates before passing them to the DetectionOutput
# currently this parameter is set for the RFCN topologies
if 'swap_proposals' in custom_attributes and custom_attributes['swap_proposals']:
proposal_node = add_convolution_to_swap_xy_coordinates(graph, proposal_node, 4)
# reshape priors boxes as Detection Output expects
reshape_priors_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 1, max_proposals * 4]),
dict(name='DetectionOutput_reshape_priors_'),
proposal_node)
mark_as_correct_data_layout(reshape_priors_node)
detection_output_op = DetectionOutput(graph, {})
for key in ('clip_before_nms', 'clip_after_nms'):
if key in match.custom_replacement_desc.custom_attributes:
detection_output_op.attrs[key] = int(match.custom_replacement_desc.custom_attributes[key])
detection_output_node = detection_output_op.create_node(
[reshape_loc_do_node, reshape_conf_node, reshape_priors_node],
dict(name=detection_output_op.attrs['type'],
share_location=int(share_box_across_classes),
variance_encoded_in_target=1,
background_label_id=background_label_id,
code_type='caffe.PriorBoxParameter.CENTER_SIZE', pad_mode='caffe.ResizeParameter.CONSTANT',
resize_mode='caffe.ResizeParameter.WARP',
num_classes=num_classes,
confidence_threshold=_value_or_raise(match, pipeline_config, 'postprocessing_score_threshold'),
top_k=_value_or_raise(match, pipeline_config, 'postprocessing_max_detections_per_class'),
keep_top_k=_value_or_raise(match, pipeline_config, 'postprocessing_max_total_detections'),
nms_threshold=_value_or_raise(match, pipeline_config, 'postprocessing_iou_threshold')))
# sets specific name to the node so we can find it in other replacers
detection_output_node.name = 'detection_output'
if coordinates_swap_method == 'swap_weights':
swap_weights_xy(graph, backward_bfs_for_operation(detection_output_node.in_node(0), ['MatMul', 'Conv2D'],
['ShapeOf']))
# when the use_matmul_crop_and_resize = True then the prior boxes were not swapped and we need to swap them from
# YXYX to XYXY before passing to the DetectionOutput operation
if pipeline_config.get_param('use_matmul_crop_and_resize'):
insert_weights_swap_xy_sub_graph(graph, detection_output_node.in_port(2).get_connection())
output_op = Result(graph, dict(name='do_OutputOp'))
output_op.create_node([detection_output_node])
print('The graph output nodes have been replaced with a single layer of type "DetectionOutput". Refer to the '
'operation set specification documentation for more information about the operation.')
return {'detection_output_node': detection_output_node}
class ObjectDetectionAPIMaskRCNNROIPoolingSecondReplacement(FrontReplacementFromConfigFileSubGraph):
replacement_id = 'ObjectDetectionAPIMaskRCNNROIPoolingSecondReplacement'
run_not_recursively = True
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def run_after(self):
return [ObjectDetectionAPIProposalReplacement]
def output_edges_match(self, graph: Graph, match: SubgraphMatch, new_sub_graph: dict):
return {match.output_node(0)[0].id: new_sub_graph['roi_pooling_node'].id}
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
roi_pool_size = _value_or_raise(match, pipeline_config, 'initial_crop_size')
detection_output_nodes_ids = [node_id for node_id, attrs in graph.nodes(data=True)
if 'name' in attrs and attrs['name'] == 'detection_output']
if len(detection_output_nodes_ids) != 1:
raise Error("Found the following nodes '{}' with 'detection_output' but there should be exactly 1.".
format(detection_output_nodes_ids))
detection_output_node = Node(graph, detection_output_nodes_ids[0])
output_nodes = [port.node for port in detection_output_node.out_port(0).get_destinations() if port.node.soft_get('type') == 'Result']
if len(output_nodes) == 1:
graph.remove_node(output_nodes[0].id)
# add reshape of Detection Output so it can be an output of the topology
reshape_detection_output_2d_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 7]),
dict(name='reshape_do_2d'),
detection_output_node)
mark_as_correct_data_layout(reshape_detection_output_2d_node)
# adds special node of type "Output" that is a marker for the output nodes of the topology
output_op = Result(graph, dict(name='do_reshaped_OutputOp'))
output_node = output_op.create_node([reshape_detection_output_2d_node])
# add attribute 'output_sort_order' so it will be used as a key to sort output nodes before generation of IR
output_node.in_edge()['data_attrs'].append('output_sort_order')
output_node.in_edge()['output_sort_order'] = [('detection_boxes', 0)]
# creates two Crop operations which get input from the DetectionOutput layer, cuts of slices of data with class
# ids and probabilities and produce a tensor with batch ids and bounding boxes only (as it is expected by the
# ROIPooling layer)
crop_batch_op = Crop(graph, dict(axis=int64_array([3]), offset=int64_array([0]), dim=int64_array([1]),
nchw_layout=True))
crop_batch_node = crop_batch_op.create_node([detection_output_node], dict(name='crop_do_batch_ids'))
crop_coordinates_op = Crop(graph, dict(axis=int64_array([3]), offset=int64_array([3]), dim=int64_array([4]),
nchw_layout=True))
crop_coordinates_node = crop_coordinates_op.create_node([detection_output_node], dict(name='crop_do_coords'))
concat_op = Concat(graph, dict(axis=3))
concat_node = concat_op.create_node([crop_batch_node, crop_coordinates_node], dict(name='batch_and_coords',
nchw_layout=True))
# reshape bounding boxes as required by ROIPooling
reshape_do_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 5]),
dict(name='reshape_do'), concat_node)
mark_as_correct_data_layout(reshape_do_node)
roi_pooling_op = ROIPooling(graph, dict(method="bilinear", spatial_scale=1, pooled_h=roi_pool_size,
pooled_w=roi_pool_size))
roi_pooling_node = roi_pooling_op.create_node([match.single_input_node(0)[0].in_node(), reshape_do_node],
dict(name='ROI_pooling_2'))
return {'roi_pooling_node': roi_pooling_node}
class ObjectDetectionAPIMaskRCNNSigmoidReplacement(FrontReplacementFromConfigFileGeneral):
"""
This replacer is used to convert Mask R-CNN topologies only.
Adds activation with sigmoid function to the end of the network producing masks tensors.
"""
replacement_id = 'ObjectDetectionAPIMaskRCNNSigmoidReplacement'
run_not_recursively = True
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def run_after(self):
return [ObjectDetectionAPIMaskRCNNROIPoolingSecondReplacement]
def transform_graph(self, graph: Graph, replacement_descriptions):
masks_node_prefix_name = replacement_descriptions.get('masks_node_prefix_name', 'SecondStageBoxPredictor')
op_outputs = graph.get_op_nodes(op='Result')
for op_output in op_outputs:
last_node = op_output.in_port(0).get_source().node
if last_node.name.startswith(masks_node_prefix_name):
sigmoid_node = Sigmoid(graph, dict(name='masks')).create_node()
op_output.in_port(0).get_connection().insert_node(sigmoid_node)
print('The predicted masks are produced by the "masks" layer for each bounding box generated with a '
'"detection_output" operation.\n Refer to operation specification in the documentation for information '
'about the DetectionOutput operation output data interpretation.\n'
'The model can be inferred using the dedicated demo "mask_rcnn_demo" from the OpenVINO Open Model Zoo.')
class ObjectDetectionAPIProposalReplacement(FrontReplacementFromConfigFileSubGraph):
"""
This class replaces sub-graph of operations with Proposal layer and additional layers transforming
tensors from layout of TensorFlow to layout required by Inference Engine.
Refer to comments inside the function for more information about performed actions.
"""
replacement_id = 'ObjectDetectionAPIProposalReplacement'
run_not_recursively = True
matched_input_nodes_to_keep = 2 # number of matched input nodes to keep
def run_after(self):
return [ObjectDetectionAPIPreprocessorReplacement, ObjectDetectionAPIPreprocessor2Replacement]
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def output_edges_match(self, graph: Graph, match: SubgraphMatch, new_sub_graph: dict):
return {match.output_node(0)[0].id: new_sub_graph['proposal_node'].id}
def nodes_to_remove(self, graph: Graph, match: SubgraphMatch):
new_list = match.matched_nodes_names().copy()
# do not remove nodes that produce box predictions and class predictions and optionally generated anchors
for port in range(self.matched_input_nodes_to_keep):
new_list.remove(match.single_input_node(port)[0].id)
return new_list
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
# the transformation configuration file specifies what operations should be included with this transformation
if match.custom_replacement_desc.custom_attributes.get('operation_to_add', 'Proposal') == 'DetectionOutput':
self.matched_input_nodes_to_keep = 3 # keep the third input with prior boxes (anchors)
return self.insert_detection_output_instead_of_proposal(graph, match, pipeline_config)
max_proposals = _value_or_raise(match, pipeline_config, 'first_stage_max_proposals')
proposal_ratios = _value_or_raise(match, pipeline_config, 'anchor_generator_aspect_ratios')
proposal_scales = _value_or_raise(match, pipeline_config, 'anchor_generator_scales')
anchors_count = len(proposal_ratios) * len(proposal_scales)
# Convolution/matmul node that produces classes predictions
# Transpose result of the tensor with classes permissions so it will be in a correct layout for Softmax
predictions_node = backward_bfs_for_operation(match.single_input_node(1)[0], ['Add'])[0]
reshape_classes_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, anchors_count, 2, -1]),
dict(name='predictions/Reshape'))
predictions_node.insert_node_after(reshape_classes_node, 0)
mark_as_correct_data_layout(reshape_classes_node)
softmax_conf_op = Softmax(graph, dict(axis=2, nchw_layout=True, name=reshape_classes_node.id + '/Softmax'))
softmax_conf_node = softmax_conf_op.create_node([reshape_classes_node])
order_const = Const(graph, dict(value=int64_array([0, 2, 1, 3]),
name=softmax_conf_node.name + '/TransposeOrder')).create_node()
permute_reshape_softmax_op = Transpose(graph, dict())
permute_reshape_softmax_node = permute_reshape_softmax_op.create_node([softmax_conf_node, order_const], dict(
name=softmax_conf_node.name + '/Transpose'))
mark_input_as_in_correct_layout(permute_reshape_softmax_node, 1)
mark_output_as_in_correct_layout(permute_reshape_softmax_node, 0)
initial_shape_op = Shape(graph, dict(name=predictions_node.id + '/Shape'))
initial_shape_node = initial_shape_op.create_node([predictions_node])
reshape_permute_op = Reshape(graph, dict(name='Reshape_Transpose_Class'))
reshape_permute_node = reshape_permute_op.create_node([permute_reshape_softmax_node, initial_shape_node])
mark_input_as_in_correct_layout(reshape_permute_node, 0)
mark_output_as_in_correct_layout(reshape_permute_node, 0)
variance_height = pipeline_config.get_param('frcnn_variance_height')
variance_width = pipeline_config.get_param('frcnn_variance_width')
variance_x = pipeline_config.get_param('frcnn_variance_x')
variance_y = pipeline_config.get_param('frcnn_variance_y')
anchor_generator_height_stride = pipeline_config.get_param('anchor_generator_height_stride')
anchor_generator_width_stride = pipeline_config.get_param('anchor_generator_width_stride')
anchor_generator_height = pipeline_config.get_param('anchor_generator_height')
anchor_generator_width = pipeline_config.get_param('anchor_generator_width')
if variance_height != variance_width:
log.error('The values for variance for height "{}" is not equal to variance for width "{}". The detection '
'results will be inaccurate.'.format(variance_height, variance_width))
if variance_x != variance_y:
log.error('The values for variance for x "{}" is not equal to variance for y "{}". The detection '
'results will be inaccurate.'.format(variance_x, variance_y))
if anchor_generator_height_stride != anchor_generator_width_stride:
log.error('The values for the anchor generator height stride "{}" is not equal to the anchor generator '
'width stride "{}". The detection results will be inaccurate.'.format(
anchor_generator_height_stride, anchor_generator_width_stride))
if anchor_generator_height != anchor_generator_width:
log.error('The values for the anchor generator height "{}" is not equal to the anchor generator width '
'stride "{}". The detection results will be inaccurate.'.format(anchor_generator_height,
anchor_generator_width))
proposal_op = ProposalOp(graph, dict(min_size=1,
framework='tensorflow',
pre_nms_topn=2 ** 31 - 1,
box_size_scale=variance_height,
box_coordinate_scale=variance_x,
post_nms_topn=max_proposals,
feat_stride=anchor_generator_height_stride,
ratio=proposal_ratios,
scale=proposal_scales,
normalize=1,
base_size=anchor_generator_height,
nms_thresh=_value_or_raise(match, pipeline_config,
'first_stage_nms_iou_threshold')))
for key in ('clip_before_nms', 'clip_after_nms'):
if key in match.custom_replacement_desc.custom_attributes:
proposal_op.attrs[key] = int(match.custom_replacement_desc.custom_attributes[key])
anchors_node = backward_bfs_for_operation(match.single_input_node(0)[0], ['Add'])[0]
# creates input to store input image height, width and scales (usually 1.0s)
# the batch size for this input is fixed because it is allowed to pass images of the same size only as input
input_op_with_image_size = Parameter(graph, dict(shape=int64_array([1, 3]), fixed_batch=True))
input_with_image_size_node = input_op_with_image_size.create_node([], dict(name='image_info'))
proposal_node = proposal_op.create_node([reshape_permute_node, anchors_node, input_with_image_size_node],
dict(name='proposals'))
# models with use_matmul_crop_and_resize = True should not swap order of elements (YX to XY) after the Proposal
swap_proposals = not match.custom_replacement_desc.custom_attributes.get('do_not_swap_proposals', False) and \
not pipeline_config.get_param('use_matmul_crop_and_resize')
if swap_proposals:
proposal_node = add_convolution_to_swap_xy_coordinates(graph, proposal_node, 5)
return {'proposal_node': ObjectDetectionAPIProposalReplacement.ie_to_tf_proposals(graph, proposal_node, match,
max_proposals,
swap_proposals)}
@staticmethod
def insert_detection_output_instead_of_proposal(graph: Graph, match: SubgraphMatch,
pipeline_config: PipelineConfig):
"""
The function inserts DetectionOutput operation instead of Proposal operation which may result in an increase of
the accuracy for some models. The function is enabled with the custom attribute "operation_to_insert" with
value "DetectionOutput" in the transformation configuration file section for the
"ObjectDetectionAPIProposalReplacement" transformation. However, this transformation should not be applied in
case when an input image should be scaled before feeding the IR because the DetectionOutput operation does not
have information about the original input image size, whilst the Proposal operation has an input which contains
this information.
:param graph: the graph to operate on
:param match: the object containing information about the matched sub-graph
:param pipeline_config: object containing information from the pipeline.config file of the model
:return: the dictionary with mapping information needed for other transformations
"""
max_proposals = _value_or_raise(match, pipeline_config, 'first_stage_max_proposals')
# Convolution/matmul node that produces classes predictions
# Transpose result of the tensor with classes permissions so it will be in a correct layout for Softmax
predictions_nodes = backward_bfs_for_operation(match.single_input_node(1)[0], ['Add'])
assert len(predictions_nodes) >= 1, 'Expected to find nodes of type "Add" starting from the node "{}" in ' \
'backward direction'.format(match.single_input_node(1)[0].id)
predictions_node = predictions_nodes[0]
# prepare input with class probabilities. The DetectionOutput operation which will consume this tensor as a
# second input expects probabilities to be normalized with SoftMax operation per each bounding box class. In
# order to do this we first reshape the tensor so the last dimension contain probability for 2 classes
# (background and foreground) for each bounding box. Before feeding this tensor to the DO operation the tensor
# is flattened to the shape [num_batches, num_classes * num_bounding_boxes]
reshape_classes_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, -1, 2]),
dict(name='predictions/Reshape'))
# transpose from NCHW to NHWC will be inserted as input to the Reshape automatically. This is expected
predictions_node.out_port(0).disconnect()
predictions_node.out_port(0).connect(reshape_classes_node.in_port(0))
softmax_conf_node = Softmax(graph, dict(axis=2, name=reshape_classes_node.id + '/Softmax')).create_node([
reshape_classes_node])
flattened_conf = create_op_node_with_second_input(graph, Reshape, int64_array([0, -1]),
dict(name=softmax_conf_node.name + '/Flatten'),
softmax_conf_node)
# prepare input with box logits
boxes_logit = backward_bfs_for_operation(match.single_input_node(0)[0], ['Add'])[0]
reshape_box_logits = create_op_node_with_second_input(
graph, Reshape, int64_array([0, -1]), dict(name=boxes_logit.soft_get('name', boxes_logit.id) + '/Flatten'),
boxes_logit)
yxyx_anchors = match.single_input_node(2)[0]
variance_height = pipeline_config.get_param('frcnn_variance_height')
variance_width = pipeline_config.get_param('frcnn_variance_width')
variance_x = pipeline_config.get_param('frcnn_variance_x')
variance_y = pipeline_config.get_param('frcnn_variance_y')
# get the input image height and width to divide the anchors values by it
initial_input_node_name = 'input_tensor' if 'input_tensor' in graph.nodes else 'image_tensor'
if initial_input_node_name not in graph.nodes():
raise Error('Input node "{}" of the graph is not found. Do not run the Model Optimizer with '
'"--input" command line parameter.'.format(initial_input_node_name))
parameter_node = Node(graph, initial_input_node_name)
input_shape = Shape(graph, {'name': parameter_node.name}).create_node([parameter_node])
input_image_hw = node_to_get_shape_value_of_indices(input_shape, [1, 2]) # NHWC layout
hwhw = create_op_with_const_inputs(graph, Tile, {1: int64_array([2])}, {'name': 'image_hwhw'}, input_image_hw)
hwhw_float = Cast(graph,
{'dst_type': data_type_str_to_np(graph.graph['cmd_params'].data_type)}).create_node([hwhw])
scaled_anchors = Div(graph, {'name': 'scaled_anchors'}).create_node([yxyx_anchors, hwhw_float])
flattened_anchors = create_op_with_const_inputs(graph, Reshape, {1: int64_array([1, 1, -1])},
{'name': 'flattened_anchors'}, scaled_anchors)
cropped_anchors = AttributedClamp(graph, {'min': 0.0, 'max': 1.0, 'name': 'clamped_xyxy',
'nchw_layout': True}).create_node([flattened_anchors])
# the input tensor "scaled_anchors" for the "flattened_anchors" may be 4D. In order to avoid inserting Transpose
# operation mark the "flattened_anchors" with the correct data layout
mark_as_correct_data_layout(flattened_anchors)
# create tensor of shape [4] with variance values which then are tiled by the number of boxes which is obtained
# from the 'yxyx_anchors' node
variances = Const(graph, {'value': np.float32([1.0 / variance_x, 1.0 / variance_y, 1.0 / variance_width,
1.0 / variance_height])}).create_node()
anchors_shape = Shape(graph, {'name': 'anchors_shape'}).create_node([yxyx_anchors])
anchors_count = node_to_get_shape_value_of_indices(anchors_shape, [0])
tiled_variances = Tile(graph, {'name': 'tiled_variances'}).create_node([variances, anchors_count])
reshaped_tiled_variances = create_op_with_const_inputs(graph, Reshape, {1: int64_array([1, 1, -1])},
{'name': 'flattened_variances'}, tiled_variances)
# now we can merge actual anchors coordinates with a tensor with variances as it is expected by the
# DetectionOutput operation
duplicate_anchors = Concat(graph, {'axis': 1, 'name': 'anchors_with_variances'}).create_node(
[cropped_anchors, reshaped_tiled_variances])
do = DetectionOutput(graph,
{'background_label_id': 0,
'clip_after_nms': True,
'clip_before_nms': False,
'code_type': 'caffe.PriorBoxParameter.CENTER_SIZE',
'confidence_threshold': 0.0,
'decrease_label_id': False,
'input_height': 1,
'input_width': 1,
'keep_top_k': max_proposals,
'normalized': True,
'num_classes': 2,
'objectness_score': 0,
'share_location': True,
'top_k': 6000,
'variance_encoded_in_target': False,
'nms_threshold': _value_or_raise(match, pipeline_config, 'first_stage_nms_iou_threshold'),
'name': 'first_do',
}).create_node([reshape_box_logits, flattened_conf, duplicate_anchors])
# DetectionOutput output tensor has YXYX box coordinates order
# switch to 3D to avoid issues that part of the model with 4D shapes should be inferred in NCHW layout
do_3d = create_op_with_const_inputs(graph, Squeeze, {1: int64_array(0)}, {'name': do.name + '/SqueezeDO'}, do)
mark_as_correct_data_layout(do_3d)
# DetectionOutput output tensor produces a tensor of tuples with the following 7 elements:
# [batch_id, class_id, confidence, x1, y1, x2, y2]. Here we split the DetectionOutput result into the 7
# tensors with each of these elements for predictions. Then we crop predicted box coordinates (scaled) to be
# within [0, 1] range (as it is predicted in the TF model) and then combine tensors back to the Proposal
# operation output format: [batch_id, x1, y1, x2, y2].
do_split = create_op_node_with_second_input(graph, Split, int64_array(2), {'num_splits': 7, 'nchw_layout': True,
'name': do.name + '/Split'}, do_3d)
xyxy_coord = Concat(graph, {'axis': -1, 'nchw_layout': True, 'in_ports_count': 4,
'name': do_split.name + '/xyxy'}).create_node()
# change output from YXYX to XYXY order
do_split.out_port(3).connect(xyxy_coord.in_port(1))
do_split.out_port(4).connect(xyxy_coord.in_port(0))
do_split.out_port(5).connect(xyxy_coord.in_port(3))
do_split.out_port(6).connect(xyxy_coord.in_port(2))
clamped_xyxy_coord = AttributedClamp(graph, {'min': 0.0, 'max': 1.0, 'name': 'clamped_xyxy',
'nchw_layout': True}).create_node([xyxy_coord])
# prepare final proposal boxes [batch_id, x1, y1, x2, y2]
proposal_node = Concat(graph, {'axis': -1, 'nchw_layout': True, 'in_ports_count': 2,
'name': 'proposals'}).create_node()
do_split.out_port(0).connect(proposal_node.in_port(0))
clamped_xyxy_coord.out_port(0).connect(proposal_node.in_port(1))
return {'proposal_node': ObjectDetectionAPIProposalReplacement.ie_to_tf_proposals(graph, proposal_node, match,
max_proposals, True)}
@staticmethod
def ie_to_tf_proposals(graph: Graph, proposal_node: Node, match: SubgraphMatch, max_proposals: int,
swap_proposals: bool = False):
"""
Builds a graph which converts the proposals data in IE format to the format of TensorFlow. This includes
swapping of XYXY to YXYX (if needed), and cropping the IE output of format [batch, x1, y1, x2, y2] to simply
[x1, y1, x2, y2] and reshaping tensor to an appropriate shape.
:param graph: the graph to operate on
:param proposal_node: the node producing IE proposals
:param match: the object containing information about matched sub-graph
:param max_proposals: maximum number of proposal boxes. Needed for the reshaping of the tensor
:param swap_proposals: flag to force swapping proposals for CropAndResize op
:return: the node producing output in the TF format.
"""
proposal_reshape_2d_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 5]),
dict(name="reshape_swap_proposals_2d"),
proposal_node)
mark_input_as_in_correct_layout(proposal_reshape_2d_node, 0)
crop_and_resize_nodes_ids = [node_id for node_id in bfs_search(graph, [match.single_input_node(0)[0].id]) if
graph.node[node_id]['op'] == 'CropAndResize']
if len(crop_and_resize_nodes_ids) != 0 and swap_proposals:
# feed the CropAndResize node with a correct boxes information produced with the Proposal layer
# find the first CropAndResize node in the BFS order. This is needed in the case when we already swapped
# box coordinates data after the Proposal node
crop_and_resize_node = Node(graph, crop_and_resize_nodes_ids[0])
# set a marker that an input with box coordinates has been pre-processed so the CropAndResizeReplacement
# transform doesn't try to merge the second and the third inputs
crop_and_resize_node['inputs_preprocessed'] = True
crop_and_resize_node.in_port(1).disconnect()
proposal_reshape_2d_node.out_port(0).connect(crop_and_resize_node.in_port(1))
tf_proposal_reshape_4d_node = create_op_node_with_second_input(graph, Reshape,
int64_array([-1, 1, max_proposals, 5]),
dict(name="reshape_proposal_4d"),
proposal_node)
crop_op = Crop(graph, dict(axis=int64_array([3]), offset=int64_array([1]), dim=int64_array([4]),
nchw_layout=True))
crop_node = crop_op.create_node([tf_proposal_reshape_4d_node], dict(name='crop_proposals'))
mark_as_correct_data_layout(tf_proposal_reshape_4d_node)
tf_proposals_crop_reshape_3d_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, -1, 4]),
dict(name="reshape_crop_3d"), crop_node)
mark_input_as_in_correct_layout(tf_proposals_crop_reshape_3d_node, 0)
return tf_proposals_crop_reshape_3d_node
class ObjectDetectionAPISSDPostprocessorReplacement(FrontReplacementFromConfigFileSubGraph):
replacement_id = 'ObjectDetectionAPISSDPostprocessorReplacement'
run_not_recursively = True
def run_after(self):
return [ObjectDetectionAPIPreprocessorReplacement, ObjectDetectionAPIPreprocessor2Replacement]
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def output_edges_match(self, graph: Graph, match: SubgraphMatch, new_sub_graph: dict):
# the DetectionOutput in IE produces single tensor, but in TF it produces two tensors, so create only one output
# edge match
return {match.output_node(0)[0].id: new_sub_graph['detection_output_node'].id}
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
has_background_class = _value_or_raise(match, pipeline_config, 'add_background_class')
num_classes = _value_or_raise(match, pipeline_config, 'num_classes') + has_background_class
# reshapes confidences to 4D before applying activation function and do not convert from NHWC to NCHW this node
expand_dims_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, 1, -1, num_classes]),
{'name': 'do_ExpandDims_conf'})
expand_dims_node.in_port(0).connect(match.input_nodes(1)[0][0].in_node(0).out_port(0))
mark_as_correct_data_layout(expand_dims_node)
activation_function = _value_or_raise(match, pipeline_config, 'postprocessing_score_converter')
activation_conf_node = add_activation_function_after_node(graph, expand_dims_node, activation_function)
# IE DetectionOutput layer consumes flattened tensors
# reshape operation to flatten locations tensor
reshape_loc_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, -1]),
{'name': 'do_reshape_loc'})
current_node = skip_nodes_by_condition(match.input_nodes(0)[0][0].in_node(0),
lambda x: x.op == 'Identity' or x.has_and_set('reinterp_shape'))
reshape_loc_node.in_port(0).connect(current_node.out_port(0))
mark_as_correct_data_layout(reshape_loc_node)
# IE DetectionOutput layer consumes flattened tensors
# reshape operation to flatten confidence tensor
reshape_conf_node = create_op_node_with_second_input(graph, Reshape, int64_array([0, -1]),
{'name': 'do_reshape_conf'}, activation_conf_node)
mark_as_correct_data_layout(reshape_conf_node)
custom_attributes = match.custom_replacement_desc.custom_attributes
if ('disable_prior_boxes_layers_generator' not in custom_attributes or
not custom_attributes['disable_prior_boxes_layers_generator']) and \
(pipeline_config.get_param('ssd_anchor_generator_num_layers') is not None or
pipeline_config.get_param('multiscale_anchor_generator_min_level') is not None):
# change the Reshape operations with hardcoded number of output elements of the convolution nodes to be
# reshape-able
_relax_reshape_nodes(graph, pipeline_config)
# create PriorBoxClustered nodes instead of a constant value with prior boxes so the model could be reshaped
if pipeline_config.get_param('ssd_anchor_generator_num_layers') is not None:
priors_node = _create_prior_boxes_node(graph, pipeline_config)
elif pipeline_config.get_param('multiscale_anchor_generator_min_level') is not None:
priors_node = _create_multiscale_prior_boxes_node(graph, pipeline_config)
else:
log.info('The anchor generator is not known. Save constant with prior-boxes to IR.')
priors_node = match.input_nodes(2)[0][0].in_node(0)
# creates DetectionOutput Node object from Op class
detection_output_op = DetectionOutput(graph, match.custom_replacement_desc.custom_attributes)
for key in ('clip_before_nms', 'clip_after_nms'):
if key in match.custom_replacement_desc.custom_attributes:
detection_output_op.attrs[key] = int(match.custom_replacement_desc.custom_attributes[key])
detection_output_op.attrs['old_infer'] = detection_output_op.attrs['infer']
detection_output_op.attrs['infer'] = __class__.do_infer
detection_output_node = detection_output_op.create_node(
[reshape_loc_node, reshape_conf_node, priors_node],
dict(name=detection_output_op.attrs['type'],
confidence_threshold=_value_or_raise(match, pipeline_config, 'postprocessing_score_threshold'),
top_k=_value_or_raise(match, pipeline_config, 'postprocessing_max_detections_per_class'),
keep_top_k=_value_or_raise(match, pipeline_config, 'postprocessing_max_total_detections'),
nms_threshold=_value_or_raise(match, pipeline_config, 'postprocessing_iou_threshold')))
# compared to the IE's DetectionOutput, the TF keeps the locations in YXYX, need to get back to the XYXY
# for last convolutions that operate the locations need to swap the X and Y for output feature weights & biases
conv_nodes = backward_bfs_for_operation(detection_output_node.in_node(0), ['Conv2D'], ['ShapeOf'])
swap_weights_xy(graph, conv_nodes)
# As outputs are replaced with a postprocessing node, outgoing tensor names are no longer
# correspond to original tensors and should be removed from output->Result edges
out_nodes = []
for out in range(match.outputs_count()):
out_nodes.append(match.output_node(out)[0])
clear_tensor_names_info(out_nodes)
return {'detection_output_node': detection_output_node}
@staticmethod
def do_infer(node: Node):
graph = node.graph
prior_boxes = node.in_node(2).value
if prior_boxes is not None:
argv = node.graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
variance = _variance_from_pipeline_config(pipeline_config)
# replicating the variance values for all prior-boxes
variances = np.tile(variance, [prior_boxes.shape[-2], 1])
# DetectionOutput Inference Engine expects the prior-boxes in the following layout: (values, variances)
prior_boxes = prior_boxes.reshape([-1, 4])
prior_boxes = np.concatenate((prior_boxes, variances), 0)
# compared to the IE's DetectionOutput, the TF keeps the prior-boxes in YXYX, need to get back to the XYXY
prior_boxes = np.concatenate((prior_boxes[:, 1:2], prior_boxes[:, 0:1],
prior_boxes[:, 3:4], prior_boxes[:, 2:3]), 1)
# adding another dimensions, as the prior-boxes are expected as 3d tensors
prior_boxes = prior_boxes.reshape((1, 2, -1))
node.in_node(2).shape = int64_array(prior_boxes.shape)
node.in_node(2).value = prior_boxes
# create Const node with an updated prior boxes values. Cannot use Port/Connection API here because we are
# in the middle of the partial inference phase and graph is in the intermediate step
graph.remove_edge(node.in_node(2).in_node(0).id, node.in_node(2).id)
const = Const(graph, {'name': 'prior_boxes', 'executable': True, 'value': prior_boxes}).create_node()
graph.create_edge(const, node.in_node(2))
node.old_infer(node)
conv_nodes = backward_bfs_for_operation(node.in_node(0), ['Conv2D'], ['ShapeOf'])
mark_squeeze_reshape_concat_before_detection_output(conv_nodes)
class ObjectDetectionAPIOutputReplacement(FrontReplacementFromConfigFileGeneral):
"""
This replacer is used to cut-off the network by specified nodes for models generated with Object Detection API.
The custom attribute for the replacer contains one value for key "outputs". This string is a comma separated list
of outputs alternatives. Each output alternative is a '|' separated list of node name which could be outputs. The
first node from each alternative that exits in the graph is chosen. Others are ignored.
For example, if the "outputs" is equal to the following string:
"Reshape_16,SecondStageBoxPredictor_1/Conv_3/BiasAdd|SecondStageBoxPredictor_1/Conv_1/BiasAdd"
then the "Reshape_16" will be an output if it exists in the graph. The second output will be
SecondStageBoxPredictor_1/Conv_3/BiasAdd if it exist in the graph, if not then
SecondStageBoxPredictor_1/Conv_1/BiasAdd will be output if it exists in the graph.
"""
replacement_id = 'ObjectDetectionAPIOutputReplacement'
run_not_recursively = True
def run_after(self):
return [ObjectDetectionAPITransformationsStart]
def run_before(self):
return [ObjectDetectionAPIPreprocessorReplacement, ObjectDetectionAPIPreprocessor2Replacement]
def transform_graph(self, graph: Graph, replacement_descriptions: dict):
if graph.graph['cmd_params'].output is not None:
log.warning('User defined output nodes are specified. Skip the graph cut-off by the '
'ObjectDetectionAPIOutputReplacement.')
return
outputs = []
outputs_string = replacement_descriptions['outputs']
for alternatives in outputs_string.split(','):
for out_node_name in alternatives.split('|'):
if graph.has_node(out_node_name):
outputs.append(out_node_name)
break
else:
log.debug('A node "{}" does not exist in the graph. Do not add it as output'.format(out_node_name))
_outputs = output_user_data_repack(graph, outputs)
add_output_ops(graph, _outputs, graph.graph['inputs'])
class ObjectDetectionAPIPSROIPoolingReplacement(FrontReplacementFromConfigFileSubGraph):
replacement_id = 'ObjectDetectionAPIPSROIPoolingReplacement'
run_not_recursively = True
def run_after(self):
return [ObjectDetectionAPIProposalReplacement]
def run_before(self):
return [ObjectDetectionAPITransformationsFinish]
def output_edges_match(self, graph: Graph, match: SubgraphMatch, new_sub_graph: dict):
return {match.output_node(0)[0].id: new_sub_graph['output_node'].id}
def generate_sub_graph(self, graph: Graph, match: SubgraphMatch):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
num_classes = _value_or_raise(match, pipeline_config, 'num_classes')
input_node = match.input_nodes(0)[0][0].in_node(0)
if 'class_predictions' in input_node.id:
psroipooling_output_dim = num_classes + 1
else:
psroipooling_output_dim = num_classes * 4
num_spatial_bins_height = pipeline_config.get_param('num_spatial_bins_height')
num_spatial_bins_width = pipeline_config.get_param('num_spatial_bins_width')
crop_height = pipeline_config.get_param('crop_height')
crop_width = pipeline_config.get_param('crop_width')
if crop_height != crop_width:
raise Error('Different "crop_height" and "crop_width" parameters from the pipeline config are not '
'supported: {} vs {}'.format(crop_height, crop_width))
if 'reshape_swap_proposals_2d' in graph.nodes():
reshape_swap_proposals_node = Node(graph, 'reshape_swap_proposals_2d')
else:
swap_proposals_node = add_convolution_to_swap_xy_coordinates(graph, Node(graph, 'proposals'), 5)
reshape_swap_proposals_node = create_op_node_with_second_input(graph, Reshape, int64_array([-1, 5]),
{'name': 'reshape_swap_proposals_2d'},
swap_proposals_node)
mark_input_as_in_correct_layout(reshape_swap_proposals_node, 0)
psroipooling_node = PSROIPoolingOp(graph, {'name': input_node.soft_get('name') + '/PSROIPooling',
'output_dim': psroipooling_output_dim,
'group_size': crop_width / num_spatial_bins_width,
'spatial_bins_x': num_spatial_bins_width,
'spatial_bins_y': num_spatial_bins_height,
'mode': 'bilinear',
'spatial_scale': 1,
}).create_node([input_node, reshape_swap_proposals_node])
reduce_node = create_op_node_with_second_input(graph, ReduceMean, int64_array([1, 2]),
{'name': 'mean', 'keep_dims': True}, psroipooling_node)
output_node = match.output_node(0)[0].out_node()
if len(output_node.in_ports()) == 2 and not output_node.in_port(1).disconnected():
output_node.in_port(1).disconnect() # disconnect the second input to make "erase_node" function work
graph.erase_node(match.output_node(0)[0].out_node())
return {'output_node': reduce_node}
class ObjectDetectionAPIConstValueOverride(FrontReplacementFromConfigFileGeneral):
"""
Transforms allows to override specific constant values in the topology. The replacement description configuration
file contains list of tuples describing the desired replacements specified in the "replacements" key of the
"custom_attributes". The first element in the tuple is the initial node name of the graph with constant value. The
second element is the name of the parameter from the pipeline configuration file which stores new value.
Usage example. The Faster-RCNNs topologies has constant node with the number specifying maximum generated proposals.
This value is specified in the pipeline configuration file in the parameter 'first_stage_max_proposals' and is
saved as a constant node in the generated topology. If the parameter is modified from it's original value then the
topology will be incorrect because the number 'first_stage_max_proposals' is used in the transforms of this file is
no more equal to the 'first_stage_max_proposals' saved as a constant.
"""
replacement_id = 'ObjectDetectionAPIConstValueOverride'
run_not_recursively = True
def run_after(self):
return [ObjectDetectionAPITransformationsStart]
def run_before(self):
return [ObjectDetectionAPIPreprocessorReplacement, ObjectDetectionAPIPreprocessor2Replacement]
def transform_graph(self, graph: Graph, replacement_descriptions: dict):
argv = graph.graph['cmd_params']
if argv.tensorflow_object_detection_api_pipeline_config is None:
raise Error(missing_param_error)
pipeline_config = PipelineConfig(argv.tensorflow_object_detection_api_pipeline_config)
for (node_id, pipeline_config_name) in replacement_descriptions['replacements']:
if node_id not in graph.nodes():
log.debug('Node with id {} does not exist in the graph'.format(node_id))
continue
node = Node(graph, node_id)
if not node.has_valid('value'):
log.debug('Node with id {} does not have value'.format(node_id))
continue
node.value = np.array(pipeline_config.get_param(pipeline_config_name))
node.value = node.value.reshape(node.shape)