Publishing 2019 R1 content
[platform/upstream/dldt.git] / model-optimizer / mo / utils / cli_parser.py
1 """
2  Copyright (c) 2018-2019 Intel Corporation
3
4  Licensed under the Apache License, Version 2.0 (the "License");
5  you may not use this file except in compliance with the License.
6  You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15 """
16
17 import argparse
18 import os
19 import re
20 import sys
21 from collections import OrderedDict
22 from itertools import zip_longest
23
24 import numpy as np
25
26 from mo.front.extractor import split_node_in_port
27 from mo.utils import import_extensions
28 from mo.utils.error import Error
29 from mo.utils.utils import refer_to_faq_msg
30
31
32 class CanonicalizePathAction(argparse.Action):
33     """
34     Expand user home directory paths and convert relative-paths to absolute.
35     """
36
37     def __call__(self, parser, namespace, values, option_string=None):
38         if values is not None:
39             list_of_values = list()
40             if isinstance(values, str):
41                 if values != "":
42                     list_of_values = values.split(',')
43             elif isinstance(values, list):
44                 list_of_values = values
45             else:
46                 raise Error('Unsupported type of command line parameter "{}" value'.format(self.dest))
47             list_of_values = [get_absolute_path(path) for path in list_of_values]
48             setattr(namespace, self.dest, ','.join(list_of_values))
49
50
51 class CanonicalizePathCheckExistenceAction(CanonicalizePathAction):
52     """
53     Expand user home directory paths and convert relative-paths to absolute and check specified file or directory
54     existence.
55     """
56
57     def __call__(self, parser, namespace, values, option_string=None):
58         super().__call__(parser, namespace, values, option_string)
59         names = getattr(namespace, self.dest)
60         for name in names.split(','):
61             if name != "" and not os.path.exists(name):
62                 raise Error('The value for command line parameter "{}" must be existing file/directory, '
63                             ' but "{}" does not exist.'.format(self.dest, name))
64
65
66 def readable_file(path: str):
67     """
68     Check that specified path is a readable file.
69     :param path: path to check
70     :return: path if the file is readable
71     """
72     if not os.path.isfile(path):
73         raise Error('The "{}" is not existing file'.format(path))
74     elif not os.access(path, os.R_OK):
75         raise Error('The "{}" is not readable'.format(path))
76     else:
77         return path
78
79
80 def readable_dirs(paths: str):
81     """
82     Checks that comma separated list of paths are readable directories.
83     :param paths: comma separated list of paths.
84     :return: comma separated list of paths.
85     """
86     paths_list = [readable_dir(path) for path in paths.split(',')]
87     return ','.join(paths_list)
88
89
90 def readable_dirs_or_empty(paths: str):
91     """
92     Checks that comma separated list of paths are readable directories of if it is empty.
93     :param paths: comma separated list of paths.
94     :return: comma separated list of paths.
95     """
96     if paths:
97         return readable_dirs(paths)
98     return paths
99
100
101 def readable_dir(path: str):
102     """
103     Check that specified path is a readable directory.
104     :param path: path to check
105     :return: path if the directory is readable
106     """
107     if not os.path.isdir(path):
108         raise Error('The "{}" is not existing directory'.format(path))
109     elif not os.access(path, os.R_OK):
110         raise Error('The "{}" is not readable'.format(path))
111     else:
112         return path
113
114
115 def writable_dir(path: str):
116     """
117     Checks that specified directory is writable. The directory may not exist but it's parent or grandparent must exist.
118     :param path: path to check that it is writable.
119     :return: path if it is writable
120     """
121     if path is None:
122         raise Error('The directory parameter is None')
123     if os.path.exists(path):
124         if os.path.isdir(path):
125             if os.access(path, os.W_OK):
126                 return path
127             else:
128                 raise Error('The directory "{}" is not writable'.format(path))
129         else:
130             raise Error('The "{}" is not a directory'.format(path))
131     else:
132         cur_path = path
133         while os.path.dirname(cur_path) != cur_path:
134             if os.path.exists(cur_path):
135                 break
136             cur_path = os.path.dirname(cur_path)
137         if cur_path == '':
138             cur_path = os.path.curdir
139         if os.access(cur_path, os.W_OK):
140             return path
141         else:
142             raise Error('The directory "{}" is not writable'.format(cur_path))
143
144
145 def get_common_cli_parser(parser: argparse.ArgumentParser = None):
146     if not parser:
147         parser = argparse.ArgumentParser()
148     common_group = parser.add_argument_group('Framework-agnostic parameters')
149     # Common parameters
150     common_group.add_argument('--input_model', '-w', '-m',
151                               help='Tensorflow*: a file with a pre-trained model ' +
152                                    ' (binary or text .pb file after freezing).\n' +
153                                    ' Caffe*: a model proto file with model weights',
154                               action=CanonicalizePathCheckExistenceAction,
155                               type=readable_file)
156     common_group.add_argument('--model_name', '-n',
157                               help='Model_name parameter passed to the final create_ir transform. ' +
158                                    'This parameter is used to name ' +
159                                    'a network in a generated IR and output .xml/.bin files.')
160     common_group.add_argument('--output_dir', '-o',
161                               help='Directory that stores the generated IR. ' +
162                                    'By default, it is the directory from where the Model Optimizer is launched.',
163                               default=get_absolute_path('.'),
164                               action=CanonicalizePathAction,
165                               type=writable_dir)
166     common_group.add_argument('--input_shape',
167                               help='Input shape(s) that should be fed to an input node(s) of the model. '
168                                    'Shape is defined as a comma-separated list of integer numbers enclosed in '
169                                    'parentheses or square brackets, for example [1,3,227,227] or (1,227,227,3), where '
170                                    'the order of dimensions depends on the framework input layout of the model. '
171                                    'For example, [N,C,H,W] is used for Caffe* models and [N,H,W,C] for TensorFlow* '
172                                    'models. Model Optimizer performs necessary transformations to convert the shape to '
173                                    'the layout required by Inference Engine (N,C,H,W). The shape should not contain '
174                                    'undefined dimensions (? or -1) and should fit the dimensions defined in the input '
175                                    'operation of the graph. If there are multiple inputs in the model, --input_shape '
176                                    'should contain definition of shape for each input separated by a comma, for '
177                                    'example: [1,3,227,227],[2,4] for a model with two inputs with 4D and 2D shapes.')
178     common_group.add_argument('--scale', '-s',
179                               type=float,
180                               help='All input values coming from original network inputs will be ' +
181                                    'divided by this ' +
182                                    'value. When a list of inputs is overridden by the --input ' +
183                                    'parameter, this scale ' +
184                                    'is not applied for any input that does not match with ' +
185                                    'the original input of the model.')
186     common_group.add_argument('--reverse_input_channels',
187                               help='Switch the input channels order from RGB to BGR (or vice versa). Applied to '
188                                    'original inputs of the model if and only if a number of channels equals 3. Applied '
189                                    'after application of --mean_values and --scale_values options, so numbers in '
190                                    '--mean_values and --scale_values go in the order of channels used in the original '
191                                    'model.',
192                               action='store_true')
193     common_group.add_argument('--log_level',
194                               help='Logger level',
195                               choices=['CRITICAL', 'ERROR', 'WARN', 'WARNING', 'INFO',
196                                        'DEBUG', 'NOTSET'],
197                               default='ERROR')
198     common_group.add_argument('--input',
199                               help='The name of the input operation of the given model. ' +
200                                    'Usually this is a name of the ' +
201                                    'input placeholder of the model.')
202     common_group.add_argument('--output',
203                               help='The name of the output operation of the model. ' +
204                                    'For TensorFlow*, do not add :0 to this name.')
205     common_group.add_argument('--mean_values', '-ms',
206                               help='Mean values to be used for the input image per channel. ' +
207                                    'Values to be provided in the (R,G,B) or [R,G,B] format. ' +
208                                    'Can be defined for desired input of the model, for example: ' +
209                                    '"--mean_values data[255,255,255],info[255,255,255]". ' +
210                                    'The exact meaning and order ' +
211                                    'of channels depend on how the original model was trained.',
212                               default=())
213     common_group.add_argument('--scale_values',
214                               help='Scale values to be used for the input image per channel. ' +
215                                    'Values are provided in the (R,G,B) or [R,G,B] format. ' +
216                                    'Can be defined for desired input of the model, for example: ' +
217                                    '"--scale_values data[255,255,255],info[255,255,255]". ' +
218                                    'The exact meaning and order ' +
219                                    'of channels depend on how the original model was trained.',
220                               default=())
221     # TODO: isn't it a weights precision type
222     common_group.add_argument('--data_type',
223                               help='Data type for all intermediate tensors and weights. ' +
224                                    'If original model is in FP32 and --data_type=FP16 is specified, all model weights ' +
225                                    'and biases are quantized to FP16.',
226                               choices=["FP16", "FP32", "half", "float"],
227                               default='float')
228     common_group.add_argument('--disable_fusing',
229                               help='Turn off fusing of linear operations to Convolution',
230                               action='store_true')
231     common_group.add_argument('--disable_resnet_optimization',
232                               help='Turn off resnet optimization',
233                               action='store_true')
234     common_group.add_argument('--finegrain_fusing',
235                               help='Regex for layers/operations that won\'t be fused. ' +
236                                    'Example: --finegrain_fusing Convolution1,.*Scale.*')
237     common_group.add_argument('--disable_gfusing',
238                               help='Turn off fusing of grouped convolutions',
239                               action='store_true')
240     common_group.add_argument('--enable_concat_optimization',
241                               help='Turn on concat optimization',
242                               action='store_true')
243     common_group.add_argument('--move_to_preprocess',
244                               help='Move mean values to IR preprocess section',
245                               action='store_true')
246     # we use CanonicalizeDirCheckExistenceAction instead of readable_dirs to handle empty strings
247     common_group.add_argument("--extensions",
248                               help="Directory or a comma separated list of directories with extensions. To disable all "
249                                    "extensions including those that are placed at the default location, pass an empty "
250                                    "string.",
251                               default=import_extensions.default_path(),
252                               action=CanonicalizePathCheckExistenceAction,
253                               type=readable_dirs_or_empty)
254     common_group.add_argument("--batch", "-b",
255                               type=check_positive,
256                               default=None,
257                               help="Input batch size")
258     common_group.add_argument("--version",
259                               action='store_true',
260                               help="Version of Model Optimizer")
261
262     common_group.add_argument('--silent',
263                               help='Prevent any output messages except those that correspond to log level equals '
264                                    'ERROR, that can be set with the following option: --log_level. '
265                                    'By default, log level is already ERROR. ',
266                               action='store_true',
267                               default=False)
268     common_group.add_argument('--freeze_placeholder_with_value', help='Replaces input layer with constant node with '
269                                                                       'provided value, e.g.: "node_name->True"',
270                               default=None)
271     common_group.add_argument('--generate_deprecated_IR_V2',
272                               help='Force to generate legacy/deprecated IR V2 to work with previous versions of the'
273                                    ' Inference Engine. The resulting IR may or may not be correctly loaded by'
274                                    ' Inference Engine API (including the most recent and old versions of Inference'
275                                    ' Engine) and provided as a partially-validated backup option for specific'
276                                    ' deployment scenarios. Use it at your own discretion. By default, without this'
277                                    ' option, the Model Optimizer generates IR V3.',
278                               action='store_true')
279     common_group.add_argument('--keep_shape_ops',
280                               help='[ Experimental feature ] Enables `Shape` operation with all children keeping. '
281                                    'This feature makes model reshapable in Inference Engine',
282                               action='store_true', default=False)
283     return parser
284
285
286 def get_common_cli_options(model_name):
287     d = OrderedDict()
288     d['input_model'] = '- Path to the Input Model'
289     d['output_dir'] = ['- Path for generated IR', lambda x: x if x != '.' else os.getcwd()]
290     d['model_name'] = ['- IR output name', lambda x: x if x else model_name]
291     d['log_level'] = '- Log level'
292     d['batch'] = ['- Batch', lambda x: x if x else 'Not specified, inherited from the model']
293     d['input'] = ['- Input layers', lambda x: x if x else 'Not specified, inherited from the model']
294     d['output'] = ['- Output layers', lambda x: x if x else 'Not specified, inherited from the model']
295     d['input_shape'] = ['- Input shapes', lambda x: x if x else 'Not specified, inherited from the model']
296     d['mean_values'] = ['- Mean values', lambda x: x if x else 'Not specified']
297     d['scale_values'] = ['- Scale values', lambda x: x if x else 'Not specified']
298     d['scale'] = ['- Scale factor', lambda x: x if x else 'Not specified']
299     d['data_type'] = ['- Precision of IR', lambda x: 'FP32' if x == 'float' else 'FP16' if x == 'half' else x]
300     d['disable_fusing'] = ['- Enable fusing', lambda x: not x]
301     d['disable_gfusing'] = ['- Enable grouped convolutions fusing', lambda x: not x]
302     d['move_to_preprocess'] = '- Move mean values to preprocess section'
303     d['reverse_input_channels'] = '- Reverse input channels'
304     return d
305
306
307 def get_caffe_cli_options():
308     d = {
309         'input_proto': ['- Path to the Input prototxt', lambda x: x],
310         'mean_file': ['- Path to a mean file', lambda x: x if x else 'Not specified'],
311         'mean_file_offsets': ['- Offsets for a mean file', lambda x: x if x else 'Not specified'],
312         'k': '- Path to CustomLayersMapping.xml',
313         'disable_resnet_optimization': ['- Enable resnet optimization', lambda x: not x],
314     }
315
316     return OrderedDict(sorted(d.items(), key=lambda t: t[0]))
317
318
319 def get_tf_cli_options():
320     d = {
321         'input_model_is_text': '- Input model in text protobuf format',
322         'tensorflow_subgraph_patterns': '- Patterns to offload',
323         'tensorflow_operation_patterns': '- Operations to offload',
324         'tensorflow_custom_operations_config_update': '- Update the configuration file with input/output node names',
325         'tensorflow_use_custom_operations_config': '- Use the config file',
326         'tensorflow_object_detection_api_pipeline_config': '- Use configuration file used to generate the model with '
327                                                            'Object Detection API',
328         'tensorflow_custom_layer_libraries': '- List of shared libraries with TensorFlow custom layers implementation',
329         'tensorboard_logdir': '- Path to model dump for TensorBoard'
330     }
331
332     return OrderedDict(sorted(d.items(), key=lambda t: t[0]))
333
334
335 def get_mxnet_cli_options():
336     d = {
337         'input_symbol': '- Deploy-ready symbol file',
338         'nd_prefix_name': '- Prefix name for args.nd and argx.nd files',
339         'pretrained_model_name': '- Pretrained model to be merged with the .nd files',
340         'save_params_from_nd': '- Enable saving built parameters file from .nd files',
341         'legacy_mxnet_model': '- Enable MXNet loader for models trained with MXNet version lower than 1.0.0'
342     }
343
344     return OrderedDict(sorted(d.items(), key=lambda t: t[0]))
345
346
347 def get_kaldi_cli_options():
348     d = {
349         'counts': '- A file name with full path to the counts file',
350         'remove_output_softmax': '- Removes the SoftMax layer that is the output layer'
351     }
352
353     return OrderedDict(sorted(d.items(), key=lambda t: t[0]))
354
355
356 def get_onnx_cli_options():
357     d = {
358     }
359
360     return OrderedDict(sorted(d.items(), key=lambda t: t[0]))
361
362
363 def get_caffe_cli_parser(parser: argparse.ArgumentParser = None):
364     """
365     Specifies cli arguments for Model Optimizer for Caffe*
366
367     Returns
368     -------
369         ArgumentParser instance
370     """
371     if not parser:
372         parser = argparse.ArgumentParser()
373         get_common_cli_parser(parser=parser)
374
375     caffe_group = parser.add_argument_group('Caffe*-specific parameters')
376
377     caffe_group.add_argument('--input_proto', '-d',
378                              help='Deploy-ready prototxt file that contains a topology structure ' +
379                                   'and layer attributes',
380                              type=str,
381                              action=CanonicalizePathCheckExistenceAction)
382     caffe_group.add_argument('-k',
383                              help='Path to CustomLayersMapping.xml to register custom layers',
384                              type=str,
385                              default=os.path.join(os.path.dirname(sys.argv[0]), 'extensions', 'front', 'caffe',
386                                                   'CustomLayersMapping.xml'),
387                              action=CanonicalizePathCheckExistenceAction)
388     caffe_group.add_argument('--mean_file', '-mf',
389                              help='Mean image to be used for the input. Should be a binaryproto file',
390                              default=None,
391                              action=CanonicalizePathCheckExistenceAction)
392     caffe_group.add_argument('--mean_file_offsets', '-mo',
393                              help='Mean image offsets to be used for the input binaryproto file. ' +
394                                   'When the mean image is bigger than the expected input, it is cropped. By default, centers ' +
395                                   'of the input image and the mean image are the same and the mean image is cropped by ' +
396                                   'dimensions of the input image. The format to pass this option is the following: "-mo (x,y)". In this ' +
397                                   'case, the mean file is cropped by dimensions of the input image with offset (x,y) ' +
398                                   'from the upper left corner of the mean image',
399                              default=None)
400     caffe_group.add_argument('--disable_omitting_optional',
401                              help='Disable omitting optional attributes to be used for custom layers. ' +
402                                   'Use this option if you want to transfer all attributes of a custom layer to IR. ' +
403                                   'Default behavior is to transfer the attributes with default values and the attributes defined by the user to IR.',
404                              action='store_true',
405                              default=False)
406     caffe_group.add_argument('--enable_flattening_nested_params',
407                              help='Enable flattening optional params to be used for custom layers. ' +
408                                   'Use this option if you want to transfer attributes of a custom layer to IR with flattened nested parameters. ' +
409                                   'Default behavior is to transfer the attributes without flattening nested parameters.',
410                              action='store_true',
411                              default=False)
412     return parser
413
414
415 def get_tf_cli_parser(parser: argparse.ArgumentParser = None):
416     """
417     Specifies cli arguments for Model Optimizer for TF
418
419     Returns
420     -------
421         ArgumentParser instance
422     """
423     if not parser:
424         parser = argparse.ArgumentParser()
425         get_common_cli_parser(parser=parser)
426
427     tf_group = parser.add_argument_group('TensorFlow*-specific parameters')
428     tf_group.add_argument('--input_model_is_text',
429                           help='TensorFlow*: treat the input model file as a text protobuf format. If not specified, ' +
430                                'the Model Optimizer treats it as a binary file by default.',
431                           action='store_true')
432     tf_group.add_argument('--input_checkpoint', type=str, default=None, help="TensorFlow*: variables file to load.",
433                           action=CanonicalizePathCheckExistenceAction)
434     tf_group.add_argument('--input_meta_graph',
435                           help='Tensorflow*: a file with a meta-graph of the model before freezing',
436                           action=CanonicalizePathCheckExistenceAction,
437                           type=readable_file)
438     tf_group.add_argument('--saved_model_dir', default=None,
439                           help="TensorFlow*: directory representing non frozen model",
440                           action=CanonicalizePathCheckExistenceAction,
441                           type=readable_dirs)
442     tf_group.add_argument('--saved_model_tags', type=str, default=None,
443                           help="Group of tag(s) of the MetaGraphDef to load, in string format, separated by ','. "
444                                "For tag-set contains multiple tags, all tags must be passed in.")
445     tf_group.add_argument('--tensorflow_subgraph_patterns',
446                           help='TensorFlow*: a list of comma separated patterns that will be applied to ' +
447                                'TensorFlow* node names to ' +
448                                'infer a part of the graph using TensorFlow*.')
449     tf_group.add_argument('--tensorflow_operation_patterns',
450                           help='TensorFlow*: a list of comma separated patterns that will be applied to ' +
451                                'TensorFlow* node type (ops) ' +
452                                'to infer these operations using TensorFlow*.')
453     tf_group.add_argument('--tensorflow_custom_operations_config_update',
454                           help='TensorFlow*: update the configuration file with node name patterns with input/output '
455                                'nodes information.',
456                           action=CanonicalizePathCheckExistenceAction)
457     tf_group.add_argument('--tensorflow_use_custom_operations_config',
458                           help='TensorFlow*: use the configuration file with custom operation description.',
459                           action=CanonicalizePathCheckExistenceAction)
460     tf_group.add_argument('--tensorflow_object_detection_api_pipeline_config',
461                           help='TensorFlow*: path to the pipeline configuration file used to generate model created '
462                                'with help of Object Detection API.',
463                           action=CanonicalizePathCheckExistenceAction)
464     tf_group.add_argument('--tensorboard_logdir',
465                           help='TensorFlow*: dump the input graph to a given directory that should be used with TensorBoard.',
466                           default=None,
467                           action=CanonicalizePathCheckExistenceAction)
468     tf_group.add_argument('--tensorflow_custom_layer_libraries',
469                           help='TensorFlow*: comma separated list of shared libraries with TensorFlow* custom '
470                                'operations implementation.',
471                           default=None,
472                           action=CanonicalizePathCheckExistenceAction)
473     tf_group.add_argument('--disable_nhwc_to_nchw',
474                           help='Disables default translation from NHWC to NCHW',
475                           action='store_true')
476     return parser
477
478
479 def get_mxnet_cli_parser(parser: argparse.ArgumentParser = None):
480     """
481     Specifies cli arguments for Model Optimizer for MXNet*
482
483     Returns
484     -------
485         ArgumentParser instance
486     """
487     if not parser:
488         parser = argparse.ArgumentParser()
489         get_common_cli_parser(parser=parser)
490
491     mx_group = parser.add_argument_group('Mxnet-specific parameters')
492
493     mx_group.add_argument('--input_symbol',
494                           help='Symbol file (for example, model-symbol.json) that contains a topology structure ' +
495                                'and layer attributes',
496                           type=str,
497                           action=CanonicalizePathCheckExistenceAction)
498     mx_group.add_argument("--nd_prefix_name",
499                           help="Prefix name for args.nd and argx.nd files.",
500                           default=None)
501     mx_group.add_argument("--pretrained_model_name",
502                           help="Name of a pretrained MXNet model without extension and epoch number. This model will be merged with args.nd and argx.nd files",
503                           default=None)
504     mx_group.add_argument("--save_params_from_nd",
505                           action='store_true',
506                           help="Enable saving built parameters file from .nd files")
507     mx_group.add_argument("--legacy_mxnet_model",
508                           action='store_true',
509                           help="Enable MXNet loader to make a model compatible with the latest MXNet version. Use only if your model was trained with MXNet version lower than 1.0.0")
510     return parser
511
512
513 def get_kaldi_cli_parser(parser: argparse.ArgumentParser = None):
514     """
515     Specifies cli arguments for Model Optimizer for MXNet*
516
517     Returns
518     -------
519         ArgumentParser instance
520     """
521     if not parser:
522         parser = argparse.ArgumentParser()
523         get_common_cli_parser(parser=parser)
524
525     kaldi_group = parser.add_argument_group('Kaldi-specific parameters')
526
527     kaldi_group.add_argument("--counts",
528                              help="Path to the counts file",
529                              default=None,
530                              action=CanonicalizePathCheckExistenceAction)
531
532     kaldi_group.add_argument("--remove_output_softmax",
533                              help="Removes the SoftMax layer that is the output layer",
534                              action='store_true')
535     return parser
536
537
538 def get_onnx_cli_parser(parser: argparse.ArgumentParser = None):
539     """
540     Specifies cli arguments for Model Optimizer for ONNX
541
542     Returns
543     -------
544         ArgumentParser instance
545     """
546     if not parser:
547         parser = argparse.ArgumentParser()
548         get_common_cli_parser(parser=parser)
549
550     tf_group = parser.add_argument_group('ONNX*-specific parameters')
551
552     return parser
553
554
555 def get_all_cli_parser():
556     """
557     Specifies cli arguments for Model Optimizer
558
559     Returns
560     -------
561         ArgumentParser instance
562     """
563     parser = argparse.ArgumentParser()
564
565     parser.add_argument('--framework',
566                         help='Name of the framework used to train the input model.',
567                         type=str,
568                         choices=['tf', 'caffe', 'mxnet', 'kaldi', 'onnx'])
569
570     get_common_cli_parser(parser=parser)
571
572     get_tf_cli_parser(parser=parser)
573     get_caffe_cli_parser(parser=parser)
574     get_mxnet_cli_parser(parser=parser)
575     get_kaldi_cli_parser(parser=parser)
576     get_onnx_cli_parser(parser=parser)
577
578     return parser
579
580
581 def get_placeholder_shapes(argv_input: str, argv_input_shape: str, argv_batch=None):
582     """
583     Parses input layers names and input shapes from the cli and returns the parsed object
584
585     Parameters
586     ----------
587     argv_input
588         string with a list of input layers: either an empty string, or strings separated with comma.
589         E.g. 'inp1,inp2'
590     argv_input_shape
591         string with a list of input shapes: either an empty string, or tuples separated with comma.
592         E.g. '(1,2),(3,4)'.
593         Only positive integers are accepted except -1, which can be on any position in a shape.
594     argv_batch
595         integer that overrides batch size in input shape
596
597     Returns
598     -------
599         parsed shapes in form of {'name of input':ndarray} if names of inputs are provided with shapes
600         parsed shapes in form of {'name of input':None} if names of inputs are provided without shapes
601         ndarray if only one shape is provided and no input name
602         None if neither shape nor input were provided
603     """
604     if argv_input_shape and argv_batch:
605         raise Error("Both --input_shape and --batch were provided. Please provide only one of them. " +
606                     refer_to_faq_msg(56))
607     shapes = list()
608     inputs = list()
609     placeholder_shapes = None
610
611     first_digit_reg = r'([0-9 ]+|-1)'
612     next_digits_reg = r'(,{})*'.format(first_digit_reg)
613     tuple_reg = r'((\({}{}\))|(\[{}{}\]))'.format(first_digit_reg, next_digits_reg,
614                                                   first_digit_reg, next_digits_reg)
615     if argv_input_shape:
616         full_reg = r'^{}(\s*,\s*{})*$|^$'.format(tuple_reg, tuple_reg)
617         if not re.match(full_reg, argv_input_shape):
618             raise Error('Input shape "{}" cannot be parsed. ' +
619                         refer_to_faq_msg(57), argv_input_shape)
620
621         shapes = re.findall(r'[(\[]([0-9, -]+)[)\]]', argv_input_shape)
622
623     if argv_input:
624         inputs = argv_input.split(',')
625
626     # check number of shapes with no input provided
627     if argv_input_shape and not argv_input:
628         if len(shapes) > 1:
629             raise Error('Please provide input layer names for input layer shapes. ' +
630                         refer_to_faq_msg(58))
631         else:
632             placeholder_shapes = np.fromstring(shapes[0], dtype=np.int64, sep=',')
633
634     # check if number of shapes does not match number of passed inputs
635     elif argv_input and (len(shapes) == len(inputs) or len(shapes) == 0):
636         placeholder_shapes = dict(zip_longest(inputs,
637                                               map(lambda x: np.fromstring(x, dtype=np.int64,
638                                                                           sep=',') if x else None, shapes)))
639     elif argv_input:
640         raise Error('Please provide each input layers with an input layer shape. ' +
641                     refer_to_faq_msg(58))
642
643     return placeholder_shapes
644
645
646 def parse_tuple_pairs(argv_values: str):
647     """
648     Gets mean/scale values from the given string parameter
649     Parameters
650     ----------
651     argv_values
652         string with a specified input name and  list of mean values: either an empty string, or a tuple
653         in a form [] or ().
654         E.g. 'data(1,2,3)' means 1 for the RED channel, 2 for the GREEN channel, 3 for the BLUE channel for the data
655         input layer, or tuple of values in a form [] or () if input is specified separately, e.g. (1,2,3),[4,5,6].
656
657     Returns
658     -------
659         dictionary with input name and tuple of values or list of values if mean/scale value is specified with input,
660         e.g.:
661         "data(10,20,30),info(11,22,33)" -> { 'data': [10,20,30], 'info': [11,22,33] }
662         "(10,20,30),(11,22,33)" -> [np.array(10,20,30), np.array(11,22,33)]
663     """
664     res = {}
665     if not argv_values:
666         return res
667
668     data_str = argv_values
669     while True:
670         tuples_matches = re.findall(r'[(\[]([0-9., -]+)[)\]]', data_str, re.IGNORECASE)
671         if not tuples_matches :
672             raise Error(
673                 "Mean/scale values should be in format: data(1,2,3),info(2,3,4)" +
674                 " or just plain set of them without naming any inputs: (1,2,3),(2,3,4). " +
675                 refer_to_faq_msg(101), argv_values)
676         tuple_value = tuples_matches[0]
677         matches = data_str.split(tuple_value)
678
679         input_name = matches[0][:-1]
680         if not input_name:
681             res = []
682             # check that other values are specified w/o names
683             words_reg = r'([a-zA-Z]+)'
684             for i in range(0, len(matches)):
685                 if re.search(words_reg, matches[i]) is not None:
686                     # error - tuple with name is also specified
687                     raise Error(
688                         "Mean/scale values should either contain names of input layers: data(1,2,3),info(2,3,4)" +
689                         " or just plain set of them without naming any inputs: (1,2,3),(2,3,4)." +
690                         refer_to_faq_msg(101), argv_values)
691             for match in tuples_matches:
692                 res.append(np.fromstring(match, dtype=float, sep=','))
693             break
694
695         res[input_name] = np.fromstring(tuple_value, dtype=float, sep=',')
696
697         parenthesis = matches[0][-1]
698         sibling = ')' if parenthesis == '(' else ']'
699         pair = '{}{}{}{}'.format(input_name, parenthesis, tuple_value, sibling)
700         idx_substr = data_str.index(pair)
701         data_str = data_str[idx_substr + len(pair) + 1:]
702
703         if not data_str:
704             break
705
706     return res
707
708
709 def get_tuple_values(argv_values: str or tuple, num_exp_values: int = 3, t=float or int):
710     """
711     Gets mean values from the given string parameter
712     Args:
713         argv_values: string with list of mean values: either an empty string, or a tuple in a form [] or ().
714         E.g. '(1,2,3)' means 1 for the RED channel, 2 for the GREEN channel, 4 for the BLUE channel.
715         t: either float or int
716         num_exp_values: number of values in tuple
717
718     Returns:
719         tuple of values
720     """
721
722     digit_reg = r'(-?[0-9. ]+)' if t == float else r'(-?[0-9 ]+)'
723
724     assert num_exp_values > 1, 'Can not parse tuple of size 1'
725     content = r'{0}\s*,{1}\s*{0}'.format(digit_reg, (digit_reg + ',') * (num_exp_values - 2))
726     tuple_reg = r'((\({0}\))|(\[{0}\]))'.format(content)
727
728     if isinstance(argv_values, tuple) and not len(argv_values):
729         return argv_values
730
731     if not len(argv_values) or not re.match(tuple_reg, argv_values):
732         raise Error('Values "{}" cannot be parsed. ' +
733                     refer_to_faq_msg(59), argv_values)
734
735     mean_values_matches = re.findall(r'[(\[]([0-9., -]+)[)\]]', argv_values)
736
737     for mean in mean_values_matches:
738         if len(mean.split(',')) != num_exp_values:
739             raise Error('{} channels are expected for given values. ' +
740                         refer_to_faq_msg(60), num_exp_values)
741
742     return mean_values_matches
743
744
745 def get_mean_scale_dictionary(mean_values, scale_values, argv_input: str):
746     """
747     This function takes mean_values and scale_values, checks and processes them into convenient structure
748
749     Parameters
750     ----------
751     mean_values dictionary, contains input name and mean values passed py user (e.g. {data: np.array[102.4, 122.1, 113.9]}),
752     or list containing values (e.g. np.array[102.4, 122.1, 113.9])
753     scale_values dictionary, contains input name and scale values passed py user (e.g. {data: np.array[102.4, 122.1, 113.9]})
754     or list containing values (e.g. np.array[102.4, 122.1, 113.9])
755
756     Returns
757     -------
758     The function returns a dictionary e.g.
759     mean = { 'data: np.array, 'info': np.array }, scale = { 'data: np.array, 'info': np.array }, input = "data, info" ->
760      { 'data': { 'mean': np.array, 'scale': np.array }, 'info': { 'mean': np.array, 'scale': np.array } }
761
762     """
763     res = {}
764     # collect input names
765     if argv_input:
766         inputs = argv_input.split(',')
767     else:
768         inputs = []
769         if type(mean_values) is dict:
770             inputs = list(mean_values.keys())
771         if type(scale_values) is dict:
772             for name in scale_values.keys():
773                 if name not in inputs:
774                     inputs.append(name)
775
776     # create unified object containing both mean and scale for input
777     if type(mean_values) is dict and type(scale_values) is dict:
778         if not mean_values and not scale_values:
779             return res
780         for inp in inputs:
781             inp, port = split_node_in_port(inp)
782             if inp in mean_values or inp in scale_values:
783                 res.update(
784                     {
785                         inp: {
786                             'mean':
787                                 mean_values[inp] if inp in mean_values else None,
788                             'scale':
789                                 scale_values[inp] if inp in scale_values else None
790                         }
791                     }
792                 )
793         return res
794
795     # user specified input and mean/scale separately - we should return dictionary
796     if inputs:
797         if mean_values and scale_values:
798             if len(inputs) != len(mean_values):
799                 raise Error('Numbers of inputs and mean values do not match. ' +
800                             refer_to_faq_msg(61))
801             if len(inputs) != len(scale_values):
802                 raise Error('Numbers of inputs and scale values do not match. ' +
803                             refer_to_faq_msg(62))
804
805             data = list(zip(mean_values, scale_values))
806
807             for i in range(len(data)):
808                 res.update(
809                     {
810                         inputs[i]: {
811                             'mean':
812                                 data[i][0],
813                             'scale':
814                                 data[i][1],
815
816                         }
817                     }
818                 )
819             return res
820         # only mean value specified
821         if mean_values:
822             data = list(mean_values)
823             for i in range(len(data)):
824                 res.update(
825                     {
826                         inputs[i]: {
827                             'mean':
828                                 data[i],
829                             'scale':
830                                 None
831
832                         }
833                     }
834                 )
835             return res
836
837         # only scale value specified
838         if scale_values:
839             data = list(scale_values)
840             for i in range(len(data)):
841                 res.update(
842                     {
843                         inputs[i]: {
844                             'mean':
845                                 None,
846                             'scale':
847                                 data[i]
848
849                         }
850                     }
851                 )
852             return res
853     # mean and scale are specified without inputs, return list, order is not guaranteed (?)
854     return list(zip_longest(mean_values, scale_values))
855
856
857 def get_model_name(path_input_model: str) -> str:
858     """
859     Deduces model name by a given path to the input model
860     Args:
861         path_input_model: path to the input model
862
863     Returns:
864         name of the output IR
865     """
866     parsed_name, extension = os.path.splitext(os.path.basename(path_input_model))
867     return 'model' if parsed_name.startswith('.') or len(parsed_name) == 0 else parsed_name
868
869
870 def get_absolute_path(path_to_file: str) -> str:
871     """
872     Deduces absolute path of the file by a given path to the file
873     Args:
874         path_to_file: path to the file
875
876     Returns:
877         absolute path of the file
878     """
879     file_path = os.path.expanduser(path_to_file)
880     if not os.path.isabs(file_path):
881         file_path = os.path.join(os.getcwd(), file_path)
882     return file_path
883
884
885 def check_positive(value):
886     try:
887         int_value = int(value)
888         if int_value <= 0:
889             raise ValueError
890     except ValueError:
891         raise argparse.ArgumentTypeError("expected a positive integer value")
892
893     return int_value
894
895
896 def depersonalize(value: str):
897     if not isinstance(value, str):
898         return value
899     res = []
900     for path in value.split(','):
901         if os.path.isdir(path):
902             res.append('DIR')
903         elif os.path.isfile(path):
904             res.append(os.path.join('DIR', os.path.split(path)[1]))
905         else:
906             res.append(path)
907     return ','.join(res)
908
909
910 def get_meta_info(argv: argparse.Namespace):
911     meta_data = {'unset': []}
912     for key, value in argv.__dict__.items():
913         if value is not None:
914             value = depersonalize(value)
915             meta_data[key] = value
916         else:
917             meta_data['unset'].append(key)
918     # The attribute 'k' is treated separately because it points to not existing file by default
919     for key in ['k']:
920         if key in meta_data:
921             meta_data[key] = ','.join([os.path.join('DIR', os.path.split(i)[1]) for i in meta_data[key].split(',')])
922     return meta_data
923