glenn-jocher commited on
Commit
c3a93d7
1 Parent(s): c47be26

Add TensorFlow formats to `export.py` (#4479)

Browse files

* Initial commit

* Remove unused export_torchscript return

* ROOT variable

* Add prefix to fcn arg

* fix ROOT

* check_yaml into run()

* interim fixes

* imgsz=(320, 320)

* Hardcode tf_raw_resize False

* Finish opt elimination

* Update representative_dataset_gen()

* Update export.py with TF methods

* SiLU and GraphDef fixes

* file_size() directory handling feature

* export fixes

* add lambda: to representative_dataset

* Detect training False default

* Fuse false for TF models

* Embed agnostic NMS arguments

* Remove lambda

* TensorFlow.js export success

* Add pb to Usage

* Add *_tfjs_model/ to ignore files

* prepend YOLOv5 to function headers

* Remove end --- comments

* parameterize tfjs export pb file

* update run() data default /ROOT

* update --include help

* update imports

* return ct_model

* Consolidate TFLite export

* pb prerequisite to tfjs

* TF modules CamelCase

* Remove exports from tf.py and cleanup

* pass agnostic NMS arguments

* CI

* CI

* ignore *_web_model/

* Add tensorflow to CI dependencies

* CI tensorflow-cpu

* Update requirements.txt

* Remove tensorflow check_requirement

* CI coreml tfjs

* export only onnx torchscript

* reorder exports torchscript first

Files changed (8) hide show
  1. .dockerignore +1 -0
  2. .github/workflows/ci-testing.yml +4 -3
  3. .gitignore +1 -0
  4. detect.py +1 -1
  5. export.py +178 -41
  6. models/tf.py +161 -272
  7. requirements.txt +11 -9
  8. utils/general.py +9 -3
.dockerignore CHANGED
@@ -22,6 +22,7 @@ data/samples/*
22
  **/*.h5
23
  **/*.pb
24
  *_saved_model/
 
25
 
26
  # Below Copied From .gitignore -----------------------------------------------------------------------------------------
27
  # Below Copied From .gitignore -----------------------------------------------------------------------------------------
 
22
  **/*.h5
23
  **/*.pb
24
  *_saved_model/
25
+ *_web_model/
26
 
27
  # Below Copied From .gitignore -----------------------------------------------------------------------------------------
28
  # Below Copied From .gitignore -----------------------------------------------------------------------------------------
.github/workflows/ci-testing.yml CHANGED
@@ -48,7 +48,7 @@ jobs:
48
  run: |
49
  python -m pip install --upgrade pip
50
  pip install -qr requirements.txt -f https://download.pytorch.org/whl/cpu/torch_stable.html
51
- pip install -q onnx onnx-simplifier coremltools # for export
52
  python --version
53
  pip --version
54
  pip list
@@ -75,6 +75,7 @@ jobs:
75
  python val.py --img 128 --batch 16 --weights runs/train/exp/weights/last.pt --device $di
76
 
77
  python hubconf.py # hub
78
- python models/yolo.py --cfg ${{ matrix.model }}.yaml # inspect
79
- python export.py --img 128 --batch 1 --weights ${{ matrix.model }}.pt --include onnx torchscript # export
 
80
  shell: bash
 
48
  run: |
49
  python -m pip install --upgrade pip
50
  pip install -qr requirements.txt -f https://download.pytorch.org/whl/cpu/torch_stable.html
51
+ pip install -q onnx tensorflow-cpu # for export
52
  python --version
53
  pip --version
54
  pip list
 
75
  python val.py --img 128 --batch 16 --weights runs/train/exp/weights/last.pt --device $di
76
 
77
  python hubconf.py # hub
78
+ python models/yolo.py --cfg ${{ matrix.model }}.yaml # build PyTorch model
79
+ python models/tf.py --weights ${{ matrix.model }}.pt # build TensorFlow model
80
+ python export.py --img 128 --batch 1 --weights ${{ matrix.model }}.pt --include torchscript onnx # export
81
  shell: bash
.gitignore CHANGED
@@ -52,6 +52,7 @@ VOC/
52
  *.tflite
53
  *.h5
54
  *_saved_model/
 
55
  darknet53.conv.74
56
  yolov3-tiny.conv.15
57
 
 
52
  *.tflite
53
  *.h5
54
  *_saved_model/
55
+ *_web_model/
56
  darknet53.conv.74
57
  yolov3-tiny.conv.15
58
 
detect.py CHANGED
@@ -253,7 +253,7 @@ def run(weights='yolov5s.pt', # model.pt path(s)
253
 
254
  def parse_opt():
255
  parser = argparse.ArgumentParser()
256
- parser.add_argument('--weights', nargs='+', type=str, default='yolov5s.pt', help='model.pt path(s)')
257
  parser.add_argument('--source', type=str, default='data/images', help='file/dir/URL/glob, 0 for webcam')
258
  parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
259
  parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
 
253
 
254
  def parse_opt():
255
  parser = argparse.ArgumentParser()
256
+ parser.add_argument('--weights', nargs='+', type=str, default='yolov5s.pt', help='model path(s)')
257
  parser.add_argument('--source', type=str, default='data/images', help='file/dir/URL/glob, 0 for webcam')
258
  parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
259
  parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
export.py CHANGED
@@ -1,12 +1,28 @@
1
  # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
  """
3
- Export a PyTorch model to TorchScript, ONNX, CoreML formats
 
4
 
5
  Usage:
6
- $ python path/to/export.py --weights yolov5s.pt --img 640 --batch 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  """
8
 
9
  import argparse
 
10
  import sys
11
  import time
12
  from pathlib import Path
@@ -16,40 +32,42 @@ import torch.nn as nn
16
  from torch.utils.mobile_optimizer import optimize_for_mobile
17
 
18
  FILE = Path(__file__).resolve()
19
- sys.path.append(FILE.parents[0].as_posix()) # add yolov5/ to path
 
20
 
21
  from models.common import Conv
22
- from models.yolo import Detect
23
  from models.experimental import attempt_load
24
- from utils.activations import Hardswish, SiLU
25
- from utils.general import colorstr, check_img_size, check_requirements, file_size, set_logging
 
 
26
  from utils.torch_utils import select_device
27
 
28
 
29
- def export_torchscript(model, img, file, optimize):
30
- # TorchScript model export
31
- prefix = colorstr('TorchScript:')
32
  try:
33
  print(f'\n{prefix} starting export with torch {torch.__version__}...')
34
  f = file.with_suffix('.torchscript.pt')
35
- ts = torch.jit.trace(model, img, strict=False)
 
36
  (optimize_for_mobile(ts) if optimize else ts).save(f)
 
37
  print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
38
- return ts
39
  except Exception as e:
40
  print(f'{prefix} export failure: {e}')
41
 
42
 
43
- def export_onnx(model, img, file, opset, train, dynamic, simplify):
44
- # ONNX model export
45
- prefix = colorstr('ONNX:')
46
  try:
47
  check_requirements(('onnx',))
48
  import onnx
49
 
50
  print(f'\n{prefix} starting export with onnx {onnx.__version__}...')
51
  f = file.with_suffix('.onnx')
52
- torch.onnx.export(model, img, f, verbose=False, opset_version=opset,
 
53
  training=torch.onnx.TrainingMode.TRAINING if train else torch.onnx.TrainingMode.EVAL,
54
  do_constant_folding=not train,
55
  input_names=['images'],
@@ -73,7 +91,7 @@ def export_onnx(model, img, file, opset, train, dynamic, simplify):
73
  model_onnx, check = onnxsim.simplify(
74
  model_onnx,
75
  dynamic_input_shape=dynamic,
76
- input_shapes={'images': list(img.shape)} if dynamic else None)
77
  assert check, 'assert check failed'
78
  onnx.save(model_onnx, f)
79
  except Exception as e:
@@ -84,26 +102,131 @@ def export_onnx(model, img, file, opset, train, dynamic, simplify):
84
  print(f'{prefix} export failure: {e}')
85
 
86
 
87
- def export_coreml(model, img, file):
88
- # CoreML model export
89
- prefix = colorstr('CoreML:')
90
  try:
91
  check_requirements(('coremltools',))
92
  import coremltools as ct
93
 
94
  print(f'\n{prefix} starting export with coremltools {ct.__version__}...')
95
  f = file.with_suffix('.mlmodel')
 
96
  model.train() # CoreML exports should be placed in model.train() mode
97
- ts = torch.jit.trace(model, img, strict=False) # TorchScript model
98
- model = ct.convert(ts, inputs=[ct.ImageType('image', shape=img.shape, scale=1 / 255.0, bias=[0, 0, 0])])
99
- model.save(f)
 
100
  print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
101
  except Exception as e:
102
  print(f'\n{prefix} export failure: {e}')
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- def run(weights='./yolov5s.pt', # weights path
106
- img_size=(640, 640), # image (height, width)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  batch_size=1, # batch size
108
  device='cpu', # cuda device, i.e. 0 or 0,1,2,3 or cpu
109
  include=('torchscript', 'onnx', 'coreml'), # include formats
@@ -117,29 +240,28 @@ def run(weights='./yolov5s.pt', # weights path
117
  ):
118
  t = time.time()
119
  include = [x.lower() for x in include]
120
- img_size *= 2 if len(img_size) == 1 else 1 # expand
 
121
  file = Path(weights)
122
 
123
  # Load PyTorch model
124
  device = select_device(device)
125
  assert not (device.type == 'cpu' and half), '--half only compatible with GPU export, i.e. use --device 0'
126
- model = attempt_load(weights, map_location=device) # load FP32 model
127
- names = model.names
128
 
129
  # Input
130
  gs = int(max(model.stride)) # grid size (max stride)
131
- img_size = [check_img_size(x, gs) for x in img_size] # verify img_size are gs-multiples
132
- img = torch.zeros(batch_size, 3, *img_size).to(device) # image size(1,3,320,192) iDetection
133
 
134
  # Update model
135
  if half:
136
- img, model = img.half(), model.half() # to FP16
137
  model.train() if train else model.eval() # training mode = no Detect() layer grid construction
138
  for k, m in model.named_modules():
139
  if isinstance(m, Conv): # assign export-friendly activations
140
- if isinstance(m.act, nn.Hardswish):
141
- m.act = Hardswish()
142
- elif isinstance(m.act, nn.SiLU):
143
  m.act = SiLU()
144
  elif isinstance(m, Detect):
145
  m.inplace = inplace
@@ -147,16 +269,28 @@ def run(weights='./yolov5s.pt', # weights path
147
  # m.forward = m.forward_export # assign forward (optional)
148
 
149
  for _ in range(2):
150
- y = model(img) # dry runs
151
  print(f"\n{colorstr('PyTorch:')} starting from {weights} ({file_size(weights):.1f} MB)")
152
 
153
  # Exports
154
  if 'torchscript' in include:
155
- export_torchscript(model, img, file, optimize)
156
  if 'onnx' in include:
157
- export_onnx(model, img, file, opset, train, dynamic, simplify)
158
  if 'coreml' in include:
159
- export_coreml(model, img, file)
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  # Finish
162
  print(f'\nExport complete ({time.time() - t:.2f}s)'
@@ -166,18 +300,21 @@ def run(weights='./yolov5s.pt', # weights path
166
 
167
  def parse_opt():
168
  parser = argparse.ArgumentParser()
169
- parser.add_argument('--weights', type=str, default='./yolov5s.pt', help='weights path')
170
- parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='image (height, width)')
 
171
  parser.add_argument('--batch-size', type=int, default=1, help='batch size')
172
  parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
173
- parser.add_argument('--include', nargs='+', default=['torchscript', 'onnx', 'coreml'], help='include formats')
174
  parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
175
  parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
176
  parser.add_argument('--train', action='store_true', help='model.train() mode')
177
  parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
178
- parser.add_argument('--dynamic', action='store_true', help='ONNX: dynamic axes')
179
  parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
180
  parser.add_argument('--opset', type=int, default=13, help='ONNX: opset version')
 
 
 
181
  opt = parser.parse_args()
182
  return opt
183
 
 
1
  # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
  """
3
+ Export a YOLOv5 PyTorch model to TorchScript, ONNX, CoreML, TensorFlow (saved_model, pb, TFLite, TF.js,) formats
4
+ TensorFlow exports authored by https://github.com/zldrobit
5
 
6
  Usage:
7
+ $ python path/to/export.py --weights yolov5s.pt --include torchscript onnx coreml saved_model pb tflite tfjs
8
+
9
+ Inference:
10
+ $ python path/to/detect.py --weights yolov5s.pt
11
+ yolov5s.onnx (must export with --dynamic)
12
+ yolov5s_saved_model
13
+ yolov5s.pb
14
+ yolov5s.tflite
15
+
16
+ TensorFlow.js:
17
+ $ # Edit yolov5s_web_model/model.json to sort Identity* in ascending order
18
+ $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
19
+ $ npm install
20
+ $ ln -s ../../yolov5/yolov5s_web_model public/yolov5s_web_model
21
+ $ npm start
22
  """
23
 
24
  import argparse
25
+ import subprocess
26
  import sys
27
  import time
28
  from pathlib import Path
 
32
  from torch.utils.mobile_optimizer import optimize_for_mobile
33
 
34
  FILE = Path(__file__).resolve()
35
+ ROOT = FILE.parents[0] # yolov5/ dir
36
+ sys.path.append(ROOT.as_posix()) # add yolov5/ to path
37
 
38
  from models.common import Conv
 
39
  from models.experimental import attempt_load
40
+ from models.yolo import Detect
41
+ from utils.activations import SiLU
42
+ from utils.datasets import LoadImages
43
+ from utils.general import colorstr, check_dataset, check_img_size, check_requirements, file_size, set_logging
44
  from utils.torch_utils import select_device
45
 
46
 
47
+ def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')):
48
+ # YOLOv5 TorchScript model export
 
49
  try:
50
  print(f'\n{prefix} starting export with torch {torch.__version__}...')
51
  f = file.with_suffix('.torchscript.pt')
52
+
53
+ ts = torch.jit.trace(model, im, strict=False)
54
  (optimize_for_mobile(ts) if optimize else ts).save(f)
55
+
56
  print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
 
57
  except Exception as e:
58
  print(f'{prefix} export failure: {e}')
59
 
60
 
61
+ def export_onnx(model, im, file, opset, train, dynamic, simplify, prefix=colorstr('ONNX:')):
62
+ # YOLOv5 ONNX export
 
63
  try:
64
  check_requirements(('onnx',))
65
  import onnx
66
 
67
  print(f'\n{prefix} starting export with onnx {onnx.__version__}...')
68
  f = file.with_suffix('.onnx')
69
+
70
+ torch.onnx.export(model, im, f, verbose=False, opset_version=opset,
71
  training=torch.onnx.TrainingMode.TRAINING if train else torch.onnx.TrainingMode.EVAL,
72
  do_constant_folding=not train,
73
  input_names=['images'],
 
91
  model_onnx, check = onnxsim.simplify(
92
  model_onnx,
93
  dynamic_input_shape=dynamic,
94
+ input_shapes={'images': list(im.shape)} if dynamic else None)
95
  assert check, 'assert check failed'
96
  onnx.save(model_onnx, f)
97
  except Exception as e:
 
102
  print(f'{prefix} export failure: {e}')
103
 
104
 
105
+ def export_coreml(model, im, file, prefix=colorstr('CoreML:')):
106
+ # YOLOv5 CoreML export
107
+ ct_model = None
108
  try:
109
  check_requirements(('coremltools',))
110
  import coremltools as ct
111
 
112
  print(f'\n{prefix} starting export with coremltools {ct.__version__}...')
113
  f = file.with_suffix('.mlmodel')
114
+
115
  model.train() # CoreML exports should be placed in model.train() mode
116
+ ts = torch.jit.trace(model, im, strict=False) # TorchScript model
117
+ ct_model = ct.convert(ts, inputs=[ct.ImageType('image', shape=im.shape, scale=1 / 255.0, bias=[0, 0, 0])])
118
+ ct_model.save(f)
119
+
120
  print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
121
  except Exception as e:
122
  print(f'\n{prefix} export failure: {e}')
123
 
124
+ return ct_model
125
+
126
+
127
+ def export_saved_model(model, im, file, dynamic,
128
+ tf_nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45,
129
+ conf_thres=0.25, prefix=colorstr('TensorFlow saved_model:')):
130
+ # YOLOv5 TensorFlow saved_model export
131
+ keras_model = None
132
+ try:
133
+ import tensorflow as tf
134
+ from tensorflow import keras
135
+ from models.tf import TFModel, TFDetect
136
+
137
+ print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
138
+ f = str(file).replace('.pt', '_saved_model')
139
+ batch_size, ch, *imgsz = list(im.shape) # BCHW
140
 
141
+ tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz)
142
+ im = tf.zeros((batch_size, *imgsz, 3)) # BHWC order for TensorFlow
143
+ y = tf_model.predict(im, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
144
+ inputs = keras.Input(shape=(*imgsz, 3), batch_size=None if dynamic else batch_size)
145
+ outputs = tf_model.predict(inputs, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
146
+ keras_model = keras.Model(inputs=inputs, outputs=outputs)
147
+ keras_model.summary()
148
+ keras_model.save(f, save_format='tf')
149
+
150
+ print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
151
+ except Exception as e:
152
+ print(f'\n{prefix} export failure: {e}')
153
+
154
+ return keras_model
155
+
156
+
157
+ def export_pb(keras_model, im, file, prefix=colorstr('TensorFlow GraphDef:')):
158
+ # YOLOv5 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow
159
+ try:
160
+ import tensorflow as tf
161
+ from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
162
+
163
+ print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
164
+ f = file.with_suffix('.pb')
165
+
166
+ m = tf.function(lambda x: keras_model(x)) # full model
167
+ m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
168
+ frozen_func = convert_variables_to_constants_v2(m)
169
+ frozen_func.graph.as_graph_def()
170
+ tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)
171
+
172
+ print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
173
+ except Exception as e:
174
+ print(f'\n{prefix} export failure: {e}')
175
+
176
+
177
+ def export_tflite(keras_model, im, file, tfl_int8, data, ncalib, prefix=colorstr('TensorFlow Lite:')):
178
+ # YOLOv5 TensorFlow Lite export
179
+ try:
180
+ import tensorflow as tf
181
+ from models.tf import representative_dataset_gen
182
+
183
+ print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
184
+ batch_size, ch, *imgsz = list(im.shape) # BCHW
185
+ f = file.with_suffix('.tflite')
186
+
187
+ converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
188
+ converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
189
+ converter.optimizations = [tf.lite.Optimize.DEFAULT]
190
+ if tfl_int8:
191
+ dataset = LoadImages(check_dataset(data)['train'], img_size=imgsz, auto=False) # representative data
192
+ converter.representative_dataset = lambda: representative_dataset_gen(dataset, ncalib)
193
+ converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
194
+ converter.inference_input_type = tf.uint8 # or tf.int8
195
+ converter.inference_output_type = tf.uint8 # or tf.int8
196
+ converter.experimental_new_quantizer = False
197
+ f = str(file).replace('.pt', '-int8.tflite')
198
+
199
+ tflite_model = converter.convert()
200
+ open(f, "wb").write(tflite_model)
201
+ print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
202
+
203
+ except Exception as e:
204
+ print(f'\n{prefix} export failure: {e}')
205
+
206
+
207
+ def export_tfjs(keras_model, im, file, prefix=colorstr('TensorFlow.js:')):
208
+ # YOLOv5 TensorFlow.js export
209
+ try:
210
+ check_requirements(('tensorflowjs',))
211
+ import tensorflowjs as tfjs
212
+
213
+ print(f'\n{prefix} starting export with tensorflowjs {tfjs.__version__}...')
214
+ f = str(file).replace('.pt', '_web_model') # js dir
215
+ f_pb = file.with_suffix('.pb') # *.pb path
216
+
217
+ cmd = f"tensorflowjs_converter --input_format=tf_frozen_model " \
218
+ f"--output_node_names='Identity,Identity_1,Identity_2,Identity_3' {f_pb} {f}"
219
+ subprocess.run(cmd, shell=True)
220
+
221
+ print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
222
+ except Exception as e:
223
+ print(f'\n{prefix} export failure: {e}')
224
+
225
+
226
+ @torch.no_grad()
227
+ def run(data=ROOT / 'data/coco128.yaml', # 'dataset.yaml path'
228
+ weights=ROOT / 'yolov5s.pt', # weights path
229
+ imgsz=(640, 640), # image (height, width)
230
  batch_size=1, # batch size
231
  device='cpu', # cuda device, i.e. 0 or 0,1,2,3 or cpu
232
  include=('torchscript', 'onnx', 'coreml'), # include formats
 
240
  ):
241
  t = time.time()
242
  include = [x.lower() for x in include]
243
+ tf_exports = list(x in include for x in ('saved_model', 'pb', 'tflite', 'tfjs')) # TensorFlow exports
244
+ imgsz *= 2 if len(imgsz) == 1 else 1 # expand
245
  file = Path(weights)
246
 
247
  # Load PyTorch model
248
  device = select_device(device)
249
  assert not (device.type == 'cpu' and half), '--half only compatible with GPU export, i.e. use --device 0'
250
+ model = attempt_load(weights, map_location=device, inplace=True, fuse=not any(tf_exports)) # load FP32 model
251
+ nc, names = model.nc, model.names # number of classes, class names
252
 
253
  # Input
254
  gs = int(max(model.stride)) # grid size (max stride)
255
+ imgsz = [check_img_size(x, gs) for x in imgsz] # verify img_size are gs-multiples
256
+ im = torch.zeros(batch_size, 3, *imgsz).to(device) # image size(1,3,320,192) BCHW iDetection
257
 
258
  # Update model
259
  if half:
260
+ im, model = im.half(), model.half() # to FP16
261
  model.train() if train else model.eval() # training mode = no Detect() layer grid construction
262
  for k, m in model.named_modules():
263
  if isinstance(m, Conv): # assign export-friendly activations
264
+ if isinstance(m.act, nn.SiLU):
 
 
265
  m.act = SiLU()
266
  elif isinstance(m, Detect):
267
  m.inplace = inplace
 
269
  # m.forward = m.forward_export # assign forward (optional)
270
 
271
  for _ in range(2):
272
+ y = model(im) # dry runs
273
  print(f"\n{colorstr('PyTorch:')} starting from {weights} ({file_size(weights):.1f} MB)")
274
 
275
  # Exports
276
  if 'torchscript' in include:
277
+ export_torchscript(model, im, file, optimize)
278
  if 'onnx' in include:
279
+ export_onnx(model, im, file, opset, train, dynamic, simplify)
280
  if 'coreml' in include:
281
+ export_coreml(model, im, file)
282
+
283
+ # TensorFlow Exports
284
+ if any(tf_exports):
285
+ pb, tflite, tfjs = tf_exports[1:]
286
+ assert not (tflite and tfjs), 'TFLite and TF.js models must be exported separately, please pass only one type.'
287
+ model = export_saved_model(model, im, file, dynamic, tf_nms=tfjs, agnostic_nms=tfjs) # keras model
288
+ if pb or tfjs: # pb prerequisite to tfjs
289
+ export_pb(model, im, file)
290
+ if tflite:
291
+ export_tflite(model, im, file, tfl_int8=False, data=data, ncalib=100)
292
+ if tfjs:
293
+ export_tfjs(model, im, file)
294
 
295
  # Finish
296
  print(f'\nExport complete ({time.time() - t:.2f}s)'
 
300
 
301
  def parse_opt():
302
  parser = argparse.ArgumentParser()
303
+ parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
304
+ parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='weights path')
305
+ parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640, 640], help='image (h, w)')
306
  parser.add_argument('--batch-size', type=int, default=1, help='batch size')
307
  parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
 
308
  parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
309
  parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
310
  parser.add_argument('--train', action='store_true', help='model.train() mode')
311
  parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
312
+ parser.add_argument('--dynamic', action='store_true', help='ONNX/TF: dynamic axes')
313
  parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
314
  parser.add_argument('--opset', type=int, default=13, help='ONNX: opset version')
315
+ parser.add_argument('--include', nargs='+',
316
+ default=['torchscript', 'onnx'],
317
+ help='available formats are (torchscript, onnx, coreml, saved_model, pb, tflite, tfjs)')
318
  opt = parser.parse_args()
319
  return opt
320
 
models/tf.py CHANGED
@@ -1,67 +1,44 @@
1
  # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
  """
3
- TensorFlow/Keras and TFLite versions of YOLOv5
4
  Authored by https://github.com/zldrobit in PR https://github.com/ultralytics/yolov5/pull/1127
5
 
6
  Usage:
7
- $ python models/tf.py --weights yolov5s.pt --cfg yolov5s.yaml
8
-
9
- Export int8 TFLite models:
10
- $ python models/tf.py --weights yolov5s.pt --cfg models/yolov5s.yaml --tfl-int8 \
11
- --source path/to/images/ --ncalib 100
12
-
13
- Detection:
14
- $ python detect.py --weights yolov5s.pb --img 320
15
- $ python detect.py --weights yolov5s_saved_model --img 320
16
- $ python detect.py --weights yolov5s-fp16.tflite --img 320
17
- $ python detect.py --weights yolov5s-int8.tflite --img 320 --tfl-int8
18
-
19
- For TensorFlow.js:
20
- $ python models/tf.py --weights yolov5s.pt --cfg models/yolov5s.yaml --img 320 --tf-nms --agnostic-nms
21
- $ pip install tensorflowjs
22
- $ tensorflowjs_converter \
23
- --input_format=tf_frozen_model \
24
- --output_node_names='Identity,Identity_1,Identity_2,Identity_3' \
25
- yolov5s.pb \
26
- web_model
27
- $ # Edit web_model/model.json to sort Identity* in ascending order
28
- $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
29
- $ npm install
30
- $ ln -s ../../yolov5/web_model public/web_model
31
- $ npm start
32
  """
33
 
34
  import argparse
35
  import logging
36
- import os
37
  import sys
38
- import traceback
39
  from copy import deepcopy
40
  from pathlib import Path
41
 
42
- sys.path.append('./') # to run '$ python *.py' files in subdirectories
 
 
43
 
44
  import numpy as np
45
  import tensorflow as tf
46
  import torch
47
  import torch.nn as nn
48
- import yaml
49
  from tensorflow import keras
50
- from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
51
 
52
  from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, autopad, C3
53
  from models.experimental import MixConv2d, CrossConv, attempt_load
54
  from models.yolo import Detect
55
- from utils.datasets import LoadImages
56
- from utils.general import check_dataset, check_yaml, make_divisible
57
 
58
- logger = logging.getLogger(__name__)
59
 
60
 
61
- class tf_BN(keras.layers.Layer):
62
  # TensorFlow BatchNormalization wrapper
63
  def __init__(self, w=None):
64
- super(tf_BN, self).__init__()
65
  self.bn = keras.layers.BatchNormalization(
66
  beta_initializer=keras.initializers.Constant(w.bias.numpy()),
67
  gamma_initializer=keras.initializers.Constant(w.weight.numpy()),
@@ -73,20 +50,20 @@ class tf_BN(keras.layers.Layer):
73
  return self.bn(inputs)
74
 
75
 
76
- class tf_Pad(keras.layers.Layer):
77
  def __init__(self, pad):
78
- super(tf_Pad, self).__init__()
79
  self.pad = tf.constant([[0, 0], [pad, pad], [pad, pad], [0, 0]])
80
 
81
  def call(self, inputs):
82
  return tf.pad(inputs, self.pad, mode='constant', constant_values=0)
83
 
84
 
85
- class tf_Conv(keras.layers.Layer):
86
  # Standard convolution
87
  def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None):
88
  # ch_in, ch_out, weights, kernel, stride, padding, groups
89
- super(tf_Conv, self).__init__()
90
  assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument"
91
  assert isinstance(k, int), "Convolution with multiple kernels are not allowed."
92
  # TensorFlow convolution padding is inconsistent with PyTorch (e.g. k=3 s=2 'SAME' padding)
@@ -95,27 +72,29 @@ class tf_Conv(keras.layers.Layer):
95
  conv = keras.layers.Conv2D(
96
  c2, k, s, 'SAME' if s == 1 else 'VALID', use_bias=False,
97
  kernel_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()))
98
- self.conv = conv if s == 1 else keras.Sequential([tf_Pad(autopad(k, p)), conv])
99
- self.bn = tf_BN(w.bn) if hasattr(w, 'bn') else tf.identity
100
 
101
  # YOLOv5 activations
102
  if isinstance(w.act, nn.LeakyReLU):
103
  self.act = (lambda x: keras.activations.relu(x, alpha=0.1)) if act else tf.identity
104
  elif isinstance(w.act, nn.Hardswish):
105
  self.act = (lambda x: x * tf.nn.relu6(x + 3) * 0.166666667) if act else tf.identity
106
- elif isinstance(w.act, nn.SiLU):
107
  self.act = (lambda x: keras.activations.swish(x)) if act else tf.identity
 
 
108
 
109
  def call(self, inputs):
110
  return self.act(self.bn(self.conv(inputs)))
111
 
112
 
113
- class tf_Focus(keras.layers.Layer):
114
  # Focus wh information into c-space
115
  def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None):
116
  # ch_in, ch_out, kernel, stride, padding, groups
117
- super(tf_Focus, self).__init__()
118
- self.conv = tf_Conv(c1 * 4, c2, k, s, p, g, act, w.conv)
119
 
120
  def call(self, inputs): # x(b,w,h,c) -> y(b,w/2,h/2,4c)
121
  # inputs = inputs / 255. # normalize 0-255 to 0-1
@@ -125,23 +104,23 @@ class tf_Focus(keras.layers.Layer):
125
  inputs[:, 1::2, 1::2, :]], 3))
126
 
127
 
128
- class tf_Bottleneck(keras.layers.Layer):
129
  # Standard bottleneck
130
  def __init__(self, c1, c2, shortcut=True, g=1, e=0.5, w=None): # ch_in, ch_out, shortcut, groups, expansion
131
- super(tf_Bottleneck, self).__init__()
132
  c_ = int(c2 * e) # hidden channels
133
- self.cv1 = tf_Conv(c1, c_, 1, 1, w=w.cv1)
134
- self.cv2 = tf_Conv(c_, c2, 3, 1, g=g, w=w.cv2)
135
  self.add = shortcut and c1 == c2
136
 
137
  def call(self, inputs):
138
  return inputs + self.cv2(self.cv1(inputs)) if self.add else self.cv2(self.cv1(inputs))
139
 
140
 
141
- class tf_Conv2d(keras.layers.Layer):
142
  # Substitution for PyTorch nn.Conv2D
143
  def __init__(self, c1, c2, k, s=1, g=1, bias=True, w=None):
144
- super(tf_Conv2d, self).__init__()
145
  assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument"
146
  self.conv = keras.layers.Conv2D(
147
  c2, k, s, 'VALID', use_bias=bias,
@@ -152,19 +131,19 @@ class tf_Conv2d(keras.layers.Layer):
152
  return self.conv(inputs)
153
 
154
 
155
- class tf_BottleneckCSP(keras.layers.Layer):
156
  # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
157
  def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None):
158
  # ch_in, ch_out, number, shortcut, groups, expansion
159
- super(tf_BottleneckCSP, self).__init__()
160
  c_ = int(c2 * e) # hidden channels
161
- self.cv1 = tf_Conv(c1, c_, 1, 1, w=w.cv1)
162
- self.cv2 = tf_Conv2d(c1, c_, 1, 1, bias=False, w=w.cv2)
163
- self.cv3 = tf_Conv2d(c_, c_, 1, 1, bias=False, w=w.cv3)
164
- self.cv4 = tf_Conv(2 * c_, c2, 1, 1, w=w.cv4)
165
- self.bn = tf_BN(w.bn)
166
  self.act = lambda x: keras.activations.relu(x, alpha=0.1)
167
- self.m = keras.Sequential([tf_Bottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)])
168
 
169
  def call(self, inputs):
170
  y1 = self.cv3(self.m(self.cv1(inputs)))
@@ -172,28 +151,28 @@ class tf_BottleneckCSP(keras.layers.Layer):
172
  return self.cv4(self.act(self.bn(tf.concat((y1, y2), axis=3))))
173
 
174
 
175
- class tf_C3(keras.layers.Layer):
176
  # CSP Bottleneck with 3 convolutions
177
  def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None):
178
  # ch_in, ch_out, number, shortcut, groups, expansion
179
- super(tf_C3, self).__init__()
180
  c_ = int(c2 * e) # hidden channels
181
- self.cv1 = tf_Conv(c1, c_, 1, 1, w=w.cv1)
182
- self.cv2 = tf_Conv(c1, c_, 1, 1, w=w.cv2)
183
- self.cv3 = tf_Conv(2 * c_, c2, 1, 1, w=w.cv3)
184
- self.m = keras.Sequential([tf_Bottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)])
185
 
186
  def call(self, inputs):
187
  return self.cv3(tf.concat((self.m(self.cv1(inputs)), self.cv2(inputs)), axis=3))
188
 
189
 
190
- class tf_SPP(keras.layers.Layer):
191
  # Spatial pyramid pooling layer used in YOLOv3-SPP
192
  def __init__(self, c1, c2, k=(5, 9, 13), w=None):
193
- super(tf_SPP, self).__init__()
194
  c_ = c1 // 2 # hidden channels
195
- self.cv1 = tf_Conv(c1, c_, 1, 1, w=w.cv1)
196
- self.cv2 = tf_Conv(c_ * (len(k) + 1), c2, 1, 1, w=w.cv2)
197
  self.m = [keras.layers.MaxPool2D(pool_size=x, strides=1, padding='SAME') for x in k]
198
 
199
  def call(self, inputs):
@@ -201,9 +180,9 @@ class tf_SPP(keras.layers.Layer):
201
  return self.cv2(tf.concat([x] + [m(x) for m in self.m], 3))
202
 
203
 
204
- class tf_Detect(keras.layers.Layer):
205
- def __init__(self, nc=80, anchors=(), ch=(), w=None): # detection layer
206
- super(tf_Detect, self).__init__()
207
  self.stride = tf.convert_to_tensor(w.stride.numpy(), dtype=tf.float32)
208
  self.nc = nc # number of classes
209
  self.no = nc + 5 # number of outputs per anchor
@@ -213,22 +192,20 @@ class tf_Detect(keras.layers.Layer):
213
  self.anchors = tf.convert_to_tensor(w.anchors.numpy(), dtype=tf.float32)
214
  self.anchor_grid = tf.reshape(tf.convert_to_tensor(w.anchor_grid.numpy(), dtype=tf.float32),
215
  [self.nl, 1, -1, 1, 2])
216
- self.m = [tf_Conv2d(x, self.no * self.na, 1, w=w.m[i]) for i, x in enumerate(ch)]
217
- self.export = False # onnx export
218
- self.training = True # set to False after building model
219
  for i in range(self.nl):
220
- ny, nx = opt.img_size[0] // self.stride[i], opt.img_size[1] // self.stride[i]
221
  self.grid[i] = self._make_grid(nx, ny)
222
 
223
  def call(self, inputs):
224
- # x = x.copy() # for profiling
225
  z = [] # inference output
226
- self.training |= self.export
227
  x = []
228
  for i in range(self.nl):
229
  x.append(self.m[i](inputs[i]))
230
  # x(bs,20,20,255) to x(bs,3,20,20,85)
231
- ny, nx = opt.img_size[0] // self.stride[i], opt.img_size[1] // self.stride[i]
232
  x[i] = tf.transpose(tf.reshape(x[i], [-1, ny * nx, self.na, self.no]), [0, 2, 1, 3])
233
 
234
  if not self.training: # inference
@@ -236,8 +213,8 @@ class tf_Detect(keras.layers.Layer):
236
  xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
237
  wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]
238
  # Normalize xywh to 0-1 to reduce calibration error
239
- xy /= tf.constant([[opt.img_size[1], opt.img_size[0]]], dtype=tf.float32)
240
- wh /= tf.constant([[opt.img_size[1], opt.img_size[0]]], dtype=tf.float32)
241
  y = tf.concat([xy, wh, y[..., 4:]], -1)
242
  z.append(tf.reshape(y, [-1, 3 * ny * nx, self.no]))
243
 
@@ -251,25 +228,23 @@ class tf_Detect(keras.layers.Layer):
251
  return tf.cast(tf.reshape(tf.stack([xv, yv], 2), [1, 1, ny * nx, 2]), dtype=tf.float32)
252
 
253
 
254
- class tf_Upsample(keras.layers.Layer):
255
- def __init__(self, size, scale_factor, mode, w=None):
256
- super(tf_Upsample, self).__init__()
257
  assert scale_factor == 2, "scale_factor must be 2"
 
258
  # self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode)
259
- if opt.tf_raw_resize:
260
- # with default arguments: align_corners=False, half_pixel_centers=False
261
- self.upsample = lambda x: tf.raw_ops.ResizeNearestNeighbor(images=x,
262
- size=(x.shape[1] * 2, x.shape[2] * 2))
263
- else:
264
- self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * 2, x.shape[2] * 2), method=mode)
265
 
266
  def call(self, inputs):
267
  return self.upsample(inputs)
268
 
269
 
270
- class tf_Concat(keras.layers.Layer):
271
  def __init__(self, dimension=1, w=None):
272
- super(tf_Concat, self).__init__()
273
  assert dimension == 1, "convert only NCHW to NHWC concat"
274
  self.d = 3
275
 
@@ -277,8 +252,8 @@ class tf_Concat(keras.layers.Layer):
277
  return tf.concat(inputs, self.d)
278
 
279
 
280
- def parse_model(d, ch, model): # model_dict, input_channels(3)
281
- logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
282
  anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
283
  na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
284
  no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
@@ -310,10 +285,11 @@ def parse_model(d, ch, model): # model_dict, input_channels(3)
310
  args.append([ch[x + 1] for x in f])
311
  if isinstance(args[1], int): # number of anchors
312
  args[1] = [list(range(args[1] * 2))] * len(f)
 
313
  else:
314
  c2 = ch[f]
315
 
316
- tf_m = eval('tf_' + m_str.replace('nn.', ''))
317
  m_ = keras.Sequential([tf_m(*args, w=model.model[i][j]) for j in range(n)]) if n > 1 \
318
  else tf_m(*args, w=model.model[i]) # module
319
 
@@ -321,16 +297,16 @@ def parse_model(d, ch, model): # model_dict, input_channels(3)
321
  t = str(m)[8:-2].replace('__main__.', '') # module type
322
  np = sum([x.numel() for x in torch_m_.parameters()]) # number params
323
  m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
324
- logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
325
  save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
326
  layers.append(m_)
327
  ch.append(c2)
328
  return keras.Sequential(layers), sorted(save)
329
 
330
 
331
- class tf_Model():
332
- def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, model=None): # model, input channels, number of classes
333
- super(tf_Model, self).__init__()
334
  if isinstance(cfg, dict):
335
  self.yaml = cfg # model dict
336
  else: # is *.yaml
@@ -343,9 +319,10 @@ class tf_Model():
343
  if nc and nc != self.yaml['nc']:
344
  print('Overriding %s nc=%g with nc=%g' % (cfg, self.yaml['nc'], nc))
345
  self.yaml['nc'] = nc # override yaml value
346
- self.model, self.savelist = parse_model(deepcopy(self.yaml), ch=[ch], model=model) # model, savelist, ch_out
347
 
348
- def predict(self, inputs, profile=False):
 
349
  y = [] # outputs
350
  x = inputs
351
  for i, m in enumerate(self.model.layers):
@@ -356,18 +333,18 @@ class tf_Model():
356
  y.append(x if m.i in self.savelist else None) # save output
357
 
358
  # Add TensorFlow NMS
359
- if opt.tf_nms:
360
- boxes = xywh2xyxy(x[0][..., :4])
361
  probs = x[0][:, :, 4:5]
362
  classes = x[0][:, :, 5:]
363
  scores = probs * classes
364
- if opt.agnostic_nms:
365
- nms = agnostic_nms_layer()((boxes, classes, scores))
366
  return nms, x[1]
367
  else:
368
  boxes = tf.expand_dims(boxes, 2)
369
  nms = tf.image.combined_non_max_suppression(
370
- boxes, scores, opt.topk_per_class, opt.topk_all, opt.iou_thres, opt.score_thres, clip_boxes=False)
371
  return nms, x[1]
372
 
373
  return x[0] # output only first tensor [1,6300,85] = [xywh, conf, class0, class1, ...]
@@ -377,182 +354,94 @@ class tf_Model():
377
  # cls = tf.reshape(tf.cast(tf.argmax(x[..., 5:], axis=1), tf.float32), (-1, 1)) # x(6300,1) classes
378
  # return tf.concat([conf, cls, xywh], 1)
379
 
 
 
 
 
 
 
380
 
381
- class agnostic_nms_layer(keras.layers.Layer):
382
- # wrap map_fn to avoid TypeSpec related error https://stackoverflow.com/a/65809989/3036450
383
- def call(self, input):
384
- return tf.map_fn(agnostic_nms, input,
 
385
  fn_output_signature=(tf.float32, tf.float32, tf.float32, tf.int32),
386
  name='agnostic_nms')
387
 
388
-
389
- def agnostic_nms(x):
390
- boxes, classes, scores = x
391
- class_inds = tf.cast(tf.argmax(classes, axis=-1), tf.float32)
392
- scores_inp = tf.reduce_max(scores, -1)
393
- selected_inds = tf.image.non_max_suppression(
394
- boxes, scores_inp, max_output_size=opt.topk_all, iou_threshold=opt.iou_thres, score_threshold=opt.score_thres)
395
- selected_boxes = tf.gather(boxes, selected_inds)
396
- padded_boxes = tf.pad(selected_boxes,
397
- paddings=[[0, opt.topk_all - tf.shape(selected_boxes)[0]], [0, 0]],
398
- mode="CONSTANT", constant_values=0.0)
399
- selected_scores = tf.gather(scores_inp, selected_inds)
400
- padded_scores = tf.pad(selected_scores,
401
- paddings=[[0, opt.topk_all - tf.shape(selected_boxes)[0]]],
402
- mode="CONSTANT", constant_values=-1.0)
403
- selected_classes = tf.gather(class_inds, selected_inds)
404
- padded_classes = tf.pad(selected_classes,
405
- paddings=[[0, opt.topk_all - tf.shape(selected_boxes)[0]]],
406
- mode="CONSTANT", constant_values=-1.0)
407
- valid_detections = tf.shape(selected_inds)[0]
408
- return padded_boxes, padded_scores, padded_classes, valid_detections
409
-
410
-
411
- def xywh2xyxy(xywh):
412
- # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
413
- x, y, w, h = tf.split(xywh, num_or_size_splits=4, axis=-1)
414
- return tf.concat([x - w / 2, y - h / 2, x + w / 2, y + h / 2], axis=-1)
415
-
416
-
417
- def representative_dataset_gen():
418
- # Representative dataset for use with converter.representative_dataset
419
- n = 0
420
- for path, img, im0s, vid_cap in dataset:
421
- # Get sample input data as a numpy array in a method of your choosing.
422
- n += 1
423
  input = np.transpose(img, [1, 2, 0])
424
  input = np.expand_dims(input, axis=0).astype(np.float32)
425
  input /= 255.0
426
  yield [input]
427
- if n >= opt.ncalib:
428
  break
429
 
430
 
431
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  parser = argparse.ArgumentParser()
433
- parser.add_argument('--cfg', type=str, default='yolov5s.yaml', help='cfg path')
434
- parser.add_argument('--weights', type=str, default='yolov5s.pt', help='weights path')
435
- parser.add_argument('--img-size', nargs='+', type=int, default=[320, 320], help='image size') # height, width
436
  parser.add_argument('--batch-size', type=int, default=1, help='batch size')
437
- parser.add_argument('--dynamic-batch-size', action='store_true', help='dynamic batch size')
438
- parser.add_argument('--source', type=str, default='../data/coco128.yaml', help='dir of images or data.yaml file')
439
- parser.add_argument('--ncalib', type=int, default=100, help='number of calibration images')
440
- parser.add_argument('--tfl-int8', action='store_true', dest='tfl_int8', help='export TFLite int8 model')
441
- parser.add_argument('--tf-nms', action='store_true', dest='tf_nms', help='TF NMS (without TFLite export)')
442
- parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
443
- parser.add_argument('--tf-raw-resize', action='store_true', dest='tf_raw_resize',
444
- help='use tf.raw_ops.ResizeNearestNeighbor for resize')
445
- parser.add_argument('--topk-per-class', type=int, default=100, help='topk per class to keep in NMS')
446
- parser.add_argument('--topk-all', type=int, default=100, help='topk for all classes to keep in NMS')
447
- parser.add_argument('--iou-thres', type=float, default=0.5, help='IOU threshold for NMS')
448
- parser.add_argument('--score-thres', type=float, default=0.4, help='score threshold for NMS')
449
  opt = parser.parse_args()
450
- opt.cfg = check_yaml(opt.cfg) # check YAML
451
- opt.img_size *= 2 if len(opt.img_size) == 1 else 1 # expand
452
- print(opt)
453
-
454
- # Input
455
- img = torch.zeros((opt.batch_size, 3, *opt.img_size)) # image size(1,3,320,192) iDetection
456
-
457
- # Load PyTorch model
458
- model = attempt_load(opt.weights, map_location=torch.device('cpu'), inplace=True, fuse=False)
459
- model.model[-1].export = False # set Detect() layer export=True
460
- y = model(img) # dry run
461
- nc = y[0].shape[-1] - 5
462
-
463
- # TensorFlow saved_model export
464
- try:
465
- print('\nStarting TensorFlow saved_model export with TensorFlow %s...' % tf.__version__)
466
- tf_model = tf_Model(opt.cfg, model=model, nc=nc)
467
- img = tf.zeros((opt.batch_size, *opt.img_size, 3)) # NHWC Input for TensorFlow
468
-
469
- m = tf_model.model.layers[-1]
470
- assert isinstance(m, tf_Detect), "the last layer must be Detect"
471
- m.training = False
472
- y = tf_model.predict(img)
473
-
474
- inputs = keras.Input(shape=(*opt.img_size, 3), batch_size=None if opt.dynamic_batch_size else opt.batch_size)
475
- keras_model = keras.Model(inputs=inputs, outputs=tf_model.predict(inputs))
476
- keras_model.summary()
477
- path = opt.weights.replace('.pt', '_saved_model') # filename
478
- keras_model.save(path, save_format='tf')
479
- print('TensorFlow saved_model export success, saved as %s' % path)
480
- except Exception as e:
481
- print('TensorFlow saved_model export failure: %s' % e)
482
- traceback.print_exc(file=sys.stdout)
483
-
484
- # TensorFlow GraphDef export
485
- try:
486
- print('\nStarting TensorFlow GraphDef export with TensorFlow %s...' % tf.__version__)
487
-
488
- # https://github.com/leimao/Frozen_Graph_TensorFlow
489
- full_model = tf.function(lambda x: keras_model(x))
490
- full_model = full_model.get_concrete_function(
491
- tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
492
-
493
- frozen_func = convert_variables_to_constants_v2(full_model)
494
- frozen_func.graph.as_graph_def()
495
- f = opt.weights.replace('.pt', '.pb') # filename
496
- tf.io.write_graph(graph_or_graph_def=frozen_func.graph,
497
- logdir=os.path.dirname(f),
498
- name=os.path.basename(f),
499
- as_text=False)
500
-
501
- print('TensorFlow GraphDef export success, saved as %s' % f)
502
- except Exception as e:
503
- print('TensorFlow GraphDef export failure: %s' % e)
504
- traceback.print_exc(file=sys.stdout)
505
-
506
- # TFLite model export
507
- if not opt.tf_nms:
508
- try:
509
- print('\nStarting TFLite export with TensorFlow %s...' % tf.__version__)
510
-
511
- # fp32 TFLite model export ---------------------------------------------------------------------------------
512
- # converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
513
- # converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
514
- # converter.allow_custom_ops = False
515
- # converter.experimental_new_converter = True
516
- # tflite_model = converter.convert()
517
- # f = opt.weights.replace('.pt', '.tflite') # filename
518
- # open(f, "wb").write(tflite_model)
519
-
520
- # fp16 TFLite model export ---------------------------------------------------------------------------------
521
- converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
522
- converter.optimizations = [tf.lite.Optimize.DEFAULT]
523
- # converter.representative_dataset = representative_dataset_gen
524
- # converter.target_spec.supported_types = [tf.float16]
525
- converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
526
- converter.allow_custom_ops = False
527
- converter.experimental_new_converter = True
528
- tflite_model = converter.convert()
529
- f = opt.weights.replace('.pt', '-fp16.tflite') # filename
530
- open(f, "wb").write(tflite_model)
531
- print('\nTFLite export success, saved as %s' % f)
532
-
533
- # int8 TFLite model export ---------------------------------------------------------------------------------
534
- if opt.tfl_int8:
535
- # Representative Dataset
536
- if opt.source.endswith('.yaml'):
537
- with open(check_yaml(opt.source)) as f:
538
- data = yaml.load(f, Loader=yaml.FullLoader) # data dict
539
- check_dataset(data) # check
540
- opt.source = data['train']
541
- dataset = LoadImages(opt.source, img_size=opt.img_size, auto=False)
542
- converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
543
- converter.optimizations = [tf.lite.Optimize.DEFAULT]
544
- converter.representative_dataset = representative_dataset_gen
545
- converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
546
- converter.inference_input_type = tf.uint8 # or tf.int8
547
- converter.inference_output_type = tf.uint8 # or tf.int8
548
- converter.allow_custom_ops = False
549
- converter.experimental_new_converter = True
550
- converter.experimental_new_quantizer = False
551
- tflite_model = converter.convert()
552
- f = opt.weights.replace('.pt', '-int8.tflite') # filename
553
- open(f, "wb").write(tflite_model)
554
- print('\nTFLite (int8) export success, saved as %s' % f)
555
-
556
- except Exception as e:
557
- print('\nTFLite export failure: %s' % e)
558
- traceback.print_exc(file=sys.stdout)
 
1
  # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
  """
3
+ TensorFlow, Keras and TFLite versions of YOLOv5
4
  Authored by https://github.com/zldrobit in PR https://github.com/ultralytics/yolov5/pull/1127
5
 
6
  Usage:
7
+ $ python models/tf.py --weights yolov5s.pt
8
+
9
+ Export:
10
+ $ python path/to/export.py --weights yolov5s.pt --include saved_model pb tflite tfjs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  """
12
 
13
  import argparse
14
  import logging
 
15
  import sys
 
16
  from copy import deepcopy
17
  from pathlib import Path
18
 
19
+ FILE = Path(__file__).resolve()
20
+ ROOT = FILE.parents[1] # yolov5/ dir
21
+ sys.path.append(ROOT.as_posix()) # add yolov5/ to path
22
 
23
  import numpy as np
24
  import tensorflow as tf
25
  import torch
26
  import torch.nn as nn
 
27
  from tensorflow import keras
 
28
 
29
  from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, autopad, C3
30
  from models.experimental import MixConv2d, CrossConv, attempt_load
31
  from models.yolo import Detect
32
+ from utils.general import colorstr, make_divisible, set_logging
33
+ from utils.activations import SiLU
34
 
35
+ LOGGER = logging.getLogger(__name__)
36
 
37
 
38
+ class TFBN(keras.layers.Layer):
39
  # TensorFlow BatchNormalization wrapper
40
  def __init__(self, w=None):
41
+ super(TFBN, self).__init__()
42
  self.bn = keras.layers.BatchNormalization(
43
  beta_initializer=keras.initializers.Constant(w.bias.numpy()),
44
  gamma_initializer=keras.initializers.Constant(w.weight.numpy()),
 
50
  return self.bn(inputs)
51
 
52
 
53
+ class TFPad(keras.layers.Layer):
54
  def __init__(self, pad):
55
+ super(TFPad, self).__init__()
56
  self.pad = tf.constant([[0, 0], [pad, pad], [pad, pad], [0, 0]])
57
 
58
  def call(self, inputs):
59
  return tf.pad(inputs, self.pad, mode='constant', constant_values=0)
60
 
61
 
62
+ class TFConv(keras.layers.Layer):
63
  # Standard convolution
64
  def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None):
65
  # ch_in, ch_out, weights, kernel, stride, padding, groups
66
+ super(TFConv, self).__init__()
67
  assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument"
68
  assert isinstance(k, int), "Convolution with multiple kernels are not allowed."
69
  # TensorFlow convolution padding is inconsistent with PyTorch (e.g. k=3 s=2 'SAME' padding)
 
72
  conv = keras.layers.Conv2D(
73
  c2, k, s, 'SAME' if s == 1 else 'VALID', use_bias=False,
74
  kernel_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()))
75
+ self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv])
76
+ self.bn = TFBN(w.bn) if hasattr(w, 'bn') else tf.identity
77
 
78
  # YOLOv5 activations
79
  if isinstance(w.act, nn.LeakyReLU):
80
  self.act = (lambda x: keras.activations.relu(x, alpha=0.1)) if act else tf.identity
81
  elif isinstance(w.act, nn.Hardswish):
82
  self.act = (lambda x: x * tf.nn.relu6(x + 3) * 0.166666667) if act else tf.identity
83
+ elif isinstance(w.act, (nn.SiLU, SiLU)):
84
  self.act = (lambda x: keras.activations.swish(x)) if act else tf.identity
85
+ else:
86
+ raise Exception(f'no matching TensorFlow activation found for {w.act}')
87
 
88
  def call(self, inputs):
89
  return self.act(self.bn(self.conv(inputs)))
90
 
91
 
92
+ class TFFocus(keras.layers.Layer):
93
  # Focus wh information into c-space
94
  def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True, w=None):
95
  # ch_in, ch_out, kernel, stride, padding, groups
96
+ super(TFFocus, self).__init__()
97
+ self.conv = TFConv(c1 * 4, c2, k, s, p, g, act, w.conv)
98
 
99
  def call(self, inputs): # x(b,w,h,c) -> y(b,w/2,h/2,4c)
100
  # inputs = inputs / 255. # normalize 0-255 to 0-1
 
104
  inputs[:, 1::2, 1::2, :]], 3))
105
 
106
 
107
+ class TFBottleneck(keras.layers.Layer):
108
  # Standard bottleneck
109
  def __init__(self, c1, c2, shortcut=True, g=1, e=0.5, w=None): # ch_in, ch_out, shortcut, groups, expansion
110
+ super(TFBottleneck, self).__init__()
111
  c_ = int(c2 * e) # hidden channels
112
+ self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
113
+ self.cv2 = TFConv(c_, c2, 3, 1, g=g, w=w.cv2)
114
  self.add = shortcut and c1 == c2
115
 
116
  def call(self, inputs):
117
  return inputs + self.cv2(self.cv1(inputs)) if self.add else self.cv2(self.cv1(inputs))
118
 
119
 
120
+ class TFConv2d(keras.layers.Layer):
121
  # Substitution for PyTorch nn.Conv2D
122
  def __init__(self, c1, c2, k, s=1, g=1, bias=True, w=None):
123
+ super(TFConv2d, self).__init__()
124
  assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument"
125
  self.conv = keras.layers.Conv2D(
126
  c2, k, s, 'VALID', use_bias=bias,
 
131
  return self.conv(inputs)
132
 
133
 
134
+ class TFBottleneckCSP(keras.layers.Layer):
135
  # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
136
  def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None):
137
  # ch_in, ch_out, number, shortcut, groups, expansion
138
+ super(TFBottleneckCSP, self).__init__()
139
  c_ = int(c2 * e) # hidden channels
140
+ self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
141
+ self.cv2 = TFConv2d(c1, c_, 1, 1, bias=False, w=w.cv2)
142
+ self.cv3 = TFConv2d(c_, c_, 1, 1, bias=False, w=w.cv3)
143
+ self.cv4 = TFConv(2 * c_, c2, 1, 1, w=w.cv4)
144
+ self.bn = TFBN(w.bn)
145
  self.act = lambda x: keras.activations.relu(x, alpha=0.1)
146
+ self.m = keras.Sequential([TFBottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)])
147
 
148
  def call(self, inputs):
149
  y1 = self.cv3(self.m(self.cv1(inputs)))
 
151
  return self.cv4(self.act(self.bn(tf.concat((y1, y2), axis=3))))
152
 
153
 
154
+ class TFC3(keras.layers.Layer):
155
  # CSP Bottleneck with 3 convolutions
156
  def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, w=None):
157
  # ch_in, ch_out, number, shortcut, groups, expansion
158
+ super(TFC3, self).__init__()
159
  c_ = int(c2 * e) # hidden channels
160
+ self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
161
+ self.cv2 = TFConv(c1, c_, 1, 1, w=w.cv2)
162
+ self.cv3 = TFConv(2 * c_, c2, 1, 1, w=w.cv3)
163
+ self.m = keras.Sequential([TFBottleneck(c_, c_, shortcut, g, e=1.0, w=w.m[j]) for j in range(n)])
164
 
165
  def call(self, inputs):
166
  return self.cv3(tf.concat((self.m(self.cv1(inputs)), self.cv2(inputs)), axis=3))
167
 
168
 
169
+ class TFSPP(keras.layers.Layer):
170
  # Spatial pyramid pooling layer used in YOLOv3-SPP
171
  def __init__(self, c1, c2, k=(5, 9, 13), w=None):
172
+ super(TFSPP, self).__init__()
173
  c_ = c1 // 2 # hidden channels
174
+ self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
175
+ self.cv2 = TFConv(c_ * (len(k) + 1), c2, 1, 1, w=w.cv2)
176
  self.m = [keras.layers.MaxPool2D(pool_size=x, strides=1, padding='SAME') for x in k]
177
 
178
  def call(self, inputs):
 
180
  return self.cv2(tf.concat([x] + [m(x) for m in self.m], 3))
181
 
182
 
183
+ class TFDetect(keras.layers.Layer):
184
+ def __init__(self, nc=80, anchors=(), ch=(), imgsz=(640, 640), w=None): # detection layer
185
+ super(TFDetect, self).__init__()
186
  self.stride = tf.convert_to_tensor(w.stride.numpy(), dtype=tf.float32)
187
  self.nc = nc # number of classes
188
  self.no = nc + 5 # number of outputs per anchor
 
192
  self.anchors = tf.convert_to_tensor(w.anchors.numpy(), dtype=tf.float32)
193
  self.anchor_grid = tf.reshape(tf.convert_to_tensor(w.anchor_grid.numpy(), dtype=tf.float32),
194
  [self.nl, 1, -1, 1, 2])
195
+ self.m = [TFConv2d(x, self.no * self.na, 1, w=w.m[i]) for i, x in enumerate(ch)]
196
+ self.training = False # set to False after building model
197
+ self.imgsz = imgsz
198
  for i in range(self.nl):
199
+ ny, nx = self.imgsz[0] // self.stride[i], self.imgsz[1] // self.stride[i]
200
  self.grid[i] = self._make_grid(nx, ny)
201
 
202
  def call(self, inputs):
 
203
  z = [] # inference output
 
204
  x = []
205
  for i in range(self.nl):
206
  x.append(self.m[i](inputs[i]))
207
  # x(bs,20,20,255) to x(bs,3,20,20,85)
208
+ ny, nx = self.imgsz[0] // self.stride[i], self.imgsz[1] // self.stride[i]
209
  x[i] = tf.transpose(tf.reshape(x[i], [-1, ny * nx, self.na, self.no]), [0, 2, 1, 3])
210
 
211
  if not self.training: # inference
 
213
  xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
214
  wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]
215
  # Normalize xywh to 0-1 to reduce calibration error
216
+ xy /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32)
217
+ wh /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32)
218
  y = tf.concat([xy, wh, y[..., 4:]], -1)
219
  z.append(tf.reshape(y, [-1, 3 * ny * nx, self.no]))
220
 
 
228
  return tf.cast(tf.reshape(tf.stack([xv, yv], 2), [1, 1, ny * nx, 2]), dtype=tf.float32)
229
 
230
 
231
+ class TFUpsample(keras.layers.Layer):
232
+ def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w'
233
+ super(TFUpsample, self).__init__()
234
  assert scale_factor == 2, "scale_factor must be 2"
235
+ self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * 2, x.shape[2] * 2), method=mode)
236
  # self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode)
237
+ # with default arguments: align_corners=False, half_pixel_centers=False
238
+ # self.upsample = lambda x: tf.raw_ops.ResizeNearestNeighbor(images=x,
239
+ # size=(x.shape[1] * 2, x.shape[2] * 2))
 
 
 
240
 
241
  def call(self, inputs):
242
  return self.upsample(inputs)
243
 
244
 
245
+ class TFConcat(keras.layers.Layer):
246
  def __init__(self, dimension=1, w=None):
247
+ super(TFConcat, self).__init__()
248
  assert dimension == 1, "convert only NCHW to NHWC concat"
249
  self.d = 3
250
 
 
252
  return tf.concat(inputs, self.d)
253
 
254
 
255
+ def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3)
256
+ LOGGER.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
257
  anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
258
  na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
259
  no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
 
285
  args.append([ch[x + 1] for x in f])
286
  if isinstance(args[1], int): # number of anchors
287
  args[1] = [list(range(args[1] * 2))] * len(f)
288
+ args.append(imgsz)
289
  else:
290
  c2 = ch[f]
291
 
292
+ tf_m = eval('TF' + m_str.replace('nn.', ''))
293
  m_ = keras.Sequential([tf_m(*args, w=model.model[i][j]) for j in range(n)]) if n > 1 \
294
  else tf_m(*args, w=model.model[i]) # module
295
 
 
297
  t = str(m)[8:-2].replace('__main__.', '') # module type
298
  np = sum([x.numel() for x in torch_m_.parameters()]) # number params
299
  m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
300
+ LOGGER.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
301
  save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
302
  layers.append(m_)
303
  ch.append(c2)
304
  return keras.Sequential(layers), sorted(save)
305
 
306
 
307
+ class TFModel:
308
+ def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, model=None, imgsz=(640, 640)): # model, channels, classes
309
+ super(TFModel, self).__init__()
310
  if isinstance(cfg, dict):
311
  self.yaml = cfg # model dict
312
  else: # is *.yaml
 
319
  if nc and nc != self.yaml['nc']:
320
  print('Overriding %s nc=%g with nc=%g' % (cfg, self.yaml['nc'], nc))
321
  self.yaml['nc'] = nc # override yaml value
322
+ self.model, self.savelist = parse_model(deepcopy(self.yaml), ch=[ch], model=model, imgsz=imgsz)
323
 
324
+ def predict(self, inputs, tf_nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45,
325
+ conf_thres=0.25):
326
  y = [] # outputs
327
  x = inputs
328
  for i, m in enumerate(self.model.layers):
 
333
  y.append(x if m.i in self.savelist else None) # save output
334
 
335
  # Add TensorFlow NMS
336
+ if tf_nms:
337
+ boxes = self._xywh2xyxy(x[0][..., :4])
338
  probs = x[0][:, :, 4:5]
339
  classes = x[0][:, :, 5:]
340
  scores = probs * classes
341
+ if agnostic_nms:
342
+ nms = AgnosticNMS()((boxes, classes, scores), topk_all, iou_thres, conf_thres)
343
  return nms, x[1]
344
  else:
345
  boxes = tf.expand_dims(boxes, 2)
346
  nms = tf.image.combined_non_max_suppression(
347
+ boxes, scores, topk_per_class, topk_all, iou_thres, conf_thres, clip_boxes=False)
348
  return nms, x[1]
349
 
350
  return x[0] # output only first tensor [1,6300,85] = [xywh, conf, class0, class1, ...]
 
354
  # cls = tf.reshape(tf.cast(tf.argmax(x[..., 5:], axis=1), tf.float32), (-1, 1)) # x(6300,1) classes
355
  # return tf.concat([conf, cls, xywh], 1)
356
 
357
+ @staticmethod
358
+ def _xywh2xyxy(xywh):
359
+ # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
360
+ x, y, w, h = tf.split(xywh, num_or_size_splits=4, axis=-1)
361
+ return tf.concat([x - w / 2, y - h / 2, x + w / 2, y + h / 2], axis=-1)
362
+
363
 
364
+ class AgnosticNMS(keras.layers.Layer):
365
+ # TF Agnostic NMS
366
+ def call(self, input, topk_all, iou_thres, conf_thres):
367
+ # wrap map_fn to avoid TypeSpec related error https://stackoverflow.com/a/65809989/3036450
368
+ return tf.map_fn(self._nms, input,
369
  fn_output_signature=(tf.float32, tf.float32, tf.float32, tf.int32),
370
  name='agnostic_nms')
371
 
372
+ @staticmethod
373
+ def _nms(x, topk_all=100, iou_thres=0.45, conf_thres=0.25): # agnostic NMS
374
+ boxes, classes, scores = x
375
+ class_inds = tf.cast(tf.argmax(classes, axis=-1), tf.float32)
376
+ scores_inp = tf.reduce_max(scores, -1)
377
+ selected_inds = tf.image.non_max_suppression(
378
+ boxes, scores_inp, max_output_size=topk_all, iou_threshold=iou_thres, score_threshold=conf_thres)
379
+ selected_boxes = tf.gather(boxes, selected_inds)
380
+ padded_boxes = tf.pad(selected_boxes,
381
+ paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]],
382
+ mode="CONSTANT", constant_values=0.0)
383
+ selected_scores = tf.gather(scores_inp, selected_inds)
384
+ padded_scores = tf.pad(selected_scores,
385
+ paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
386
+ mode="CONSTANT", constant_values=-1.0)
387
+ selected_classes = tf.gather(class_inds, selected_inds)
388
+ padded_classes = tf.pad(selected_classes,
389
+ paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
390
+ mode="CONSTANT", constant_values=-1.0)
391
+ valid_detections = tf.shape(selected_inds)[0]
392
+ return padded_boxes, padded_scores, padded_classes, valid_detections
393
+
394
+
395
+ def representative_dataset_gen(dataset, ncalib=100):
396
+ # Representative dataset generator for use with converter.representative_dataset, returns a generator of np arrays
397
+ for n, (path, img, im0s, vid_cap) in enumerate(dataset):
 
 
 
 
 
 
 
 
 
398
  input = np.transpose(img, [1, 2, 0])
399
  input = np.expand_dims(input, axis=0).astype(np.float32)
400
  input /= 255.0
401
  yield [input]
402
+ if n >= ncalib:
403
  break
404
 
405
 
406
+ def run(weights=ROOT / 'yolov5s.pt', # weights path
407
+ imgsz=(640, 640), # inference size h,w
408
+ batch_size=1, # batch size
409
+ dynamic=False, # dynamic batch size
410
+ ):
411
+ # PyTorch model
412
+ im = torch.zeros((batch_size, 3, *imgsz)) # BCHW image
413
+ model = attempt_load(weights, map_location=torch.device('cpu'), inplace=True, fuse=False)
414
+ y = model(im) # inference
415
+ model.info()
416
+
417
+ # TensorFlow model
418
+ im = tf.zeros((batch_size, *imgsz, 3)) # BHWC image
419
+ tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz)
420
+ y = tf_model.predict(im) # inference
421
+
422
+ # Keras model
423
+ im = keras.Input(shape=(*imgsz, 3), batch_size=None if dynamic else batch_size)
424
+ keras_model = keras.Model(inputs=im, outputs=tf_model.predict(im))
425
+ keras_model.summary()
426
+
427
+
428
+ def parse_opt():
429
  parser = argparse.ArgumentParser()
430
+ parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='weights path')
431
+ parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
 
432
  parser.add_argument('--batch-size', type=int, default=1, help='batch size')
433
+ parser.add_argument('--dynamic', action='store_true', help='dynamic batch size')
 
 
 
 
 
 
 
 
 
 
 
434
  opt = parser.parse_args()
435
+ opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
436
+ return opt
437
+
438
+
439
+ def main(opt):
440
+ set_logging()
441
+ print(colorstr('tf.py: ') + ', '.join(f'{k}={v}' for k, v in vars(opt).items()))
442
+ run(**vars(opt))
443
+
444
+
445
+ if __name__ == "__main__":
446
+ opt = parse_opt()
447
+ main(opt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
  # pip install -r requirements.txt
2
 
3
- # base ----------------------------------------
4
  matplotlib>=3.2.2
5
  numpy>=1.18.5
6
  opencv-python>=4.1.2
@@ -11,21 +11,23 @@ torch>=1.7.0
11
  torchvision>=0.8.1
12
  tqdm>=4.41.0
13
 
14
- # logging -------------------------------------
15
  tensorboard>=2.4.1
16
  # wandb
17
 
18
- # plotting ------------------------------------
19
  seaborn>=0.11.0
20
  pandas
21
 
22
- # export --------------------------------------
23
- # coremltools>=4.1
24
- # onnx>=1.9.0
25
- # scikit-learn==0.19.2 # for coreml quantization
26
- # tensorflow==2.4.1 # for TFLite export
 
 
27
 
28
- # extras --------------------------------------
29
  # Cython # for pycocotools https://github.com/cocodataset/cocoapi/issues/172
30
  # pycocotools>=2.0 # COCO mAP
31
  # albumentations>=1.0.3
 
1
  # pip install -r requirements.txt
2
 
3
+ # Base ----------------------------------------
4
  matplotlib>=3.2.2
5
  numpy>=1.18.5
6
  opencv-python>=4.1.2
 
11
  torchvision>=0.8.1
12
  tqdm>=4.41.0
13
 
14
+ # Logging -------------------------------------
15
  tensorboard>=2.4.1
16
  # wandb
17
 
18
+ # Plotting ------------------------------------
19
  seaborn>=0.11.0
20
  pandas
21
 
22
+ # Export --------------------------------------
23
+ # coremltools>=4.1 # CoreML export
24
+ # onnx>=1.9.0 # ONNX export
25
+ # onnx-simplifier>=0.3.6 # ONNX simplifier
26
+ # scikit-learn==0.19.2 # CoreML quantization
27
+ # tensorflow>=2.4.1 # TFLite export
28
+ # tensorflowjs>=3.9.0 # TF.js export
29
 
30
+ # Extras --------------------------------------
31
  # Cython # for pycocotools https://github.com/cocodataset/cocoapi/issues/172
32
  # pycocotools>=2.0 # COCO mAP
33
  # albumentations>=1.0.3
utils/general.py CHANGED
@@ -161,9 +161,15 @@ def emojis(str=''):
161
  return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
162
 
163
 
164
- def file_size(file):
165
- # Return file size in MB
166
- return Path(file).stat().st_size / 1e6
 
 
 
 
 
 
167
 
168
 
169
  def check_online():
 
161
  return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
162
 
163
 
164
+ def file_size(path):
165
+ # Return file/dir size (MB)
166
+ path = Path(path)
167
+ if path.is_file():
168
+ return path.stat().st_size / 1E6
169
+ elif path.is_dir():
170
+ return sum(f.stat().st_size for f in path.glob('**/*') if f.is_file()) / 1E6
171
+ else:
172
+ return 0.0
173
 
174
 
175
  def check_online():