Files
openvino/model-optimizer/mo/back/preprocessing.py
T
6ef59ce3e4 [OV2.0] Model Optimizer: mean/scale/reverse_input_channels/layout for new frontends (#8751)
* Preprocessing API - base classes

Includes API definition for trivial mean/scale operations (which don't require layout)

Mean/scale with 'layout' support will be done under separate task together
 with Layout

Current test code coverage: 100%

* Python bindings for base preprocessing API

* remove pre_post_process directory from ngraph/core

* remove files from ngraph/python dir

* move pyngraph pre_post_process files from ngraph/python to runtime

* remove pre_post_process test from CMakeList

* move include to the header

* update include path for pre_post_process

* style fix

* bind InputTensorInfo::set_layout

* cleaned test_preprocess

* fix test expected output

* remove duplicate test

* update description of set_element_type

* fix style

* move preprocess from pyngraph to pyopenvino/graph

* update test_preprocess imports and remove unnecessary test

* remove duplicate import

* update custom method

* update test

* update test

* create decorator that changes Node into Output<Node>

* create function that cast Node to Output<Node>

* update test_preprocess to use decorator for custom function

* change _cast_to_output -> _from_node

* move frontend folder to pyopenvino

* rename includes and add compile options

* include frontend to pyopenvino

* move __init__.py

* move tests

* remove mock from tests_compatibility

* rename import module

* Fix code style cpp

* refactor a few lines

* style fix

* update few lines in mo

* add tests fro scale and mean with vector input

* style fix

* add docstring for custom_preprocess_function

* bind InputInfo network method

* style fix

* Add pyopenvino to dependencies

* bind OutputInfo

* fix description of preprocess submodule

* fix style

* update copyright year

* Fix mock

* update docstring

* bind OutputTensorInfo

* bind OutputNetworkInfo and InputNetworkInfo

* bind ColorFormat and ResizeAlgorithm

* clean imports

* fix typo

* add PostProcessSteps to init

* bind PreProcessSteps

* create additional tests

* Fix mo test

* remove module local

* fix code style

* update comment

* fix return type

* update docs

* fix code style

* change ngraph.Type to ov.Type

* fix typo

* move _from_node to node_output.hpp

* add read_model from buffer

* update imports

* add new line

* remove bad quotes

* update imports

* style fix

* add new line

* rename functin args

* remove Type import

* update tests

* style fix

* test clean

* remove blank line

* update PrePostProcessor init and build methods

* create test with model update tests with new PrePostProcessor init and build

* # Conflicts:
#	inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api.pyx
#	inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.cpp
#	inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl.hpp
#	inference-engine/ie_bridges/python/src/openvino/offline_transformations/offline_transformations_api_impl_defs.pxd
#	inference-engine/tests/ie_test_utils/common_test_utils/ngraph_test_utils.cpp
#	inference-engine/tests/ie_test_utils/common_test_utils/ngraph_test_utils.hpp
#	model-optimizer/mo/moc_frontend/serialize.py
#	thirdparty/gflags/gflags
#	thirdparty/gtest/gtest

* Stash

* move preprocess module from openvino.impl to openvino

* fix building

* fix code style

* try to move MO to use new api

* Intermediate commit

* try to move MO to use new api

* Test pybind11 custom holder for Preprocessing types (InputInfo and PreProcessingSteps)

* Initial code for source_target layout handling for preprocessing
Initial implementation of reverse input channels

* Use input's tensor names instead of friendly names

* Skeleton for guessing layouts and clearing it after preprocessing

* updated package_BOM.txt

* Use reference_wrapper for preprocess bindings

* Update tests

* Layout::find_permutation - support of dynamic layouts
Covered case for 'trivial convert' where no permutation is needed
It is needed for Model Optimizer for logic which will guess model's layout, like "?c??"

* Stash

* add bindings to I420_SINGLE_PLANE and I420_THREE_PLANES

* remove init from all classes except PrePostProcessor and add RGBX and BGRX to ColorFormat enum

* Guess layout so that existing mean/scale tests passed

* update test name

* Draft to guess layout for 'reverse_input_channels'

* More unit tests (error cases)

* pylint & flake8

* pylint - ignore import error

* Stash

* Moved preprocessing to 'back' folder

* More tests

* Update package_BOM

* Support layout_values with no names
Support layout set for 'outputs'
Tests

* Export more enum names from nrgaph

* Basic --layout parsing

* removed debug prints

* Further updates after rebase

* Update imports

* Removed part from 8829

* Fix imports in test code

* Minor cosmetics

* Don't guess 'C' if layout is already set by model
Expose 'Layout::empty' method

* Style fix

* Apply review comments
Restricted 'heuristics'

C++: Added 'fp16', 'fp64' support to mean/scale

* Applied review comments

* Added some dynamic test cases

* Move call of 'apply_preprocessing' to 'serialize.py'

* Unnecessary change

* Added more comments to code

Co-authored-by: pszmel <piotr.szmelczynski@intel.com>
Co-authored-by: Alexey Lebedev <alexey.lebedev@intel.com>
Co-authored-by: bszmelcz <bartosz.szmelczynski@intel.com>
Co-authored-by: Anastasia Kuporosova <anastasia.kuporosova@intel.com>
Co-authored-by: y <ilya.lavrenov@intel.com>
Co-authored-by: Vafin, Maxim <maxim.vafin@intel.com>
2021-12-07 14:31:55 +03:00

402 lines
18 KiB
Python

# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import argparse
import logging as log
from mo.utils.error import Error
from mo.utils.utils import refer_to_faq_msg
import numpy as np
from openvino.preprocess import PrePostProcessor # pylint: disable=no-name-in-module,import-error
# pylint: disable=no-name-in-module,import-error
from openvino.runtime import Function, Layout, PartialShape, layout_helpers
def update_mean_scale_to_dict(input_nodes: list, mean_scale_val, scale):
"""
Internal function. Updates mean/scale values from array to dictionary
:param: input_nodes Inputs of model
:param: mean_scale_val Parsed 'mean_scale_val' object from command line arguments
:param: scale Global scale factor for all inputs from --scale command line arguments
"""
if not isinstance(mean_scale_val, dict):
if len(mean_scale_val) != len(input_nodes):
raise Error('Numbers of inputs and mean/scale values do not match. ' + refer_to_faq_msg(61))
data = np.copy(mean_scale_val)
mean_scale_val = {}
for idx, node in enumerate(input_nodes):
names_list = list(node.get_tensor().get_names())
if not names_list:
continue
node_name = names_list[0]
mean_scale_val.update(
{
node_name: {
'mean': data[idx][0],
'scale': data[idx][1]
}
}
)
if scale:
for node in input_nodes:
names_list = list(node.get_tensor().get_names())
if not names_list:
continue
node_name = names_list[0]
old_val = mean_scale_val[node_name] if node_name in mean_scale_val else None
mean_scale_val.update(
{
node_name: {
'mean': old_val['mean'] if old_val and 'mean' in old_val else None,
'scale': scale
}
}
)
return mean_scale_val
def check_keys_valid(ov_function: Function, keys: list, search_outputs: bool):
"""
Internal function: checks if keys from cmd line arguments correspond to ov_function's inputs/outputs
Throws if some key is not found
Throws if some different keys point to the same actual input/output
"""
nodes_used = {}
nodes = ov_function.inputs
if search_outputs:
nodes += ov_function.outputs
for name in keys:
node_found = False
for ov_node in nodes:
if name in ov_node.get_tensor().get_names():
if ov_node in nodes_used:
raise Error('Key for {} and {} point to same model input/output.'
.format(name, nodes_used[ov_node]))
nodes_used[ov_node] = name
node_found = True
break
if not node_found:
if not search_outputs:
raise Error('Input with name {} wasn\'t found! {}'.format(name, refer_to_faq_msg(83)))
else:
raise Error('Input/Output with name {} wasn\'t found! {}'.format(name, refer_to_faq_msg(83)))
def update_layout_is_input_flag(ov_function: Function, layout_values: dict):
"""
Internal function: updates layout_values with flag whether each layout belongs to input or to output
"""
for name, layout_value in layout_values.items():
layout_value['is_input'] = False
for ov_input in ov_function.inputs:
if name in ov_input.get_tensor().get_names():
layout_value['is_input'] = True
break
return layout_values
def find_channels_dimension(shape: PartialShape, num_channels: int, name: str, layout_values):
"""
Internal function. Finds dimension index matching with expected channels number
Raises error if there is no candidates or number of candidates is > 1
:param: shape Parameter's partial shape
:param: num_channels Number of channels to find in shape
:param: name Parameter's name, used for Error-handling purposes
:param: layout_values Existing source/target layout items specified by user
:return: updated layout items with guessed layouts
"""
if shape.rank.is_dynamic:
raise Error('Can\'t determine channels dimension for dynamic shape for parameter {}.'
.format(name))
dim_idx_found = -1
for dim_idx in range(shape.rank.get_length()):
dim = shape.get_dimension(dim_idx)
if dim.is_static and dim.get_length() == num_channels:
if dim_idx_found >= 0:
raise Error('Can\'t determine channels dimension for {}. '
'Input shape is {}, needed channels {}. '
'Conflicting dimensions: {} and {}. Please specify layout manually.'
.format(name, shape, num_channels, dim_idx_found, dim_idx))
dim_idx_found = dim_idx
if dim_idx_found < 0:
raise Error('Can\'t determine channels dimension for {}. '
'Input shape is {}, needed channels {}'
.format(name, shape, num_channels))
# Restrict guessed channels index to particular position depending on tensor shape(3d, 4d, 5d)
if shape.rank.get_length() == 3:
# CHW or HWC, possible channels index is 0 or 2
if dim_idx_found != 0 and dim_idx_found != 2:
raise Error('Can\'t determine channels dimension for 3D input {} (CHW or HWC) with shape {}. '
'Please specify layout containing \'C\' channels manually.'.format(name, shape))
elif shape.rank.get_length() == 4:
# NCHW or NHWC, possible channels index is 1 or 3
if dim_idx_found != 1 and dim_idx_found != 3:
raise Error('Can\'t determine channels dimension for 4D input {} (NCHW or NHWC) with shape {}. '
'Please specify layout containing \'C\' channels manually.'.format(name, shape))
elif shape.rank.get_length() == 5:
# NCDHW or NDHWC, possible channels index is 1 or 4
if dim_idx_found != 1 and dim_idx_found != 4:
raise Error('Can\'t determine channels dimension for 5D input {} (NCDHW or NDHWC) with shape {}. '
'Please specify layout containing \'C\' channels manually.'.format(name, shape))
else:
raise Error('Can\'t determine channels dimension for {}D input {} with shape {}.'
'Please specify layout containing \'C\' channels manually.'
.format(shape.rank.get_length(), name, shape))
layout_str = "?" * shape.rank.get_length()
layout_str = layout_str[:dim_idx_found] + 'C' + layout_str[dim_idx_found+1:]
layout_values[name] = {
'source_layout': layout_str,
'target_layout': None,
'source_guessed': True,
'is_input': True
}
return layout_values
def guess_source_layouts_by_mean_scale(ov_function: Function, layout_values, mean_scale_values: dict):
"""
Internal function. Try to guess source layout for input by its shape and/or framework
:param: ov_function Original model
:param: layout_values Existing source/target layout items specified by user
:param: mean_scale_values Dictionary with mean/scale values defined for each argument
:return: updated layout items with guessed layouts
"""
for ms_name, mean_scale in mean_scale_values.items():
num_channels_mean = len(mean_scale['mean']) if mean_scale['mean'] is not None else 0
num_channels_scale = len(mean_scale['scale']) if hasattr(mean_scale['scale'], '__len__') else 0
if num_channels_mean > 1 and \
num_channels_scale > 1 and \
num_channels_mean is not num_channels_scale:
raise Error('Mean/Scale values for {} have different sizes: {} {}'
.format(ms_name, num_channels_mean, num_channels_scale))
need_guess_channels = num_channels_mean > 1 or num_channels_scale > 1
if not need_guess_channels: # Mean/scale is complex and needs 'channels' specified in layout
continue
num_channels = num_channels_mean if num_channels_mean > 1 else num_channels_scale
for i in range(0, len(ov_function.inputs)):
ov_input = ov_function.input(i)
if not ov_function.get_parameters()[i].layout.empty:
continue
if ms_name not in ov_input.get_tensor().get_names():
continue
layout_item = None
for name in ov_input.get_tensor().get_names():
if name in layout_values:
layout_item = layout_values[name]
break
if layout_item is not None:
# User specified some layout, skip guessing
continue
# Guess layout is applicable only when number of channels is '3'
if num_channels != 3:
raise Error('Can\'t determine channels dimension for {}. '
'When number of mean/scale values is {} (not 3), '
'please specify layout for input manually'.format(ms_name, num_channels))
layout_values = find_channels_dimension(shape=ov_input.get_partial_shape(),
num_channels=num_channels,
name=ms_name,
layout_values=layout_values)
return layout_values
def check_suitable_for_reverse(layout: Layout, ov_input):
"""
Internal function. Checks if input with layout is suitable for reversing channels
:param: layout Existing source/target layout items specified by user
:param: ov_input Model's input
:return: True if reverse channels can be applied to input
"""
if not layout_helpers.has_channels(layout):
return False
if ov_input.get_partial_shape().rank.is_dynamic:
return False
c_idx = layout_helpers.channels_idx(layout)
rank = ov_input.get_partial_shape().rank.get_length()
if c_idx < 0:
c_idx += rank
if c_idx >= rank:
raise Error('Layout {} for input {} is inconsistent with shape {}'.format(
layout, ov_input.get_tensor().get_any_name(), ov_input.get_partial_shape()))
c_num = ov_input.get_partial_shape()[c_idx]
return c_num.is_dynamic or c_num.get_length() == 3
def guess_source_layouts_for_reverse_channels(ov_function: Function, layout_values):
"""
Internal function. Try to guess source layout for input by finding dimension with size=3 (RGB/BGR)
Additionally checks existing layouts and detects suitable inputs for reversing of input channels
:param: ov_function Original model
:param: layout_values Existing source/target layout items specified by user
:return: array with suitable parameters for reversing of input channels
"""
all_params = []
suitable_params = []
for i in range(0, len(ov_function.inputs)):
ov_input = ov_function.input(i)
param_info = [ov_input.get_tensor().get_any_name(), ov_input.get_partial_shape()]
all_params.append(param_info)
if not ov_function.get_parameters()[i].layout.empty:
if check_suitable_for_reverse(ov_function.get_parameters()[i].layout, ov_input):
suitable_params.append(param_info)
continue
layout_item = None
first_name = ov_input.get_tensor().get_any_name()
for name in ov_input.get_tensor().get_names():
if name in layout_values:
layout_item = layout_values[name]
break
if layout_item is not None:
if layout_item.get('target_layout'):
if check_suitable_for_reverse(Layout(layout_item['target_layout']), ov_input):
suitable_params.append(param_info)
elif layout_item.get('source_layout'):
if check_suitable_for_reverse(Layout(layout_item['source_layout']), ov_input):
suitable_params.append(param_info)
continue
try:
layout_values = find_channels_dimension(shape=ov_input.get_partial_shape(),
num_channels=3,
name=first_name,
layout_values=layout_values)
except Error as e:
log.debug('Reverse input channels guess did not succeed {}'.format(e))
else:
layout = layout_values[first_name].get('source_layout')
if layout and check_suitable_for_reverse(Layout(layout), ov_input):
suitable_params.append(param_info)
if len(suitable_params) < len(all_params):
log.error('Network has {} inputs overall, but only {} of them are suitable for input channels reversing.\n'
'Suitable for input channel reversing inputs are 4-dimensional with 3 channels\nAll inputs: {}\n'
'Suitable inputs {}'.format(len(all_params), len(suitable_params), all_params, suitable_params),
extra={'is_warning': True})
return suitable_params
def apply_preprocessing(ov_function: Function, argv: argparse.Namespace):
"""
Applies pre-processing of model inputs by adding appropriate operations
On return, 'ov_function' object will be updated
Expected 'argv.mean_scale_values' formats examples:
a) Dict: {'inputName': {'mean': [1., 2., 3.], 'scale': [2., 4., 8.]}}
b) List: list(np.array([(np.array([1., 2., 3.]), np.array([2., 4., 6.])),
(np.array([7., 8., 9.]), np.array([5., 6., 7.])))
Expected 'argv.layout_values' format examples:
a) Specific layouts for inputs and outputs
{ 'input1': {
'source_layout': 'nchw',
'target_layout': 'nhwc'
},
'output2': {
'source_layout': 'nhwc'
}
}
b) Layout for single input: {'': {'source_layout': 'nchw'}}
:param: ov_function OV function for applying mean/scale pre-processing
:param: argv Parsed command line arguments
"""
prep = PrePostProcessor(ov_function)
if 'mean_scale_values' in argv and argv.mean_scale_values:
mean_scale_values = argv.mean_scale_values
else:
mean_scale_values = {}
mean_scale_values = update_mean_scale_to_dict(input_nodes=ov_function.inputs,
mean_scale_val=mean_scale_values,
scale=argv.scale)
# On return, mean_scale_values is a dictionary with input names as key and mean/scale pair as value
# {'inputName': {'mean': [1., 2., 3.], 'scale': [2.]}}
layout_values = {}
if 'layout_values' in argv and argv.layout_values:
layout_values = argv.layout_values
if '' in layout_values:
if len(ov_function.inputs) > 1:
input_names = [list(ov_input.get_tensor().get_names())[0] for ov_input in ov_function.inputs]
raise Error('Layout without name can be specified for models with only one input, '
'but provided model has {} inputs: \'{}\'. '
'Please specify explicitly input/output name for --layout option'
.format(len(input_names), input_names))
layout_values = {
list(ov_function.input().get_tensor().get_names())[0]: {
'source_layout': layout_values[''].get('source_layout'),
'target_layout': layout_values[''].get('target_layout')
}
}
check_keys_valid(ov_function=ov_function, keys=mean_scale_values.keys(), search_outputs=False)
check_keys_valid(ov_function=ov_function, keys=layout_values.keys(), search_outputs=True)
layout_values = update_layout_is_input_flag(ov_function, layout_values)
layout_values = guess_source_layouts_by_mean_scale(ov_function, layout_values, mean_scale_values)
need_reverse = 'reverse_input_channels' in argv and argv.reverse_input_channels
suitable_params_ric = []
if need_reverse:
suitable_params_ric = guess_source_layouts_for_reverse_channels(ov_function=ov_function,
layout_values=layout_values)
for node_name, layout_value in layout_values.items():
if layout_value.get('source_layout'):
if layout_value.get('is_input'):
prep.input(node_name).network().set_layout(Layout(layout_value['source_layout']))
else:
prep.output(node_name).network().set_layout(Layout(layout_value['source_layout']))
if layout_value.get('target_layout'):
if layout_value.get('is_input'):
prep.input(node_name).tensor().set_layout(Layout(layout_value['target_layout']))
else:
prep.output(node_name).tensor().set_layout(Layout(layout_value['target_layout']))
for node_name, node_mean_scale_values in mean_scale_values.items():
# Apply mean first, then scale
if node_mean_scale_values['mean'] is not None:
prep.input(node_name).preprocess().mean(node_mean_scale_values['mean'])
if node_mean_scale_values['scale'] is not None:
prep.input(node_name).preprocess().scale(node_mean_scale_values['scale'])
log.debug('Mean/Scale pre-processing applied to {}'.format(node_name))
# Apply reverse_input_channels
if need_reverse:
for name, _ in suitable_params_ric:
prep.input(name).preprocess().reverse_channels()
log.debug('reverse_input_channels pre-processing applied to {}'.format(name))
# Apply pre-processing builder to a function
ov_function = prep.build()
# Remove guessed layout values from ov_function (these values shall not be serialized to IR
for node_name, layout_value in layout_values.items():
if layout_value.get('source_guessed') and \
not layout_value.get('target_layout'):
# search for parameter object
for idx, ov_input in enumerate(ov_function.inputs):
if node_name in ov_input.get_tensor().get_names():
log.debug('Clearing guessed layout {} for {}'
.format(layout_value['source_layout'], node_name))
ov_function.get_parameters()[idx].layout = Layout()