Publishing 2019 R1 content
[platform/upstream/dldt.git] / model-optimizer / mo / front / caffe / loader.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 logging as log
18 import mmap
19 import os
20
21 import numpy as np
22 from google.protobuf import text_format
23 from google.protobuf.internal import api_implementation
24
25 from mo.front.caffe.proto import caffe_pb2
26 from mo.graph.graph import Node, Graph
27 from mo.utils.error import Error, FrameworkError
28 from mo.utils.utils import refer_to_faq_msg
29
30
31 def parse_mean(file_path: str, in_shape: np.ndarray, mean_file_offsets: [tuple, None]):
32     blob = caffe_pb2.BlobProto()
33     with open(file_path, 'rb') as file:
34         data = file.read()
35
36     if not data:
37         raise Error('Mean file "{}" is empty.' + refer_to_faq_msg(5),
38                     file_path)
39
40     try:
41         blob.ParseFromString(data)
42         data = np.array(blob.data)  # pylint: disable=no-member
43
44         if blob.HasField('channels') or blob.HasField('height') or blob.HasField('width'):
45             data = data.reshape(blob.channels, blob.height, blob.width)  # pylint: disable=no-member
46         else:
47             data = data.reshape(blob.shape.dim)  # pylint: disable=no-member
48         # crop mean image according to input size
49         if in_shape[2] > data.shape[1] or in_shape[3] > data.shape[2]:
50             raise Error(
51                 'Input image of shape {} is larger than mean image {} from file "{}". ' +
52                 refer_to_faq_msg(4),
53                 in_shape,
54                 data.shape,
55                 file_path
56             )
57
58         if mean_file_offsets is not None and len(mean_file_offsets) == 2:
59             offset_x = mean_file_offsets[0]
60             offset_y = mean_file_offsets[1]
61         else:
62             offset_x = int((data.shape[1] - in_shape[2]) / 2)
63             offset_y = int((data.shape[2] - in_shape[3]) / 2)
64
65         mean = []
66         for i in range(in_shape[1]):
67             data_channel = np.zeros(in_shape[2] * in_shape[3], dtype=np.float32)
68             for x in range(in_shape[2]):
69                 for y in range(in_shape[3]):
70                     data_channel[x * in_shape[3] + y] = data[i, x + offset_x, y + offset_y]
71             mean.append(data_channel)
72
73         return mean
74
75     except Exception as err:
76         raise Error(
77             'While processing mean file "{}": {}. Probably mean file has incorrect format. ' +
78             refer_to_faq_msg(6),
79             file_path,
80             str(err)) from err
81
82
83 def load_caffe_proto_model(proto_path: str, model_path: [str, None] = None):
84     # 1. python protobuf is used
85     if api_implementation._implementation_type == 'python':
86         message = 'Please expect that Model Optimizer conversion might be slow. ' \
87                   'You are currently using Python protobuf library implementation. \n'
88         try:
89             from google.protobuf.pyext import cpp_message
90             # Check os windows and env variable PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION
91             if os.name == 'nt' and os.environ.get('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', default='') != 'cpp':
92                 # 2. cpp implementaion is available but not used
93                 message += 'However, cpp implementation is available, you can boost ' \
94                            'model conversion by setting PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION env variable to cpp. \n' \
95                            'Run: set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp \n'
96         except ImportError:
97             # 3. cpp implementaion is not available
98             message += 'However you can use the C++ protobuf implementation that is supplied with the OpenVINO toolkit' \
99                        'or build protobuf library from sources. \n' \
100                        'Navigate to "install_prerequisites" folder and run: ' \
101                        'python -m easy_install protobuf-3.5.1-py($your_python_version)-win-amd64.egg \n' \
102                        'set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp'
103         print(message + '\n\n' + refer_to_faq_msg(80))
104
105     # Read proto layers
106     try:
107         proto = caffe_pb2.NetParameter()
108         with open(proto_path, "r") as file:
109             text_format.Merge(str(file.read()), proto)
110     except Exception as e:
111         log.error('Exception message: {}\n\n'.format(e) +
112                   '    Possible reasons:\n' +
113                   '      1. {} does not exist\n'.format(proto_path) +
114                   '      2. {} does not have a valid structure, for example, it was downloaded as html\n'.format(proto_path) +
115                   '      3. {} contains custom layers or attributes that are not supported\n'.format(proto_path) +
116                   '         in Model Optimizer by default.\n\n' +
117                   '    After you made sure that {} has a valid structure and still see this issue, then\n'.format(proto_path) +
118                   '    you need to generate a python parser for caffe.proto that was used when the model\n' +
119                   '    was created.\n' +
120                   '    Run "python3 generate_caffe_pb2.py --input_proto ${PATH_TO_CAFFE}/src/caffe/proto/caffe.proto"' +
121                   refer_to_faq_msg(1) + '\n\n', extra={'framework_error': True})
122         raise FrameworkError('Model Optimizer is not able to parse {}'.format(proto_path)) from e
123
124     # Read model layer if exists
125     model = None
126     try:
127         if model_path:
128             model = caffe_pb2.NetParameter()
129             with open(model_path, "rb") as infile:
130                 map = mmap.mmap(infile.fileno(), 0, access=mmap.ACCESS_READ)
131                 model.MergeFromString(map)
132     except Exception as e:
133         log.error('Exception message: {}\n\n'.format(e) +
134                   '    Possible reasons:\n' +
135                   '      1. {} does not exist\n'.format(model_path) +
136                   '      2. {} does not have a valid structure\n'.format(model_path), extra={'framework_error': True})
137         raise FrameworkError('Model Optimizer is not able to parse {}'.format(model_path)) from e
138
139     return proto, model
140
141
142 def get_layers(proto):
143     if len(proto.layer):
144         return proto.layer
145     elif len(proto.layers):
146         return proto.layers
147     else:
148         raise Error('Invalid proto file: there is neither "layer" nor "layers" top-level messages. ' +
149                     refer_to_faq_msg(7))
150
151
152 def caffe_pb_to_nx(proto, model):
153     """
154     Converts proto/model layers to a graph. Edges are restored by bottom/top attributes.
155     Graph nodes has two attributes: pb for prototxt definition and model_pb for caffemodel definition.
156
157     Parameters
158     ----------
159     proto : NetParameter
160        Protobuf message for NetParameter, representing .prototxt.
161     model : NetParameter
162        Protobuf message for NetParameter, representing .caffemodel.
163
164     Returns
165     ----------
166         Graph
167         built NX Directed graph.
168     """
169     graph = Graph()
170     # Blobs in prototxt model can be reused by inplace layer.
171     # This requires loading of pb layers in order and tracking the latest
172     # layer that writes a particular blob.
173     blob_producers = {}  # maps layer blob name to the layer name and port
174     proto_layers = get_layers(proto)
175     model_layers = None
176     if model:
177         model_layers = get_layers(model)
178
179     input_dims = []
180     input_names = []
181     if len(proto.input_dim) > 0 and len(list(proto.input)) > 1:
182         # example of proto input
183         # input: "data"
184         # input_dim: 1
185         # input_dim: 3
186         # input_dim: 500
187         # input_dim: 500
188         # input: "info"
189         # input_dim: 1
190         # input_dim: 3
191         raise Error('Old-style inputs (via "input_dims") are not supported. ' +
192                     'Please specify inputs via  "input_shape". ' +
193                     refer_to_faq_msg(8))
194     elif len(list(proto.input)) == 1 and len(list(proto.input_dim)):
195         # example of proto input
196         # input: "data"
197         # input_dim: 1
198         # input_dim: 3
199         # input_dim: 500
200         # input_dim: 500
201         input_dims = [np.array(list(proto.input_dim), dtype=np.int64)]
202         input_names = [proto.input[0]]
203
204     elif len(list(proto.input)) == 1 and len(list(proto.input_shape)):
205         # example of proto input
206         # input: "data"
207         # input_shape
208         # {
209         #     dim: 1
210         #     dim: 3
211         #     dim: 227
212         #     dim: 227
213         # }
214         input_dims = [np.array(proto.input_shape[0].dim, dtype=np.int64)]
215         input_names = [proto.input[0]]
216
217     elif len(proto.input_shape) > 0:
218         # example of proto input
219         # input: "data"
220         # input_shape
221         # {
222         #     dim: 1
223         #     dim: 3
224         #     dim: 600
225         #     dim: 1000
226         # }
227         # input: "im_info"
228         # input_shape
229         # {
230         #     dim: 1
231         #     dim: 3
232         # }
233         for i in range(len(proto.input_shape)):
234             input_dims.append(np.array(proto.input_shape[i].dim, dtype=np.int64))
235             input_names.append(proto.input[i])
236
237     for i in range(len(input_names)):
238         input_name = input_names[i]
239         input_dim = input_dims[i]
240         # Input is defined at the top level of proto instead of distinct Input layer
241         graph.add_node(input_name, pb=None, model_pb=None, type='GlobalInput', name=input_name, shape=input_dim,
242                        kind='op')
243         blob_producers[input_name] = (input_name, 0)
244
245     for i, layer in enumerate(proto_layers):
246
247         model_layer = None
248
249         if model_layers:
250             for ml in model_layers:
251                 if ml.name == layer.name:
252                     model_layer = ml
253                     break
254         if layer.type == 'Input':
255             if hasattr(layer, 'input_param'):
256                 input_param = layer.input_param
257             else:
258                 raise Error('Input layer has no input dims. ' +
259                             refer_to_faq_msg(8))
260             if hasattr(input_param, 'shape'):
261                 """
262                 example of proto input
263                 layer
264                 {
265                     name: "data"
266                     type: "Input"
267                     top: "data"
268                     input_param {shape: {dim: 1 dim: 3 dim: 600 dim: 1000}}
269                 }
270
271                 layer
272                 {
273                     name: "im_info"
274                     type: "Input"
275                     top: "im_info"
276                     input_param {shape: {dim: 1 dim: 3}}
277                 }
278                 """
279                 dims = map(int, list(filter(None, str(list(input_param.shape)[0]).split('dim:'))))
280                 input_dims.append(np.array(list(dims), dtype=np.int64))
281                 input_names.append(layer.name)
282
283         layer.name = graph.unique_id(layer.name)
284         graph.add_node(layer.name, pb=layer, model_pb=model_layer, kind='op')
285
286         # connect inputs based on blob_producers dictionary
287         for dst_port, bottom in enumerate(layer.bottom):
288             src_layer = blob_producers[bottom][0]
289             src_port = blob_producers[bottom][1]
290             assert (graph.has_node(src_layer))
291             edge_attrs = {
292                 'out': src_port,
293                 'in': dst_port,
294                 'name': bottom,
295                 'fw_tensor_debug_info': [(src_layer, bottom)],  # debug anchor for a framework tensor name and port
296                 'in_attrs': ['in', 'name'],
297                 'out_attrs': ['out', 'name'],
298                 'data_attrs': ['fw_tensor_debug_info']
299             }
300             graph.add_edge(src_layer, layer.name, **edge_attrs)
301
302         # update blob producers dictionary by output ports
303         for src_port, top in enumerate(layer.top):
304             if top in blob_producers:
305                 log.debug("Detected reuse of blob {} by layer {}".format(top, layer.name))
306             blob_producers[top] = (layer.name, src_port)
307
308     if len(input_names) <= 0:
309         raise Error('The topology contains no "input" layers. ' +
310                     refer_to_faq_msg(79))
311     return graph, {name: shape for (name, shape) in zip(input_names, input_dims)}