Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Tetragramm
GitHub Repository: Tetragramm/opencv
Path: blob/master/samples/dnn/tf_text_graph_ssd.py
16337 views
1
# This file is a part of OpenCV project.
2
# It is a subject to the license terms in the LICENSE file found in the top-level directory
3
# of this distribution and at http://opencv.org/license.html.
4
#
5
# Copyright (C) 2018, Intel Corporation, all rights reserved.
6
# Third party copyrights are property of their respective owners.
7
#
8
# Use this script to get the text graph representation (.pbtxt) of SSD-based
9
# deep learning network trained in TensorFlow Object Detection API.
10
# Then you can import it with a binary frozen graph (.pb) using readNetFromTensorflow() function.
11
# See details and examples on the following wiki page: https://github.com/opencv/opencv/wiki/TensorFlow-Object-Detection-API
12
import argparse
13
from math import sqrt
14
from tf_text_graph_common import *
15
16
def createSSDGraph(modelPath, configPath, outputPath):
17
# Nodes that should be kept.
18
keepOps = ['Conv2D', 'BiasAdd', 'Add', 'Relu6', 'Placeholder', 'FusedBatchNorm',
19
'DepthwiseConv2dNative', 'ConcatV2', 'Mul', 'MaxPool', 'AvgPool', 'Identity',
20
'Sub']
21
22
# Node with which prefixes should be removed
23
prefixesToRemove = ('MultipleGridAnchorGenerator/', 'Postprocessor/', 'Preprocessor/map')
24
25
# Load a config file.
26
config = readTextMessage(configPath)
27
config = config['model'][0]['ssd'][0]
28
num_classes = int(config['num_classes'][0])
29
30
ssd_anchor_generator = config['anchor_generator'][0]['ssd_anchor_generator'][0]
31
min_scale = float(ssd_anchor_generator['min_scale'][0])
32
max_scale = float(ssd_anchor_generator['max_scale'][0])
33
num_layers = int(ssd_anchor_generator['num_layers'][0])
34
aspect_ratios = [float(ar) for ar in ssd_anchor_generator['aspect_ratios']]
35
reduce_boxes_in_lowest_layer = True
36
if 'reduce_boxes_in_lowest_layer' in ssd_anchor_generator:
37
reduce_boxes_in_lowest_layer = ssd_anchor_generator['reduce_boxes_in_lowest_layer'][0] == 'true'
38
39
fixed_shape_resizer = config['image_resizer'][0]['fixed_shape_resizer'][0]
40
image_width = int(fixed_shape_resizer['width'][0])
41
image_height = int(fixed_shape_resizer['height'][0])
42
43
box_predictor = 'convolutional' if 'convolutional_box_predictor' in config['box_predictor'][0] else 'weight_shared_convolutional'
44
45
print('Number of classes: %d' % num_classes)
46
print('Number of layers: %d' % num_layers)
47
print('Scale: [%f-%f]' % (min_scale, max_scale))
48
print('Aspect ratios: %s' % str(aspect_ratios))
49
print('Reduce boxes in the lowest layer: %s' % str(reduce_boxes_in_lowest_layer))
50
print('box predictor: %s' % box_predictor)
51
print('Input image size: %dx%d' % (image_width, image_height))
52
53
# Read the graph.
54
inpNames = ['image_tensor']
55
outNames = ['num_detections', 'detection_scores', 'detection_boxes', 'detection_classes']
56
57
writeTextGraph(modelPath, outputPath, outNames)
58
graph_def = parseTextGraph(outputPath)
59
60
def getUnconnectedNodes():
61
unconnected = []
62
for node in graph_def.node:
63
unconnected.append(node.name)
64
for inp in node.input:
65
if inp in unconnected:
66
unconnected.remove(inp)
67
return unconnected
68
69
70
# Detect unfused batch normalization nodes and fuse them.
71
def fuse_batch_normalization():
72
# Add_0 <-- moving_variance, add_y
73
# Rsqrt <-- Add_0
74
# Mul_0 <-- Rsqrt, gamma
75
# Mul_1 <-- input, Mul_0
76
# Mul_2 <-- moving_mean, Mul_0
77
# Sub_0 <-- beta, Mul_2
78
# Add_1 <-- Mul_1, Sub_0
79
nodesMap = {node.name: node for node in graph_def.node}
80
subgraph = ['Add',
81
['Mul', 'input', ['Mul', ['Rsqrt', ['Add', 'moving_variance', 'add_y']], 'gamma']],
82
['Sub', 'beta', ['Mul', 'moving_mean', 'Mul_0']]]
83
def checkSubgraph(node, targetNode, inputs, fusedNodes):
84
op = targetNode[0]
85
if node.op == op and (len(node.input) >= len(targetNode) - 1):
86
fusedNodes.append(node)
87
for i, inpOp in enumerate(targetNode[1:]):
88
if isinstance(inpOp, list):
89
if not node.input[i] in nodesMap or \
90
not checkSubgraph(nodesMap[node.input[i]], inpOp, inputs, fusedNodes):
91
return False
92
else:
93
inputs[inpOp] = node.input[i]
94
95
return True
96
else:
97
return False
98
99
nodesToRemove = []
100
for node in graph_def.node:
101
inputs = {}
102
fusedNodes = []
103
if checkSubgraph(node, subgraph, inputs, fusedNodes):
104
name = node.name
105
node.Clear()
106
node.name = name
107
node.op = 'FusedBatchNorm'
108
node.input.append(inputs['input'])
109
node.input.append(inputs['gamma'])
110
node.input.append(inputs['beta'])
111
node.input.append(inputs['moving_mean'])
112
node.input.append(inputs['moving_variance'])
113
node.addAttr('epsilon', 0.001)
114
nodesToRemove += fusedNodes[1:]
115
for node in nodesToRemove:
116
graph_def.node.remove(node)
117
118
fuse_batch_normalization()
119
120
removeIdentity(graph_def)
121
122
def to_remove(name, op):
123
return (not op in keepOps) or name.startswith(prefixesToRemove)
124
125
removeUnusedNodesAndAttrs(to_remove, graph_def)
126
127
128
# Connect input node to the first layer
129
assert(graph_def.node[0].op == 'Placeholder')
130
# assert(graph_def.node[1].op == 'Conv2D')
131
weights = graph_def.node[1].input[0]
132
for i in range(len(graph_def.node[1].input)):
133
graph_def.node[1].input.pop()
134
graph_def.node[1].input.append(graph_def.node[0].name)
135
graph_def.node[1].input.append(weights)
136
137
# Create SSD postprocessing head ###############################################
138
139
# Concatenate predictions of classes, predictions of bounding boxes and proposals.
140
def addConcatNode(name, inputs, axisNodeName):
141
concat = NodeDef()
142
concat.name = name
143
concat.op = 'ConcatV2'
144
for inp in inputs:
145
concat.input.append(inp)
146
concat.input.append(axisNodeName)
147
graph_def.node.extend([concat])
148
149
addConstNode('concat/axis_flatten', [-1], graph_def)
150
addConstNode('PriorBox/concat/axis', [-2], graph_def)
151
152
for label in ['ClassPredictor', 'BoxEncodingPredictor' if box_predictor is 'convolutional' else 'BoxPredictor']:
153
concatInputs = []
154
for i in range(num_layers):
155
# Flatten predictions
156
flatten = NodeDef()
157
if box_predictor is 'convolutional':
158
inpName = 'BoxPredictor_%d/%s/BiasAdd' % (i, label)
159
else:
160
if i == 0:
161
inpName = 'WeightSharedConvolutionalBoxPredictor/%s/BiasAdd' % label
162
else:
163
inpName = 'WeightSharedConvolutionalBoxPredictor_%d/%s/BiasAdd' % (i, label)
164
flatten.input.append(inpName)
165
flatten.name = inpName + '/Flatten'
166
flatten.op = 'Flatten'
167
168
concatInputs.append(flatten.name)
169
graph_def.node.extend([flatten])
170
addConcatNode('%s/concat' % label, concatInputs, 'concat/axis_flatten')
171
172
idx = 0
173
for node in graph_def.node:
174
if node.name == ('BoxPredictor_%d/BoxEncodingPredictor/Conv2D' % idx) or \
175
node.name == ('WeightSharedConvolutionalBoxPredictor_%d/BoxPredictor/Conv2D' % idx) or \
176
node.name == 'WeightSharedConvolutionalBoxPredictor/BoxPredictor/Conv2D':
177
node.addAttr('loc_pred_transposed', True)
178
idx += 1
179
assert(idx == num_layers)
180
181
# Add layers that generate anchors (bounding boxes proposals).
182
scales = [min_scale + (max_scale - min_scale) * i / (num_layers - 1)
183
for i in range(num_layers)] + [1.0]
184
185
priorBoxes = []
186
for i in range(num_layers):
187
priorBox = NodeDef()
188
priorBox.name = 'PriorBox_%d' % i
189
priorBox.op = 'PriorBox'
190
if box_predictor is 'convolutional':
191
priorBox.input.append('BoxPredictor_%d/BoxEncodingPredictor/BiasAdd' % i)
192
else:
193
if i == 0:
194
priorBox.input.append('WeightSharedConvolutionalBoxPredictor/BoxPredictor/Conv2D')
195
else:
196
priorBox.input.append('WeightSharedConvolutionalBoxPredictor_%d/BoxPredictor/BiasAdd' % i)
197
priorBox.input.append(graph_def.node[0].name) # image_tensor
198
199
priorBox.addAttr('flip', False)
200
priorBox.addAttr('clip', False)
201
202
if i == 0 and reduce_boxes_in_lowest_layer:
203
widths = [0.1, min_scale * sqrt(2.0), min_scale * sqrt(0.5)]
204
heights = [0.1, min_scale / sqrt(2.0), min_scale / sqrt(0.5)]
205
else:
206
widths = [scales[i] * sqrt(ar) for ar in aspect_ratios]
207
heights = [scales[i] / sqrt(ar) for ar in aspect_ratios]
208
209
widths += [sqrt(scales[i] * scales[i + 1])]
210
heights += [sqrt(scales[i] * scales[i + 1])]
211
widths = [w * image_width for w in widths]
212
heights = [h * image_height for h in heights]
213
priorBox.addAttr('width', widths)
214
priorBox.addAttr('height', heights)
215
priorBox.addAttr('variance', [0.1, 0.1, 0.2, 0.2])
216
217
graph_def.node.extend([priorBox])
218
priorBoxes.append(priorBox.name)
219
220
addConcatNode('PriorBox/concat', priorBoxes, 'concat/axis_flatten')
221
222
# Sigmoid for classes predictions and DetectionOutput layer
223
sigmoid = NodeDef()
224
sigmoid.name = 'ClassPredictor/concat/sigmoid'
225
sigmoid.op = 'Sigmoid'
226
sigmoid.input.append('ClassPredictor/concat')
227
graph_def.node.extend([sigmoid])
228
229
detectionOut = NodeDef()
230
detectionOut.name = 'detection_out'
231
detectionOut.op = 'DetectionOutput'
232
233
if box_predictor == 'convolutional':
234
detectionOut.input.append('BoxEncodingPredictor/concat')
235
else:
236
detectionOut.input.append('BoxPredictor/concat')
237
detectionOut.input.append(sigmoid.name)
238
detectionOut.input.append('PriorBox/concat')
239
240
detectionOut.addAttr('num_classes', num_classes + 1)
241
detectionOut.addAttr('share_location', True)
242
detectionOut.addAttr('background_label_id', 0)
243
detectionOut.addAttr('nms_threshold', 0.6)
244
detectionOut.addAttr('top_k', 100)
245
detectionOut.addAttr('code_type', "CENTER_SIZE")
246
detectionOut.addAttr('keep_top_k', 100)
247
detectionOut.addAttr('confidence_threshold', 0.01)
248
249
graph_def.node.extend([detectionOut])
250
251
while True:
252
unconnectedNodes = getUnconnectedNodes()
253
unconnectedNodes.remove(detectionOut.name)
254
if not unconnectedNodes:
255
break
256
257
for name in unconnectedNodes:
258
for i in range(len(graph_def.node)):
259
if graph_def.node[i].name == name:
260
del graph_def.node[i]
261
break
262
263
# Save as text.
264
graph_def.save(outputPath)
265
266
267
if __name__ == "__main__":
268
parser = argparse.ArgumentParser(description='Run this script to get a text graph of '
269
'SSD model from TensorFlow Object Detection API. '
270
'Then pass it with .pb file to cv::dnn::readNetFromTensorflow function.')
271
parser.add_argument('--input', required=True, help='Path to frozen TensorFlow graph.')
272
parser.add_argument('--output', required=True, help='Path to output text graph.')
273
parser.add_argument('--config', required=True, help='Path to a *.config file is used for training.')
274
args = parser.parse_args()
275
276
createSSDGraph(args.input, args.config, args.output)
277
278